chiark / gitweb /
If programmer based auto-detection of serial port fails, try all ports and baudrates...
[cura.git] / Cura / util / machineCom.py
index 1c0e60ee74456f89002bb72ab2e5418a006ce9f1..8e14aac35c5ce3e6754bbda01e2d113a7a9dfad4 100644 (file)
@@ -1,47 +1,71 @@
 from __future__ import absolute_import
-import __init__
+__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
 
-import os, glob, sys, time, math, re, traceback, threading
+import os
+import glob
+import sys
+import time
+import math
+import re
+import traceback
+import threading
+import platform
 import Queue as queue
 
 import serial
 
-from avr_isp import stk500v2
-from avr_isp import ispBase
-from avr_isp import intelHex
+from Cura.avr_isp import stk500v2
+from Cura.avr_isp import ispBase
 
-from util import profile
-from util import version
+from Cura.util import profile
+from Cura.util import version
 
 try:
        import _winreg
 except:
        pass
 
-def serialList():
+def serialList(forAutoDetect=False):
        baselist=[]
-       if os.name=="nt":
+       if platform.system() == "Windows":
                try:
                        key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
                        i=0
-                       while(1):
-                               baselist+=[_winreg.EnumValue(key,i)[1]]
+                       while True:
+                               values = _winreg.EnumValue(key, i)
+                               if not forAutoDetect or 'USBSER' in values[0]:
+                                       baselist+=[values[1]]
                                i+=1
                except:
                        pass
-       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)
-       if version.isDevVersion():
+       if forAutoDetect:
+               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/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*")
+       if version.isDevVersion() and not forAutoDetect:
                baselist.append('VIRTUAL')
        return baselist
 
+def machineIsConnected():
+       #UltiGCode is designed for SD-Card printing, so never auto-detect the serial port.
+       port = profile.getMachineSetting('serial_port')
+       if port == 'AUTO':
+               if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
+                       return False
+               return len(serialList(True)) > 0
+       if platform.system() == "Windows":
+               return port in serialList()
+       return os.path.isfile(port)
+
 def baudrateList():
        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)
@@ -57,17 +81,17 @@ class VirtualPrinter():
                self.bedTargetTemp = 1.0
        
        def write(self, data):
-               if self.readList == None:
+               if self.readList is None:
                        return
                #print "Send: %s" % (data.rstrip())
                if 'M104' in data or 'M109' in data:
                        try:
-                               self.targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
+                               self.targetTemp = float(re.search('S([0-9]+)', data).group(1))
                        except:
                                pass
                if 'M140' in data or 'M190' in data:
                        try:
-                               self.bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
+                               self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
                        except:
                                pass
                if 'M105' in data:
@@ -76,21 +100,21 @@ class VirtualPrinter():
                        self.readList.append("ok\n")
 
        def readline(self):
-               if self.readList == None:
+               if self.readList is None:
                        return ''
                n = 0
                timeDiff = self.lastTempAt - time.time()
                self.lastTempAt = time.time()
                if abs(self.temp - self.targetTemp) > 1:
-                       self.temp += math.copysign(timeDiff, self.targetTemp - self.temp)
+                       self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp)
                if abs(self.bedTemp - self.bedTargetTemp) > 1:
-                       self.bedTemp += math.copysign(timeDiff, self.bedTargetTemp - self.bedTemp)
+                       self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
                while len(self.readList) < 1:
                        time.sleep(0.1)
                        n += 1
                        if n == 20:
                                return ''
-                       if self.readList == None:
+                       if self.readList is None:
                                return ''
                time.sleep(0.001)
                #print "Recv: %s" % (self.readList[0].rstrip())
@@ -132,14 +156,14 @@ class MachineCom(object):
        STATE_CLOSED_WITH_ERROR = 10
        
        def __init__(self, port = None, baudrate = None, callbackObject = None):
-               if port == None:
-                       port = profile.getPreference('serial_port')
-               if baudrate == None:
-                       if profile.getPreference('serial_baud') == 'AUTO':
+               if port is None:
+                       port = profile.getMachineSetting('serial_port')
+               if baudrate is None:
+                       if profile.getMachineSetting('serial_baud') == 'AUTO':
                                baudrate = 0
                        else:
-                               baudrate = int(profile.getPreference('serial_baud'))
-               if callbackObject == None:
+                               baudrate = int(profile.getMachineSetting('serial_baud'))
+               if callbackObject is None:
                        callbackObject = MachineComPrintCallback()
 
                self._port = port
@@ -147,11 +171,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
@@ -161,6 +188,7 @@ class MachineCom(object):
                self._currentZ = -1
                self._heatupWaitStartTime = 0
                self._heatupWaitTimeLost = 0.0
+               self._printStartTime100 = None
                
                self.thread = threading.Thread(target=self._monitor)
                self.thread.daemon = True
@@ -206,6 +234,9 @@ class MachineCom(object):
                if len(self._errorValue) < 20:
                        return self._errorValue
                return self._errorValue[:20] + "..."
+
+       def getErrorString(self):
+               return self._errorValue
        
        def isClosedOrError(self):
                return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
@@ -226,7 +257,15 @@ class MachineCom(object):
                return self._gcodePos
        
        def getPrintTime(self):
-               return time.time() - self._printStartTime - self._heatupWaitTimeLost
+               return time.time() - self._printStartTime
+
+       def getPrintTimeRemainingEstimate(self):
+               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)
+               printTimeLeft = printTimeTotal - printTime
+               return printTimeLeft
        
        def getTemp(self):
                return self._temp
@@ -246,14 +285,14 @@ class MachineCom(object):
                #Open the serial port.
                if self._port == 'AUTO':
                        self._changeState(self.STATE_DETECT_SERIAL)
+                       self._log("Serial port list: %s" % (str(serialList(True))))
                        programmer = stk500v2.Stk500v2()
-                       self._log("Serial port list: %s" % (str(serialList())))
-                       for p in serialList():
+                       for p in serialList(True):
                                try:
                                        self._log("Connecting to: %s" % (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)))
@@ -261,6 +300,8 @@ 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._serialDetectList = serialList(True)
                elif self._port == 'VIRTUAL':
                        self._changeState(self.STATE_OPEN_SERIAL)
                        self._serial = VirtualPrinter()
@@ -269,31 +310,35 @@ class MachineCom(object):
                        try:
                                self._log("Connecting to: %s" % (self._port))
                                if self._baudrate == 0:
-                                       self._serial = serial.Serial(self._port, 115200, timeout=0.1, writeTimeout=10000)
+                                       self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
                                else:
-                                       self._serial = serial.Serial(self._port, self._baudrate, timeout=2, writeTimeout=10000)
+                                       self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, 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)
+                       self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=0.1, writeTimeout=10000)
                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.
-               timeout = time.time() + 5
+               if self._state == self.STATE_CONNECTING:
+                       timeout = time.time() + 15
+               else:
+                       timeout = time.time() + 5
                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"
@@ -302,25 +347,30 @@ 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:
-                                       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 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()
@@ -333,9 +383,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=0.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
@@ -346,7 +409,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))
@@ -354,12 +417,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:        # 'wait' needed for Repetier (kind of watchdog)
                                        self._sendCommand("M105")
                                elif 'ok' in line:
                                        self._changeState(self.STATE_OPERATIONAL)
@@ -368,15 +431,23 @@ 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:
                                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.
+                               #Even when printing request the temperature every 5 seconds.
                                if time.time() > tempRequestTimeout:
-                                       self._commandQueue.put("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
                                if 'ok' in line:
                                        timeout = time.time() + 5
@@ -391,7 +462,30 @@ class MachineCom(object):
                                                if "rs" in line:
                                                        self._gcodePos = int(line.split()[1])
                self._log("Connection closed, closing down monitor")
-       
+
+       def _setBaudrate(self, baudrate):
+               #For linux the pyserial implementation lacks TCGETS2 support. So do that ourselves
+               if sys.platform.startswith('linux'):
+                       try:
+                               self._serial.baudrate = baudrate
+                       except:
+                               try:
+                                       # set custom speed
+                                       import fcntl, array, termios
+                                       TCGETS2 = 0x802C542A
+                                       TCSETS2 = 0x402C542B
+                                       BOTHER = 0o010000
+                                       buf = array.array('i', [0] * 64)
+                                       fcntl.ioctl(self._serial.fd, TCGETS2, buf)
+                                       buf[2] &= ~termios.CBAUD
+                                       buf[2] |= BOTHER
+                                       buf[9] = buf[10] = baudrate
+                                       fcntl.ioctl(self._serial.fd, TCSETS2, buf)
+                               except:
+                                       print getExceptionString()
+               else:
+                       self._serial.baudrate = baudrate
+
        def _log(self, message):
                self._callback.mcLog(message)
                try:
@@ -399,7 +493,10 @@ class MachineCom(object):
                except:
                        #If the log queue is full, remove the first message and append the new message again
                        self._logQueue.get()
-                       self._logQueue.put(message, False)
+                       try:
+                               self._logQueue.put(message, False)
+                       except:
+                               pass
 
        def _readline(self):
                if self._serial == None:
@@ -430,18 +527,21 @@ class MachineCom(object):
                self.close()
        
        def _sendCommand(self, cmd):
-               if self._serial == None:
+               if self._serial is None:
                        return
                if 'M109' in cmd or 'M190' in cmd:
                        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:
                        try:
-                               self._bedTargetTemp = float(re.search('S([0-9]+)').group(1))
+                               self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
                        except:
                                pass
                self._log('Send: %s' % (cmd))
@@ -450,6 +550,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()))
@@ -464,6 +565,8 @@ class MachineCom(object):
                if self._gcodePos >= len(self._gcodeList):
                        self._changeState(self.STATE_OPERATIONAL)
                        return
+               if self._gcodePos == 100:
+                       self._printStartTime100 = time.time()
                line = self._gcodeList[self._gcodePos]
                if type(line) is tuple:
                        self._printSection = line[1]
@@ -498,10 +601,11 @@ class MachineCom(object):
                        return
                self._gcodeList = gcodeList
                self._gcodePos = 0
+               self._printStartTime100 = None
                self._printSection = 'CUSTOM'
                self._changeState(self.STATE_PRINTING)
                self._printStartTime = time.time()
-               for i in xrange(0, 6):
+               for i in xrange(0, 4):
                        self._sendNext()
        
        def cancelPrint(self):