File Coverage

blib/lib/Gcode/Interpreter/Ultimaker.pm
Criterion Covered Total %
statement 123 191 64.4
branch 39 114 34.2
condition 3 39 7.6
subroutine 16 17 94.1
pod 7 7 100.0
total 188 368 51.0


line stmt bran cond sub pod time code
1             package Gcode::Interpreter::Ultimaker;
2              
3 4     4   21 use strict;
  4         8  
  4         122  
4 4     4   19 use warnings;
  4         7  
  4         96  
5              
6 4     4   20 use Exporter;
  4         7  
  4         134  
7 4     4   20 use vars qw($VERSION @ISA);
  4         5  
  4         349  
8              
9             $VERSION = 1.00;
10             @ISA = qw(Exporter);
11              
12 4     4   22 use constant CALC_METHOD_FAST => 0;
  4         7  
  4         359  
13 4     4   21 use constant CALC_METHOD_TABLE => 1;
  4         7  
  4         190  
14 4     4   20 use constant CALC_METHOD_CALC => 2;
  4         6  
  4         14254  
15              
16             $Gcode::Interpreter::Ultimaker::method = CALC_METHOD_FAST;
17              
18             @Gcode::Interpreter::Ultimaker::xy_distances = (0.1,0.5,1,2,3,4,5,10,50,100,10000);
19             @Gcode::Interpreter::Ultimaker::xy_speeds = (3,16.2086,28.2828,35.8809,47.2937,55.3019,63.1498,68.9727,92.3058,132.9859,140.7281);
20              
21             @Gcode::Interpreter::Ultimaker::z_distances = (0.1,0.5,1,2,5,10,50,10000);
22             @Gcode::Interpreter::Ultimaker::z_speeds = (1.5,3.4294,5.7529,7.2993,8.4196,9.2844,9.6132,9.9128);
23              
24             sub new {
25 7     7 1 15 my ($class) = @_;
26              
27 7         89 my $self = {
28             'position' => [0, 0, 0, 0],
29             'zero_adj' => [undef,undef,undef,undef],
30             'temperature' => {
31             'T0' => undef,
32             },
33             'feedrate' => 3600.0,
34             'duration' => 0.0,
35             'extruded' => 0.0,
36             'current_extruder' => 0,
37             'pos_abs' => 0,
38             'ext_abs' => 1,
39             'scale' => 1,
40             'ext_scale' => 1,
41             };
42 7         20 $self->{LINE} = 0;
43 7         17 $self->{OUTPUT} = [];
44              
45 7         20 bless $self, $class;
46              
47 7         55 return $self;
48             }
49              
50             # return X,Y,Z,E
51             sub position {
52 7     7 1 13 my ($self) = @_;
53 7         24 return $self->{'position'};
54             }
55              
56             # Return duration and amount extruded
57             sub stats {
58 5     5 1 17 my ($self) = @_;
59             return {
60 5         32 'duration' => $self->{'duration'},
61             'extruded' => $self->{'extruded'},
62             };
63             }
64              
65             # Confugure which method we're going to use to calculate print duration.
66             sub set_method {
67 6     6 1 27 my ($self, $method) = @_;
68              
69 6 100       75 if($method eq 'fast') {
    50          
70 5         12 $Gcode::Interpreter::Ultimaker::method = CALC_METHOD_FAST;
71             } elsif($method eq 'table') {
72 1         3 $Gcode::Interpreter::Ultimaker::method = CALC_METHOD_TABLE;
73             } else {
74 0         0 return 0;
75             }
76 6         51 return 1;
77             }
78              
79             # Get a number from a string like "S220.2"
80             # This function gets called *a lot*. As such, it is performance
81             # optimised:
82             # - It's a function not a method
83             # - it doesn't use regexes, and instead uses substr to do the work. This
84             # makes it faster, but less tolerant of minor gcode weirdness.
85             # - It's had a few shuffles about to make it profile better
86             sub num_from_code {
87 3     3 1 6 my $code = shift(@_);
88 3         5 my $string = shift(@_);
89              
90 3         7 my $index = index($string, $code);
91 3 100       11 if($index == -1) {
92             # Not found
93 1         7 return undef;
94             }
95 2         3 $index++;
96              
97             # Remove anything beyond the number in the string
98             # Look for the first whitespace after the code and number
99 2         5 my $end = index($string, ' ', $index);
100 2 50       5 if($end > 0) {
101 0         0 $string = substr($string, $index, $end - $index);
102             } else {
103 2         5 $string = substr($string, $index);
104             }
105              
106 2         3 $string = eval { $string + 0.0; };
  2         5  
107 2 50       6 if($@) {
108 0         0 return undef;
109             }
110 2         7 return $string;
111             }
112              
113             # Get 'everything' from a reference to a list of "words"
114             # these 'words' are something like:
115             # X1.0, Y2.23, Z45.2, E3.123
116             # This is performance optimised, and requires the words
117             # which are assumed to have been sought when parsing the line
118             # in the first place
119             sub xyze_from_words {
120 8     8 1 15 my $words_ref = shift(@_);
121              
122 8         17 my @out = (undef, undef, undef, undef);
123              
124 8         17 foreach my $word (@$words_ref) {
125 32         50 my $char = substr($word, 0, 1);
126 32 100       88 if($char eq 'X') {
    100          
    100          
    50          
127 8         17 $out[0] = substr($word, 1);
128             } elsif($char eq 'Y') {
129 8         16 $out[1] = substr($word, 1);
130             } elsif($char eq 'Z') {
131 8         18 $out[2] = substr($word, 1);
132             } elsif($char eq 'E') {
133 8         22 $out[3] = substr($word, 1);
134             }
135             }
136              
137 8         23 return \@out;
138             }
139              
140             # Simulate a move. We first of all update out position based
141             # on whatever we've been told to do.
142             #
143             # We also update the object's total for duration and extruded
144             # amount. This can be done in a few different ways, depending
145             # on the method configured into the object.
146             sub _move {
147 8     8   11 my $printer = shift(@_);
148 8         10 my $adj_ref = shift(@_);
149              
150 8         12 my $position = $printer->{'position'};
151              
152 8         11 my @originals = @{$position};
  8         20  
153              
154 8         22 for(my $i = 0; $i < 4; $i++) {
155 32         45 my $adj = shift(@$adj_ref);
156 32 50       52 if(defined($adj)) {
157 32 100       54 if($i == 3) { # is E
158 8 50       18 if($printer->{'ext_abs'}) {
159 8         14 ${$position}[3] = $adj * $printer->{'ext_scale'};
  8         26  
160             } else {
161 0         0 ${$position}[3] = ${$position}[3] + ($adj * $printer->{'ext_scale'});
  0         0  
  0         0  
162             }
163             } else { # is x,y or z
164 24 50       46 if($printer->{'pos_abs'}) {
165 0         0 ${$position}[$i] = $adj * $printer->{'scale'};
  0         0  
166             } else {
167 24         24 ${$position}[$i] = ${$position}[$i] + ($adj * $printer->{'scale'});
  24         70  
  24         63  
168             }
169             }
170             }
171             }
172              
173             # Now work out how far we've travelled, and so how long it took
174             #math.sqrt(diffX * diffX + diffY * diffY) / feedRate
175 8         26 my @diffs = (0,0,0,0);
176 8         22 for(my $i = 0; $i < 4; $i++) {
177 32         29 $diffs[$i] = ${$position}[$i] - $originals[$i];
  32         87  
178             }
179              
180 8         10 my $duration = 0;
181 8 50       20 if($Gcode::Interpreter::Ultimaker::method == CALC_METHOD_FAST) {
    0          
182             # z^2 + b^2 = c^2
183 8         34 my $distance = sqrt($diffs[0] * $diffs[0] + $diffs[1] * $diffs[1]);
184 8         22 $duration = ($distance / $printer->{'feedrate'}) * 60;
185             } elsif($Gcode::Interpreter::Ultimaker::method == CALC_METHOD_TABLE) {
186             # This method involves the look up tables defined at the top of this file.
187             # Essentially, we look along the distances to find the one that's closest
188             # to the distance we're actually moving. We then look to see the speed
189             # over that distance. We then work out the longest time of the X, Y or
190             # Z move part of the movement and use that as the duration of the move.
191             # This isn't strictly necessary with G1 moves because they should all
192             # take the same time. We could optimise out some of the maths here to
193             # get a speed increase...?
194 0         0 for(my $i = 0; $i < 2; $i++) {
195 0 0       0 next if($diffs[$i] == 0);
196 0         0 my $diff = abs($diffs[$i]);
197 0         0 for(my $j = 0; $j <= $#Gcode::Interpreter::Ultimaker::xy_distances; $j++) {
198             #print "Checking axis $axis distance $distance for diff $diff (max=$max)\n";
199 0 0       0 if($diff <= $Gcode::Interpreter::Ultimaker::xy_distances[$j]) {
200             # We've found our entry in the table
201 0         0 my $time = $diff / $Gcode::Interpreter::Ultimaker::xy_speeds[$j];
202 0 0       0 if($time > $duration) {
203 0         0 $duration = $time;
204             }
205             #print "Got time = $time, max = $max\n";
206 0         0 last;
207             }
208             }
209             }
210             # Now do Z
211 0 0       0 if($diffs[2] != 0) {
212 0         0 my $diff = abs($diffs[2]);
213 0         0 for(my $i = 0; $i <= $#Gcode::Interpreter::Ultimaker::z_distances; $i++) {
214 0 0       0 if($diff <= $Gcode::Interpreter::Ultimaker::z_distances[$i]) {
215 0         0 my $time = $diff / $Gcode::Interpreter::Ultimaker::z_speeds[$i];
216 0 0       0 if($time > $duration) {
217 0         0 $duration = $time;
218             }
219             }
220             }
221             }
222             }
223 8         16 $printer->{'duration'} = $printer->{'duration'} + $duration;
224              
225             # Also total up how much we extruded
226 8         15 $printer->{'extruded'} = $printer->{'extruded'} + $diffs[3];
227 8         20 return 1;
228             }
229            
230             # Parse any G* command (Eg. G0, G1 etc)
231             # This gets called *lots* when parsing most Gcode,
232             # so it's performance optimised (and so not quite
233             # so readable).
234             sub _parse_g {
235             # Don't reassign argument variables - it uses up a few
236             # microseconds per call, so multiple seconds
237             # on any reasonable length gcode file
238             # $_[0] = $printer
239             # $_[1] = $line (the preprocessed original gcode line)
240             # $_[2] = $g (numeric value of the G command)
241             # $_[3] = $words_ref (reference to a list of words in the original
242             # gcode line
243 10 100 100 10   666 if($_[2] == 0 || $_[2] == 1) {
    100          
    50          
    50          
    50          
    50          
    50          
    0          
    0          
    0          
244             # Move (0 = fast move, 1 = interpolated move)
245 2         5 my $args = &xyze_from_words($_[3]);
246 2         5 &_move($_[0], $args);
247             } elsif($_[2] == 4) {
248             # Delay
249 2 100       7 if(my $s = &num_from_code('S', $_[1])) {
    50          
250 1         4 $_[0]->{'duration'} = $_[0]->{'duration'} + $s;
251             } elsif(my $p = &num_from_code('P', $_[1])) {
252 1         4 $_[0]->{'duration'} = $_[0]->{'duration'} + ($p / 1000);
253             }
254             } elsif($_[2] == 10) {
255             # Retract
256             # What's wrong with a G1 E-?
257             } elsif($_[2] == 11) {
258             # Push back after retract
259             } elsif($_[2] == 20) {
260             # Units -> inches
261 0         0 $_[0]->{'scale'} = 25.4;
262             } elsif($_[2] == 21) {
263             # Units -> millimeters
264 0         0 $_[0]->{'scale'} = 1;
265             } elsif($_[2] == 28) {
266             # Home all axies
267 6         10 my $duration = 0;
268 6         20 my $pos = &xyze_from_words($_[3]);
269 6         19 my @pos = (@$pos);
270              
271             # First, do a move to the real home, if we know where that is.
272 6         14 my @move = (undef,undef,undef,undef);
273 6         15 my $printer_zero_adj = $_[0]->{'zero_adj'};
274 6         19 for(my $i = 0; $i < 4; $i++) {
275 24 50       50 if(defined($pos[$i])) {
276 24 100       24 my $adj = defined(${$printer_zero_adj}[$i]) ? ${$printer_zero_adj}[$i] : 0;
  24         48  
  4         8  
277 24         58 $move[$i] = $adj;
278             }
279             }
280             # This move alters duration and so forth by the right amount
281 6         18 &_move($_[0], \@move);
282              
283             # Now make the current position whatever we've been told to use,
284             # and make a note of any adjustment so we know where we really are
285 6         14 my $position = $_[0]->{'position'};
286 6         19 for(my $i = 0; $i < 4; $i++) {
287 24 50       43 if(defined($pos[$i])) {
288 24         24 ${$position}[$i] = $pos[$i];
  24         41  
289 24         26 ${$printer_zero_adj}[$i] = $pos[$i];
  24         78  
290             }
291             }
292             } elsif($_[2] == 90) {
293 0         0 $_[0]->{'pos_abs'} = 1;
294 0         0 $_[0]->{'ext_abs'} = 1;
295             } elsif($_[2] == 91) {
296 0         0 $_[0]->{'pos_abs'} = 0;
297 0         0 $_[0]->{'ext_abs'} = 0;
298             } elsif($_[2] = 92) {
299             # Set current position to co-ordinates given
300 0         0 my $pos = &xyze_from_words($_[3]);
301 0         0 my @pos = (@$pos);
302 0         0 for(my $i = 0; $i < 4; $i++) {
303 0 0       0 if(defined($pos[$i])) {
304 0 0       0 my $adj = defined(${$_[0]->{'zero_adj'}}[$i]) ? ${$_[0]->{'zero_adj'}}[$i] : 0;
  0         0  
  0         0  
305 0         0 ${$_[0]->{'zero_adj'}}[$i] = $pos[$i] - $adj;
  0         0  
306 0         0 ${$_[0]->{'position'}}[$i] = $pos[$i];
  0         0  
307             }
308             }
309             } else {
310 0         0 print "Unsupported G command: ". $_[1] . "\n";
311 0         0 return 0;
312             }
313 10         50 return 1;
314             }
315              
316             # Parse any M command
317             # This doesn't get used very much in an average gcode file
318             # so doesn't need to be especially well optimised.
319             sub _parse_m {
320 0     0   0 my ($printer, $line, $m) = @_;
321              
322 0 0 0     0 if($m == 0 || $m == 1 || $m == 80 || $m == 81) {
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
      0        
      0        
323             # Ignore all of these:
324             # 0
325             # 1
326             # 80 = Enable power supply
327             # 81 = Suicide Pin
328             } elsif($m == 82) {
329             # Absolute E
330 0         0 $printer->{'abs_ext'} = 1;
331             } elsif($m == 83) {
332             # Relative E
333 0         0 $printer->{'abs_ext'} = 0;
334             } elsif($m == 84 || $m == 92 || $m == 101 || $m == 103) {
335             # Ignore all of these:
336             # 84 = Disable steppers
337             # 92 = Set steps per unit
338             # 101 = Enable extruder
339             # 103 = Disable extruder
340             } elsif($m == 104 || $m == 109) {
341             # Set current extruder temperature
342 0         0 my $temp = &num_from_code('S', $line);
343 0 0       0 if(defined($temp)) {
344             # Set the temperature
345 0         0 $printer->{'temperature'}->{'T' . $printer->{'current_extruder'}} = $temp;
346             }
347 0 0       0 if($m == 109) {
348             # This now waits for the temperature to reach the required
349             # value. It's not possible to know how long this will take on
350             # a real machine, so we just add 5 seconds to the print
351 0         0 $printer->{'duration'} = $printer->{'duration'} + 5;
352             }
353             } elsif($m == 105) {
354             # Return the current temperatures
355             } elsif($m == 106 || $m == 107 || $m == 108) {
356             # 106 = turn on the fan
357             # 107 = turn off the fan
358             # 108 = Set Extruder RPM
359             } elsif($m == 110 || $m == 113 || $m == 117) {
360             # 110 = Reset Gcode N count
361             # 113 = Set Extruder PWM
362             # 117 = Set display message
363             } elsif($m == 140 || $m == 190) {
364             # Set bed temperature
365 0         0 my $temp = &num_from_code('S', $line);
366 0 0       0 if(defined($temp)) {
367             # Set the temperature
368 0         0 $printer->{'temperature'}->{'B0'} = $temp;
369             }
370 0 0       0 if($m == 190) {
371             # Wait for bed to reach temperature - how long to wait?
372 0         0 $printer->{'duration'} = $printer->{'duration'} + 5;
373             }
374             } elsif($m == 221) {
375             # Set extruder amount multiplier
376 0         0 my $new = &num_from_code('S', $line)
377             # ???
378             } else {
379 0         0 print "Unsupported M command: $line\n";
380 0         0 return 0;
381             }
382 0         0 return 1;
383             }
384              
385             # parse_line(line_of_gcode)
386             # This method obviously gets called a great deal, but it necessarily
387             # has to be a method, and has to do some heavy lifting. It has had
388             # some performance optimisations though.
389             sub parse_line {
390 10     10 1 838 my ($self, $line) = @_;
391              
392 10         49 $self->{LINE}++;
393            
394             # Others use the comments, we just strip them
395             # This seems to be quicker than using substr/index to do this
396 10         25 $line =~ s/\s*;.*$//;
397 10         14 $line =~ s/\s*\(.*\)//;
398              
399 10         81 my ($cmd,@words) = split(/\s/, $line);
400              
401             # This is effectively a blank line check
402 10 50       35 return 0 if(!defined($cmd));
403              
404             # Get the "code" from the command and the numeric
405             # number too. Eg. M107 -> M and 107
406 10         21 my $code = substr($cmd, 0, 1);
407 10         600 my $num = substr($cmd, 1);
408              
409             # This shouldn't happen on generated gcode, but
410             # messes us up quite a bit if it happens so we
411             # have to check for it (easier here than in
412             # the _parse_X() functions)
413 10 50       24 if(!defined($num)) {
414 0         0 print "Malformed Gcode line \"$line\", line " . $self->{LINE} ."\n";
415 0         0 return 0;
416             }
417 10 50       22 if($code eq 'G') {
    0          
    0          
418 10         31 return &_parse_g($self, $line, $num, \@words);
419             } elsif($code eq 'M') {
420 0           return &_parse_m($self, $line, $num);
421             } elsif($code eq 'T') {
422 0           $self->{'current_extruder'} = $num;
423 0           return 1;
424             }
425              
426 0           print "Unsupported Gcode \"$line\", line " . $self->{LINE} . "\n";
427 0           return 0;
428             }
429              
430             1;
431              
432             __END__