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:'):
383 tempRequestTimeout = time.time() + 5
385 self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
390 self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
393 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
394 #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.
395 if not 'ok' in line and self._heatupWaitStartTime != 0:
397 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
398 self._heatupWaitStartTime = t
399 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and \
400 not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and \
401 not line.startswith('Error:No Checksum with line number') and not line.startswith('Error:No Line Number with checksum') and \
402 line != 'echo:Unknown command:""\n' and self.isOperational():
403 self._callback.mcMessage(line)
405 if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
406 if line == '' or time.time() > timeout:
407 if len(self._baudrateDetectList) < 1:
409 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
410 self._changeState(self.STATE_ERROR)
411 elif self._baudrateDetectRetry > 0:
412 self._baudrateDetectRetry -= 1
413 self._serial.write('\n')
414 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
415 self._sendCommand("M105")
416 self._testingBaudrate = True
418 if self._state == self.STATE_DETECT_SERIAL:
419 if len(self._serialDetectList) == 0:
420 if len(self._baudrateDetectList) == 0:
421 self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
422 self._errorValue = 'Failed to autodetect serial port.'
423 self._changeState(self.STATE_ERROR)
426 self._serialDetectList = serialList(True)
427 baudrate = self._baudrateDetectList.pop(0)
429 self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=2.5, writeTimeout=10000)
431 baudrate = self._baudrateDetectList.pop(0)
433 self._setBaudrate(baudrate)
434 self._serial.timeout = 0.5
435 self._log("Trying baudrate: %d" % (baudrate))
436 self._baudrateDetectRetry = 5
437 self._baudrateDetectTestOk = 0
438 timeout = time.time() + 5
439 self._serial.write('\n')
440 self._sendCommand("M105")
441 self._testingBaudrate = True
443 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
445 self._baudrateDetectTestOk += 1
446 if self._baudrateDetectTestOk < 10:
447 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
448 self._sendCommand("M105")
450 self._sendCommand("M999")
451 self._serial.timeout = 2
452 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
453 self._changeState(self.STATE_OPERATIONAL)
455 self._testingBaudrate = False
456 elif self._state == self.STATE_CONNECTING:
457 if line == '' or 'wait' in line or 'start' in line: # 'wait' needed for Repetier (kind of watchdog)
458 self._sendCommand("M105")
460 self._changeState(self.STATE_OPERATIONAL)
461 if time.time() > timeout:
463 elif self._state == self.STATE_OPERATIONAL:
464 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
466 if self._extruderCount > 0:
467 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
468 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
470 self.sendCommand("M105")
471 tempRequestTimeout = time.time() + 5
472 elif self._state == self.STATE_PRINTING:
473 #Even when printing request the temperature every 5 seconds.
474 if time.time() > tempRequestTimeout:
475 if self._extruderCount > 0:
476 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
477 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
479 self.sendCommand("M105")
480 tempRequestTimeout = time.time() + 5
481 if line == '' and time.time() > timeout:
482 self._log("Communication timeout during printing, forcing a line")
485 timeout = time.time() + 5
486 if not self._commandQueue.empty():
487 self._sendCommand(self._commandQueue.get())
490 elif "resend" in line.lower() or "rs" in line:
491 newPos = self._gcodePos
493 newPos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
496 newPos = int(line.split()[1])
497 # If we need to resend more than 10 lines, we can assume that the machine
498 # was shut down and turned back on or something else that's weird just happened.
499 # In that case, it can be dangerous to restart the print, so we'd better kill it
500 if newPos == 1 or self._gcodePos > newPos + 100:
501 self._callback.mcMessage("Print canceled due to loss of communication to printer (USB unplugged or power lost)")
504 self._gcodePos = newPos
505 elif self._state == self.STATE_PAUSED:
507 timeout = time.time() + 5
508 if not self._commandQueue.empty():
509 self._sendCommand(self._commandQueue.get())
511 self._log("Connection closed, closing down monitor")
513 def _setBaudrate(self, baudrate):
515 self._serial.baudrate = baudrate
517 print getExceptionString()
519 def _log(self, message):
520 self._callback.mcLog(message)
522 self._logQueue.put(message, False)
524 #If the log queue is full, remove the first message and append the new message again
527 self._logQueue.put(message, False)
532 if self._serial is None:
535 ret = self._serial.readline()
537 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
538 self._errorValue = getExceptionString()
542 #self._log("Recv: TIMEOUT")
544 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
547 def close(self, isError = False):
548 if self._serial != None:
551 self._changeState(self.STATE_CLOSED_WITH_ERROR)
553 self._changeState(self.STATE_CLOSED)
559 def _sendCommand(self, cmd):
560 if self._serial is None:
562 if 'M109' in cmd or 'M190' in cmd:
563 self._heatupWaitStartTime = time.time()
564 if 'M104' in cmd or 'M109' in cmd:
568 t = int(re.search('T([0-9]+)', cmd).group(1))
569 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
572 if 'M140' in cmd or 'M190' in cmd:
574 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
577 self._log('Send: %s' % (cmd))
579 self._serial.write(cmd + '\n')
580 except serial.SerialTimeoutException:
581 self._log("Serial timeout while writing to serial port, trying again.")
584 self._serial.write(cmd + '\n')
586 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
587 self._errorValue = getExceptionString()
590 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
591 self._errorValue = getExceptionString()
595 if self._gcodePos >= len(self._gcodeList):
596 self._changeState(self.STATE_OPERATIONAL)
598 if self._gcodePos == 100:
599 self._printStartTime100 = time.time()
600 line = self._gcodeList[self._gcodePos]
601 if type(line) is tuple:
602 self._printSection = line[1]
605 if line == 'M0' or line == 'M1':
607 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
608 if self._printSection in self._feedRateModifier:
609 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
610 if ('G0' in line or 'G1' in line) and 'Z' in line:
611 z = float(re.search('Z(-?[0-9\.]*)', line).group(1))
612 if self._currentZ != z:
614 self._callback.mcZChange(z)
616 self._log("Unexpected error: %s" % (getExceptionString()))
617 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
620 self._sendCommand("N%d%s*%d" % (pos, line, checksum))
621 self._callback.mcProgress(self._gcodePos)
623 def sendCommand(self, cmd):
624 cmd = cmd.encode('ascii', 'replace')
625 if self.isPrinting():
626 self._commandQueue.put(cmd)
627 elif self.isOperational():
628 self._sendCommand(cmd)
630 def printGCode(self, gcodeList):
631 if not self.isOperational() or self.isPrinting():
633 self._gcodeList = gcodeList
635 self._printStartTime100 = None
636 self._printSection = 'CUSTOM'
637 self._changeState(self.STATE_PRINTING)
638 self._printStartTime = time.time()
639 for i in xrange(0, 2):
642 def cancelPrint(self):
643 if self.isOperational():
644 self._changeState(self.STATE_OPERATIONAL)
646 def setPause(self, pause):
647 if not pause and self.isPaused():
648 self._changeState(self.STATE_PRINTING)
649 for i in xrange(0, 2):
651 if pause and self.isPrinting():
652 self._changeState(self.STATE_PAUSED)
654 def setFeedrateModifier(self, type, value):
655 self._feedRateModifier[type] = value
657 def getExceptionString():
658 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
659 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])