Wed, 20 Jan 2021 10:15:13 +0100
updated and added new files for printrun
15 | 1 | # This file is part of the Printrun suite. |
2 | # | |
3 | # Printrun is free software: you can redistribute it and/or modify | |
4 | # it under the terms of the GNU General Public License as published by | |
5 | # the Free Software Foundation, either version 3 of the License, or | |
6 | # (at your option) any later version. | |
7 | # | |
8 | # Printrun is distributed in the hope that it will be useful, | |
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | # GNU General Public License for more details. | |
12 | # | |
13 | # You should have received a copy of the GNU General Public License | |
14 | # along with Printrun. If not, see <http://www.gnu.org/licenses/>. | |
15 | ||
46 | 16 | __version__ = "2.0.0rc7" |
15 | 17 | |
46 | 18 | import sys |
19 | if sys.version_info.major < 3: | |
20 | print("You need to run this on Python 3") | |
21 | sys.exit(-1) | |
22 | ||
23 | from serial import Serial, SerialException, PARITY_ODD, PARITY_NONE | |
15 | 24 | from select import error as SelectError |
25 | import threading | |
46 | 26 | from queue import Queue, Empty as QueueEmpty |
15 | 27 | import time |
28 | import platform | |
29 | import os | |
30 | import logging | |
31 | import traceback | |
32 | import errno | |
33 | import socket | |
34 | import re | |
46 | 35 | import selectors |
36 | from functools import wraps, reduce | |
15 | 37 | from collections import deque |
38 | from printrun import gcoder | |
46 | 39 | from .utils import set_utf8_locale, install_locale, decode_utf8 |
40 | try: | |
41 | set_utf8_locale() | |
42 | except: | |
43 | pass | |
15 | 44 | install_locale('pronterface') |
46 | 45 | from printrun.plugins import PRINTCORE_HANDLER |
15 | 46 | |
47 | def locked(f): | |
48 | @wraps(f) | |
49 | def inner(*args, **kw): | |
50 | with inner.lock: | |
51 | return f(*args, **kw) | |
52 | inner.lock = threading.Lock() | |
53 | return inner | |
54 | ||
55 | def control_ttyhup(port, disable_hup): | |
56 | """Controls the HUPCL""" | |
57 | if platform.system() == "Linux": | |
58 | if disable_hup: | |
59 | os.system("stty -F %s -hup" % port) | |
60 | else: | |
61 | os.system("stty -F %s hup" % port) | |
62 | ||
63 | def enable_hup(port): | |
64 | control_ttyhup(port, False) | |
65 | ||
66 | def disable_hup(port): | |
67 | control_ttyhup(port, True) | |
68 | ||
46 | 69 | PR_EOF = None #printrun's marker for EOF |
70 | PR_AGAIN = b'' #printrun's marker for timeout/no data | |
71 | SYS_EOF = b'' #python's marker for EOF | |
72 | SYS_AGAIN = None #python's marker for timeout/no data | |
73 | ||
15 | 74 | class printcore(): |
75 | def __init__(self, port = None, baud = None, dtr=None): | |
76 | """Initializes a printcore instance. Pass the port and baud rate to | |
77 | connect immediately""" | |
78 | self.baud = None | |
79 | self.dtr = None | |
80 | self.port = None | |
81 | self.analyzer = gcoder.GCode() | |
82 | # Serial instance connected to the printer, should be None when | |
83 | # disconnected | |
84 | self.printer = None | |
85 | # clear to send, enabled after responses | |
86 | # FIXME: should probably be changed to a sliding window approach | |
87 | self.clear = 0 | |
88 | # The printer has responded to the initial command and is active | |
89 | self.online = False | |
90 | # is a print currently running, true if printing, false if paused | |
91 | self.printing = False | |
92 | self.mainqueue = None | |
93 | self.priqueue = Queue(0) | |
94 | self.queueindex = 0 | |
95 | self.lineno = 0 | |
96 | self.resendfrom = -1 | |
97 | self.paused = False | |
98 | self.sentlines = {} | |
99 | self.log = deque(maxlen = 10000) | |
100 | self.sent = [] | |
101 | self.writefailures = 0 | |
102 | self.tempcb = None # impl (wholeline) | |
103 | self.recvcb = None # impl (wholeline) | |
104 | self.sendcb = None # impl (wholeline) | |
105 | self.preprintsendcb = None # impl (wholeline) | |
106 | self.printsendcb = None # impl (wholeline) | |
107 | self.layerchangecb = None # impl (wholeline) | |
108 | self.errorcb = None # impl (wholeline) | |
109 | self.startcb = None # impl () | |
110 | self.endcb = None # impl () | |
111 | self.onlinecb = None # impl () | |
112 | self.loud = False # emit sent and received lines to terminal | |
113 | self.tcp_streaming_mode = False | |
114 | self.greetings = ['start', 'Grbl '] | |
115 | self.wait = 0 # default wait period for send(), send_now() | |
116 | self.read_thread = None | |
117 | self.stop_read_thread = False | |
118 | self.send_thread = None | |
119 | self.stop_send_thread = False | |
120 | self.print_thread = None | |
46 | 121 | self.readline_buf = [] |
122 | self.selector = None | |
123 | self.event_handler = PRINTCORE_HANDLER | |
124 | # Not all platforms need to do this parity workaround, and some drivers | |
125 | # don't support it. Limit it to platforms that actually require it | |
126 | # here to avoid doing redundant work elsewhere and potentially breaking | |
127 | # things. | |
128 | self.needs_parity_workaround = platform.system() == "linux" and os.path.exists("/etc/debian") | |
129 | for handler in self.event_handler: | |
130 | try: handler.on_init() | |
131 | except: logging.error(traceback.format_exc()) | |
15 | 132 | if port is not None and baud is not None: |
133 | self.connect(port, baud) | |
134 | self.xy_feedrate = None | |
135 | self.z_feedrate = None | |
136 | ||
46 | 137 | def addEventHandler(self, handler): |
138 | ''' | |
139 | Adds an event handler. | |
140 | ||
141 | @param handler: The handler to be added. | |
142 | ''' | |
143 | self.event_handler.append(handler) | |
144 | ||
15 | 145 | def logError(self, error): |
46 | 146 | for handler in self.event_handler: |
147 | try: handler.on_error(error) | |
148 | except: logging.error(traceback.format_exc()) | |
15 | 149 | if self.errorcb: |
150 | try: self.errorcb(error) | |
151 | except: logging.error(traceback.format_exc()) | |
152 | else: | |
153 | logging.error(error) | |
154 | ||
155 | @locked | |
156 | def disconnect(self): | |
157 | """Disconnects from printer and pauses the print | |
158 | """ | |
159 | if self.printer: | |
160 | if self.read_thread: | |
161 | self.stop_read_thread = True | |
162 | if threading.current_thread() != self.read_thread: | |
163 | self.read_thread.join() | |
164 | self.read_thread = None | |
165 | if self.print_thread: | |
166 | self.printing = False | |
167 | self.print_thread.join() | |
168 | self._stop_sender() | |
169 | try: | |
46 | 170 | if self.selector is not None: |
171 | self.selector.unregister(self.printer_tcp) | |
172 | self.selector.close() | |
173 | self.selector = None | |
174 | if self.printer_tcp is not None: | |
175 | self.printer_tcp.close() | |
176 | self.printer_tcp = None | |
15 | 177 | self.printer.close() |
178 | except socket.error: | |
46 | 179 | logger.error(traceback.format_exc()) |
15 | 180 | pass |
181 | except OSError: | |
46 | 182 | logger.error(traceback.format_exc()) |
15 | 183 | pass |
46 | 184 | for handler in self.event_handler: |
185 | try: handler.on_disconnect() | |
186 | except: logging.error(traceback.format_exc()) | |
15 | 187 | self.printer = None |
188 | self.online = False | |
189 | self.printing = False | |
190 | ||
191 | @locked | |
192 | def connect(self, port = None, baud = None, dtr=None): | |
193 | """Set port and baudrate if given, then connect to printer | |
194 | """ | |
195 | if self.printer: | |
196 | self.disconnect() | |
197 | if port is not None: | |
198 | self.port = port | |
199 | if baud is not None: | |
200 | self.baud = baud | |
201 | if dtr is not None: | |
202 | self.dtr = dtr | |
203 | if self.port is not None and self.baud is not None: | |
204 | # Connect to socket if "port" is an IP, device if not | |
205 | host_regexp = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$") | |
206 | is_serial = True | |
46 | 207 | if ":" in self.port: |
208 | bits = self.port.split(":") | |
15 | 209 | if len(bits) == 2: |
210 | hostname = bits[0] | |
211 | try: | |
46 | 212 | port_number = int(bits[1]) |
213 | if host_regexp.match(hostname) and 1 <= port_number <= 65535: | |
15 | 214 | is_serial = False |
215 | except: | |
216 | pass | |
217 | self.writefailures = 0 | |
218 | if not is_serial: | |
219 | self.printer_tcp = socket.socket(socket.AF_INET, | |
220 | socket.SOCK_STREAM) | |
221 | self.printer_tcp.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) | |
222 | self.timeout = 0.25 | |
223 | self.printer_tcp.settimeout(1.0) | |
224 | try: | |
46 | 225 | self.printer_tcp.connect((hostname, port_number)) |
226 | #a single read timeout raises OSError for all later reads | |
227 | #probably since python 3.5 | |
228 | #use non blocking instead | |
229 | self.printer_tcp.settimeout(0) | |
230 | self.printer = self.printer_tcp.makefile('rwb', buffering=0) | |
231 | self.selector = selectors.DefaultSelector() | |
232 | self.selector.register(self.printer_tcp, selectors.EVENT_READ) | |
15 | 233 | except socket.error as e: |
234 | if(e.strerror is None): e.strerror="" | |
46 | 235 | self.logError(_("Could not connect to %s:%s:") % (hostname, port_number) + |
15 | 236 | "\n" + _("Socket error %s:") % e.errno + |
237 | "\n" + e.strerror) | |
238 | self.printer = None | |
239 | self.printer_tcp = None | |
240 | return | |
241 | else: | |
242 | disable_hup(self.port) | |
243 | self.printer_tcp = None | |
244 | try: | |
46 | 245 | if self.needs_parity_workaround: |
246 | self.printer = Serial(port = self.port, | |
247 | baudrate = self.baud, | |
248 | timeout = 0.25, | |
249 | parity = PARITY_ODD) | |
250 | self.printer.close() | |
251 | self.printer.parity = PARITY_NONE | |
252 | else: | |
253 | self.printer = Serial(baudrate = self.baud, | |
254 | timeout = 0.25, | |
255 | parity = PARITY_NONE) | |
256 | self.printer.port = self.port | |
15 | 257 | try: #this appears not to work on many platforms, so we're going to call it but not care if it fails |
46 | 258 | self.printer.dtr = dtr |
15 | 259 | except: |
260 | #self.logError(_("Could not set DTR on this platform")) #not sure whether to output an error message | |
261 | pass | |
262 | self.printer.open() | |
263 | except SerialException as e: | |
264 | self.logError(_("Could not connect to %s at baudrate %s:") % (self.port, self.baud) + | |
265 | "\n" + _("Serial error: %s") % e) | |
266 | self.printer = None | |
267 | return | |
268 | except IOError as e: | |
269 | self.logError(_("Could not connect to %s at baudrate %s:") % (self.port, self.baud) + | |
270 | "\n" + _("IO error: %s") % e) | |
271 | self.printer = None | |
272 | return | |
46 | 273 | for handler in self.event_handler: |
274 | try: handler.on_connect() | |
275 | except: logging.error(traceback.format_exc()) | |
15 | 276 | self.stop_read_thread = False |
46 | 277 | self.read_thread = threading.Thread(target = self._listen, |
278 | name='read thread') | |
15 | 279 | self.read_thread.start() |
280 | self._start_sender() | |
281 | ||
282 | def reset(self): | |
283 | """Reset the printer | |
284 | """ | |
285 | if self.printer and not self.printer_tcp: | |
46 | 286 | self.printer.dtr = 1 |
15 | 287 | time.sleep(0.2) |
46 | 288 | self.printer.dtr = 0 |
289 | ||
290 | def _readline_buf(self): | |
291 | "Try to readline from buffer" | |
292 | if len(self.readline_buf): | |
293 | chunk = self.readline_buf[-1] | |
294 | eol = chunk.find(b'\n') | |
295 | if eol >= 0: | |
296 | line = b''.join(self.readline_buf[:-1]) + chunk[:(eol+1)] | |
297 | self.readline_buf = [] | |
298 | if eol + 1 < len(chunk): | |
299 | self.readline_buf.append(chunk[(eol+1):]) | |
300 | return line | |
301 | return PR_AGAIN | |
302 | ||
303 | def _readline_nb(self): | |
304 | "Non blocking readline. Socket based files do not support non blocking or timeouting readline" | |
305 | if self.printer_tcp: | |
306 | line = self._readline_buf() | |
307 | if line: | |
308 | return line | |
309 | chunk_size = 256 | |
310 | while True: | |
311 | chunk = self.printer.read(chunk_size) | |
312 | if chunk is SYS_AGAIN and self.selector.select(self.timeout): | |
313 | chunk = self.printer.read(chunk_size) | |
314 | #print('_readline_nb chunk', chunk, type(chunk)) | |
315 | if chunk: | |
316 | self.readline_buf.append(chunk) | |
317 | line = self._readline_buf() | |
318 | if line: | |
319 | return line | |
320 | elif chunk is SYS_AGAIN: | |
321 | return PR_AGAIN | |
322 | else: | |
323 | #chunk == b'' means EOF | |
324 | line = b''.join(self.readline_buf) | |
325 | self.readline_buf = [] | |
326 | self.stop_read_thread = True | |
327 | return line if line else PR_EOF | |
328 | else: # serial port | |
329 | return self.printer.readline() | |
15 | 330 | |
331 | def _readline(self): | |
332 | try: | |
46 | 333 | line_bytes = self._readline_nb() |
334 | if line_bytes is PR_EOF: | |
335 | self.logError(_("Can't read from printer (disconnected?). line_bytes is None")) | |
336 | return PR_EOF | |
337 | line = line_bytes.decode('utf-8') | |
15 | 338 | |
339 | if len(line) > 1: | |
340 | self.log.append(line) | |
46 | 341 | for handler in self.event_handler: |
342 | try: handler.on_recv(line) | |
343 | except: logging.error(traceback.format_exc()) | |
15 | 344 | if self.recvcb: |
345 | try: self.recvcb(line) | |
346 | except: self.logError(traceback.format_exc()) | |
347 | if self.loud: logging.info("RECV: %s" % line.rstrip()) | |
348 | return line | |
46 | 349 | except UnicodeDecodeError: |
350 | self.logError(_("Got rubbish reply from %s at baudrate %s:") % (self.port, self.baud) + | |
351 | "\n" + _("Maybe a bad baudrate?")) | |
352 | return None | |
15 | 353 | except SelectError as e: |
354 | if 'Bad file descriptor' in e.args[1]: | |
46 | 355 | self.logError(_("Can't read from printer (disconnected?) (SelectError {0}): {1}").format(e.errno, decode_utf8(e.strerror))) |
15 | 356 | return None |
357 | else: | |
46 | 358 | self.logError(_("SelectError ({0}): {1}").format(e.errno, decode_utf8(e.strerror))) |
15 | 359 | raise |
360 | except SerialException as e: | |
46 | 361 | self.logError(_("Can't read from printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e)))) |
15 | 362 | return None |
363 | except socket.error as e: | |
46 | 364 | self.logError(_("Can't read from printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror))) |
15 | 365 | return None |
366 | except OSError as e: | |
367 | if e.errno == errno.EAGAIN: # Not a real error, no data was available | |
368 | return "" | |
46 | 369 | self.logError(_("Can't read from printer (disconnected?) (OS Error {0}): {1}").format(e.errno, e.strerror)) |
15 | 370 | return None |
371 | ||
372 | def _listen_can_continue(self): | |
373 | if self.printer_tcp: | |
374 | return not self.stop_read_thread and self.printer | |
375 | return (not self.stop_read_thread | |
376 | and self.printer | |
46 | 377 | and self.printer.is_open) |
15 | 378 | |
379 | def _listen_until_online(self): | |
380 | while not self.online and self._listen_can_continue(): | |
381 | self._send("M105") | |
382 | if self.writefailures >= 4: | |
383 | logging.error(_("Aborting connection attempt after 4 failed writes.")) | |
384 | return | |
385 | empty_lines = 0 | |
386 | while self._listen_can_continue(): | |
387 | line = self._readline() | |
388 | if line is None: break # connection problem | |
389 | # workaround cases where M105 was sent before printer Serial | |
390 | # was online an empty line means read timeout was reached, | |
391 | # meaning no data was received thus we count those empty lines, | |
392 | # and once we have seen 15 in a row, we just break and send a | |
393 | # new M105 | |
394 | # 15 was chosen based on the fact that it gives enough time for | |
395 | # Gen7 bootloader to time out, and that the non received M105 | |
396 | # issues should be quite rare so we can wait for a long time | |
397 | # before resending | |
398 | if not line: | |
399 | empty_lines += 1 | |
400 | if empty_lines == 15: break | |
401 | else: empty_lines = 0 | |
402 | if line.startswith(tuple(self.greetings)) \ | |
403 | or line.startswith('ok') or "T:" in line: | |
404 | self.online = True | |
46 | 405 | for handler in self.event_handler: |
406 | try: handler.on_online() | |
407 | except: logging.error(traceback.format_exc()) | |
15 | 408 | if self.onlinecb: |
409 | try: self.onlinecb() | |
410 | except: self.logError(traceback.format_exc()) | |
411 | return | |
412 | ||
413 | def _listen(self): | |
414 | """This function acts on messages from the firmware | |
415 | """ | |
416 | self.clear = True | |
417 | if not self.printing: | |
418 | self._listen_until_online() | |
419 | while self._listen_can_continue(): | |
420 | line = self._readline() | |
421 | if line is None: | |
46 | 422 | logging.debug('_readline() is None, exiting _listen()') |
15 | 423 | break |
424 | if line.startswith('DEBUG_'): | |
425 | continue | |
426 | if line.startswith(tuple(self.greetings)) or line.startswith('ok'): | |
427 | self.clear = True | |
46 | 428 | if line.startswith('ok') and "T:" in line: |
429 | for handler in self.event_handler: | |
430 | try: handler.on_temp(line) | |
431 | except: logging.error(traceback.format_exc()) | |
432 | if self.tempcb: | |
433 | # callback for temp, status, whatever | |
434 | try: self.tempcb(line) | |
435 | except: self.logError(traceback.format_exc()) | |
15 | 436 | elif line.startswith('Error'): |
437 | self.logError(line) | |
438 | # Teststrings for resend parsing # Firmware exp. result | |
439 | # line="rs N2 Expected checksum 67" # Teacup 2 | |
440 | if line.lower().startswith("resend") or line.startswith("rs"): | |
441 | for haystack in ["N:", "N", ":"]: | |
442 | line = line.replace(haystack, " ") | |
443 | linewords = line.split() | |
444 | while len(linewords) != 0: | |
445 | try: | |
446 | toresend = int(linewords.pop(0)) | |
447 | self.resendfrom = toresend | |
448 | break | |
449 | except: | |
450 | pass | |
451 | self.clear = True | |
452 | self.clear = True | |
46 | 453 | logging.debug('Exiting read thread') |
15 | 454 | |
455 | def _start_sender(self): | |
456 | self.stop_send_thread = False | |
46 | 457 | self.send_thread = threading.Thread(target = self._sender, |
458 | name = 'send thread') | |
15 | 459 | self.send_thread.start() |
460 | ||
461 | def _stop_sender(self): | |
462 | if self.send_thread: | |
463 | self.stop_send_thread = True | |
464 | self.send_thread.join() | |
465 | self.send_thread = None | |
466 | ||
467 | def _sender(self): | |
468 | while not self.stop_send_thread: | |
469 | try: | |
470 | command = self.priqueue.get(True, 0.1) | |
471 | except QueueEmpty: | |
472 | continue | |
473 | while self.printer and self.printing and not self.clear: | |
474 | time.sleep(0.001) | |
475 | self._send(command) | |
476 | while self.printer and self.printing and not self.clear: | |
477 | time.sleep(0.001) | |
478 | ||
479 | def _checksum(self, command): | |
480 | return reduce(lambda x, y: x ^ y, map(ord, command)) | |
481 | ||
482 | def startprint(self, gcode, startindex = 0): | |
483 | """Start a print, gcode is an array of gcode commands. | |
484 | returns True on success, False if already printing. | |
485 | The print queue will be replaced with the contents of the data array, | |
486 | the next line will be set to 0 and the firmware notified. Printing | |
487 | will then start in a parallel thread. | |
488 | """ | |
489 | if self.printing or not self.online or not self.printer: | |
490 | return False | |
491 | self.queueindex = startindex | |
492 | self.mainqueue = gcode | |
493 | self.printing = True | |
494 | self.lineno = 0 | |
495 | self.resendfrom = -1 | |
496 | self._send("M110", -1, True) | |
497 | if not gcode or not gcode.lines: | |
498 | return True | |
499 | self.clear = False | |
500 | resuming = (startindex != 0) | |
501 | self.print_thread = threading.Thread(target = self._print, | |
46 | 502 | name = 'print thread', |
15 | 503 | kwargs = {"resuming": resuming}) |
504 | self.print_thread.start() | |
505 | return True | |
506 | ||
507 | def cancelprint(self): | |
508 | self.pause() | |
509 | self.paused = False | |
510 | self.mainqueue = None | |
511 | self.clear = True | |
512 | ||
513 | # run a simple script if it exists, no multithreading | |
514 | def runSmallScript(self, filename): | |
515 | if filename is None: return | |
516 | f = None | |
517 | try: | |
518 | with open(filename) as f: | |
519 | for i in f: | |
520 | l = i.replace("\n", "") | |
521 | l = l[:l.find(";")] # remove comments | |
522 | self.send_now(l) | |
523 | except: | |
524 | pass | |
525 | ||
526 | def pause(self): | |
527 | """Pauses the print, saving the current position. | |
528 | """ | |
529 | if not self.printing: return False | |
530 | self.paused = True | |
531 | self.printing = False | |
532 | ||
46 | 533 | # ';@pause' in the gcode file calls pause from the print thread |
534 | if not threading.current_thread() is self.print_thread: | |
535 | try: | |
536 | self.print_thread.join() | |
537 | except: | |
15 | 538 | self.logError(traceback.format_exc()) |
539 | ||
540 | self.print_thread = None | |
541 | ||
542 | # saves the status | |
543 | self.pauseX = self.analyzer.abs_x | |
544 | self.pauseY = self.analyzer.abs_y | |
545 | self.pauseZ = self.analyzer.abs_z | |
546 | self.pauseE = self.analyzer.abs_e | |
547 | self.pauseF = self.analyzer.current_f | |
548 | self.pauseRelative = self.analyzer.relative | |
46 | 549 | self.pauseRelativeE = self.analyzer.relative_e |
15 | 550 | |
551 | def resume(self): | |
46 | 552 | """Resumes a paused print.""" |
15 | 553 | if not self.paused: return False |
46 | 554 | # restores the status |
555 | self.send_now("G90") # go to absolute coordinates | |
556 | ||
557 | xyFeed = '' if self.xy_feedrate is None else ' F' + str(self.xy_feedrate) | |
558 | zFeed = '' if self.z_feedrate is None else ' F' + str(self.z_feedrate) | |
15 | 559 | |
46 | 560 | self.send_now("G1 X%s Y%s%s" % (self.pauseX, self.pauseY, xyFeed)) |
561 | self.send_now("G1 Z" + str(self.pauseZ) + zFeed) | |
562 | self.send_now("G92 E" + str(self.pauseE)) | |
15 | 563 | |
46 | 564 | # go back to relative if needed |
565 | if self.pauseRelative: | |
566 | self.send_now("G91") | |
567 | if self.pauseRelativeE: | |
568 | self.send_now('M83') | |
569 | # reset old feed rate | |
570 | self.send_now("G1 F" + str(self.pauseF)) | |
15 | 571 | |
572 | self.paused = False | |
573 | self.printing = True | |
574 | self.print_thread = threading.Thread(target = self._print, | |
46 | 575 | name = 'print thread', |
15 | 576 | kwargs = {"resuming": True}) |
577 | self.print_thread.start() | |
578 | ||
579 | def send(self, command, wait = 0): | |
580 | """Adds a command to the checksummed main command queue if printing, or | |
581 | sends the command immediately if not printing""" | |
582 | ||
583 | if self.online: | |
584 | if self.printing: | |
585 | self.mainqueue.append(command) | |
586 | else: | |
587 | self.priqueue.put_nowait(command) | |
588 | else: | |
589 | self.logError(_("Not connected to printer.")) | |
590 | ||
591 | def send_now(self, command, wait = 0): | |
592 | """Sends a command to the printer ahead of the command queue, without a | |
593 | checksum""" | |
594 | if self.online: | |
595 | self.priqueue.put_nowait(command) | |
596 | else: | |
597 | self.logError(_("Not connected to printer.")) | |
598 | ||
599 | def _print(self, resuming = False): | |
600 | self._stop_sender() | |
601 | try: | |
46 | 602 | for handler in self.event_handler: |
603 | try: handler.on_start(resuming) | |
604 | except: logging.error(traceback.format_exc()) | |
15 | 605 | if self.startcb: |
606 | # callback for printing started | |
607 | try: self.startcb(resuming) | |
608 | except: | |
609 | self.logError(_("Print start callback failed with:") + | |
610 | "\n" + traceback.format_exc()) | |
611 | while self.printing and self.printer and self.online: | |
612 | self._sendnext() | |
613 | self.sentlines = {} | |
614 | self.log.clear() | |
615 | self.sent = [] | |
46 | 616 | for handler in self.event_handler: |
617 | try: handler.on_end() | |
618 | except: logging.error(traceback.format_exc()) | |
15 | 619 | if self.endcb: |
620 | # callback for printing done | |
621 | try: self.endcb() | |
622 | except: | |
623 | self.logError(_("Print end callback failed with:") + | |
624 | "\n" + traceback.format_exc()) | |
625 | except: | |
626 | self.logError(_("Print thread died due to the following error:") + | |
627 | "\n" + traceback.format_exc()) | |
628 | finally: | |
629 | self.print_thread = None | |
630 | self._start_sender() | |
631 | ||
632 | def process_host_command(self, command): | |
633 | """only ;@pause command is implemented as a host command in printcore, but hosts are free to reimplement this method""" | |
634 | command = command.lstrip() | |
635 | if command.startswith(";@pause"): | |
636 | self.pause() | |
637 | ||
638 | def _sendnext(self): | |
639 | if not self.printer: | |
640 | return | |
641 | while self.printer and self.printing and not self.clear: | |
642 | time.sleep(0.001) | |
643 | # Only wait for oks when using serial connections or when not using tcp | |
644 | # in streaming mode | |
645 | if not self.printer_tcp or not self.tcp_streaming_mode: | |
646 | self.clear = False | |
647 | if not (self.printing and self.printer and self.online): | |
648 | self.clear = True | |
649 | return | |
650 | if self.resendfrom < self.lineno and self.resendfrom > -1: | |
651 | self._send(self.sentlines[self.resendfrom], self.resendfrom, False) | |
652 | self.resendfrom += 1 | |
653 | return | |
654 | self.resendfrom = -1 | |
655 | if not self.priqueue.empty(): | |
656 | self._send(self.priqueue.get_nowait()) | |
657 | self.priqueue.task_done() | |
658 | return | |
46 | 659 | if self.printing and self.mainqueue.has_index(self.queueindex): |
15 | 660 | (layer, line) = self.mainqueue.idxs(self.queueindex) |
661 | gline = self.mainqueue.all_layers[layer][line] | |
46 | 662 | if self.queueindex > 0: |
663 | (prev_layer, prev_line) = self.mainqueue.idxs(self.queueindex - 1) | |
664 | if prev_layer != layer: | |
665 | for handler in self.event_handler: | |
666 | try: handler.on_layerchange(layer) | |
667 | except: logging.error(traceback.format_exc()) | |
15 | 668 | if self.layerchangecb and self.queueindex > 0: |
669 | (prev_layer, prev_line) = self.mainqueue.idxs(self.queueindex - 1) | |
670 | if prev_layer != layer: | |
671 | try: self.layerchangecb(layer) | |
672 | except: self.logError(traceback.format_exc()) | |
46 | 673 | for handler in self.event_handler: |
674 | try: handler.on_preprintsend(gline, self.queueindex, self.mainqueue) | |
675 | except: logging.error(traceback.format_exc()) | |
15 | 676 | if self.preprintsendcb: |
46 | 677 | if self.mainqueue.has_index(self.queueindex + 1): |
15 | 678 | (next_layer, next_line) = self.mainqueue.idxs(self.queueindex + 1) |
679 | next_gline = self.mainqueue.all_layers[next_layer][next_line] | |
680 | else: | |
681 | next_gline = None | |
682 | gline = self.preprintsendcb(gline, next_gline) | |
683 | if gline is None: | |
684 | self.queueindex += 1 | |
685 | self.clear = True | |
686 | return | |
687 | tline = gline.raw | |
688 | if tline.lstrip().startswith(";@"): # check for host command | |
689 | self.process_host_command(tline) | |
690 | self.queueindex += 1 | |
691 | self.clear = True | |
692 | return | |
693 | ||
694 | # Strip comments | |
695 | tline = gcoder.gcode_strip_comment_exp.sub("", tline).strip() | |
696 | if tline: | |
697 | self._send(tline, self.lineno, True) | |
698 | self.lineno += 1 | |
46 | 699 | for handler in self.event_handler: |
700 | try: handler.on_printsend(gline) | |
701 | except: logging.error(traceback.format_exc()) | |
15 | 702 | if self.printsendcb: |
703 | try: self.printsendcb(gline) | |
704 | except: self.logError(traceback.format_exc()) | |
705 | else: | |
706 | self.clear = True | |
707 | self.queueindex += 1 | |
708 | else: | |
709 | self.printing = False | |
710 | self.clear = True | |
711 | if not self.paused: | |
712 | self.queueindex = 0 | |
713 | self.lineno = 0 | |
714 | self._send("M110", -1, True) | |
715 | ||
716 | def _send(self, command, lineno = 0, calcchecksum = False): | |
717 | # Only add checksums if over serial (tcp does the flow control itself) | |
718 | if calcchecksum and not self.printer_tcp: | |
719 | prefix = "N" + str(lineno) + " " + command | |
720 | command = prefix + "*" + str(self._checksum(prefix)) | |
721 | if "M110" not in command: | |
722 | self.sentlines[lineno] = command | |
723 | if self.printer: | |
724 | self.sent.append(command) | |
725 | # run the command through the analyzer | |
726 | gline = None | |
727 | try: | |
728 | gline = self.analyzer.append(command, store = False) | |
729 | except: | |
730 | logging.warning(_("Could not analyze command %s:") % command + | |
731 | "\n" + traceback.format_exc()) | |
732 | if self.loud: | |
733 | logging.info("SENT: %s" % command) | |
46 | 734 | |
735 | for handler in self.event_handler: | |
736 | try: handler.on_send(command, gline) | |
737 | except: logging.error(traceback.format_exc()) | |
15 | 738 | if self.sendcb: |
739 | try: self.sendcb(command, gline) | |
740 | except: self.logError(traceback.format_exc()) | |
741 | try: | |
46 | 742 | self.printer.write((command + "\n").encode('ascii')) |
15 | 743 | if self.printer_tcp: |
744 | try: | |
745 | self.printer.flush() | |
746 | except socket.timeout: | |
747 | pass | |
748 | self.writefailures = 0 | |
749 | except socket.error as e: | |
750 | if e.errno is None: | |
46 | 751 | self.logError(_("Can't write to printer (disconnected ?):") + |
15 | 752 | "\n" + traceback.format_exc()) |
753 | else: | |
46 | 754 | self.logError(_("Can't write to printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror))) |
15 | 755 | self.writefailures += 1 |
756 | except SerialException as e: | |
46 | 757 | self.logError(_("Can't write to printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e)))) |
15 | 758 | self.writefailures += 1 |
759 | except RuntimeError as e: | |
46 | 760 | self.logError(_("Socket connection broken, disconnected. ({0}): {1}").format(e.errno, decode_utf8(e.strerror))) |
15 | 761 | self.writefailures += 1 |