chiark / gitweb /
Speed up getting operational by sending M105 as soon as Marlin is ready, not after...
[cura.git] / Cura / util / machineCom.py
index 9cefa3ed491ea7d02aecb2e64be69d8473563fee..0282fd7c7d67c8567bcf54c249826e9aab27f564 100644 (file)
@@ -1,4 +1,8 @@
-from __future__ import absolute_import
+"""
+MachineCom handles communication with GCode based printers trough (USB) serial ports.
+For actual printing of objects this module is used from Cura.serialCommunication and ran in a separate process.
+"""
+__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
 
 import os
 import glob
@@ -25,6 +29,11 @@ except:
        pass
 
 def serialList(forAutoDetect=False):
+       """
+               Retrieve a list of serial ports found in the system.
+       :param forAutoDetect: if true then only the USB serial ports are listed. Else all ports are listed.
+       :return: A list of strings where each string is a serial port.
+       """
        baselist=[]
        if platform.system() == "Windows":
                try:
@@ -38,36 +47,36 @@ def serialList(forAutoDetect=False):
                except:
                        pass
        if forAutoDetect:
-               baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*")
+               baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.usb*")
                baselist = filter(lambda s: not 'Bluetooth' in s, baselist)
+               prev = profile.getMachineSetting('serial_port_auto')
+               if prev in baselist:
+                       baselist.remove(prev)
+                       baselist.insert(0, prev)
        else:
-               baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*") + glob.glob("/dev/rfcomm*")
-       prev = profile.getPreference('serial_port_auto')
-       if prev in baselist:
-               baselist.remove(prev)
-               baselist.insert(0, prev)
+               baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*") + glob.glob('/dev/serial/by-id/*')
        if version.isDevVersion() and not forAutoDetect:
                baselist.append('VIRTUAL')
        return baselist
 
-def machineIsConnected():
-       port = profile.getPreference('serial_port')
-       if port == 'AUTO':
-               return len(serialList(True)) > 0
-       if platform.system() == "Windows":
-               return port in serialList(True)
-       return os.path.isfile(port)
-
 def baudrateList():
+       """
+       :return: a list of integers containing all possible baudrates at which we can communicate.
+                       Used for auto-baudrate detection as well as manual baudrate selection.
+       """
        ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
-       if profile.getPreference('serial_baud_auto') != '':
-               prev = int(profile.getPreference('serial_baud_auto'))
+       if profile.getMachineSetting('serial_baud_auto') != '':
+               prev = int(profile.getMachineSetting('serial_baud_auto'))
                if prev in ret:
                        ret.remove(prev)
                        ret.insert(0, prev)
        return ret
 
 class VirtualPrinter():
+       """
+       A virtual printer class used for debugging. Acts as a serial.Serial class, but without connecting to any port.
+       Only available when running the development version of Cura.
+       """
        def __init__(self):
                self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
                self.temp = 0.0
@@ -120,6 +129,10 @@ class VirtualPrinter():
                self.readList = None
 
 class MachineComPrintCallback(object):
+       """
+       Base class for callbacks from the MachineCom class.
+       This class has all empty implementations and is attached to the MachineCom if no other callback object is attached.
+       """
        def mcLog(self, message):
                pass
        
@@ -139,6 +152,10 @@ class MachineComPrintCallback(object):
                pass
 
 class MachineCom(object):
+       """
+       Class for (USB) serial communication with 3D printers.
+       This class keeps track of if the connection is still live, can auto-detect serial ports and baudrates.
+       """
        STATE_NONE = 0
        STATE_OPEN_SERIAL = 1
        STATE_DETECT_SERIAL = 2
@@ -153,12 +170,12 @@ class MachineCom(object):
        
        def __init__(self, port = None, baudrate = None, callbackObject = None):
                if port is None:
-                       port = profile.getPreference('serial_port')
+                       port = profile.getMachineSetting('serial_port')
                if baudrate is None:
-                       if profile.getPreference('serial_baud') == 'AUTO':
+                       if profile.getMachineSetting('serial_baud') == 'AUTO':
                                baudrate = 0
                        else:
-                               baudrate = int(profile.getPreference('serial_baud'))
+                               baudrate = int(profile.getMachineSetting('serial_baud'))
                if callbackObject is None:
                        callbackObject = MachineComPrintCallback()
 
@@ -167,11 +184,14 @@ class MachineCom(object):
                self._callback = callbackObject
                self._state = self.STATE_NONE
                self._serial = None
+               self._serialDetectList = []
                self._baudrateDetectList = baudrateList()
                self._baudrateDetectRetry = 0
-               self._temp = 0
+               self._extruderCount = int(profile.getMachineSetting('extruder_amount'))
+               self._temperatureRequestExtruder = 0
+               self._temp = [0] * self._extruderCount
+               self._targetTemp = [0] * self._extruderCount
                self._bedTemp = 0
-               self._targetTemp = 0
                self._bedTargetTemp = 0
                self._gcodeList = None
                self._gcodePos = 0
@@ -224,13 +244,16 @@ class MachineCom(object):
                return "?%d?" % (self._state)
        
        def getShortErrorString(self):
-               if len(self._errorValue) < 20:
+               if len(self._errorValue) < 35:
                        return self._errorValue
-               return self._errorValue[:20] + "..."
+               return self._errorValue[:35] + "..."
 
        def getErrorString(self):
                return self._errorValue
-       
+
+       def isClosed(self):
+               return self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
+
        def isClosedOrError(self):
                return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
 
@@ -253,7 +276,7 @@ class MachineCom(object):
                return time.time() - self._printStartTime
 
        def getPrintTimeRemainingEstimate(self):
-               if self._printStartTime100 == None or self.getPrintPos() < 200:
+               if self._printStartTime100 is None or self.getPrintPos() < 200:
                        return None
                printTime = (time.time() - self._printStartTime100) / 60
                printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
@@ -279,13 +302,12 @@ class MachineCom(object):
                if self._port == 'AUTO':
                        self._changeState(self.STATE_DETECT_SERIAL)
                        programmer = stk500v2.Stk500v2()
-                       self._log("Serial port list: %s" % (str(serialList(True))))
                        for p in serialList(True):
                                try:
-                                       self._log("Connecting to: %s" % (p))
+                                       self._log("Connecting to: %s (programmer)" % (p))
                                        programmer.connect(p)
                                        self._serial = programmer.leaveISP()
-                                       profile.putPreference('serial_port_auto', p)
+                                       profile.putMachineSetting('serial_port_auto', p)
                                        break
                                except ispBase.IspError as (e):
                                        self._log("Error while connecting to %s: %s" % (p, str(e)))
@@ -293,29 +315,44 @@ class MachineCom(object):
                                except:
                                        self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
                                programmer.close()
+                       if self._serial is None:
+                               self._log("Serial port list: %s" % (str(serialList(True))))
+                               self._serialDetectList = serialList(True)
                elif self._port == 'VIRTUAL':
                        self._changeState(self.STATE_OPEN_SERIAL)
                        self._serial = VirtualPrinter()
                else:
                        self._changeState(self.STATE_OPEN_SERIAL)
                        try:
-                               self._log("Connecting to: %s" % (self._port))
                                if self._baudrate == 0:
-                                       self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
+                                       self._log("Connecting to: %s with baudrate: 115200 (fallback)" % (self._port))
+                                       self._serial = serial.Serial(str(self._port), 115200, timeout=3, writeTimeout=10000)
                                else:
-                                       self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
+                                       self._log("Connecting to: %s with baudrate: %s (configured)" % (self._port, self._baudrate))
+                                       self._serial = serial.Serial(str(self._port), self._baudrate, timeout=5, writeTimeout=10000)
                        except:
                                self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
-               if self._serial == None:
-                       self._log("Failed to open serial port (%s)" % (self._port))
-                       self._errorValue = 'Failed to autodetect serial port.'
-                       self._changeState(self.STATE_ERROR)
-                       return
-               self._log("Connected to: %s, starting monitor" % (self._serial))
-               if self._baudrate == 0:
-                       self._changeState(self.STATE_DETECT_BAUDRATE)
+               if self._serial is None:
+                       baudrate = self._baudrate
+                       if baudrate == 0:
+                               baudrate = self._baudrateDetectList.pop(0)
+                       if len(self._serialDetectList) < 1:
+                               self._log("Found no ports to try for auto detection")
+                               self._errorValue = 'Failed to autodetect serial port.'
+                               self._changeState(self.STATE_ERROR)
+                               return
+                       port = self._serialDetectList.pop(0)
+                       self._log("Connecting to: %s with baudrate: %s (auto)" % (port, baudrate))
+                       try:
+                               self._serial = serial.Serial(port, baudrate, timeout=3, writeTimeout=10000)
+                       except:
+                               pass
                else:
-                       self._changeState(self.STATE_CONNECTING)
+                       self._log("Connected to: %s, starting monitor" % (self._serial))
+                       if self._baudrate == 0:
+                               self._changeState(self.STATE_DETECT_BAUDRATE)
+                       else:
+                               self._changeState(self.STATE_CONNECTING)
 
                #Start monitoring the serial port.
                if self._state == self.STATE_CONNECTING:
@@ -325,10 +362,11 @@ class MachineCom(object):
                tempRequestTimeout = timeout
                while True:
                        line = self._readline()
-                       if line == None:
+                       if line is None:
                                break
-                       
-                       #No matter the state, if we see an error, goto the error state and store the error for reference.
+
+                       #No matter the state, if we see an fatal error, goto the error state and store the error for reference.
+                       # Only goto error on known fatal errors.
                        if line.startswith('Error:'):
                                #Oh YEAH, consistency.
                                # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
@@ -337,25 +375,33 @@ class MachineCom(object):
                                if re.match('Error:[0-9]\n', line):
                                        line = line.rstrip() + self._readline()
                                #Skip the communication errors, as those get corrected.
-                               if 'checksum mismatch' in line or 'Line Number is not Last Line Number' in line or 'No Line Number with checksum' in line or 'No Checksum with line number' in line:
-                                       pass
-                               elif not self.isError():
-                                       self._errorValue = line[6:]
-                                       self._changeState(self.STATE_ERROR)
+                               if 'Extruder switched off' in line or 'Temperature heated bed switched off' in line or 'Something is wrong, please turn off the printer.' in line:
+                                       if not self.isError():
+                                               self._errorValue = line[6:]
+                                               self._changeState(self.STATE_ERROR)
                        if ' T:' in line or line.startswith('T:'):
-                               self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
-                               if ' B:' in line:
-                                       self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
+                               try:
+                                       self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
+                               except:
+                                       pass
+                               if 'B:' in line:
+                                       try:
+                                               self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
+                                       except:
+                                               pass
                                self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
                                #If we are waiting for an M109 or M190 then measure the time we lost during heatup, so we can remove that time from our printing time estimate.
                                if not 'ok' in line and self._heatupWaitStartTime != 0:
                                        t = time.time()
                                        self._heatupWaitTimeLost = t - self._heatupWaitStartTime
                                        self._heatupWaitStartTime = t
-                       elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
+                       elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and \
+                                not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and \
+                                not line.startswith('Error:No Checksum with line number') and not line.startswith('Error:No Line Number with checksum') and \
+                                line != 'echo:Unknown command:""\n' and self.isOperational():
                                self._callback.mcMessage(line)
 
-                       if self._state == self.STATE_DETECT_BAUDRATE:
+                       if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
                                if line == '' or time.time() > timeout:
                                        if len(self._baudrateDetectList) < 1:
                                                self.close()
@@ -368,9 +414,22 @@ class MachineCom(object):
                                                self._sendCommand("M105")
                                                self._testingBaudrate = True
                                        else:
-                                               baudrate = self._baudrateDetectList.pop(0)
+                                               if self._state == self.STATE_DETECT_SERIAL:
+                                                       if len(self._serialDetectList) == 0:
+                                                               if len(self._baudrateDetectList) == 0:
+                                                                       self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
+                                                                       self._errorValue = 'Failed to autodetect serial port.'
+                                                                       self._changeState(self.STATE_ERROR)
+                                                                       return
+                                                               else:
+                                                                       self._serialDetectList = serialList(True)
+                                                                       baudrate = self._baudrateDetectList.pop(0)
+                                                       self._serial.close()
+                                                       self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=2.5, writeTimeout=10000)
+                                               else:
+                                                       baudrate = self._baudrateDetectList.pop(0)
                                                try:
-                                                       self._serial.baudrate = baudrate
+                                                       self._setBaudrate(baudrate)
                                                        self._serial.timeout = 0.5
                                                        self._log("Trying baudrate: %d" % (baudrate))
                                                        self._baudrateDetectRetry = 5
@@ -381,7 +440,7 @@ class MachineCom(object):
                                                        self._testingBaudrate = True
                                                except:
                                                        self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
-                               elif 'ok' in line and 'T:' in line:
+                               elif 'T:' in line:
                                        self._baudrateDetectTestOk += 1
                                        if self._baudrateDetectTestOk < 10:
                                                self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
@@ -389,12 +448,12 @@ class MachineCom(object):
                                        else:
                                                self._sendCommand("M999")
                                                self._serial.timeout = 2
-                                               profile.putPreference('serial_baud_auto', self._serial.baudrate)
+                                               profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
                                                self._changeState(self.STATE_OPERATIONAL)
                                else:
                                        self._testingBaudrate = False
                        elif self._state == self.STATE_CONNECTING:
-                               if line == '':
+                               if line == '' or 'wait' in line or 'start' in line:        # 'wait' needed for Repetier (kind of watchdog)
                                        self._sendCommand("M105")
                                elif 'ok' in line:
                                        self._changeState(self.STATE_OPERATIONAL)
@@ -403,16 +462,24 @@ class MachineCom(object):
                        elif self._state == self.STATE_OPERATIONAL:
                                #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
                                if line == '':
-                                       self._sendCommand("M105")
+                                       if self._extruderCount > 0:
+                                               self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
+                                               self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
+                                       else:
+                                               self.sendCommand("M105")
                                        tempRequestTimeout = time.time() + 5
                        elif self._state == self.STATE_PRINTING:
+                               #Even when printing request the temperature every 5 seconds.
+                               if time.time() > tempRequestTimeout:
+                                       if self._extruderCount > 0:
+                                               self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
+                                               self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
+                                       else:
+                                               self.sendCommand("M105")
+                                       tempRequestTimeout = time.time() + 5
                                if line == '' and time.time() > timeout:
                                        self._log("Communication timeout during printing, forcing a line")
                                        line = 'ok'
-                               #Even when printing request the temperture every 5 seconds.
-                               if time.time() > tempRequestTimeout:
-                                       self._commandQueue.put("M105")
-                                       tempRequestTimeout = time.time() + 5
                                if 'ok' in line:
                                        timeout = time.time() + 5
                                        if not self._commandQueue.empty():
@@ -420,13 +487,34 @@ class MachineCom(object):
                                        else:
                                                self._sendNext()
                                elif "resend" in line.lower() or "rs" in line:
+                                       newPos = self._gcodePos
                                        try:
-                                               self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
+                                               newPos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
                                        except:
                                                if "rs" in line:
-                                                       self._gcodePos = int(line.split()[1])
+                                                       newPos = int(line.split()[1])
+                                       # If we need to resend more than 10 lines, we can assume that the machine
+                                       # was shut down and turned back on or something else that's weird just happened.
+                                       # In that case, it can be dangerous to restart the print, so we'd better kill it
+                                       if newPos == 1 or self._gcodePos > newPos + 100:
+                                               self._callback.mcMessage("Print canceled due to loss of communication to printer (USB unplugged or power lost)")
+                                               self.cancelPrint()
+                                       else:
+                                               self._gcodePos = newPos
+                       elif self._state == self.STATE_PAUSED:
+                               if 'ok' in line:
+                                       timeout = time.time() + 5
+                                       if not self._commandQueue.empty():
+                                               self._sendCommand(self._commandQueue.get())
+
                self._log("Connection closed, closing down monitor")
-       
+
+       def _setBaudrate(self, baudrate):
+               try:
+                       self._serial.baudrate = baudrate
+               except:
+                       print getExceptionString()
+
        def _log(self, message):
                self._callback.mcLog(message)
                try:
@@ -440,7 +528,7 @@ class MachineCom(object):
                                pass
 
        def _readline(self):
-               if self._serial == None:
+               if self._serial is None:
                        return None
                try:
                        ret = self._serial.readline()
@@ -474,7 +562,10 @@ class MachineCom(object):
                        self._heatupWaitStartTime = time.time()
                if 'M104' in cmd or 'M109' in cmd:
                        try:
-                               self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
+                               t = 0
+                               if 'T' in cmd:
+                                       t = int(re.search('T([0-9]+)', cmd).group(1))
+                               self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
                        except:
                                pass
                if 'M140' in cmd or 'M190' in cmd:
@@ -488,6 +579,7 @@ class MachineCom(object):
                except serial.SerialTimeoutException:
                        self._log("Serial timeout while writing to serial port, trying again.")
                        try:
+                               time.sleep(0.5)
                                self._serial.write(cmd + '\n')
                        except:
                                self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
@@ -497,7 +589,7 @@ class MachineCom(object):
                        self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
                        self._errorValue = getExceptionString()
                        self.close(True)
-       
+
        def _sendNext(self):
                if self._gcodePos >= len(self._gcodeList):
                        self._changeState(self.STATE_OPERATIONAL)
@@ -515,7 +607,7 @@ class MachineCom(object):
                        if self._printSection in self._feedRateModifier:
                                line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
                        if ('G0' in line or 'G1' in line) and 'Z' in line:
-                               z = float(re.search('Z([0-9\.]*)', line).group(1))
+                               z = float(re.search('Z(-?[0-9\.]*)', line).group(1))
                                if self._currentZ != z:
                                        self._currentZ = z
                                        self._callback.mcZChange(z)
@@ -525,14 +617,14 @@ class MachineCom(object):
                self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
                self._gcodePos += 1
                self._callback.mcProgress(self._gcodePos)
-       
+
        def sendCommand(self, cmd):
                cmd = cmd.encode('ascii', 'replace')
                if self.isPrinting():
                        self._commandQueue.put(cmd)
                elif self.isOperational():
                        self._sendCommand(cmd)
-       
+
        def printGCode(self, gcodeList):
                if not self.isOperational() or self.isPrinting():
                        return
@@ -542,7 +634,7 @@ class MachineCom(object):
                self._printSection = 'CUSTOM'
                self._changeState(self.STATE_PRINTING)
                self._printStartTime = time.time()
-               for i in xrange(0, 4):
+               for i in xrange(0, 2):
                        self._sendNext()
        
        def cancelPrint(self):
@@ -552,7 +644,7 @@ class MachineCom(object):
        def setPause(self, pause):
                if not pause and self.isPaused():
                        self._changeState(self.STATE_PRINTING)
-                       for i in xrange(0, 6):
+                       for i in xrange(0, 2):
                                self._sendNext()
                if pause and self.isPrinting():
                        self._changeState(self.STATE_PAUSED)