1 from __future__ import absolute_import
2 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
17 from Cura.avr_isp import stk500v2
18 from Cura.avr_isp import ispBase
20 from Cura.util import profile
21 from Cura.util import version
28 def serialList(forAutoDetect=False):
30 if platform.system() == "Windows":
32 key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
35 values = _winreg.EnumValue(key, i)
36 if not forAutoDetect or 'USBSER' in values[0]:
42 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.usb*")
43 baselist = filter(lambda s: not 'Bluetooth' in s, baselist)
44 prev = profile.getMachineSetting('serial_port_auto')
47 baselist.insert(0, prev)
49 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*")
50 if version.isDevVersion() and not forAutoDetect:
51 baselist.append('VIRTUAL')
54 def machineIsConnected():
55 #UltiGCode is designed for SD-Card printing, so never auto-detect the serial port.
56 port = profile.getMachineSetting('serial_port')
58 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
60 return len(serialList(True)) > 0
61 if platform.system() == "Windows":
62 return port in serialList()
63 return os.path.isfile(port)
66 ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
67 if profile.getMachineSetting('serial_baud_auto') != '':
68 prev = int(profile.getMachineSetting('serial_baud_auto'))
74 class VirtualPrinter():
76 self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
79 self.lastTempAt = time.time()
81 self.bedTargetTemp = 1.0
83 def write(self, data):
84 if self.readList is None:
86 #print "Send: %s" % (data.rstrip())
87 if 'M104' in data or 'M109' in data:
89 self.targetTemp = float(re.search('S([0-9]+)', data).group(1))
92 if 'M140' in data or 'M190' in data:
94 self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
98 self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
99 elif len(data.strip()) > 0:
100 self.readList.append("ok\n")
103 if self.readList is None:
106 timeDiff = self.lastTempAt - time.time()
107 self.lastTempAt = time.time()
108 if abs(self.temp - self.targetTemp) > 1:
109 self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp)
110 if abs(self.bedTemp - self.bedTargetTemp) > 1:
111 self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
112 while len(self.readList) < 1:
117 if self.readList is None:
120 #print "Recv: %s" % (self.readList[0].rstrip())
121 return self.readList.pop(0)
126 class MachineComPrintCallback(object):
127 def mcLog(self, message):
130 def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
133 def mcStateChange(self, state):
136 def mcMessage(self, message):
139 def mcProgress(self, lineNr):
142 def mcZChange(self, newZ):
145 class MachineCom(object):
147 STATE_OPEN_SERIAL = 1
148 STATE_DETECT_SERIAL = 2
149 STATE_DETECT_BAUDRATE = 3
151 STATE_OPERATIONAL = 5
156 STATE_CLOSED_WITH_ERROR = 10
158 def __init__(self, port = None, baudrate = None, callbackObject = None):
160 port = profile.getMachineSetting('serial_port')
162 if profile.getMachineSetting('serial_baud') == 'AUTO':
165 baudrate = int(profile.getMachineSetting('serial_baud'))
166 if callbackObject is None:
167 callbackObject = MachineComPrintCallback()
170 self._baudrate = baudrate
171 self._callback = callbackObject
172 self._state = self.STATE_NONE
174 self._serialDetectList = []
175 self._baudrateDetectList = baudrateList()
176 self._baudrateDetectRetry = 0
177 self._extruderCount = int(profile.getMachineSetting('extruder_amount'))
178 self._temperatureRequestExtruder = 0
179 self._temp = [0] * self._extruderCount
180 self._targetTemp = [0] * self._extruderCount
182 self._bedTargetTemp = 0
183 self._gcodeList = None
185 self._commandQueue = queue.Queue()
186 self._logQueue = queue.Queue(256)
187 self._feedRateModifier = {}
189 self._heatupWaitStartTime = 0
190 self._heatupWaitTimeLost = 0.0
191 self._printStartTime100 = None
193 self.thread = threading.Thread(target=self._monitor)
194 self.thread.daemon = True
197 def _changeState(self, newState):
198 if self._state == newState:
200 oldState = self.getStateString()
201 self._state = newState
202 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
203 self._callback.mcStateChange(newState)
208 def getStateString(self):
209 if self._state == self.STATE_NONE:
211 if self._state == self.STATE_OPEN_SERIAL:
212 return "Opening serial port"
213 if self._state == self.STATE_DETECT_SERIAL:
214 return "Detecting serial port"
215 if self._state == self.STATE_DETECT_BAUDRATE:
216 return "Detecting baudrate"
217 if self._state == self.STATE_CONNECTING:
219 if self._state == self.STATE_OPERATIONAL:
221 if self._state == self.STATE_PRINTING:
223 if self._state == self.STATE_PAUSED:
225 if self._state == self.STATE_CLOSED:
227 if self._state == self.STATE_ERROR:
228 return "Error: %s" % (self.getShortErrorString())
229 if self._state == self.STATE_CLOSED_WITH_ERROR:
230 return "Error: %s" % (self.getShortErrorString())
231 return "?%d?" % (self._state)
233 def getShortErrorString(self):
234 if len(self._errorValue) < 20:
235 return self._errorValue
236 return self._errorValue[:20] + "..."
238 def getErrorString(self):
239 return self._errorValue
241 def isClosedOrError(self):
242 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
245 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
247 def isOperational(self):
248 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
250 def isPrinting(self):
251 return self._state == self.STATE_PRINTING
254 return self._state == self.STATE_PAUSED
256 def getPrintPos(self):
257 return self._gcodePos
259 def getPrintTime(self):
260 return time.time() - self._printStartTime
262 def getPrintTimeRemainingEstimate(self):
263 if self._printStartTime100 is None or self.getPrintPos() < 200:
265 printTime = (time.time() - self._printStartTime100) / 60
266 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
267 printTimeLeft = printTimeTotal - printTime
273 def getBedTemp(self):
278 while not self._logQueue.empty():
279 ret.append(self._logQueue.get())
281 self._logQueue.put(line, False)
285 #Open the serial port.
286 if self._port == 'AUTO':
287 self._changeState(self.STATE_DETECT_SERIAL)
288 self._log("Serial port list: %s" % (str(serialList(True))))
289 programmer = stk500v2.Stk500v2()
290 for p in serialList(True):
292 self._log("Connecting to: %s" % (p))
293 programmer.connect(p)
294 self._serial = programmer.leaveISP()
295 profile.putMachineSetting('serial_port_auto', p)
297 except ispBase.IspError as (e):
298 self._log("Error while connecting to %s: %s" % (p, str(e)))
301 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
303 if self._serial is None:
304 self._serialDetectList = serialList(True)
305 elif self._port == 'VIRTUAL':
306 self._changeState(self.STATE_OPEN_SERIAL)
307 self._serial = VirtualPrinter()
309 self._changeState(self.STATE_OPEN_SERIAL)
311 self._log("Connecting to: %s" % (self._port))
312 if self._baudrate == 0:
313 self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
315 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
317 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
318 if self._serial is None:
319 baudrate = self._baudrate
321 baudrate = self._baudrateDetectList.pop(0)
322 self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=0.1, writeTimeout=10000)
324 self._log("Connected to: %s, starting monitor" % (self._serial))
325 if self._baudrate == 0:
326 self._changeState(self.STATE_DETECT_BAUDRATE)
328 self._changeState(self.STATE_CONNECTING)
329 #Start monitoring the serial port.
330 if self._state == self.STATE_CONNECTING:
331 timeout = time.time() + 15
333 timeout = time.time() + 5
334 tempRequestTimeout = timeout
336 line = self._readline()
340 #No matter the state, if we see an fatal error, goto the error state and store the error for reference.
341 # Only goto error on known fatal errors.
342 if line.startswith('Error:'):
343 #Oh YEAH, consistency.
344 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
345 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
346 # So we can have an extra newline in the most common case. Awesome work people.
347 if re.match('Error:[0-9]\n', line):
348 line = line.rstrip() + self._readline()
349 #Skip the communication errors, as those get corrected.
350 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:
351 if not self.isError():
352 self._errorValue = line[6:]
353 self._changeState(self.STATE_ERROR)
354 if ' T:' in line or line.startswith('T:'):
356 self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
361 self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
364 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
365 #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.
366 if not 'ok' in line and self._heatupWaitStartTime != 0:
368 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
369 self._heatupWaitStartTime = t
370 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():
371 self._callback.mcMessage(line)
373 if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
374 if line == '' or time.time() > timeout:
375 if len(self._baudrateDetectList) < 1:
377 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
378 self._changeState(self.STATE_ERROR)
379 elif self._baudrateDetectRetry > 0:
380 self._baudrateDetectRetry -= 1
381 self._serial.write('\n')
382 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
383 self._sendCommand("M105")
384 self._testingBaudrate = True
386 if self._state == self.STATE_DETECT_SERIAL:
387 if len(self._serialDetectList) == 0:
388 if len(self._baudrateDetectList) == 0:
389 self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
390 self._errorValue = 'Failed to autodetect serial port.'
391 self._changeState(self.STATE_ERROR)
394 self._serialDetectList = serialList(True)
395 baudrate = self._baudrateDetectList.pop(0)
397 self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=0.5, writeTimeout=10000)
399 baudrate = self._baudrateDetectList.pop(0)
401 self._setBaudrate(baudrate)
402 self._serial.timeout = 0.5
403 self._log("Trying baudrate: %d" % (baudrate))
404 self._baudrateDetectRetry = 5
405 self._baudrateDetectTestOk = 0
406 timeout = time.time() + 5
407 self._serial.write('\n')
408 self._sendCommand("M105")
409 self._testingBaudrate = True
411 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
413 self._baudrateDetectTestOk += 1
414 if self._baudrateDetectTestOk < 10:
415 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
416 self._sendCommand("M105")
418 self._sendCommand("M999")
419 self._serial.timeout = 2
420 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
421 self._changeState(self.STATE_OPERATIONAL)
423 self._testingBaudrate = False
424 elif self._state == self.STATE_CONNECTING:
425 if line == '' or 'wait' in line: # 'wait' needed for Repetier (kind of watchdog)
426 self._sendCommand("M105")
428 self._changeState(self.STATE_OPERATIONAL)
429 if time.time() > timeout:
431 elif self._state == self.STATE_OPERATIONAL:
432 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
434 if self._extruderCount > 0:
435 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
436 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
438 self._sendCommand("M105")
439 tempRequestTimeout = time.time() + 5
440 elif self._state == self.STATE_PRINTING:
441 if line == '' and time.time() > timeout:
442 self._log("Communication timeout during printing, forcing a line")
444 #Even when printing request the temperature every 5 seconds.
445 if time.time() > tempRequestTimeout:
446 if self._extruderCount > 0:
447 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
448 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
450 self._sendCommand("M105")
451 tempRequestTimeout = time.time() + 5
453 timeout = time.time() + 5
454 if not self._commandQueue.empty():
455 self._sendCommand(self._commandQueue.get())
458 elif "resend" in line.lower() or "rs" in line:
460 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
463 self._gcodePos = int(line.split()[1])
464 self._log("Connection closed, closing down monitor")
466 def _setBaudrate(self, baudrate):
467 #For linux the pyserial implementation lacks TCGETS2 support. So do that ourselves
468 if sys.platform.startswith('linux'):
470 self._serial.baudrate = baudrate
474 import fcntl, array, termios
478 buf = array.array('i', [0] * 64)
479 fcntl.ioctl(self._serial.fd, TCGETS2, buf)
480 buf[2] &= ~termios.CBAUD
482 buf[9] = buf[10] = baudrate
483 fcntl.ioctl(self._serial.fd, TCSETS2, buf)
485 print getExceptionString()
487 self._serial.baudrate = baudrate
489 def _log(self, message):
490 self._callback.mcLog(message)
492 self._logQueue.put(message, False)
494 #If the log queue is full, remove the first message and append the new message again
497 self._logQueue.put(message, False)
502 if self._serial == None:
505 ret = self._serial.readline()
507 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
508 self._errorValue = getExceptionString()
512 #self._log("Recv: TIMEOUT")
514 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
517 def close(self, isError = False):
518 if self._serial != None:
521 self._changeState(self.STATE_CLOSED_WITH_ERROR)
523 self._changeState(self.STATE_CLOSED)
529 def _sendCommand(self, cmd):
530 if self._serial is None:
532 if 'M109' in cmd or 'M190' in cmd:
533 self._heatupWaitStartTime = time.time()
534 if 'M104' in cmd or 'M109' in cmd:
538 t = int(re.search('T([0-9]+)', cmd).group(1))
539 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
542 if 'M140' in cmd or 'M190' in cmd:
544 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
547 self._log('Send: %s' % (cmd))
549 self._serial.write(cmd + '\n')
550 except serial.SerialTimeoutException:
551 self._log("Serial timeout while writing to serial port, trying again.")
554 self._serial.write(cmd + '\n')
556 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
557 self._errorValue = getExceptionString()
560 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
561 self._errorValue = getExceptionString()
565 if self._gcodePos >= len(self._gcodeList):
566 self._changeState(self.STATE_OPERATIONAL)
568 if self._gcodePos == 100:
569 self._printStartTime100 = time.time()
570 line = self._gcodeList[self._gcodePos]
571 if type(line) is tuple:
572 self._printSection = line[1]
575 if line == 'M0' or line == 'M1':
577 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
578 if self._printSection in self._feedRateModifier:
579 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
580 if ('G0' in line or 'G1' in line) and 'Z' in line:
581 z = float(re.search('Z([0-9\.]*)', line).group(1))
582 if self._currentZ != z:
584 self._callback.mcZChange(z)
586 self._log("Unexpected error: %s" % (getExceptionString()))
587 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
588 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
590 self._callback.mcProgress(self._gcodePos)
592 def sendCommand(self, cmd):
593 cmd = cmd.encode('ascii', 'replace')
594 if self.isPrinting():
595 self._commandQueue.put(cmd)
596 elif self.isOperational():
597 self._sendCommand(cmd)
599 def printGCode(self, gcodeList):
600 if not self.isOperational() or self.isPrinting():
602 self._gcodeList = gcodeList
604 self._printStartTime100 = None
605 self._printSection = 'CUSTOM'
606 self._changeState(self.STATE_PRINTING)
607 self._printStartTime = time.time()
608 for i in xrange(0, 4):
611 def cancelPrint(self):
612 if self.isOperational():
613 self._changeState(self.STATE_OPERATIONAL)
615 def setPause(self, pause):
616 if not pause and self.isPaused():
617 self._changeState(self.STATE_PRINTING)
618 for i in xrange(0, 6):
620 if pause and self.isPrinting():
621 self._changeState(self.STATE_PAUSED)
623 def setFeedrateModifier(self, type, value):
624 self._feedRateModifier[type] = value
626 def getExceptionString():
627 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
628 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])