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
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, writeTimeout=10000)
345 self._log("Connecting to: %s with baudrate: %s (configured)" % (self._port, self._baudrate))
346 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=5, writeTimeout=10000)
348 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
349 if self._serial is None:
350 baudrate = self._baudrate
352 baudrate = self._baudrateDetectList.pop(0)
353 if len(self._serialDetectList) < 1:
354 self._log("Found no ports to try for auto detection")
355 self._errorValue = 'Failed to autodetect serial port.'
356 self._changeState(self.STATE_ERROR)
358 port = self._serialDetectList.pop(0)
359 self._log("Connecting to: %s with baudrate: %s (auto)" % (port, baudrate))
361 self._serial = serial.Serial(port, baudrate, timeout=3, writeTimeout=10000)
365 self._log("Connected to: %s, starting monitor" % (self._serial))
366 if self._baudrate == 0:
367 self._changeState(self.STATE_DETECT_BAUDRATE)
369 self._changeState(self.STATE_CONNECTING)
371 #Start monitoring the serial port.
372 if self._state == self.STATE_CONNECTING:
373 timeout = time.time() + 15
375 timeout = time.time() + 5
376 tempRequestTimeout = timeout
378 line = self._readline()
382 #No matter the state, if we see an fatal error, goto the error state and store the error for reference.
383 # Only goto error on known fatal errors.
384 if line.startswith('Error:'):
385 #Oh YEAH, consistency.
386 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
387 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
388 # So we can have an extra newline in the most common case. Awesome work people.
389 if re.match('Error:[0-9]\n', line):
390 line = line.rstrip() + self._readline()
391 #Skip the communication errors, as those get corrected.
392 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:
393 if not self.isError():
394 self._errorValue = line[6:]
395 self._changeState(self.STATE_ERROR)
396 if ' T:' in line or line.startswith('T:'):
397 tempRequestTimeout = time.time() + 5
399 self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
404 self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
407 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
408 #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.
409 if not 'ok' in line and self._heatupWaitStartTime != 0:
411 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
412 self._heatupWaitStartTime = t
413 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and \
414 not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and \
415 not line.startswith('Error:No Checksum with line number') and not line.startswith('Error:No Line Number with checksum') and \
416 line != 'echo:Unknown command:""\n' and self.isOperational():
417 self._callback.mcMessage(line)
419 if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
420 if line == '' or time.time() > timeout:
421 if len(self._baudrateDetectList) < 1:
423 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
424 self._changeState(self.STATE_ERROR)
425 elif self._baudrateDetectRetry > 0:
426 self._baudrateDetectRetry -= 1
427 self._serial.write('\n')
428 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
429 self._sendCommand("M105")
430 self._testingBaudrate = True
432 if self._state == self.STATE_DETECT_SERIAL:
433 if len(self._serialDetectList) == 0:
434 if len(self._baudrateDetectList) == 0:
435 self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
436 self._errorValue = 'Failed to autodetect serial port.'
437 self._changeState(self.STATE_ERROR)
440 self._serialDetectList = serialList(True)
441 baudrate = self._baudrateDetectList.pop(0)
443 self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=2.5, writeTimeout=10000)
445 baudrate = self._baudrateDetectList.pop(0)
447 self._setBaudrate(baudrate)
448 self._serial.timeout = 0.5
449 self._log("Trying baudrate: %d" % (baudrate))
450 self._baudrateDetectRetry = 5
451 self._baudrateDetectTestOk = 0
452 timeout = time.time() + 5
453 self._serial.write('\n')
454 self._sendCommand("M105")
455 self._testingBaudrate = True
457 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
459 self._baudrateDetectTestOk += 1
460 if self._baudrateDetectTestOk < 10:
461 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
462 self._sendCommand("M105")
464 self._sendCommand("M999")
465 self._serial.timeout = 2
466 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
467 self._changeState(self.STATE_OPERATIONAL)
469 self._testingBaudrate = False
470 elif self._state == self.STATE_CONNECTING:
471 if line == '' or 'wait' in line or 'start' in line: # 'wait' needed for Repetier (kind of watchdog)
472 self._sendCommand("M105")
474 self._changeState(self.STATE_OPERATIONAL)
475 if time.time() > timeout:
477 elif self._state == self.STATE_OPERATIONAL:
478 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
480 if self._extruderCount > 0:
481 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
482 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
484 self.sendCommand("M105")
485 tempRequestTimeout = time.time() + 5
488 elif 'start' in line:
489 self._currentCommands = []
490 elif self._state == self.STATE_PRINTING:
491 #Even when printing request the temperature every 5 seconds.
492 if time.time() > tempRequestTimeout:
493 if self._extruderCount > 0:
494 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
495 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
497 self.sendCommand("M105")
498 tempRequestTimeout = time.time() + 5
499 if line == '' and time.time() > timeout:
500 self._log("Communication timeout during printing, forcing a line")
504 timeout = time.time() + 5
505 if not self._commandQueue.empty():
506 self._sendCommand(self._commandQueue.get())
509 if "G28" in self._currentCommands[0] or "G29" in self._currentCommands[0] or \
510 "M109" in self._currentCommands[0] or "M190" in self._currentCommands[0]:
511 # Long command detected. Timeout is now set to 60s to avoid forcing 'ok'
512 # every 5 seconds while it's not needed
513 timeout = time.time() + 60
515 elif 'start' in line:
516 self._currentCommands = []
517 elif "resend" in line.lower() or "rs" in line:
518 newPos = self._gcodePos
520 newPos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
523 newPos = int(line.split()[1])
524 # If we need to resend more than 10 lines, we can assume that the machine
525 # was shut down and turned back on or something else that's weird just happened.
526 # In that case, it can be dangerous to restart the print, so we'd better kill it
527 if newPos == 1 or self._gcodePos > newPos + 100:
528 self._callback.mcMessage("Print canceled due to loss of communication to printer (USB unplugged or power lost)")
531 self._gcodePos = newPos
532 elif self._state == self.STATE_PAUSED:
533 #Even when printing request the temperature every 5 seconds.
534 if time.time() > tempRequestTimeout:
535 if self._extruderCount > 0:
536 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
537 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
539 self.sendCommand("M105")
540 tempRequestTimeout = time.time() + 5
541 if line == '' and time.time() > timeout:
545 timeout = time.time() + 5
546 if not self._commandQueue.empty():
547 self._sendCommand(self._commandQueue.get())
548 elif 'start' in line:
549 self._currentCommands = []
551 self._log("Connection closed, closing down monitor")
553 def _setBaudrate(self, baudrate):
555 self._serial.baudrate = baudrate
557 print getExceptionString()
559 def _log(self, message):
560 #sys.stderr.write(message + "\n");
561 self._callback.mcLog(message)
563 self._logQueue.put(message, False)
565 #If the log queue is full, remove the first message and append the new message again
568 self._logQueue.put(message, False)
573 if self._serial is None:
576 ret = self._serial.readline()
578 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
579 self._errorValue = getExceptionString()
583 #self._log("Recv: TIMEOUT")
585 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
588 def close(self, isError = False):
589 if self._serial != None:
592 self._changeState(self.STATE_CLOSED_WITH_ERROR)
594 self._changeState(self.STATE_CLOSED)
600 def _sendCommand(self, cmd):
601 self._thread_lock.acquire(True)
602 if self._serial is None:
603 self._thread_lock.release()
605 if 'M109' in cmd or 'M190' in cmd:
606 self._heatupWaitStartTime = time.time()
607 if 'M104' in cmd or 'M109' in cmd:
611 t = int(re.search('T([0-9]+)', cmd).group(1))
612 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
615 if 'M140' in cmd or 'M190' in cmd:
617 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
620 self._log('Send: %s' % (cmd))
621 if self.isOperational():
622 self._currentCommands.append(cmd)
624 self._serial.write(cmd + '\n')
625 except serial.SerialTimeoutException:
626 self._log("Serial timeout while writing to serial port, trying again.")
629 self._serial.write(cmd + '\n')
631 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
632 self._errorValue = getExceptionString()
635 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
636 self._errorValue = getExceptionString()
638 self._thread_lock.release()
641 if self._gcodePos >= len(self._gcodeList):
642 self._changeState(self.STATE_OPERATIONAL)
644 if self._gcodePos == 100:
645 self._printStartTime100 = time.time()
646 line = self._gcodeList[self._gcodePos]
647 if type(line) is tuple:
648 self._printSection = line[1]
651 if line == 'M0' or line == 'M1':
653 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
654 if self._printSection in self._feedRateModifier:
655 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
656 if ('G0' in line or 'G1' in line) and 'Z' in line:
657 z = float(re.search('Z(-?[0-9\.]*)', line).group(1))
658 if self._currentZ != z:
660 self._callback.mcZChange(z)
662 self._log("Unexpected error: %s" % (getExceptionString()))
663 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
666 self._sendCommand("N%d%s*%d" % (pos, line, checksum))
667 self._callback.mcProgress(self._gcodePos)
669 def sendCommand(self, cmd):
670 cmd = cmd.encode('ascii', 'replace')
671 if self.isPrinting() or self.isPaused():
672 self._commandQueue.put(cmd)
673 if len(self._currentCommands) == 0:
674 self._sendCommand(self._commandQueue.get())
675 elif self.isOperational():
676 self._sendCommand(cmd)
678 def printGCode(self, gcodeList):
679 if not self.isOperational() or self.isPrinting() or self.isPaused():
681 self._gcodeList = gcodeList
683 self._printStartTime100 = None
684 self._printSection = 'CUSTOM'
685 self._changeState(self.STATE_PRINTING)
686 self._printStartTime = time.time()
687 for i in xrange(0, 4):
690 def cancelPrint(self):
691 if self.isOperational():
692 self._changeState(self.STATE_OPERATIONAL)
694 def setPause(self, pause):
695 if not pause and self.isPaused():
696 self._changeState(self.STATE_PRINTING)
697 for i in xrange(0, 4):
698 if not self._commandQueue.empty():
699 self._sendCommand(self._commandQueue.get())
702 if pause and self.isPrinting():
703 self._changeState(self.STATE_PAUSED)
705 def setFeedrateModifier(self, type, value):
706 self._feedRateModifier[type] = value
708 def getExceptionString():
709 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
710 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])