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._baudrateDetectList = baudrateList()
175 self._baudrateDetectRetry = 0
176 self._extruderCount = int(profile.getMachineSetting('extruder_amount'))
177 self._temperatureRequestExtruder = 0
178 self._temp = [0] * self._extruderCount
179 self._targetTemp = [0] * self._extruderCount
181 self._bedTargetTemp = 0
182 self._gcodeList = None
184 self._commandQueue = queue.Queue()
185 self._logQueue = queue.Queue(256)
186 self._feedRateModifier = {}
188 self._heatupWaitStartTime = 0
189 self._heatupWaitTimeLost = 0.0
190 self._printStartTime100 = None
192 self.thread = threading.Thread(target=self._monitor)
193 self.thread.daemon = True
196 def _changeState(self, newState):
197 if self._state == newState:
199 oldState = self.getStateString()
200 self._state = newState
201 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
202 self._callback.mcStateChange(newState)
207 def getStateString(self):
208 if self._state == self.STATE_NONE:
210 if self._state == self.STATE_OPEN_SERIAL:
211 return "Opening serial port"
212 if self._state == self.STATE_DETECT_SERIAL:
213 return "Detecting serial port"
214 if self._state == self.STATE_DETECT_BAUDRATE:
215 return "Detecting baudrate"
216 if self._state == self.STATE_CONNECTING:
218 if self._state == self.STATE_OPERATIONAL:
220 if self._state == self.STATE_PRINTING:
222 if self._state == self.STATE_PAUSED:
224 if self._state == self.STATE_CLOSED:
226 if self._state == self.STATE_ERROR:
227 return "Error: %s" % (self.getShortErrorString())
228 if self._state == self.STATE_CLOSED_WITH_ERROR:
229 return "Error: %s" % (self.getShortErrorString())
230 return "?%d?" % (self._state)
232 def getShortErrorString(self):
233 if len(self._errorValue) < 20:
234 return self._errorValue
235 return self._errorValue[:20] + "..."
237 def getErrorString(self):
238 return self._errorValue
240 def isClosedOrError(self):
241 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
244 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
246 def isOperational(self):
247 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
249 def isPrinting(self):
250 return self._state == self.STATE_PRINTING
253 return self._state == self.STATE_PAUSED
255 def getPrintPos(self):
256 return self._gcodePos
258 def getPrintTime(self):
259 return time.time() - self._printStartTime
261 def getPrintTimeRemainingEstimate(self):
262 if self._printStartTime100 is None or self.getPrintPos() < 200:
264 printTime = (time.time() - self._printStartTime100) / 60
265 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
266 printTimeLeft = printTimeTotal - printTime
272 def getBedTemp(self):
277 while not self._logQueue.empty():
278 ret.append(self._logQueue.get())
280 self._logQueue.put(line, False)
284 #Open the serial port.
285 if self._port == 'AUTO':
286 self._changeState(self.STATE_DETECT_SERIAL)
287 programmer = stk500v2.Stk500v2()
288 self._log("Serial port list: %s" % (str(serialList(True))))
289 for p in serialList(True):
291 self._log("Connecting to: %s" % (p))
292 programmer.connect(p)
293 self._serial = programmer.leaveISP()
294 profile.putMachineSetting('serial_port_auto', p)
296 except ispBase.IspError as (e):
297 self._log("Error while connecting to %s: %s" % (p, str(e)))
300 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
302 elif self._port == 'VIRTUAL':
303 self._changeState(self.STATE_OPEN_SERIAL)
304 self._serial = VirtualPrinter()
306 self._changeState(self.STATE_OPEN_SERIAL)
308 self._log("Connecting to: %s" % (self._port))
309 if self._baudrate == 0:
310 self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
312 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
314 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
315 if self._serial == None:
316 self._log("Failed to open serial port (%s)" % (self._port))
317 self._errorValue = 'Failed to autodetect serial port.'
318 self._changeState(self.STATE_ERROR)
320 self._log("Connected to: %s, starting monitor" % (self._serial))
321 if self._baudrate == 0:
322 self._changeState(self.STATE_DETECT_BAUDRATE)
324 self._changeState(self.STATE_CONNECTING)
326 #Start monitoring the serial port.
327 if self._state == self.STATE_CONNECTING:
328 timeout = time.time() + 15
330 timeout = time.time() + 5
331 tempRequestTimeout = timeout
333 line = self._readline()
337 #No matter the state, if we see an error, goto the error state and store the error for reference.
338 if line.startswith('Error:'):
339 #Oh YEAH, consistency.
340 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
341 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
342 # So we can have an extra newline in the most common case. Awesome work people.
343 if re.match('Error:[0-9]\n', line):
344 line = line.rstrip() + self._readline()
345 #Skip the communication errors, as those get corrected.
346 if 'checksum mismatch' in line or 'Line Number is not Last Line Number' in line or 'No Line Number with checksum' in line or 'No Checksum with line number' in line:
348 elif not self.isError():
349 self._errorValue = line[6:]
350 self._changeState(self.STATE_ERROR)
351 if ' T:' in line or line.startswith('T:'):
352 self._temp[self._temperatureRequestExtruder] = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
354 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
355 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
356 #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.
357 if not 'ok' in line and self._heatupWaitStartTime != 0:
359 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
360 self._heatupWaitStartTime = t
361 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
362 self._callback.mcMessage(line)
364 if self._state == self.STATE_DETECT_BAUDRATE:
365 if line == '' or time.time() > timeout:
366 if len(self._baudrateDetectList) < 1:
368 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
369 self._changeState(self.STATE_ERROR)
370 elif self._baudrateDetectRetry > 0:
371 self._baudrateDetectRetry -= 1
372 self._serial.write('\n')
373 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
374 self._sendCommand("M105")
375 self._testingBaudrate = True
377 baudrate = self._baudrateDetectList.pop(0)
379 self._setBaudrate(baudrate)
380 self._serial.timeout = 0.5
381 self._log("Trying baudrate: %d" % (baudrate))
382 self._baudrateDetectRetry = 5
383 self._baudrateDetectTestOk = 0
384 timeout = time.time() + 5
385 self._serial.write('\n')
386 self._sendCommand("M105")
387 self._testingBaudrate = True
389 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
391 self._baudrateDetectTestOk += 1
392 if self._baudrateDetectTestOk < 10:
393 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
394 self._sendCommand("M105")
396 self._sendCommand("M999")
397 self._serial.timeout = 2
398 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
399 self._changeState(self.STATE_OPERATIONAL)
401 self._testingBaudrate = False
402 elif self._state == self.STATE_CONNECTING:
404 self._sendCommand("M105")
406 self._changeState(self.STATE_OPERATIONAL)
407 if time.time() > timeout:
409 elif self._state == self.STATE_OPERATIONAL:
410 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
412 if self._extruderCount > 0:
413 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
414 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
416 self._sendCommand("M105")
417 tempRequestTimeout = time.time() + 5
418 elif self._state == self.STATE_PRINTING:
419 if line == '' and time.time() > timeout:
420 self._log("Communication timeout during printing, forcing a line")
422 #Even when printing request the temperature every 5 seconds.
423 if time.time() > tempRequestTimeout:
424 if self._extruderCount > 0:
425 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
426 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
428 self._sendCommand("M105")
429 tempRequestTimeout = time.time() + 5
431 timeout = time.time() + 5
432 if not self._commandQueue.empty():
433 self._sendCommand(self._commandQueue.get())
436 elif "resend" in line.lower() or "rs" in line:
438 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
441 self._gcodePos = int(line.split()[1])
442 self._log("Connection closed, closing down monitor")
444 def _setBaudrate(self, baudrate):
445 #For linux the pyserial implementation lacks TCGETS2 support. So do that ourselves
446 if sys.platform.startswith('linux'):
448 self._serial.baudrate = baudrate
452 import fcntl, array, termios
456 buf = array.array('i', [0] * 64)
457 fcntl.ioctl(self._serial.fd, TCGETS2, buf)
458 buf[2] &= ~termios.CBAUD
460 buf[9] = buf[10] = baudrate
461 fcntl.ioctl(self._serial.fd, TCSETS2, buf)
463 print getExceptionString()
465 self._serial.baudrate = baudrate
467 def _log(self, message):
468 self._callback.mcLog(message)
470 self._logQueue.put(message, False)
472 #If the log queue is full, remove the first message and append the new message again
475 self._logQueue.put(message, False)
480 if self._serial == None:
483 ret = self._serial.readline()
485 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
486 self._errorValue = getExceptionString()
490 #self._log("Recv: TIMEOUT")
492 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
495 def close(self, isError = False):
496 if self._serial != None:
499 self._changeState(self.STATE_CLOSED_WITH_ERROR)
501 self._changeState(self.STATE_CLOSED)
507 def _sendCommand(self, cmd):
508 if self._serial is None:
510 if 'M109' in cmd or 'M190' in cmd:
511 self._heatupWaitStartTime = time.time()
512 if 'M104' in cmd or 'M109' in cmd:
516 t = int(re.search('T([0-9]+)', cmd).group(1))
517 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
520 if 'M140' in cmd or 'M190' in cmd:
522 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
525 self._log('Send: %s' % (cmd))
527 self._serial.write(cmd + '\n')
528 except serial.SerialTimeoutException:
529 self._log("Serial timeout while writing to serial port, trying again.")
532 self._serial.write(cmd + '\n')
534 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
535 self._errorValue = getExceptionString()
538 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
539 self._errorValue = getExceptionString()
543 if self._gcodePos >= len(self._gcodeList):
544 self._changeState(self.STATE_OPERATIONAL)
546 if self._gcodePos == 100:
547 self._printStartTime100 = time.time()
548 line = self._gcodeList[self._gcodePos]
549 if type(line) is tuple:
550 self._printSection = line[1]
553 if line == 'M0' or line == 'M1':
555 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
556 if self._printSection in self._feedRateModifier:
557 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
558 if ('G0' in line or 'G1' in line) and 'Z' in line:
559 z = float(re.search('Z([0-9\.]*)', line).group(1))
560 if self._currentZ != z:
562 self._callback.mcZChange(z)
564 self._log("Unexpected error: %s" % (getExceptionString()))
565 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
566 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
568 self._callback.mcProgress(self._gcodePos)
570 def sendCommand(self, cmd):
571 cmd = cmd.encode('ascii', 'replace')
572 if self.isPrinting():
573 self._commandQueue.put(cmd)
574 elif self.isOperational():
575 self._sendCommand(cmd)
577 def printGCode(self, gcodeList):
578 if not self.isOperational() or self.isPrinting():
580 self._gcodeList = gcodeList
582 self._printStartTime100 = None
583 self._printSection = 'CUSTOM'
584 self._changeState(self.STATE_PRINTING)
585 self._printStartTime = time.time()
586 for i in xrange(0, 4):
589 def cancelPrint(self):
590 if self.isOperational():
591 self._changeState(self.STATE_OPERATIONAL)
593 def setPause(self, pause):
594 if not pause and self.isPaused():
595 self._changeState(self.STATE_PRINTING)
596 for i in xrange(0, 6):
598 if pause and self.isPrinting():
599 self._changeState(self.STATE_PAUSED)
601 def setFeedrateModifier(self, type, value):
602 self._feedRateModifier[type] = value
604 def getExceptionString():
605 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
606 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])