2 MachineCom handles communication with GCode based printers trough (USB) serial ports.
3 For actual printing of objects this module is used from Cura.serialCommunication and ran in a separate process.
5 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
20 from Cura.avr_isp import stk500v2
21 from Cura.avr_isp import ispBase
23 from Cura.util import profile
24 from Cura.util import version
31 def serialList(forAutoDetect=False):
33 Retrieve a list of serial ports found in the system.
34 :param forAutoDetect: if true then only the USB serial ports are listed. Else all ports are listed.
35 :return: A list of strings where each string is a serial port.
38 if platform.system() == "Windows":
40 key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
43 values = _winreg.EnumValue(key, i)
44 if not forAutoDetect or 'USBSER' in values[0]:
50 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.usb*")
51 baselist = filter(lambda s: not 'Bluetooth' in s, baselist)
52 prev = profile.getMachineSetting('serial_port_auto')
55 baselist.insert(0, prev)
57 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/*')
58 if version.isDevVersion() and not forAutoDetect:
59 baselist.append('VIRTUAL')
64 :return: a list of integers containing all possible baudrates at which we can communicate.
65 Used for auto-baudrate detection as well as manual baudrate selection.
67 ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
68 if profile.getMachineSetting('serial_baud_auto') != '':
69 prev = int(profile.getMachineSetting('serial_baud_auto'))
75 class VirtualPrinter():
77 A virtual printer class used for debugging. Acts as a serial.Serial class, but without connecting to any port.
78 Only available when running the development version of Cura.
81 self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
84 self.lastTempAt = time.time()
86 self.bedTargetTemp = 1.0
88 def write(self, data):
89 if self.readList is None:
91 #print "Send: %s" % (data.rstrip())
92 if 'M104' in data or 'M109' in data:
94 self.targetTemp = float(re.search('S([0-9]+)', data).group(1))
97 if 'M140' in data or 'M190' in data:
99 self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
103 self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
104 elif len(data.strip()) > 0:
105 self.readList.append("ok\n")
108 if self.readList is None:
111 timeDiff = self.lastTempAt - time.time()
112 self.lastTempAt = time.time()
113 if abs(self.temp - self.targetTemp) > 1:
114 self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp)
115 if abs(self.bedTemp - self.bedTargetTemp) > 1:
116 self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
117 while len(self.readList) < 1:
122 if self.readList is None:
125 #print "Recv: %s" % (self.readList[0].rstrip())
126 return self.readList.pop(0)
131 class MachineComPrintCallback(object):
133 Base class for callbacks from the MachineCom class.
134 This class has all empty implementations and is attached to the MachineCom if no other callback object is attached.
136 def mcLog(self, message):
139 def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
142 def mcStateChange(self, state):
145 def mcMessage(self, message):
148 def mcProgress(self, lineNr):
151 def mcZChange(self, newZ):
154 class MachineCom(object):
156 Class for (USB) serial communication with 3D printers.
157 This class keeps track of if the connection is still live, can auto-detect serial ports and baudrates.
160 STATE_OPEN_SERIAL = 1
161 STATE_DETECT_SERIAL = 2
162 STATE_DETECT_BAUDRATE = 3
164 STATE_OPERATIONAL = 5
169 STATE_CLOSED_WITH_ERROR = 10
171 def __init__(self, port = None, baudrate = None, callbackObject = None):
173 port = profile.getMachineSetting('serial_port')
175 if profile.getMachineSetting('serial_baud') == 'AUTO':
178 baudrate = int(profile.getMachineSetting('serial_baud'))
179 if callbackObject is None:
180 callbackObject = MachineComPrintCallback()
183 self._baudrate = baudrate
184 self._callback = callbackObject
185 self._state = self.STATE_NONE
187 self._serialDetectList = []
188 self._baudrateDetectList = baudrateList()
189 self._baudrateDetectRetry = 0
190 self._extruderCount = int(profile.getMachineSetting('extruder_amount'))
191 self._temperatureRequestExtruder = 0
192 self._temp = [0] * self._extruderCount
193 self._targetTemp = [0] * self._extruderCount
195 self._bedTargetTemp = 0
196 self._gcodeList = None
198 self._commandQueue = queue.Queue()
199 self._logQueue = queue.Queue(256)
200 self._feedRateModifier = {}
202 self._heatupWaitStartTime = 0
203 self._heatupWaitTimeLost = 0.0
204 self._printStartTime100 = None
206 self.thread = threading.Thread(target=self._monitor)
207 self.thread.daemon = True
210 def _changeState(self, newState):
211 if self._state == newState:
213 oldState = self.getStateString()
214 self._state = newState
215 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
216 self._callback.mcStateChange(newState)
221 def getStateString(self):
222 if self._state == self.STATE_NONE:
224 if self._state == self.STATE_OPEN_SERIAL:
225 return "Opening serial port"
226 if self._state == self.STATE_DETECT_SERIAL:
227 return "Detecting serial port"
228 if self._state == self.STATE_DETECT_BAUDRATE:
229 return "Detecting baudrate"
230 if self._state == self.STATE_CONNECTING:
232 if self._state == self.STATE_OPERATIONAL:
234 if self._state == self.STATE_PRINTING:
236 if self._state == self.STATE_PAUSED:
238 if self._state == self.STATE_CLOSED:
240 if self._state == self.STATE_ERROR:
241 return "Error: %s" % (self.getShortErrorString())
242 if self._state == self.STATE_CLOSED_WITH_ERROR:
243 return "Error: %s" % (self.getShortErrorString())
244 return "?%d?" % (self._state)
246 def getShortErrorString(self):
247 if len(self._errorValue) < 35:
248 return self._errorValue
249 return self._errorValue[:35] + "..."
251 def getErrorString(self):
252 return self._errorValue
255 return self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
257 def isClosedOrError(self):
258 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
261 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
263 def isOperational(self):
264 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
266 def isPrinting(self):
267 return self._state == self.STATE_PRINTING
270 return self._state == self.STATE_PAUSED
272 def getPrintPos(self):
273 return self._gcodePos
275 def getPrintTime(self):
276 return time.time() - self._printStartTime
278 def getPrintTimeRemainingEstimate(self):
279 if self._printStartTime100 is None or self.getPrintPos() < 200:
281 printTime = (time.time() - self._printStartTime100) / 60
282 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
283 printTimeLeft = printTimeTotal - printTime
289 def getBedTemp(self):
294 while not self._logQueue.empty():
295 ret.append(self._logQueue.get())
297 self._logQueue.put(line, False)
301 #Open the serial port.
302 if self._port == 'AUTO':
303 self._changeState(self.STATE_DETECT_SERIAL)
304 programmer = stk500v2.Stk500v2()
305 for p in serialList(True):
307 self._log("Connecting to: %s (programmer)" % (p))
308 programmer.connect(p)
309 self._serial = programmer.leaveISP()
310 profile.putMachineSetting('serial_port_auto', p)
312 except ispBase.IspError as (e):
313 self._log("Error while connecting to %s: %s" % (p, str(e)))
316 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
318 if self._serial is None:
319 self._log("Serial port list: %s" % (str(serialList(True))))
320 self._serialDetectList = serialList(True)
321 elif self._port == 'VIRTUAL':
322 self._changeState(self.STATE_OPEN_SERIAL)
323 self._serial = VirtualPrinter()
325 self._changeState(self.STATE_OPEN_SERIAL)
327 if self._baudrate == 0:
328 self._log("Connecting to: %s with baudrate: 115200 (fallback)" % (self._port))
329 self._serial = serial.Serial(str(self._port), 115200, timeout=3, writeTimeout=10000)
331 self._log("Connecting to: %s with baudrate: %s (configured)" % (self._port, self._baudrate))
332 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=5, writeTimeout=10000)
334 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
335 if self._serial is None:
336 baudrate = self._baudrate
338 baudrate = self._baudrateDetectList.pop(0)
339 if len(self._serialDetectList) < 1:
340 self._log("Found no ports to try for auto detection")
341 self._errorValue = 'Failed to autodetect serial port.'
342 self._changeState(self.STATE_ERROR)
344 port = self._serialDetectList.pop(0)
345 self._log("Connecting to: %s with baudrate: %s (auto)" % (port, baudrate))
347 self._serial = serial.Serial(port, baudrate, timeout=3, writeTimeout=10000)
351 self._log("Connected to: %s, starting monitor" % (self._serial))
352 if self._baudrate == 0:
353 self._changeState(self.STATE_DETECT_BAUDRATE)
355 self._changeState(self.STATE_CONNECTING)
357 #Start monitoring the serial port.
358 if self._state == self.STATE_CONNECTING:
359 timeout = time.time() + 15
361 timeout = time.time() + 5
362 tempRequestTimeout = timeout
364 line = self._readline()
368 #No matter the state, if we see an fatal error, goto the error state and store the error for reference.
369 # Only goto error on known fatal errors.
370 if line.startswith('Error:'):
371 #Oh YEAH, consistency.
372 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
373 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
374 # So we can have an extra newline in the most common case. Awesome work people.
375 if re.match('Error:[0-9]\n', line):
376 line = line.rstrip() + self._readline()
377 #Skip the communication errors, as those get corrected.
378 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:
379 if not self.isError():
380 self._errorValue = line[6:]
381 self._changeState(self.STATE_ERROR)
382 if ' T:' in line or line.startswith('T:'):
384 self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
389 self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
392 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
393 #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.
394 if not 'ok' in line and self._heatupWaitStartTime != 0:
396 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
397 self._heatupWaitStartTime = t
398 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and \
399 not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and \
400 not line.startswith('Error:No Checksum with line number') and not line.startswith('Error:No Line Number with checksum') and \
401 line != 'echo:Unknown command:""\n' and self.isOperational():
402 self._callback.mcMessage(line)
404 if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
405 if line == '' or time.time() > timeout:
406 if len(self._baudrateDetectList) < 1:
408 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
409 self._changeState(self.STATE_ERROR)
410 elif self._baudrateDetectRetry > 0:
411 self._baudrateDetectRetry -= 1
412 self._serial.write('\n')
413 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
414 self._sendCommand("M105")
415 self._testingBaudrate = True
417 if self._state == self.STATE_DETECT_SERIAL:
418 if len(self._serialDetectList) == 0:
419 if len(self._baudrateDetectList) == 0:
420 self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
421 self._errorValue = 'Failed to autodetect serial port.'
422 self._changeState(self.STATE_ERROR)
425 self._serialDetectList = serialList(True)
426 baudrate = self._baudrateDetectList.pop(0)
428 self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=2.5, writeTimeout=10000)
430 baudrate = self._baudrateDetectList.pop(0)
432 self._setBaudrate(baudrate)
433 self._serial.timeout = 0.5
434 self._log("Trying baudrate: %d" % (baudrate))
435 self._baudrateDetectRetry = 5
436 self._baudrateDetectTestOk = 0
437 timeout = time.time() + 5
438 self._serial.write('\n')
439 self._sendCommand("M105")
440 self._testingBaudrate = True
442 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
444 self._baudrateDetectTestOk += 1
445 if self._baudrateDetectTestOk < 10:
446 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
447 self._sendCommand("M105")
449 self._sendCommand("M999")
450 self._serial.timeout = 2
451 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
452 self._changeState(self.STATE_OPERATIONAL)
454 self._testingBaudrate = False
455 elif self._state == self.STATE_CONNECTING:
456 if line == '' or 'wait' in line: # 'wait' needed for Repetier (kind of watchdog)
457 self._sendCommand("M105")
459 self._changeState(self.STATE_OPERATIONAL)
460 if time.time() > timeout:
462 elif self._state == self.STATE_OPERATIONAL:
463 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
465 if self._extruderCount > 0:
466 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
467 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
469 self.sendCommand("M105")
470 tempRequestTimeout = time.time() + 5
471 elif self._state == self.STATE_PRINTING:
472 #Even when printing request the temperature every 5 seconds.
473 if time.time() > tempRequestTimeout:
474 if self._extruderCount > 0:
475 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
476 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
478 self.sendCommand("M105")
479 tempRequestTimeout = time.time() + 5
480 if line == '' and time.time() > timeout:
481 self._log("Communication timeout during printing, forcing a line")
484 timeout = time.time() + 5
485 if not self._commandQueue.empty():
486 self._sendCommand(self._commandQueue.get())
489 elif "resend" in line.lower() or "rs" in line:
490 newPos = self._gcodePos
492 newPos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
495 newPos = int(line.split()[1])
496 # If we need to resend more than 10 lines, we can assume that the machine
497 # was shut down and turned back on or something else that's weird just happened.
498 # In that case, it can be dangerous to restart the print, so we'd better kill it
499 if newPos == 1 or self._gcodePos > newPos + 100:
500 self._callback.mcMessage("Print canceled due to loss of communication to printer (USB unplugged or power lost)")
503 self._gcodePos = newPos
505 self._log("Connection closed, closing down monitor")
507 def _setBaudrate(self, baudrate):
509 self._serial.baudrate = baudrate
511 print getExceptionString()
513 def _log(self, message):
514 self._callback.mcLog(message)
516 self._logQueue.put(message, False)
518 #If the log queue is full, remove the first message and append the new message again
521 self._logQueue.put(message, False)
526 if self._serial is None:
529 ret = self._serial.readline()
531 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
532 self._errorValue = getExceptionString()
536 #self._log("Recv: TIMEOUT")
538 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
541 def close(self, isError = False):
542 if self._serial != None:
545 self._changeState(self.STATE_CLOSED_WITH_ERROR)
547 self._changeState(self.STATE_CLOSED)
553 def _sendCommand(self, cmd):
554 if self._serial is None:
556 if 'M109' in cmd or 'M190' in cmd:
557 self._heatupWaitStartTime = time.time()
558 if 'M104' in cmd or 'M109' in cmd:
562 t = int(re.search('T([0-9]+)', cmd).group(1))
563 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
566 if 'M140' in cmd or 'M190' in cmd:
568 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
571 self._log('Send: %s' % (cmd))
573 self._serial.write(cmd + '\n')
574 except serial.SerialTimeoutException:
575 self._log("Serial timeout while writing to serial port, trying again.")
578 self._serial.write(cmd + '\n')
580 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
581 self._errorValue = getExceptionString()
584 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
585 self._errorValue = getExceptionString()
589 if self._gcodePos >= len(self._gcodeList):
590 self._changeState(self.STATE_OPERATIONAL)
592 if self._gcodePos == 100:
593 self._printStartTime100 = time.time()
594 line = self._gcodeList[self._gcodePos]
595 if type(line) is tuple:
596 self._printSection = line[1]
599 if line == 'M0' or line == 'M1':
601 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
602 if self._printSection in self._feedRateModifier:
603 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
604 if ('G0' in line or 'G1' in line) and 'Z' in line:
605 z = float(re.search('Z([0-9\.]*)', line).group(1))
606 if self._currentZ != z:
608 self._callback.mcZChange(z)
610 self._log("Unexpected error: %s" % (getExceptionString()))
611 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
612 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
614 self._callback.mcProgress(self._gcodePos)
616 def sendCommand(self, cmd):
617 cmd = cmd.encode('ascii', 'replace')
618 if self.isPrinting():
619 self._commandQueue.put(cmd)
620 elif self.isOperational():
621 self._sendCommand(cmd)
623 def printGCode(self, gcodeList):
624 if not self.isOperational() or self.isPrinting():
626 self._gcodeList = gcodeList
628 self._printStartTime100 = None
629 self._printSection = 'CUSTOM'
630 self._changeState(self.STATE_PRINTING)
631 self._printStartTime = time.time()
632 for i in xrange(0, 2):
635 def cancelPrint(self):
636 if self.isOperational():
637 self._changeState(self.STATE_OPERATIONAL)
639 def setPause(self, pause):
640 if not pause and self.isPaused():
641 self._changeState(self.STATE_PRINTING)
642 for i in xrange(0, 2):
644 if pause and self.isPrinting():
645 self._changeState(self.STATE_PAUSED)
647 def setFeedrateModifier(self, type, value):
648 self._feedRateModifier[type] = value
650 def getExceptionString():
651 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
652 return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])