--- a/printrun-src/printrun/printcore.py Tue Jan 19 20:45:09 2021 +0100 +++ b/printrun-src/printrun/printcore.py Wed Jan 20 10:15:13 2021 +0100 @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # This file is part of the Printrun suite. # # Printrun is free software: you can redistribute it and/or modify @@ -15,29 +13,36 @@ # You should have received a copy of the GNU General Public License # along with Printrun. If not, see <http://www.gnu.org/licenses/>. -__version__ = "2015.03.10" +__version__ = "2.0.0rc7" -from serialWrapper import Serial, SerialException, PARITY_ODD, PARITY_NONE +import sys +if sys.version_info.major < 3: + print("You need to run this on Python 3") + sys.exit(-1) + +from serial import Serial, SerialException, PARITY_ODD, PARITY_NONE from select import error as SelectError import threading -from Queue import Queue, Empty as QueueEmpty +from queue import Queue, Empty as QueueEmpty import time import platform import os -import sys -stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr -reload(sys).setdefaultencoding('utf8') -sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr import logging import traceback import errno import socket import re -from functools import wraps +import selectors +from functools import wraps, reduce from collections import deque from printrun import gcoder -from .utils import install_locale, decode_utf8 +from .utils import set_utf8_locale, install_locale, decode_utf8 +try: + set_utf8_locale() +except: + pass install_locale('pronterface') +from printrun.plugins import PRINTCORE_HANDLER def locked(f): @wraps(f) @@ -61,6 +66,11 @@ def disable_hup(port): control_ttyhup(port, True) +PR_EOF = None #printrun's marker for EOF +PR_AGAIN = b'' #printrun's marker for timeout/no data +SYS_EOF = b'' #python's marker for EOF +SYS_AGAIN = None #python's marker for timeout/no data + class printcore(): def __init__(self, port = None, baud = None, dtr=None): """Initializes a printcore instance. Pass the port and baud rate to @@ -108,12 +118,34 @@ self.send_thread = None self.stop_send_thread = False self.print_thread = None + self.readline_buf = [] + self.selector = None + self.event_handler = PRINTCORE_HANDLER + # Not all platforms need to do this parity workaround, and some drivers + # don't support it. Limit it to platforms that actually require it + # here to avoid doing redundant work elsewhere and potentially breaking + # things. + self.needs_parity_workaround = platform.system() == "linux" and os.path.exists("/etc/debian") + for handler in self.event_handler: + try: handler.on_init() + except: logging.error(traceback.format_exc()) if port is not None and baud is not None: self.connect(port, baud) self.xy_feedrate = None self.z_feedrate = None + def addEventHandler(self, handler): + ''' + Adds an event handler. + + @param handler: The handler to be added. + ''' + self.event_handler.append(handler) + def logError(self, error): + for handler in self.event_handler: + try: handler.on_error(error) + except: logging.error(traceback.format_exc()) if self.errorcb: try: self.errorcb(error) except: logging.error(traceback.format_exc()) @@ -135,11 +167,23 @@ self.print_thread.join() self._stop_sender() try: + if self.selector is not None: + self.selector.unregister(self.printer_tcp) + self.selector.close() + self.selector = None + if self.printer_tcp is not None: + self.printer_tcp.close() + self.printer_tcp = None self.printer.close() except socket.error: + logger.error(traceback.format_exc()) pass except OSError: + logger.error(traceback.format_exc()) pass + for handler in self.event_handler: + try: handler.on_disconnect() + except: logging.error(traceback.format_exc()) self.printer = None self.online = False self.printing = False @@ -160,13 +204,13 @@ # Connect to socket if "port" is an IP, device if not 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])$") is_serial = True - if ":" in port: - bits = port.split(":") + if ":" in self.port: + bits = self.port.split(":") if len(bits) == 2: hostname = bits[0] try: - port = int(bits[1]) - if host_regexp.match(hostname) and 1 <= port <= 65535: + port_number = int(bits[1]) + if host_regexp.match(hostname) and 1 <= port_number <= 65535: is_serial = False except: pass @@ -178,12 +222,17 @@ self.timeout = 0.25 self.printer_tcp.settimeout(1.0) try: - self.printer_tcp.connect((hostname, port)) - self.printer_tcp.settimeout(self.timeout) - self.printer = self.printer_tcp.makefile() + self.printer_tcp.connect((hostname, port_number)) + #a single read timeout raises OSError for all later reads + #probably since python 3.5 + #use non blocking instead + self.printer_tcp.settimeout(0) + self.printer = self.printer_tcp.makefile('rwb', buffering=0) + self.selector = selectors.DefaultSelector() + self.selector.register(self.printer_tcp, selectors.EVENT_READ) except socket.error as e: if(e.strerror is None): e.strerror="" - self.logError(_("Could not connect to %s:%s:") % (hostname, port) + + self.logError(_("Could not connect to %s:%s:") % (hostname, port_number) + "\n" + _("Socket error %s:") % e.errno + "\n" + e.strerror) self.printer = None @@ -193,14 +242,20 @@ disable_hup(self.port) self.printer_tcp = None try: - self.printer = Serial(port = self.port, - baudrate = self.baud, - timeout = 0.25, - parity = PARITY_ODD) - self.printer.close() - self.printer.parity = PARITY_NONE + if self.needs_parity_workaround: + self.printer = Serial(port = self.port, + baudrate = self.baud, + timeout = 0.25, + parity = PARITY_ODD) + self.printer.close() + self.printer.parity = PARITY_NONE + else: + self.printer = Serial(baudrate = self.baud, + timeout = 0.25, + parity = PARITY_NONE) + self.printer.port = self.port try: #this appears not to work on many platforms, so we're going to call it but not care if it fails - self.printer.setDTR(dtr); + self.printer.dtr = dtr except: #self.logError(_("Could not set DTR on this platform")) #not sure whether to output an error message pass @@ -215,8 +270,12 @@ "\n" + _("IO error: %s") % e) self.printer = None return + for handler in self.event_handler: + try: handler.on_connect() + except: logging.error(traceback.format_exc()) self.stop_read_thread = False - self.read_thread = threading.Thread(target = self._listen) + self.read_thread = threading.Thread(target = self._listen, + name='read thread') self.read_thread.start() self._start_sender() @@ -224,43 +283,90 @@ """Reset the printer """ if self.printer and not self.printer_tcp: - self.printer.setDTR(1) + self.printer.dtr = 1 time.sleep(0.2) - self.printer.setDTR(0) + self.printer.dtr = 0 + + def _readline_buf(self): + "Try to readline from buffer" + if len(self.readline_buf): + chunk = self.readline_buf[-1] + eol = chunk.find(b'\n') + if eol >= 0: + line = b''.join(self.readline_buf[:-1]) + chunk[:(eol+1)] + self.readline_buf = [] + if eol + 1 < len(chunk): + self.readline_buf.append(chunk[(eol+1):]) + return line + return PR_AGAIN + + def _readline_nb(self): + "Non blocking readline. Socket based files do not support non blocking or timeouting readline" + if self.printer_tcp: + line = self._readline_buf() + if line: + return line + chunk_size = 256 + while True: + chunk = self.printer.read(chunk_size) + if chunk is SYS_AGAIN and self.selector.select(self.timeout): + chunk = self.printer.read(chunk_size) + #print('_readline_nb chunk', chunk, type(chunk)) + if chunk: + self.readline_buf.append(chunk) + line = self._readline_buf() + if line: + return line + elif chunk is SYS_AGAIN: + return PR_AGAIN + else: + #chunk == b'' means EOF + line = b''.join(self.readline_buf) + self.readline_buf = [] + self.stop_read_thread = True + return line if line else PR_EOF + else: # serial port + return self.printer.readline() def _readline(self): try: - try: - line = self.printer.readline() - if self.printer_tcp and not line: - raise OSError(-1, "Read EOF from socket") - except socket.timeout: - return "" + line_bytes = self._readline_nb() + if line_bytes is PR_EOF: + self.logError(_("Can't read from printer (disconnected?). line_bytes is None")) + return PR_EOF + line = line_bytes.decode('utf-8') if len(line) > 1: self.log.append(line) + for handler in self.event_handler: + try: handler.on_recv(line) + except: logging.error(traceback.format_exc()) if self.recvcb: try: self.recvcb(line) except: self.logError(traceback.format_exc()) if self.loud: logging.info("RECV: %s" % line.rstrip()) return line + except UnicodeDecodeError: + self.logError(_("Got rubbish reply from %s at baudrate %s:") % (self.port, self.baud) + + "\n" + _("Maybe a bad baudrate?")) + return None except SelectError as e: if 'Bad file descriptor' in e.args[1]: - self.logError(_(u"Can't read from printer (disconnected?) (SelectError {0}): {1}").format(e.errno, decode_utf8(e.strerror))) + self.logError(_("Can't read from printer (disconnected?) (SelectError {0}): {1}").format(e.errno, decode_utf8(e.strerror))) return None else: - self.logError(_(u"SelectError ({0}): {1}").format(e.errno, decode_utf8(e.strerror))) + self.logError(_("SelectError ({0}): {1}").format(e.errno, decode_utf8(e.strerror))) raise except SerialException as e: - self.logError(_(u"Can't read from printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e)))) + self.logError(_("Can't read from printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e)))) return None except socket.error as e: - self.logError(_(u"Can't read from printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror))) + self.logError(_("Can't read from printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror))) return None except OSError as e: if e.errno == errno.EAGAIN: # Not a real error, no data was available return "" - self.logError(_(u"Can't read from printer (disconnected?) (OS Error {0}): {1}").format(e.errno, e.strerror)) + self.logError(_("Can't read from printer (disconnected?) (OS Error {0}): {1}").format(e.errno, e.strerror)) return None def _listen_can_continue(self): @@ -268,7 +374,7 @@ return not self.stop_read_thread and self.printer return (not self.stop_read_thread and self.printer - and self.printer.isOpen()) + and self.printer.is_open) def _listen_until_online(self): while not self.online and self._listen_can_continue(): @@ -296,6 +402,9 @@ if line.startswith(tuple(self.greetings)) \ or line.startswith('ok') or "T:" in line: self.online = True + for handler in self.event_handler: + try: handler.on_online() + except: logging.error(traceback.format_exc()) if self.onlinecb: try: self.onlinecb() except: self.logError(traceback.format_exc()) @@ -310,15 +419,20 @@ while self._listen_can_continue(): line = self._readline() if line is None: + logging.debug('_readline() is None, exiting _listen()') break if line.startswith('DEBUG_'): continue if line.startswith(tuple(self.greetings)) or line.startswith('ok'): self.clear = True - if line.startswith('ok') and "T:" in line and self.tempcb: - # callback for temp, status, whatever - try: self.tempcb(line) - except: self.logError(traceback.format_exc()) + if line.startswith('ok') and "T:" in line: + for handler in self.event_handler: + try: handler.on_temp(line) + except: logging.error(traceback.format_exc()) + if self.tempcb: + # callback for temp, status, whatever + try: self.tempcb(line) + except: self.logError(traceback.format_exc()) elif line.startswith('Error'): self.logError(line) # Teststrings for resend parsing # Firmware exp. result @@ -336,10 +450,12 @@ pass self.clear = True self.clear = True + logging.debug('Exiting read thread') def _start_sender(self): self.stop_send_thread = False - self.send_thread = threading.Thread(target = self._sender) + self.send_thread = threading.Thread(target = self._sender, + name = 'send thread') self.send_thread.start() def _stop_sender(self): @@ -383,6 +499,7 @@ self.clear = False resuming = (startindex != 0) self.print_thread = threading.Thread(target = self._print, + name = 'print thread', kwargs = {"resuming": resuming}) self.print_thread.start() return True @@ -413,17 +530,12 @@ self.paused = True self.printing = False - # try joining the print thread: enclose it in try/except because we - # might be calling it from the thread itself - try: - self.print_thread.join() - except RuntimeError, e: - if e.message == "cannot join current thread": - pass - else: + # ';@pause' in the gcode file calls pause from the print thread + if not threading.current_thread() is self.print_thread: + try: + self.print_thread.join() + except: self.logError(traceback.format_exc()) - except: - self.logError(traceback.format_exc()) self.print_thread = None @@ -434,35 +546,33 @@ self.pauseE = self.analyzer.abs_e self.pauseF = self.analyzer.current_f self.pauseRelative = self.analyzer.relative + self.pauseRelativeE = self.analyzer.relative_e def resume(self): - """Resumes a paused print. - """ + """Resumes a paused print.""" if not self.paused: return False - if self.paused: - # restores the status - self.send_now("G90") # go to absolute coordinates + # restores the status + self.send_now("G90") # go to absolute coordinates + + xyFeed = '' if self.xy_feedrate is None else ' F' + str(self.xy_feedrate) + zFeed = '' if self.z_feedrate is None else ' F' + str(self.z_feedrate) - xyFeedString = "" - zFeedString = "" - if self.xy_feedrate is not None: - xyFeedString = " F" + str(self.xy_feedrate) - if self.z_feedrate is not None: - zFeedString = " F" + str(self.z_feedrate) + self.send_now("G1 X%s Y%s%s" % (self.pauseX, self.pauseY, xyFeed)) + self.send_now("G1 Z" + str(self.pauseZ) + zFeed) + self.send_now("G92 E" + str(self.pauseE)) - self.send_now("G1 X%s Y%s%s" % (self.pauseX, self.pauseY, - xyFeedString)) - self.send_now("G1 Z" + str(self.pauseZ) + zFeedString) - self.send_now("G92 E" + str(self.pauseE)) - - # go back to relative if needed - if self.pauseRelative: self.send_now("G91") - # reset old feed rate - self.send_now("G1 F" + str(self.pauseF)) + # go back to relative if needed + if self.pauseRelative: + self.send_now("G91") + if self.pauseRelativeE: + self.send_now('M83') + # reset old feed rate + self.send_now("G1 F" + str(self.pauseF)) self.paused = False self.printing = True self.print_thread = threading.Thread(target = self._print, + name = 'print thread', kwargs = {"resuming": True}) self.print_thread.start() @@ -489,6 +599,9 @@ def _print(self, resuming = False): self._stop_sender() try: + for handler in self.event_handler: + try: handler.on_start(resuming) + except: logging.error(traceback.format_exc()) if self.startcb: # callback for printing started try: self.startcb(resuming) @@ -500,6 +613,9 @@ self.sentlines = {} self.log.clear() self.sent = [] + for handler in self.event_handler: + try: handler.on_end() + except: logging.error(traceback.format_exc()) if self.endcb: # callback for printing done try: self.endcb() @@ -540,16 +656,25 @@ self._send(self.priqueue.get_nowait()) self.priqueue.task_done() return - if self.printing and self.queueindex < len(self.mainqueue): + if self.printing and self.mainqueue.has_index(self.queueindex): (layer, line) = self.mainqueue.idxs(self.queueindex) gline = self.mainqueue.all_layers[layer][line] + if self.queueindex > 0: + (prev_layer, prev_line) = self.mainqueue.idxs(self.queueindex - 1) + if prev_layer != layer: + for handler in self.event_handler: + try: handler.on_layerchange(layer) + except: logging.error(traceback.format_exc()) if self.layerchangecb and self.queueindex > 0: (prev_layer, prev_line) = self.mainqueue.idxs(self.queueindex - 1) if prev_layer != layer: try: self.layerchangecb(layer) except: self.logError(traceback.format_exc()) + for handler in self.event_handler: + try: handler.on_preprintsend(gline, self.queueindex, self.mainqueue) + except: logging.error(traceback.format_exc()) if self.preprintsendcb: - if self.queueindex + 1 < len(self.mainqueue): + if self.mainqueue.has_index(self.queueindex + 1): (next_layer, next_line) = self.mainqueue.idxs(self.queueindex + 1) next_gline = self.mainqueue.all_layers[next_layer][next_line] else: @@ -571,6 +696,9 @@ if tline: self._send(tline, self.lineno, True) self.lineno += 1 + for handler in self.event_handler: + try: handler.on_printsend(gline) + except: logging.error(traceback.format_exc()) if self.printsendcb: try: self.printsendcb(gline) except: self.logError(traceback.format_exc()) @@ -603,11 +731,15 @@ "\n" + traceback.format_exc()) if self.loud: logging.info("SENT: %s" % command) + + for handler in self.event_handler: + try: handler.on_send(command, gline) + except: logging.error(traceback.format_exc()) if self.sendcb: try: self.sendcb(command, gline) except: self.logError(traceback.format_exc()) try: - self.printer.write(str(command + "\n")) + self.printer.write((command + "\n").encode('ascii')) if self.printer_tcp: try: self.printer.flush() @@ -616,14 +748,14 @@ self.writefailures = 0 except socket.error as e: if e.errno is None: - self.logError(_(u"Can't write to printer (disconnected ?):") + + self.logError(_("Can't write to printer (disconnected ?):") + "\n" + traceback.format_exc()) else: - self.logError(_(u"Can't write to printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror))) + self.logError(_("Can't write to printer (disconnected?) (Socket error {0}): {1}").format(e.errno, decode_utf8(e.strerror))) self.writefailures += 1 except SerialException as e: - self.logError(_(u"Can't write to printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e)))) + self.logError(_("Can't write to printer (disconnected?) (SerialException): {0}").format(decode_utf8(str(e)))) self.writefailures += 1 except RuntimeError as e: - self.logError(_(u"Socket connection broken, disconnected. ({0}): {1}").format(e.errno, decode_utf8(e.strerror))) + self.logError(_("Socket connection broken, disconnected. ({0}): {1}").format(e.errno, decode_utf8(e.strerror))) self.writefailures += 1