printrun-src/printrun/printcore.py

changeset 15
0bbb006204fc
child 46
cce0af6351f0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/printrun-src/printrun/printcore.py	Fri Jun 03 09:16:07 2016 +0200
@@ -0,0 +1,629 @@
+#!/usr/bin/env python
+
+# This file is part of the Printrun suite.
+#
+# Printrun is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Printrun is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# 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"
+
+from serialWrapper import Serial, SerialException, PARITY_ODD, PARITY_NONE
+from select import error as SelectError
+import threading
+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
+from collections import deque
+from printrun import gcoder
+from .utils import install_locale, decode_utf8
+install_locale('pronterface')
+
+def locked(f):
+    @wraps(f)
+    def inner(*args, **kw):
+        with inner.lock:
+            return f(*args, **kw)
+    inner.lock = threading.Lock()
+    return inner
+
+def control_ttyhup(port, disable_hup):
+    """Controls the HUPCL"""
+    if platform.system() == "Linux":
+        if disable_hup:
+            os.system("stty -F %s -hup" % port)
+        else:
+            os.system("stty -F %s hup" % port)
+
+def enable_hup(port):
+    control_ttyhup(port, False)
+
+def disable_hup(port):
+    control_ttyhup(port, True)
+
+class printcore():
+    def __init__(self, port = None, baud = None, dtr=None):
+        """Initializes a printcore instance. Pass the port and baud rate to
+           connect immediately"""
+        self.baud = None
+        self.dtr = None
+        self.port = None
+        self.analyzer = gcoder.GCode()
+        # Serial instance connected to the printer, should be None when
+        # disconnected
+        self.printer = None
+        # clear to send, enabled after responses
+        # FIXME: should probably be changed to a sliding window approach
+        self.clear = 0
+        # The printer has responded to the initial command and is active
+        self.online = False
+        # is a print currently running, true if printing, false if paused
+        self.printing = False
+        self.mainqueue = None
+        self.priqueue = Queue(0)
+        self.queueindex = 0
+        self.lineno = 0
+        self.resendfrom = -1
+        self.paused = False
+        self.sentlines = {}
+        self.log = deque(maxlen = 10000)
+        self.sent = []
+        self.writefailures = 0
+        self.tempcb = None  # impl (wholeline)
+        self.recvcb = None  # impl (wholeline)
+        self.sendcb = None  # impl (wholeline)
+        self.preprintsendcb = None  # impl (wholeline)
+        self.printsendcb = None  # impl (wholeline)
+        self.layerchangecb = None  # impl (wholeline)
+        self.errorcb = None  # impl (wholeline)
+        self.startcb = None  # impl ()
+        self.endcb = None  # impl ()
+        self.onlinecb = None  # impl ()
+        self.loud = False  # emit sent and received lines to terminal
+        self.tcp_streaming_mode = False
+        self.greetings = ['start', 'Grbl ']
+        self.wait = 0  # default wait period for send(), send_now()
+        self.read_thread = None
+        self.stop_read_thread = False
+        self.send_thread = None
+        self.stop_send_thread = False
+        self.print_thread = None
+        if port is not None and baud is not None:
+            self.connect(port, baud)
+        self.xy_feedrate = None
+        self.z_feedrate = None
+
+    def logError(self, error):
+        if self.errorcb:
+            try: self.errorcb(error)
+            except: logging.error(traceback.format_exc())
+        else:
+            logging.error(error)
+
+    @locked
+    def disconnect(self):
+        """Disconnects from printer and pauses the print
+        """
+        if self.printer:
+            if self.read_thread:
+                self.stop_read_thread = True
+                if threading.current_thread() != self.read_thread:
+                    self.read_thread.join()
+                self.read_thread = None
+            if self.print_thread:
+                self.printing = False
+                self.print_thread.join()
+            self._stop_sender()
+            try:
+                self.printer.close()
+            except socket.error:
+                pass
+            except OSError:
+                pass
+        self.printer = None
+        self.online = False
+        self.printing = False
+
+    @locked
+    def connect(self, port = None, baud = None, dtr=None):
+        """Set port and baudrate if given, then connect to printer
+        """
+        if self.printer:
+            self.disconnect()
+        if port is not None:
+            self.port = port
+        if baud is not None:
+            self.baud = baud
+        if dtr is not None:
+            self.dtr = dtr
+        if self.port is not None and self.baud is not None:
+            # 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 len(bits) == 2:
+                    hostname = bits[0]
+                    try:
+                        port = int(bits[1])
+                        if host_regexp.match(hostname) and 1 <= port <= 65535:
+                            is_serial = False
+                    except:
+                        pass
+            self.writefailures = 0
+            if not is_serial:
+                self.printer_tcp = socket.socket(socket.AF_INET,
+                                                 socket.SOCK_STREAM)
+                self.printer_tcp.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+                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()
+                except socket.error as e:
+                    if(e.strerror is None): e.strerror=""
+                    self.logError(_("Could not connect to %s:%s:") % (hostname, port) +
+                                  "\n" + _("Socket error %s:") % e.errno +
+                                  "\n" + e.strerror)
+                    self.printer = None
+                    self.printer_tcp = None
+                    return
+            else:
+                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
+                    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);
+                    except:
+                        #self.logError(_("Could not set DTR on this platform")) #not sure whether to output an error message
+                        pass
+                    self.printer.open()
+                except SerialException as e:
+                    self.logError(_("Could not connect to %s at baudrate %s:") % (self.port, self.baud) +
+                                  "\n" + _("Serial error: %s") % e)
+                    self.printer = None
+                    return
+                except IOError as e:
+                    self.logError(_("Could not connect to %s at baudrate %s:") % (self.port, self.baud) +
+                                  "\n" + _("IO error: %s") % e)
+                    self.printer = None
+                    return
+            self.stop_read_thread = False
+            self.read_thread = threading.Thread(target = self._listen)
+            self.read_thread.start()
+            self._start_sender()
+
+    def reset(self):
+        """Reset the printer
+        """
+        if self.printer and not self.printer_tcp:
+            self.printer.setDTR(1)
+            time.sleep(0.2)
+            self.printer.setDTR(0)
+
+    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 ""
+
+            if len(line) > 1:
+                self.log.append(line)
+                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 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)))
+                return None
+            else:
+                self.logError(_(u"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))))
+            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)))
+            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))
+            return None
+
+    def _listen_can_continue(self):
+        if self.printer_tcp:
+            return not self.stop_read_thread and self.printer
+        return (not self.stop_read_thread
+                and self.printer
+                and self.printer.isOpen())
+
+    def _listen_until_online(self):
+        while not self.online and self._listen_can_continue():
+            self._send("M105")
+            if self.writefailures >= 4:
+                logging.error(_("Aborting connection attempt after 4 failed writes."))
+                return
+            empty_lines = 0
+            while self._listen_can_continue():
+                line = self._readline()
+                if line is None: break  # connection problem
+                # workaround cases where M105 was sent before printer Serial
+                # was online an empty line means read timeout was reached,
+                # meaning no data was received thus we count those empty lines,
+                # and once we have seen 15 in a row, we just break and send a
+                # new M105
+                # 15 was chosen based on the fact that it gives enough time for
+                # Gen7 bootloader to time out, and that the non received M105
+                # issues should be quite rare so we can wait for a long time
+                # before resending
+                if not line:
+                    empty_lines += 1
+                    if empty_lines == 15: break
+                else: empty_lines = 0
+                if line.startswith(tuple(self.greetings)) \
+                   or line.startswith('ok') or "T:" in line:
+                    self.online = True
+                    if self.onlinecb:
+                        try: self.onlinecb()
+                        except: self.logError(traceback.format_exc())
+                    return
+
+    def _listen(self):
+        """This function acts on messages from the firmware
+        """
+        self.clear = True
+        if not self.printing:
+            self._listen_until_online()
+        while self._listen_can_continue():
+            line = self._readline()
+            if line is None:
+                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())
+            elif line.startswith('Error'):
+                self.logError(line)
+            # Teststrings for resend parsing       # Firmware     exp. result
+            # line="rs N2 Expected checksum 67"    # Teacup       2
+            if line.lower().startswith("resend") or line.startswith("rs"):
+                for haystack in ["N:", "N", ":"]:
+                    line = line.replace(haystack, " ")
+                linewords = line.split()
+                while len(linewords) != 0:
+                    try:
+                        toresend = int(linewords.pop(0))
+                        self.resendfrom = toresend
+                        break
+                    except:
+                        pass
+                self.clear = True
+        self.clear = True
+
+    def _start_sender(self):
+        self.stop_send_thread = False
+        self.send_thread = threading.Thread(target = self._sender)
+        self.send_thread.start()
+
+    def _stop_sender(self):
+        if self.send_thread:
+            self.stop_send_thread = True
+            self.send_thread.join()
+            self.send_thread = None
+
+    def _sender(self):
+        while not self.stop_send_thread:
+            try:
+                command = self.priqueue.get(True, 0.1)
+            except QueueEmpty:
+                continue
+            while self.printer and self.printing and not self.clear:
+                time.sleep(0.001)
+            self._send(command)
+            while self.printer and self.printing and not self.clear:
+                time.sleep(0.001)
+
+    def _checksum(self, command):
+        return reduce(lambda x, y: x ^ y, map(ord, command))
+
+    def startprint(self, gcode, startindex = 0):
+        """Start a print, gcode is an array of gcode commands.
+        returns True on success, False if already printing.
+        The print queue will be replaced with the contents of the data array,
+        the next line will be set to 0 and the firmware notified. Printing
+        will then start in a parallel thread.
+        """
+        if self.printing or not self.online or not self.printer:
+            return False
+        self.queueindex = startindex
+        self.mainqueue = gcode
+        self.printing = True
+        self.lineno = 0
+        self.resendfrom = -1
+        self._send("M110", -1, True)
+        if not gcode or not gcode.lines:
+            return True
+        self.clear = False
+        resuming = (startindex != 0)
+        self.print_thread = threading.Thread(target = self._print,
+                                             kwargs = {"resuming": resuming})
+        self.print_thread.start()
+        return True
+
+    def cancelprint(self):
+        self.pause()
+        self.paused = False
+        self.mainqueue = None
+        self.clear = True
+
+    # run a simple script if it exists, no multithreading
+    def runSmallScript(self, filename):
+        if filename is None: return
+        f = None
+        try:
+            with open(filename) as f:
+                for i in f:
+                    l = i.replace("\n", "")
+                    l = l[:l.find(";")]  # remove comments
+                    self.send_now(l)
+        except:
+            pass
+
+    def pause(self):
+        """Pauses the print, saving the current position.
+        """
+        if not self.printing: return False
+        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:
+                self.logError(traceback.format_exc())
+        except:
+            self.logError(traceback.format_exc())
+
+        self.print_thread = None
+
+        # saves the status
+        self.pauseX = self.analyzer.abs_x
+        self.pauseY = self.analyzer.abs_y
+        self.pauseZ = self.analyzer.abs_z
+        self.pauseE = self.analyzer.abs_e
+        self.pauseF = self.analyzer.current_f
+        self.pauseRelative = self.analyzer.relative
+
+    def resume(self):
+        """Resumes a paused print.
+        """
+        if not self.paused: return False
+        if self.paused:
+            # restores the status
+            self.send_now("G90")  # go to absolute coordinates
+
+            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,
+                                            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))
+
+        self.paused = False
+        self.printing = True
+        self.print_thread = threading.Thread(target = self._print,
+                                             kwargs = {"resuming": True})
+        self.print_thread.start()
+
+    def send(self, command, wait = 0):
+        """Adds a command to the checksummed main command queue if printing, or
+        sends the command immediately if not printing"""
+
+        if self.online:
+            if self.printing:
+                self.mainqueue.append(command)
+            else:
+                self.priqueue.put_nowait(command)
+        else:
+            self.logError(_("Not connected to printer."))
+
+    def send_now(self, command, wait = 0):
+        """Sends a command to the printer ahead of the command queue, without a
+        checksum"""
+        if self.online:
+            self.priqueue.put_nowait(command)
+        else:
+            self.logError(_("Not connected to printer."))
+
+    def _print(self, resuming = False):
+        self._stop_sender()
+        try:
+            if self.startcb:
+                # callback for printing started
+                try: self.startcb(resuming)
+                except:
+                    self.logError(_("Print start callback failed with:") +
+                                  "\n" + traceback.format_exc())
+            while self.printing and self.printer and self.online:
+                self._sendnext()
+            self.sentlines = {}
+            self.log.clear()
+            self.sent = []
+            if self.endcb:
+                # callback for printing done
+                try: self.endcb()
+                except:
+                    self.logError(_("Print end callback failed with:") +
+                                  "\n" + traceback.format_exc())
+        except:
+            self.logError(_("Print thread died due to the following error:") +
+                          "\n" + traceback.format_exc())
+        finally:
+            self.print_thread = None
+            self._start_sender()
+
+    def process_host_command(self, command):
+        """only ;@pause command is implemented as a host command in printcore, but hosts are free to reimplement this method"""
+        command = command.lstrip()
+        if command.startswith(";@pause"):
+            self.pause()
+
+    def _sendnext(self):
+        if not self.printer:
+            return
+        while self.printer and self.printing and not self.clear:
+            time.sleep(0.001)
+        # Only wait for oks when using serial connections or when not using tcp
+        # in streaming mode
+        if not self.printer_tcp or not self.tcp_streaming_mode:
+            self.clear = False
+        if not (self.printing and self.printer and self.online):
+            self.clear = True
+            return
+        if self.resendfrom < self.lineno and self.resendfrom > -1:
+            self._send(self.sentlines[self.resendfrom], self.resendfrom, False)
+            self.resendfrom += 1
+            return
+        self.resendfrom = -1
+        if not self.priqueue.empty():
+            self._send(self.priqueue.get_nowait())
+            self.priqueue.task_done()
+            return
+        if self.printing and self.queueindex < len(self.mainqueue):
+            (layer, line) = self.mainqueue.idxs(self.queueindex)
+            gline = self.mainqueue.all_layers[layer][line]
+            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())
+            if self.preprintsendcb:
+                if self.queueindex + 1 < len(self.mainqueue):
+                    (next_layer, next_line) = self.mainqueue.idxs(self.queueindex + 1)
+                    next_gline = self.mainqueue.all_layers[next_layer][next_line]
+                else:
+                    next_gline = None
+                gline = self.preprintsendcb(gline, next_gline)
+            if gline is None:
+                self.queueindex += 1
+                self.clear = True
+                return
+            tline = gline.raw
+            if tline.lstrip().startswith(";@"):  # check for host command
+                self.process_host_command(tline)
+                self.queueindex += 1
+                self.clear = True
+                return
+
+            # Strip comments
+            tline = gcoder.gcode_strip_comment_exp.sub("", tline).strip()
+            if tline:
+                self._send(tline, self.lineno, True)
+                self.lineno += 1
+                if self.printsendcb:
+                    try: self.printsendcb(gline)
+                    except: self.logError(traceback.format_exc())
+            else:
+                self.clear = True
+            self.queueindex += 1
+        else:
+            self.printing = False
+            self.clear = True
+            if not self.paused:
+                self.queueindex = 0
+                self.lineno = 0
+                self._send("M110", -1, True)
+
+    def _send(self, command, lineno = 0, calcchecksum = False):
+        # Only add checksums if over serial (tcp does the flow control itself)
+        if calcchecksum and not self.printer_tcp:
+            prefix = "N" + str(lineno) + " " + command
+            command = prefix + "*" + str(self._checksum(prefix))
+            if "M110" not in command:
+                self.sentlines[lineno] = command
+        if self.printer:
+            self.sent.append(command)
+            # run the command through the analyzer
+            gline = None
+            try:
+                gline = self.analyzer.append(command, store = False)
+            except:
+                logging.warning(_("Could not analyze command %s:") % command +
+                                "\n" + traceback.format_exc())
+            if self.loud:
+                logging.info("SENT: %s" % command)
+            if self.sendcb:
+                try: self.sendcb(command, gline)
+                except: self.logError(traceback.format_exc())
+            try:
+                self.printer.write(str(command + "\n"))
+                if self.printer_tcp:
+                    try:
+                        self.printer.flush()
+                    except socket.timeout:
+                        pass
+                self.writefailures = 0
+            except socket.error as e:
+                if e.errno is None:
+                    self.logError(_(u"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.writefailures += 1
+            except SerialException as e:
+                self.logError(_(u"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.writefailures += 1

mercurial