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*")
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 not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and line != 'echo:Unknown command:""\n' and self.isOperational():
399 self._callback.mcMessage(line)
401 if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
402 if line == '' or time.time() > timeout:
403 if len(self._baudrateDetectList) < 1:
405 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
406 self._changeState(self.STATE_ERROR)
407 elif self._baudrateDetectRetry > 0:
408 self._baudrateDetectRetry -= 1
409 self._serial.write('\n')
410 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
411 self._sendCommand("M105")
412 self._testingBaudrate = True
414 if self._state == self.STATE_DETECT_SERIAL:
415 if len(self._serialDetectList) == 0:
416 if len(self._baudrateDetectList) == 0:
417 self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
418 self._errorValue = 'Failed to autodetect serial port.'
419 self._changeState(self.STATE_ERROR)
422 self._serialDetectList = serialList(True)
423 baudrate = self._baudrateDetectList.pop(0)
425 self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=2.5, writeTimeout=10000)
427 baudrate = self._baudrateDetectList.pop(0)
429 self._setBaudrate(baudrate)
430 self._serial.timeout = 0.5
431 self._log("Trying baudrate: %d" % (baudrate))
432 self._baudrateDetectRetry = 5
433 self._baudrateDetectTestOk = 0
434 timeout = time.time() + 5
435 self._serial.write('\n')
436 self._sendCommand("M105")
437 self._testingBaudrate = True
439 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
441 self._baudrateDetectTestOk += 1
442 if self._baudrateDetectTestOk < 10:
443 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
444 self._sendCommand("M105")
446 self._sendCommand("M999")
447 self._serial.timeout = 2
448 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
449 self._changeState(self.STATE_OPERATIONAL)
451 self._testingBaudrate = False
452 elif self._state == self.STATE_CONNECTING:
453 if line == '' or 'wait' in line: # 'wait' needed for Repetier (kind of watchdog)
454 self._sendCommand("M105")
456 self._changeState(self.STATE_OPERATIONAL)
457 if time.time() > timeout:
459 elif self._state == self.STATE_OPERATIONAL:
460 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
462 if self._extruderCount > 0:
463 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
464 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
466 self.sendCommand("M105")
467 tempRequestTimeout = time.time() + 5
468 elif self._state == self.STATE_PRINTING:
469 #Even when printing request the temperature every 5 seconds.
470 if time.time() > tempRequestTimeout:
471 if self._extruderCount > 0:
472 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
473 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
475 self.sendCommand("M105")
476 tempRequestTimeout = time.time() + 5
477 if line == '' and time.time() > timeout:
478 self._log("Communication timeout during printing, forcing a line")
481 timeout = time.time() + 5
482 if not self._commandQueue.empty():
483 self._sendCommand(self._commandQueue.get())
486 elif "resend" in line.lower() or "rs" in line:
488 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
491 self._gcodePos = int(line.split()[1])
492 self._log("Connection closed, closing down monitor")
494 def _setBaudrate(self, baudrate):
495 #For linux the pyserial implementation lacks TCGETS2 support. So do that ourselves
496 if sys.platform.startswith('linux'):
498 self._serial.baudrate = baudrate
502 import fcntl, array, termios
506 buf = array.array('i', [0] * 64)
507 fcntl.ioctl(self._serial.fd, TCGETS2, buf)
508 buf[2] &= ~termios.CBAUD
510 buf[9] = buf[10] = baudrate
511 fcntl.ioctl(self._serial.fd, TCSETS2, buf)
513 print getExceptionString()
515 self._serial.baudrate = baudrate
517 def _log(self, message):
518 self._callback.mcLog(message)
520 self._logQueue.put(message, False)
522 #If the log queue is full, remove the first message and append the new message again
525 self._logQueue.put(message, False)
530 if self._serial is None:
533 ret = self._serial.readline()
535 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
536 self._errorValue = getExceptionString()
540 #self._log("Recv: TIMEOUT")
542 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
545 def close(self, isError = False):
546 if self._serial != None:
549 self._changeState(self.STATE_CLOSED_WITH_ERROR)
551 self._changeState(self.STATE_CLOSED)
557 def _sendCommand(self, cmd):
558 if self._serial is None:
560 if 'M109' in cmd or 'M190' in cmd:
561 self._heatupWaitStartTime = time.time()
562 if 'M104' in cmd or 'M109' in cmd:
566 t = int(re.search('T([0-9]+)', cmd).group(1))
567 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
570 if 'M140' in cmd or 'M190' in cmd:
572 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
575 self._log('Send: %s' % (cmd))
577 self._serial.write(cmd + '\n')
578 except serial.SerialTimeoutException:
579 self._log("Serial timeout while writing to serial port, trying again.")
582 self._serial.write(cmd + '\n')
584 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
585 self._errorValue = getExceptionString()
588 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
589 self._errorValue = getExceptionString()
593 if self._gcodePos >= len(self._gcodeList):
594 self._changeState(self.STATE_OPERATIONAL)
596 if self._gcodePos == 100:
597 self._printStartTime100 = time.time()
598 line = self._gcodeList[self._gcodePos]
599 if type(line) is tuple:
600 self._printSection = line[1]
603 if line == 'M0' or line == 'M1':
605 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
606 if self._printSection in self._feedRateModifier:
607 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
608 if ('G0' in line or 'G1' in line) and 'Z' in line:
609 z = float(re.search('Z([0-9\.]*)', line).group(1))
610 if self._currentZ != z:
612 self._callback.mcZChange(z)
614 self._log("Unexpected error: %s" % (getExceptionString()))
615 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
616 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
618 self._callback.mcProgress(self._gcodePos)
620 def sendCommand(self, cmd):
621 cmd = cmd.encode('ascii', 'replace')
622 if self.isPrinting():
623 self._commandQueue.put(cmd)
624 elif self.isOperational():
625 self._sendCommand(cmd)
627 def printGCode(self, gcodeList):
628 if not self.isOperational() or self.isPrinting():
630 self._gcodeList = gcodeList
632 self._printStartTime100 = None
633 self._printSection = 'CUSTOM'
634 self._changeState(self.STATE_PRINTING)
635 self._printStartTime = time.time()
636 for i in xrange(0, 4):
639 def cancelPrint(self):
640 if self.isOperational():
641 self._changeState(self.STATE_OPERATIONAL)
643 def setPause(self, pause):
644 if not pause and self.isPaused():
645 self._changeState(self.STATE_PRINTING)
646 for i in xrange(0, 6):
648 if pause and self.isPrinting():
649 self._changeState(self.STATE_PAUSED)
651 def setFeedrateModifier(self, type, value):
652 self._feedRateModifier[type] = value
654 def getExceptionString():
655 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
656 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])