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"
18 from Cura.util import serialWrapper as serial
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
205 self._currentCommands = []
207 self._thread_lock = threading.Lock()
208 self.thread = threading.Thread(target=self._monitor)
209 self.thread.daemon = True
212 def _changeState(self, newState):
213 if self._state == newState:
215 oldState = self.getStateString()
216 self._state = newState
217 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
218 self._callback.mcStateChange(newState)
223 def getStateString(self):
224 if self._state == self.STATE_NONE:
226 if self._state == self.STATE_OPEN_SERIAL:
227 return "Opening serial port"
228 if self._state == self.STATE_DETECT_SERIAL:
229 return "Detecting serial port"
230 if self._state == self.STATE_DETECT_BAUDRATE:
231 return "Detecting baudrate"
232 if self._state == self.STATE_CONNECTING:
234 if self._state == self.STATE_OPERATIONAL:
236 if self._state == self.STATE_PRINTING:
238 if self._state == self.STATE_PAUSED:
240 if self._state == self.STATE_CLOSED:
242 if self._state == self.STATE_ERROR:
243 return "Error: %s" % (self.getShortErrorString())
244 if self._state == self.STATE_CLOSED_WITH_ERROR:
245 return "Error: %s" % (self.getShortErrorString())
246 return "?%d?" % (self._state)
248 def getShortErrorString(self):
249 if len(self._errorValue) < 35:
250 return self._errorValue
251 return self._errorValue[:35] + "..."
253 def getErrorString(self):
254 return self._errorValue
257 return self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
259 def isClosedOrError(self):
260 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
263 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
265 def isOperational(self):
266 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
268 def isPrinting(self):
269 return self._state == self.STATE_PRINTING
272 return self._state == self.STATE_PAUSED
274 def getPrintPos(self):
275 return self._gcodePos
277 def getPrintTime(self):
278 return time.time() - self._printStartTime
280 def getPrintTimeRemainingEstimate(self):
281 if self._printStartTime100 is None or self.getPrintPos() < 200:
283 printTime = (time.time() - self._printStartTime100) / 60
284 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
285 printTimeLeft = printTimeTotal - printTime
291 def getBedTemp(self):
296 while not self._logQueue.empty():
297 ret.append(self._logQueue.get())
299 self._logQueue.put(line, False)
302 def receivedOK(self):
303 if len(self._currentCommands) > 0:
304 # Marlin will answer 'ok' immediatly to G[0-3] commands
305 for i in xrange(0, len(self._currentCommands)):
306 if "G0 " in self._currentCommands[i] or \
307 "G1 " in self._currentCommands[i] or \
308 "G2 " in self._currentCommands[i] or \
309 "G3 " in self._currentCommands[i]:
310 self._currentCommands.pop(i)
312 self._currentCommands.pop(0)
315 #Open the serial port.
316 if self._port == 'AUTO':
317 self._changeState(self.STATE_DETECT_SERIAL)
318 programmer = stk500v2.Stk500v2()
319 for p in serialList(True):
321 self._log("Connecting to: %s (programmer)" % (p))
322 programmer.connect(p)
323 self._serial = programmer.leaveISP()
324 profile.putMachineSetting('serial_port_auto', p)
326 except ispBase.IspError as (e):
327 self._log("Error while connecting to %s: %s" % (p, str(e)))
330 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
332 if self._serial is None:
333 self._log("Serial port list: %s" % (str(serialList(True))))
334 self._serialDetectList = serialList(True)
335 elif self._port == 'VIRTUAL':
336 self._changeState(self.STATE_OPEN_SERIAL)
337 self._serial = VirtualPrinter()
339 self._changeState(self.STATE_OPEN_SERIAL)
341 if self._baudrate == 0:
342 self._log("Connecting to: %s with baudrate: 115200 (fallback)" % (self._port))
343 self._serial = serial.Serial(str(self._port), 115200, timeout=3)
344 # Need to set writeTimeout separately in order to be compatible with pyserial 3.0
345 self._serial.writeTimeout=10000
347 self._log("Connecting to: %s with baudrate: %s (configured)" % (self._port, self._baudrate))
348 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=5)
349 # Need to set writeTimeout separately in order to be compatible with pyserial 3.0
350 self._serial.writeTimeout=10000
352 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
353 if self._serial is None:
354 baudrate = self._baudrate
356 baudrate = self._baudrateDetectList.pop(0)
357 if len(self._serialDetectList) < 1:
358 self._log("Found no ports to try for auto detection")
359 self._errorValue = 'Failed to autodetect serial port.'
360 self._changeState(self.STATE_ERROR)
362 port = self._serialDetectList.pop(0)
363 self._log("Connecting to: %s with baudrate: %s (auto)" % (port, baudrate))
365 self._serial = serial.Serial(port, baudrate, timeout=3)
366 # Need to set writeTimeout separately in order to be compatible with pyserial 3.0
367 self._serial.writeTimeout=10000
371 self._log("Connected to: %s, starting monitor" % (self._serial))
372 if self._baudrate == 0:
373 self._changeState(self.STATE_DETECT_BAUDRATE)
375 self._changeState(self.STATE_CONNECTING)
377 #Start monitoring the serial port.
378 if self._state == self.STATE_CONNECTING:
379 timeout = time.time() + 15
381 timeout = time.time() + 5
382 tempRequestTimeout = timeout
384 line = self._readline()
388 #No matter the state, if we see an fatal error, goto the error state and store the error for reference.
389 # Only goto error on known fatal errors.
390 if line.startswith('Error:'):
391 #Oh YEAH, consistency.
392 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
393 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
394 # So we can have an extra newline in the most common case. Awesome work people.
395 if re.match('Error:[0-9]\n', line):
396 line = line.rstrip() + self._readline()
397 #Skip the communication errors, as those get corrected.
398 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:
399 if not self.isError():
400 self._errorValue = line[6:]
401 self._changeState(self.STATE_ERROR)
402 if ' T:' in line or line.startswith('T:'):
403 tempRequestTimeout = time.time() + 5
405 self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
410 self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
413 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
414 #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.
415 if not 'ok' in line and self._heatupWaitStartTime != 0:
417 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
418 self._heatupWaitStartTime = t
419 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and \
420 not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and \
421 not line.startswith('Error:No Checksum with line number') and not line.startswith('Error:No Line Number with checksum') and \
422 line != 'echo:Unknown command:""\n' and self.isOperational():
423 self._callback.mcMessage(line)
425 if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
426 if line == '' or time.time() > timeout:
427 if len(self._baudrateDetectList) < 1:
429 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
430 self._changeState(self.STATE_ERROR)
431 elif self._baudrateDetectRetry > 0:
432 self._baudrateDetectRetry -= 1
433 self._serial.write('\n')
434 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
435 self._sendCommand("M105")
436 self._testingBaudrate = True
438 if self._state == self.STATE_DETECT_SERIAL:
439 if len(self._serialDetectList) == 0:
440 if len(self._baudrateDetectList) == 0:
441 self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
442 self._errorValue = 'Failed to autodetect serial port.'
443 self._changeState(self.STATE_ERROR)
446 self._serialDetectList = serialList(True)
447 baudrate = self._baudrateDetectList.pop(0)
449 self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=2.5)
450 # Need to set writeTimeout separately in order to be compatible with pyserial 3.0
451 self._serial.writeTimeout=10000
453 baudrate = self._baudrateDetectList.pop(0)
455 self._setBaudrate(baudrate)
456 self._serial.timeout = 0.5
457 self._log("Trying baudrate: %d" % (baudrate))
458 self._baudrateDetectRetry = 5
459 self._baudrateDetectTestOk = 0
460 timeout = time.time() + 5
461 self._serial.write('\n')
462 self._sendCommand("M105")
463 self._testingBaudrate = True
465 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
467 self._baudrateDetectTestOk += 1
468 if self._baudrateDetectTestOk < 10:
469 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
470 self._sendCommand("M105")
472 self._sendCommand("M999")
473 self._serial.timeout = 2
474 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
475 self._changeState(self.STATE_OPERATIONAL)
477 self._testingBaudrate = False
478 elif self._state == self.STATE_CONNECTING:
479 if line == '' or 'wait' in line or 'start' in line: # 'wait' needed for Repetier (kind of watchdog)
480 self._sendCommand("M105")
482 self._changeState(self.STATE_OPERATIONAL)
483 if time.time() > timeout:
485 elif self._state == self.STATE_OPERATIONAL:
486 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
488 if self._extruderCount > 0:
489 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
490 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
492 self.sendCommand("M105")
493 tempRequestTimeout = time.time() + 5
496 elif 'start' in line:
497 self._currentCommands = []
498 elif self._state == self.STATE_PRINTING:
499 #Even when printing request the temperature every 5 seconds.
500 if time.time() > tempRequestTimeout:
501 if self._extruderCount > 0:
502 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
503 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
505 self.sendCommand("M105")
506 tempRequestTimeout = time.time() + 5
507 if line == '' and time.time() > timeout:
508 self._log("Communication timeout during printing, forcing a line")
512 timeout = time.time() + 5
513 if not self._commandQueue.empty():
514 self._sendCommand(self._commandQueue.get())
517 if "G28" in self._currentCommands[0] or "G29" in self._currentCommands[0] or \
518 "M109" in self._currentCommands[0] or "M190" in self._currentCommands[0]:
519 # Long command detected. Timeout is now set to 60s to avoid forcing 'ok'
520 # every 5 seconds while it's not needed
521 timeout = time.time() + 60
523 elif 'start' in line:
524 self._currentCommands = []
525 elif "resend" in line.lower() or "rs" in line:
526 newPos = self._gcodePos
528 newPos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
531 newPos = int(line.split()[1])
532 # If we need to resend more than 10 lines, we can assume that the machine
533 # was shut down and turned back on or something else that's weird just happened.
534 # In that case, it can be dangerous to restart the print, so we'd better kill it
535 if newPos == 1 or self._gcodePos > newPos + 100:
536 self._callback.mcMessage("Print canceled due to loss of communication to printer (USB unplugged or power lost)")
539 self._gcodePos = newPos
540 elif self._state == self.STATE_PAUSED:
541 #Even when printing request the temperature every 5 seconds.
542 if time.time() > tempRequestTimeout:
543 if self._extruderCount > 0:
544 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
545 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
547 self.sendCommand("M105")
548 tempRequestTimeout = time.time() + 5
549 if line == '' and time.time() > timeout:
553 timeout = time.time() + 5
554 if not self._commandQueue.empty():
555 self._sendCommand(self._commandQueue.get())
556 elif 'start' in line:
557 self._currentCommands = []
559 self._log("Connection closed, closing down monitor")
561 def _setBaudrate(self, baudrate):
563 self._serial.baudrate = baudrate
565 print getExceptionString()
567 def _log(self, message):
568 #sys.stderr.write(message + "\n");
569 self._callback.mcLog(message)
571 self._logQueue.put(message, False)
573 #If the log queue is full, remove the first message and append the new message again
576 self._logQueue.put(message, False)
581 if self._serial is None:
584 ret = self._serial.readline()
586 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
587 self._errorValue = getExceptionString()
591 #self._log("Recv: TIMEOUT")
593 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
596 def close(self, isError = False):
597 if self._serial != None:
600 self._changeState(self.STATE_CLOSED_WITH_ERROR)
602 self._changeState(self.STATE_CLOSED)
608 def _sendCommand(self, cmd):
609 self._thread_lock.acquire(True)
610 if self._serial is None:
611 self._thread_lock.release()
613 if 'M109' in cmd or 'M190' in cmd:
614 self._heatupWaitStartTime = time.time()
615 if 'M104' in cmd or 'M109' in cmd:
619 t = int(re.search('T([0-9]+)', cmd).group(1))
620 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
623 if 'M140' in cmd or 'M190' in cmd:
625 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
628 self._log('Send: %s' % (cmd))
629 if self.isOperational():
630 self._currentCommands.append(cmd)
632 self._serial.write(cmd + '\n')
633 except serial.SerialTimeoutException:
634 self._log("Serial timeout while writing to serial port, trying again.")
637 self._serial.write(cmd + '\n')
639 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
640 self._errorValue = getExceptionString()
643 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
644 self._errorValue = getExceptionString()
646 self._thread_lock.release()
649 if self._gcodePos >= len(self._gcodeList):
650 self._changeState(self.STATE_OPERATIONAL)
652 if self._gcodePos == 100:
653 self._printStartTime100 = time.time()
654 line = self._gcodeList[self._gcodePos]
655 if type(line) is tuple:
656 self._printSection = line[1]
659 if line == 'M0' or line == 'M1':
661 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
662 if self._printSection in self._feedRateModifier:
663 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
664 if ('G0' in line or 'G1' in line) and 'Z' in line:
665 z = float(re.search('Z(-?[0-9\.]*)', line).group(1))
666 if self._currentZ != z:
668 self._callback.mcZChange(z)
670 self._log("Unexpected error: %s" % (getExceptionString()))
671 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
674 self._sendCommand("N%d%s*%d" % (pos, line, checksum))
675 self._callback.mcProgress(self._gcodePos)
677 def sendCommand(self, cmd):
678 cmd = cmd.encode('ascii', 'replace')
679 if self.isPrinting() or self.isPaused():
680 self._commandQueue.put(cmd)
681 if len(self._currentCommands) == 0:
682 self._sendCommand(self._commandQueue.get())
683 elif self.isOperational():
684 self._sendCommand(cmd)
686 def printGCode(self, gcodeList):
687 if not self.isOperational() or self.isPrinting() or self.isPaused():
689 self._gcodeList = gcodeList
691 self._printStartTime100 = None
692 self._printSection = 'CUSTOM'
693 self._changeState(self.STATE_PRINTING)
694 self._printStartTime = time.time()
695 for i in xrange(0, 4):
698 def cancelPrint(self):
699 if self.isOperational():
700 self._changeState(self.STATE_OPERATIONAL)
702 def setPause(self, pause):
703 if not pause and self.isPaused():
704 self._changeState(self.STATE_PRINTING)
705 for i in xrange(0, 4):
706 if not self._commandQueue.empty():
707 self._sendCommand(self._commandQueue.get())
710 if pause and self.isPrinting():
711 self._changeState(self.STATE_PAUSED)
713 def setFeedrateModifier(self, type, value):
714 self._feedRateModifier[type] = value
716 def getExceptionString():
717 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
718 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])