-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
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:
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
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
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
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()
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
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
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)
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)))
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:
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"
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()
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
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))
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)
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():
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:
pass
def _readline(self):
- if self._serial == None:
+ if self._serial is None:
return None
try:
ret = self._serial.readline()
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:
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()))
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)
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)
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
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):
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)