Wed, 20 Jan 2021 10:15:13 +0100
updated and added new files for printrun
46 | 1 | #!/usr/bin/env python3 |
15 | 2 | # |
46 | 3 | # This file is part of the Printrun suite. |
4 | # | |
5 | # Printrun is free software: you can redistribute it and/or modify | |
15 | 6 | # it under the terms of the GNU General Public License as published by |
7 | # the Free Software Foundation, either version 3 of the License, or | |
8 | # (at your option) any later version. | |
9 | # | |
46 | 10 | # Printrun is distributed in the hope that it will be useful, |
15 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | # GNU General Public License for more details. | |
14 | # | |
15 | # You should have received a copy of the GNU General Public License | |
16 | # along with Printrun. If not, see <http://www.gnu.org/licenses/>. | |
17 | ||
18 | import sys | |
19 | import re | |
20 | import math | |
21 | import datetime | |
22 | import logging | |
23 | from array import array | |
24 | ||
25 | gcode_parsed_args = ["x", "y", "e", "f", "z", "i", "j"] | |
46 | 26 | gcode_parsed_nonargs = 'gtmnd' |
27 | to_parse = "".join(gcode_parsed_args) + gcode_parsed_nonargs | |
28 | gcode_exp = re.compile("\([^\(\)]*\)|;.*|[/\*].*\n|([%s])\s*([-+]?[0-9]*\.?[0-9]*)" % to_parse) | |
15 | 29 | gcode_strip_comment_exp = re.compile("\([^\(\)]*\)|;.*|[/\*].*\n") |
30 | m114_exp = re.compile("\([^\(\)]*\)|[/\*].*\n|([XYZ]):?([-+]?[0-9]*\.?[0-9]*)") | |
31 | specific_exp = "(?:\([^\(\)]*\))|(?:;.*)|(?:[/\*].*\n)|(%s[-+]?[0-9]*\.?[0-9]*)" | |
32 | move_gcodes = ["G0", "G1", "G2", "G3"] | |
33 | ||
46 | 34 | class PyLine: |
15 | 35 | |
36 | __slots__ = ('x', 'y', 'z', 'e', 'f', 'i', 'j', | |
37 | 'raw', 'command', 'is_move', | |
38 | 'relative', 'relative_e', | |
39 | 'current_x', 'current_y', 'current_z', 'extruding', | |
40 | 'current_tool', | |
41 | 'gcview_end_vertex') | |
42 | ||
43 | def __init__(self, l): | |
44 | self.raw = l | |
45 | ||
46 | def __getattr__(self, name): | |
47 | return None | |
48 | ||
46 | 49 | class PyLightLine: |
15 | 50 | |
51 | __slots__ = ('raw', 'command') | |
52 | ||
53 | def __init__(self, l): | |
54 | self.raw = l | |
55 | ||
56 | def __getattr__(self, name): | |
57 | return None | |
58 | ||
59 | try: | |
46 | 60 | from . import gcoder_line |
15 | 61 | Line = gcoder_line.GLine |
62 | LightLine = gcoder_line.GLightLine | |
46 | 63 | except Exception as e: |
15 | 64 | logging.warning("Memory-efficient GCoder implementation unavailable: %s" % e) |
65 | Line = PyLine | |
66 | LightLine = PyLightLine | |
67 | ||
68 | def find_specific_code(line, code): | |
69 | exp = specific_exp % code | |
70 | bits = [bit for bit in re.findall(exp, line.raw) if bit] | |
71 | if not bits: return None | |
72 | else: return float(bits[0][1:]) | |
73 | ||
74 | def S(line): | |
75 | return find_specific_code(line, "S") | |
76 | ||
77 | def P(line): | |
78 | return find_specific_code(line, "P") | |
79 | ||
80 | def split(line): | |
81 | split_raw = gcode_exp.findall(line.raw.lower()) | |
82 | if split_raw and split_raw[0][0] == "n": | |
83 | del split_raw[0] | |
84 | if not split_raw: | |
85 | line.command = line.raw | |
86 | line.is_move = False | |
87 | logging.warning("raw G-Code line \"%s\" could not be parsed" % line.raw) | |
88 | return [line.raw] | |
89 | command = split_raw[0] | |
90 | line.command = command[0].upper() + command[1] | |
91 | line.is_move = line.command in move_gcodes | |
92 | return split_raw | |
93 | ||
94 | def parse_coordinates(line, split_raw, imperial = False, force = False): | |
95 | # Not a G-line, we don't want to parse its arguments | |
96 | if not force and line.command[0] != "G": | |
97 | return | |
98 | unit_factor = 25.4 if imperial else 1 | |
99 | for bit in split_raw: | |
100 | code = bit[0] | |
101 | if code not in gcode_parsed_nonargs and bit[1]: | |
102 | setattr(line, code, unit_factor * float(bit[1])) | |
103 | ||
104 | class Layer(list): | |
105 | ||
106 | __slots__ = ("duration", "z") | |
107 | ||
108 | def __init__(self, lines, z = None): | |
109 | super(Layer, self).__init__(lines) | |
110 | self.z = z | |
46 | 111 | self.duration = 0 |
15 | 112 | |
46 | 113 | class GCode: |
15 | 114 | |
115 | line_class = Line | |
116 | ||
117 | lines = None | |
118 | layers = None | |
119 | all_layers = None | |
120 | layer_idxs = None | |
121 | line_idxs = None | |
122 | append_layer = None | |
123 | append_layer_id = None | |
124 | ||
125 | imperial = False | |
46 | 126 | cutting = False |
15 | 127 | relative = False |
128 | relative_e = False | |
129 | current_tool = 0 | |
130 | # Home position: current absolute position counted from machine origin | |
131 | home_x = 0 | |
132 | home_y = 0 | |
133 | home_z = 0 | |
134 | # Current position: current absolute position counted from machine origin | |
135 | current_x = 0 | |
136 | current_y = 0 | |
137 | current_z = 0 | |
138 | # For E this is the absolute position from machine start | |
139 | current_e = 0 | |
140 | current_e_multi=[0] | |
141 | total_e = 0 | |
142 | total_e_multi=[0] | |
143 | max_e = 0 | |
144 | max_e_multi=[0] | |
145 | # Current feedrate | |
146 | current_f = 0 | |
147 | # Offset: current offset between the machine origin and the machine current | |
148 | # absolute coordinate system (as shifted by G92s) | |
149 | offset_x = 0 | |
150 | offset_y = 0 | |
151 | offset_z = 0 | |
152 | offset_e = 0 | |
153 | offset_e_multi = [0] | |
154 | ||
155 | # Expected behavior: | |
156 | # - G28 X => X axis is homed, offset_x <- 0, current_x <- home_x | |
157 | # - G92 Xk => X axis does not move, so current_x does not change | |
158 | # and offset_x <- current_x - k, | |
159 | # - absolute G1 Xk => X axis moves, current_x <- offset_x + k | |
160 | # How to get... | |
161 | # current abs X from machine origin: current_x | |
162 | # current abs X in machine current coordinate system: current_x - offset_x | |
163 | ||
164 | filament_length = None | |
165 | filament_length_multi=[0] | |
166 | duration = None | |
167 | xmin = None | |
168 | xmax = None | |
169 | ymin = None | |
170 | ymax = None | |
171 | zmin = None | |
172 | zmax = None | |
173 | width = None | |
174 | depth = None | |
175 | height = None | |
176 | ||
177 | est_layer_height = None | |
178 | ||
179 | # abs_x is the current absolute X in machine current coordinate system | |
180 | # (after the various G92 transformations) and can be used to store the | |
181 | # absolute position of the head at a given time | |
182 | def _get_abs_x(self): | |
183 | return self.current_x - self.offset_x | |
184 | abs_x = property(_get_abs_x) | |
185 | ||
186 | def _get_abs_y(self): | |
187 | return self.current_y - self.offset_y | |
188 | abs_y = property(_get_abs_y) | |
189 | ||
190 | def _get_abs_z(self): | |
191 | return self.current_z - self.offset_z | |
192 | abs_z = property(_get_abs_z) | |
193 | ||
194 | def _get_abs_e(self): | |
195 | return self.current_e - self.offset_e | |
196 | abs_e = property(_get_abs_e) | |
197 | ||
198 | def _get_abs_e_multi(self,i): | |
199 | return self.current_e_multi[i] - self.offset_e_multi[i] | |
200 | abs_e = property(_get_abs_e) | |
201 | ||
202 | def _get_abs_pos(self): | |
203 | return (self.abs_x, self.abs_y, self.abs_z) | |
204 | abs_pos = property(_get_abs_pos) | |
205 | ||
206 | def _get_current_pos(self): | |
207 | return (self.current_x, self.current_y, self.current_z) | |
208 | current_pos = property(_get_current_pos) | |
209 | ||
210 | def _get_home_pos(self): | |
211 | return (self.home_x, self.home_y, self.home_z) | |
212 | ||
213 | def _set_home_pos(self, home_pos): | |
214 | if home_pos: | |
215 | self.home_x, self.home_y, self.home_z = home_pos | |
216 | home_pos = property(_get_home_pos, _set_home_pos) | |
217 | ||
218 | def _get_layers_count(self): | |
219 | return len(self.all_zs) | |
220 | layers_count = property(_get_layers_count) | |
221 | ||
222 | def __init__(self, data = None, home_pos = None, | |
46 | 223 | layer_callback = None, deferred = False, |
224 | cutting_as_extrusion = False): | |
225 | self.cutting_as_extrusion = cutting_as_extrusion | |
15 | 226 | if not deferred: |
227 | self.prepare(data, home_pos, layer_callback) | |
228 | ||
229 | def prepare(self, data = None, home_pos = None, layer_callback = None): | |
230 | self.home_pos = home_pos | |
231 | if data: | |
232 | line_class = self.line_class | |
233 | self.lines = [line_class(l2) for l2 in | |
234 | (l.strip() for l in data) | |
235 | if l2] | |
236 | self._preprocess(build_layers = True, | |
237 | layer_callback = layer_callback) | |
238 | else: | |
239 | self.lines = [] | |
240 | self.append_layer_id = 0 | |
241 | self.append_layer = Layer([]) | |
242 | self.all_layers = [self.append_layer] | |
243 | self.all_zs = set() | |
244 | self.layers = {} | |
245 | self.layer_idxs = array('I', []) | |
246 | self.line_idxs = array('I', []) | |
247 | ||
46 | 248 | def has_index(self, i): |
249 | return i < len(self) | |
15 | 250 | def __len__(self): |
251 | return len(self.line_idxs) | |
252 | ||
253 | def __iter__(self): | |
254 | return self.lines.__iter__() | |
255 | ||
256 | def prepend_to_layer(self, commands, layer_idx): | |
257 | # Prepend commands in reverse order | |
258 | commands = [c.strip() for c in commands[::-1] if c.strip()] | |
259 | layer = self.all_layers[layer_idx] | |
260 | # Find start index to append lines | |
261 | # and end index to append new indices | |
262 | start_index = self.layer_idxs.index(layer_idx) | |
263 | for i in range(start_index, len(self.layer_idxs)): | |
264 | if self.layer_idxs[i] != layer_idx: | |
265 | end_index = i | |
266 | break | |
267 | else: | |
268 | end_index = i + 1 | |
269 | end_line = self.line_idxs[end_index - 1] | |
270 | for i, command in enumerate(commands): | |
271 | gline = Line(command) | |
272 | # Split to get command | |
273 | split(gline) | |
274 | # Force is_move to False | |
275 | gline.is_move = False | |
276 | # Insert gline at beginning of layer | |
277 | layer.insert(0, gline) | |
278 | # Insert gline at beginning of list | |
279 | self.lines.insert(start_index, gline) | |
280 | # Update indices arrays & global gcodes list | |
281 | self.layer_idxs.insert(end_index + i, layer_idx) | |
282 | self.line_idxs.insert(end_index + i, end_line + i + 1) | |
283 | return commands[::-1] | |
284 | ||
285 | def rewrite_layer(self, commands, layer_idx): | |
286 | # Prepend commands in reverse order | |
287 | commands = [c.strip() for c in commands[::-1] if c.strip()] | |
288 | layer = self.all_layers[layer_idx] | |
289 | # Find start index to append lines | |
290 | # and end index to append new indices | |
291 | start_index = self.layer_idxs.index(layer_idx) | |
292 | for i in range(start_index, len(self.layer_idxs)): | |
293 | if self.layer_idxs[i] != layer_idx: | |
294 | end_index = i | |
295 | break | |
296 | else: | |
297 | end_index = i + 1 | |
298 | self.layer_idxs = self.layer_idxs[:start_index] + array('I', len(commands) * [layer_idx]) + self.layer_idxs[end_index:] | |
299 | self.line_idxs = self.line_idxs[:start_index] + array('I', range(len(commands))) + self.line_idxs[end_index:] | |
300 | del self.lines[start_index:end_index] | |
301 | del layer[:] | |
302 | for i, command in enumerate(commands): | |
303 | gline = Line(command) | |
304 | # Split to get command | |
305 | split(gline) | |
306 | # Force is_move to False | |
307 | gline.is_move = False | |
308 | # Insert gline at beginning of layer | |
309 | layer.insert(0, gline) | |
310 | # Insert gline at beginning of list | |
311 | self.lines.insert(start_index, gline) | |
312 | return commands[::-1] | |
313 | ||
314 | def append(self, command, store = True): | |
315 | command = command.strip() | |
316 | if not command: | |
317 | return | |
318 | gline = Line(command) | |
319 | self._preprocess([gline]) | |
320 | if store: | |
321 | self.lines.append(gline) | |
322 | self.append_layer.append(gline) | |
323 | self.layer_idxs.append(self.append_layer_id) | |
46 | 324 | self.line_idxs.append(len(self.append_layer)-1) |
15 | 325 | return gline |
326 | ||
327 | def _preprocess(self, lines = None, build_layers = False, | |
328 | layer_callback = None): | |
329 | """Checks for imperial/relativeness settings and tool changes""" | |
330 | if not lines: | |
331 | lines = self.lines | |
332 | imperial = self.imperial | |
333 | relative = self.relative | |
334 | relative_e = self.relative_e | |
335 | current_tool = self.current_tool | |
336 | current_x = self.current_x | |
337 | current_y = self.current_y | |
338 | current_z = self.current_z | |
339 | offset_x = self.offset_x | |
340 | offset_y = self.offset_y | |
341 | offset_z = self.offset_z | |
342 | ||
343 | # Extrusion computation | |
344 | current_e = self.current_e | |
345 | offset_e = self.offset_e | |
346 | total_e = self.total_e | |
347 | max_e = self.max_e | |
46 | 348 | cutting = self.cutting |
15 | 349 | |
350 | current_e_multi = self.current_e_multi[current_tool] | |
351 | offset_e_multi = self.offset_e_multi[current_tool] | |
352 | total_e_multi = self.total_e_multi[current_tool] | |
353 | max_e_multi = self.max_e_multi[current_tool] | |
354 | ||
355 | # Store this one out of the build_layers scope for efficiency | |
356 | cur_layer_has_extrusion = False | |
357 | ||
358 | # Initialize layers and other global computations | |
359 | if build_layers: | |
360 | # Bounding box computation | |
361 | xmin = float("inf") | |
362 | ymin = float("inf") | |
363 | zmin = 0 | |
364 | xmax = float("-inf") | |
365 | ymax = float("-inf") | |
366 | zmax = float("-inf") | |
367 | # Also compute extrusion-only values | |
368 | xmin_e = float("inf") | |
369 | ymin_e = float("inf") | |
370 | xmax_e = float("-inf") | |
371 | ymax_e = float("-inf") | |
372 | ||
373 | # Duration estimation | |
374 | # TODO: | |
375 | # get device caps from firmware: max speed, acceleration/axis | |
376 | # (including extruder) | |
377 | # calculate the maximum move duration accounting for above ;) | |
46 | 378 | lastx = lasty = lastz = None |
379 | laste = lastf = 0 | |
15 | 380 | lastdx = 0 |
381 | lastdy = 0 | |
382 | x = y = e = f = 0.0 | |
383 | currenttravel = 0.0 | |
384 | moveduration = 0.0 | |
385 | totalduration = 0.0 | |
386 | acceleration = 2000.0 # mm/s^2 | |
387 | layerbeginduration = 0.0 | |
388 | ||
389 | # Initialize layers | |
390 | all_layers = self.all_layers = [] | |
391 | all_zs = self.all_zs = set() | |
392 | layer_idxs = self.layer_idxs = [] | |
393 | line_idxs = self.line_idxs = [] | |
394 | ||
395 | ||
396 | last_layer_z = None | |
397 | prev_z = None | |
398 | cur_z = None | |
399 | cur_lines = [] | |
400 | ||
46 | 401 | def append_lines(lines, isEnd): |
402 | if not build_layers: | |
403 | return | |
404 | nonlocal layerbeginduration, last_layer_z | |
405 | if cur_layer_has_extrusion and prev_z != last_layer_z \ | |
406 | or not all_layers or isEnd: | |
407 | layer = Layer([], prev_z) | |
408 | last_layer_z = prev_z | |
409 | finished_layer = len(all_layers)-1 if all_layers else None | |
410 | all_layers.append(layer) | |
411 | else: | |
412 | layer = all_layers[-1] | |
413 | finished_layer = None | |
414 | layer_id = len(all_layers)-1 | |
415 | layer_line = len(layer) | |
416 | for i, ln in enumerate(lines): | |
417 | layer.append(ln) | |
418 | layer_idxs.append(layer_id) | |
419 | line_idxs.append(layer_line+i) | |
420 | layer.duration += totalduration - layerbeginduration | |
421 | layerbeginduration = totalduration | |
422 | if layer_callback: | |
423 | # we finish a layer when inserting the next | |
424 | if finished_layer is not None: | |
425 | layer_callback(self, finished_layer) | |
426 | # notify about end layer, there will not be next | |
427 | if isEnd: | |
428 | layer_callback(self, layer_id) | |
429 | ||
15 | 430 | if self.line_class != Line: |
431 | get_line = lambda l: Line(l.raw) | |
432 | else: | |
433 | get_line = lambda l: l | |
434 | for true_line in lines: | |
435 | # # Parse line | |
436 | # Use a heavy copy of the light line to preprocess | |
437 | line = get_line(true_line) | |
438 | split_raw = split(line) | |
439 | if line.command: | |
440 | # Update properties | |
441 | if line.is_move: | |
442 | line.relative = relative | |
443 | line.relative_e = relative_e | |
444 | line.current_tool = current_tool | |
445 | elif line.command == "G20": | |
446 | imperial = True | |
447 | elif line.command == "G21": | |
448 | imperial = False | |
449 | elif line.command == "G90": | |
450 | relative = False | |
451 | relative_e = False | |
452 | elif line.command == "G91": | |
453 | relative = True | |
454 | relative_e = True | |
455 | elif line.command == "M82": | |
456 | relative_e = False | |
457 | elif line.command == "M83": | |
458 | relative_e = True | |
459 | elif line.command[0] == "T": | |
46 | 460 | try: |
461 | current_tool = int(line.command[1:]) | |
462 | except: | |
463 | pass #handle T? by treating it as no tool change | |
464 | while current_tool+1 > len(self.current_e_multi): | |
15 | 465 | self.current_e_multi+=[0] |
466 | self.offset_e_multi+=[0] | |
467 | self.total_e_multi+=[0] | |
468 | self.max_e_multi+=[0] | |
46 | 469 | elif line.command == "M3" or line.command == "M4": |
470 | cutting = True | |
471 | elif line.command == "M5": | |
472 | cutting = False | |
473 | ||
15 | 474 | current_e_multi = self.current_e_multi[current_tool] |
475 | offset_e_multi = self.offset_e_multi[current_tool] | |
476 | total_e_multi = self.total_e_multi[current_tool] | |
477 | max_e_multi = self.max_e_multi[current_tool] | |
478 | ||
479 | ||
480 | if line.command[0] == "G": | |
481 | parse_coordinates(line, split_raw, imperial) | |
482 | ||
483 | # Compute current position | |
484 | if line.is_move: | |
485 | x = line.x | |
486 | y = line.y | |
487 | z = line.z | |
488 | ||
489 | if line.f is not None: | |
490 | self.current_f = line.f | |
491 | ||
492 | if line.relative: | |
493 | x = current_x + (x or 0) | |
494 | y = current_y + (y or 0) | |
495 | z = current_z + (z or 0) | |
496 | else: | |
497 | if x is not None: x = x + offset_x | |
498 | if y is not None: y = y + offset_y | |
499 | if z is not None: z = z + offset_z | |
500 | ||
501 | if x is not None: current_x = x | |
502 | if y is not None: current_y = y | |
503 | if z is not None: current_z = z | |
504 | ||
505 | elif line.command == "G28": | |
506 | home_all = not any([line.x, line.y, line.z]) | |
507 | if home_all or line.x is not None: | |
508 | offset_x = 0 | |
509 | current_x = self.home_x | |
510 | if home_all or line.y is not None: | |
511 | offset_y = 0 | |
512 | current_y = self.home_y | |
513 | if home_all or line.z is not None: | |
514 | offset_z = 0 | |
515 | current_z = self.home_z | |
516 | ||
517 | elif line.command == "G92": | |
518 | if line.x is not None: offset_x = current_x - line.x | |
519 | if line.y is not None: offset_y = current_y - line.y | |
520 | if line.z is not None: offset_z = current_z - line.z | |
521 | ||
522 | line.current_x = current_x | |
523 | line.current_y = current_y | |
524 | line.current_z = current_z | |
525 | ||
526 | # # Process extrusion | |
527 | if line.e is not None: | |
528 | if line.is_move: | |
529 | if line.relative_e: | |
530 | line.extruding = line.e > 0 | |
531 | total_e += line.e | |
532 | current_e += line.e | |
533 | total_e_multi += line.e | |
534 | current_e_multi += line.e | |
535 | else: | |
536 | new_e = line.e + offset_e | |
537 | line.extruding = new_e > current_e | |
538 | total_e += new_e - current_e | |
539 | current_e = new_e | |
540 | new_e_multi = line.e + offset_e_multi | |
541 | total_e_multi += new_e_multi - current_e_multi | |
542 | current_e_multi = new_e_multi | |
543 | ||
544 | max_e = max(max_e, total_e) | |
545 | max_e_multi=max(max_e_multi, total_e_multi) | |
546 | cur_layer_has_extrusion |= line.extruding | |
547 | elif line.command == "G92": | |
548 | offset_e = current_e - line.e | |
549 | offset_e_multi = current_e_multi - line.e | |
46 | 550 | if cutting and self.cutting_as_extrusion: |
551 | line.extruding = True | |
15 | 552 | |
553 | self.current_e_multi[current_tool]=current_e_multi | |
554 | self.offset_e_multi[current_tool]=offset_e_multi | |
555 | self.max_e_multi[current_tool]=max_e_multi | |
556 | self.total_e_multi[current_tool]=total_e_multi | |
557 | ||
558 | # # Create layers and perform global computations | |
559 | if build_layers: | |
560 | # Update bounding box | |
561 | if line.is_move: | |
562 | if line.extruding: | |
563 | if line.current_x is not None: | |
46 | 564 | # G0 X10 ; G1 X20 E5 results in 10..20 even as G0 is not extruding |
565 | xmin_e = min(xmin_e, line.current_x, xmin_e if lastx is None else lastx) | |
566 | xmax_e = max(xmax_e, line.current_x, xmax_e if lastx is None else lastx) | |
15 | 567 | if line.current_y is not None: |
46 | 568 | ymin_e = min(ymin_e, line.current_y, ymin_e if lasty is None else lasty) |
569 | ymax_e = max(ymax_e, line.current_y, ymax_e if lasty is None else lasty) | |
15 | 570 | if max_e <= 0: |
571 | if line.current_x is not None: | |
572 | xmin = min(xmin, line.current_x) | |
573 | xmax = max(xmax, line.current_x) | |
574 | if line.current_y is not None: | |
575 | ymin = min(ymin, line.current_y) | |
576 | ymax = max(ymax, line.current_y) | |
577 | ||
578 | # Compute duration | |
579 | if line.command == "G0" or line.command == "G1": | |
46 | 580 | x = line.x if line.x is not None else (lastx or 0) |
581 | y = line.y if line.y is not None else (lasty or 0) | |
582 | z = line.z if line.z is not None else (lastz or 0) | |
15 | 583 | e = line.e if line.e is not None else laste |
584 | # mm/s vs mm/m => divide by 60 | |
585 | f = line.f / 60.0 if line.f is not None else lastf | |
586 | ||
587 | # given last feedrate and current feedrate calculate the | |
588 | # distance needed to achieve current feedrate. | |
589 | # if travel is longer than req'd distance, then subtract | |
590 | # distance to achieve full speed, and add the time it took | |
591 | # to get there. | |
592 | # then calculate the time taken to complete the remaining | |
593 | # distance | |
594 | ||
595 | # FIXME: this code has been proven to be super wrong when 2 | |
596 | # subsquent moves are in opposite directions, as requested | |
597 | # speed is constant but printer has to fully decellerate | |
598 | # and reaccelerate | |
599 | # The following code tries to fix it by forcing a full | |
600 | # reacceleration if this move is in the opposite direction | |
601 | # of the previous one | |
46 | 602 | dx = x - (lastx or 0) |
603 | dy = y - (lasty or 0) | |
15 | 604 | if dx * lastdx + dy * lastdy <= 0: |
605 | lastf = 0 | |
606 | ||
607 | currenttravel = math.hypot(dx, dy) | |
608 | if currenttravel == 0: | |
609 | if line.z is not None: | |
46 | 610 | currenttravel = abs(line.z) if line.relative else abs(line.z - (lastz or 0)) |
15 | 611 | elif line.e is not None: |
612 | currenttravel = abs(line.e) if line.relative_e else abs(line.e - laste) | |
613 | # Feedrate hasn't changed, no acceleration/decceleration planned | |
614 | if f == lastf: | |
615 | moveduration = currenttravel / f if f != 0 else 0. | |
616 | else: | |
617 | # FIXME: review this better | |
618 | # this looks wrong : there's little chance that the feedrate we'll decelerate to is the previous feedrate | |
619 | # shouldn't we instead look at three consecutive moves ? | |
620 | distance = 2 * abs(((lastf + f) * (f - lastf) * 0.5) / acceleration) # multiply by 2 because we have to accelerate and decelerate | |
621 | if distance <= currenttravel and lastf + f != 0 and f != 0: | |
622 | moveduration = 2 * distance / (lastf + f) # This is distance / mean(lastf, f) | |
623 | moveduration += (currenttravel - distance) / f | |
624 | else: | |
625 | moveduration = 2 * currenttravel / (lastf + f) # This is currenttravel / mean(lastf, f) | |
626 | # FIXME: probably a little bit optimistic, but probably a much better estimate than the previous one: | |
627 | # moveduration = math.sqrt(2 * distance / acceleration) # probably buggy : not taking actual travel into account | |
628 | ||
629 | lastdx = dx | |
630 | lastdy = dy | |
631 | ||
632 | totalduration += moveduration | |
633 | ||
634 | lastx = x | |
635 | lasty = y | |
636 | lastz = z | |
637 | laste = e | |
638 | lastf = f | |
639 | elif line.command == "G4": | |
640 | moveduration = P(line) | |
641 | if moveduration: | |
642 | moveduration /= 1000.0 | |
643 | totalduration += moveduration | |
644 | ||
645 | # FIXME : looks like this needs to be tested with "lift Z on move" | |
646 | if line.z is not None: | |
647 | if line.command == "G92": | |
648 | cur_z = line.z | |
649 | elif line.is_move: | |
650 | if line.relative and cur_z is not None: | |
651 | cur_z += line.z | |
652 | else: | |
653 | cur_z = line.z | |
654 | ||
46 | 655 | if cur_z != prev_z and cur_layer_has_extrusion: |
656 | append_lines(cur_lines, False) | |
657 | all_zs.add(prev_z) | |
658 | cur_lines = [] | |
659 | cur_layer_has_extrusion = False | |
15 | 660 | |
661 | if build_layers: | |
662 | cur_lines.append(true_line) | |
663 | prev_z = cur_z | |
664 | # ## Loop done | |
665 | ||
666 | # Store current status | |
667 | self.imperial = imperial | |
668 | self.relative = relative | |
669 | self.relative_e = relative_e | |
670 | self.current_tool = current_tool | |
671 | self.current_x = current_x | |
672 | self.current_y = current_y | |
673 | self.current_z = current_z | |
674 | self.offset_x = offset_x | |
675 | self.offset_y = offset_y | |
676 | self.offset_z = offset_z | |
677 | self.current_e = current_e | |
678 | self.offset_e = offset_e | |
679 | self.max_e = max_e | |
680 | self.total_e = total_e | |
681 | self.current_e_multi[current_tool]=current_e_multi | |
682 | self.offset_e_multi[current_tool]=offset_e_multi | |
683 | self.max_e_multi[current_tool]=max_e_multi | |
684 | self.total_e_multi[current_tool]=total_e_multi | |
46 | 685 | self.cutting = cutting |
15 | 686 | |
687 | ||
688 | # Finalize layers | |
689 | if build_layers: | |
690 | if cur_lines: | |
46 | 691 | append_lines(cur_lines, True) |
692 | all_zs.add(prev_z) | |
15 | 693 | |
694 | self.append_layer_id = len(all_layers) | |
695 | self.append_layer = Layer([]) | |
696 | self.append_layer.duration = 0 | |
697 | all_layers.append(self.append_layer) | |
698 | self.layer_idxs = array('I', layer_idxs) | |
699 | self.line_idxs = array('I', line_idxs) | |
700 | ||
701 | # Compute bounding box | |
46 | 702 | all_zs = self.all_zs.union({zmin}).difference({None}) |
15 | 703 | zmin = min(all_zs) |
704 | zmax = max(all_zs) | |
705 | ||
706 | self.filament_length = self.max_e | |
707 | while len(self.filament_length_multi)<len(self.max_e_multi): | |
708 | self.filament_length_multi+=[0] | |
709 | for i in enumerate(self.max_e_multi): | |
710 | self.filament_length_multi[i[0]]=i[1] | |
711 | ||
712 | ||
713 | if self.filament_length > 0: | |
714 | self.xmin = xmin_e if not math.isinf(xmin_e) else 0 | |
715 | self.xmax = xmax_e if not math.isinf(xmax_e) else 0 | |
716 | self.ymin = ymin_e if not math.isinf(ymin_e) else 0 | |
717 | self.ymax = ymax_e if not math.isinf(ymax_e) else 0 | |
718 | else: | |
719 | self.xmin = xmin if not math.isinf(xmin) else 0 | |
720 | self.xmax = xmax if not math.isinf(xmax) else 0 | |
721 | self.ymin = ymin if not math.isinf(ymin) else 0 | |
722 | self.ymax = ymax if not math.isinf(ymax) else 0 | |
723 | self.zmin = zmin if not math.isinf(zmin) else 0 | |
724 | self.zmax = zmax if not math.isinf(zmax) else 0 | |
725 | self.width = self.xmax - self.xmin | |
726 | self.depth = self.ymax - self.ymin | |
727 | self.height = self.zmax - self.zmin | |
728 | ||
729 | # Finalize duration | |
730 | totaltime = datetime.timedelta(seconds = int(totalduration)) | |
731 | self.duration = totaltime | |
732 | ||
733 | def idxs(self, i): | |
734 | return self.layer_idxs[i], self.line_idxs[i] | |
735 | ||
736 | def estimate_duration(self): | |
737 | return self.layers_count, self.duration | |
738 | ||
739 | class LightGCode(GCode): | |
740 | line_class = LightLine | |
741 | ||
742 | def main(): | |
743 | if len(sys.argv) < 2: | |
46 | 744 | print("usage: %s filename.gcode" % sys.argv[0]) |
15 | 745 | return |
746 | ||
46 | 747 | print("Line object size:", sys.getsizeof(Line("G0 X0"))) |
748 | print("Light line object size:", sys.getsizeof(LightLine("G0 X0"))) | |
15 | 749 | gcode = GCode(open(sys.argv[1], "rU")) |
750 | ||
46 | 751 | print("Dimensions:") |
15 | 752 | xdims = (gcode.xmin, gcode.xmax, gcode.width) |
46 | 753 | print("\tX: %0.02f - %0.02f (%0.02f)" % xdims) |
15 | 754 | ydims = (gcode.ymin, gcode.ymax, gcode.depth) |
46 | 755 | print("\tY: %0.02f - %0.02f (%0.02f)" % ydims) |
15 | 756 | zdims = (gcode.zmin, gcode.zmax, gcode.height) |
46 | 757 | print("\tZ: %0.02f - %0.02f (%0.02f)" % zdims) |
758 | print("Filament used: %0.02fmm" % gcode.filament_length) | |
15 | 759 | for i in enumerate(gcode.filament_length_multi): |
46 | 760 | print("E%d %0.02fmm" % (i[0],i[1])) |
761 | print("Number of layers: %d" % gcode.layers_count) | |
762 | print("Estimated duration: %s" % gcode.estimate_duration()[1]) | |
15 | 763 | |
764 | if __name__ == '__main__': | |
765 | main() |