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) < 30:
235 return self._errorValue
236 return self._errorValue[:30] + "..."
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 programmer = stk500v2.Stk500v2()
289 for p in serialList(True):
291 self._log("Connecting to: %s (programmer)" % (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 if self._serial is None:
303 self._log("Serial port list: %s" % (str(serialList(True))))
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 if self._baudrate == 0:
312 self._log("Connecting to: %s with baudrate: 115200 (fallback)" % (self._port))
313 self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
315 self._log("Connecting to: %s with baudrate: %s (configured)" % (self._port, self._baudrate))
316 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
318 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
319 if self._serial is None:
320 baudrate = self._baudrate
322 baudrate = self._baudrateDetectList.pop(0)
323 port = self._serialDetectList.pop(0)
324 self._log("Connecting to: %s with baudrate: %s (auto)" % (port, baudrate))
326 self._serial = serial.Serial(port, baudrate, timeout=0.1, writeTimeout=10000)
330 self._log("Connected to: %s, starting monitor" % (self._serial))
331 if self._baudrate == 0:
332 self._changeState(self.STATE_DETECT_BAUDRATE)
334 self._changeState(self.STATE_CONNECTING)
336 #Start monitoring the serial port.
337 if self._state == self.STATE_CONNECTING:
338 timeout = time.time() + 15
340 timeout = time.time() + 5
341 tempRequestTimeout = timeout
343 line = self._readline()
347 #No matter the state, if we see an fatal error, goto the error state and store the error for reference.
348 # Only goto error on known fatal errors.
349 if line.startswith('Error:'):
350 #Oh YEAH, consistency.
351 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
352 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
353 # So we can have an extra newline in the most common case. Awesome work people.
354 if re.match('Error:[0-9]\n', line):
355 line = line.rstrip() + self._readline()
356 #Skip the communication errors, as those get corrected.
357 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:
358 if not self.isError():
359 self._errorValue = line[6:]
360 self._changeState(self.STATE_ERROR)
361 if ' T:' in line or line.startswith('T:'):
363 self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
368 self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
371 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
372 #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.
373 if not 'ok' in line and self._heatupWaitStartTime != 0:
375 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
376 self._heatupWaitStartTime = t
377 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():
378 self._callback.mcMessage(line)
380 if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
381 if line == '' or time.time() > timeout:
382 if len(self._baudrateDetectList) < 1:
384 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
385 self._changeState(self.STATE_ERROR)
386 elif self._baudrateDetectRetry > 0:
387 self._baudrateDetectRetry -= 1
388 self._serial.write('\n')
389 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
390 self._sendCommand("M105")
391 self._testingBaudrate = True
393 if self._state == self.STATE_DETECT_SERIAL:
394 if len(self._serialDetectList) == 0:
395 if len(self._baudrateDetectList) == 0:
396 self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
397 self._errorValue = 'Failed to autodetect serial port.'
398 self._changeState(self.STATE_ERROR)
401 self._serialDetectList = serialList(True)
402 baudrate = self._baudrateDetectList.pop(0)
404 self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=0.5, writeTimeout=10000)
406 baudrate = self._baudrateDetectList.pop(0)
408 self._setBaudrate(baudrate)
409 self._serial.timeout = 0.5
410 self._log("Trying baudrate: %d" % (baudrate))
411 self._baudrateDetectRetry = 5
412 self._baudrateDetectTestOk = 0
413 timeout = time.time() + 5
414 self._serial.write('\n')
415 self._sendCommand("M105")
416 self._testingBaudrate = True
418 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
420 self._baudrateDetectTestOk += 1
421 if self._baudrateDetectTestOk < 10:
422 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
423 self._sendCommand("M105")
425 self._sendCommand("M999")
426 self._serial.timeout = 2
427 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
428 self._changeState(self.STATE_OPERATIONAL)
430 self._testingBaudrate = False
431 elif self._state == self.STATE_CONNECTING:
432 if line == '' or 'wait' in line: # 'wait' needed for Repetier (kind of watchdog)
433 self._sendCommand("M105")
435 self._changeState(self.STATE_OPERATIONAL)
436 if time.time() > timeout:
438 elif self._state == self.STATE_OPERATIONAL:
439 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
441 if self._extruderCount > 0:
442 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
443 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
445 self._sendCommand("M105")
446 tempRequestTimeout = time.time() + 5
447 elif self._state == self.STATE_PRINTING:
448 if line == '' and time.time() > timeout:
449 self._log("Communication timeout during printing, forcing a line")
451 #Even when printing request the temperature every 5 seconds.
452 if time.time() > tempRequestTimeout:
453 if self._extruderCount > 0:
454 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
455 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
457 self._sendCommand("M105")
458 tempRequestTimeout = time.time() + 5
460 timeout = time.time() + 5
461 if not self._commandQueue.empty():
462 self._sendCommand(self._commandQueue.get())
465 elif "resend" in line.lower() or "rs" in line:
467 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
470 self._gcodePos = int(line.split()[1])
471 self._log("Connection closed, closing down monitor")
473 def _setBaudrate(self, baudrate):
474 #For linux the pyserial implementation lacks TCGETS2 support. So do that ourselves
475 if sys.platform.startswith('linux'):
477 self._serial.baudrate = baudrate
481 import fcntl, array, termios
485 buf = array.array('i', [0] * 64)
486 fcntl.ioctl(self._serial.fd, TCGETS2, buf)
487 buf[2] &= ~termios.CBAUD
489 buf[9] = buf[10] = baudrate
490 fcntl.ioctl(self._serial.fd, TCSETS2, buf)
492 print getExceptionString()
494 self._serial.baudrate = baudrate
496 def _log(self, message):
497 self._callback.mcLog(message)
499 self._logQueue.put(message, False)
501 #If the log queue is full, remove the first message and append the new message again
504 self._logQueue.put(message, False)
509 if self._serial == None:
512 ret = self._serial.readline()
514 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
515 self._errorValue = getExceptionString()
519 #self._log("Recv: TIMEOUT")
521 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
524 def close(self, isError = False):
525 if self._serial != None:
528 self._changeState(self.STATE_CLOSED_WITH_ERROR)
530 self._changeState(self.STATE_CLOSED)
536 def _sendCommand(self, cmd):
537 if self._serial is None:
539 if 'M109' in cmd or 'M190' in cmd:
540 self._heatupWaitStartTime = time.time()
541 if 'M104' in cmd or 'M109' in cmd:
545 t = int(re.search('T([0-9]+)', cmd).group(1))
546 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
549 if 'M140' in cmd or 'M190' in cmd:
551 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
554 self._log('Send: %s' % (cmd))
556 self._serial.write(cmd + '\n')
557 except serial.SerialTimeoutException:
558 self._log("Serial timeout while writing to serial port, trying again.")
561 self._serial.write(cmd + '\n')
563 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
564 self._errorValue = getExceptionString()
567 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
568 self._errorValue = getExceptionString()
572 if self._gcodePos >= len(self._gcodeList):
573 self._changeState(self.STATE_OPERATIONAL)
575 if self._gcodePos == 100:
576 self._printStartTime100 = time.time()
577 line = self._gcodeList[self._gcodePos]
578 if type(line) is tuple:
579 self._printSection = line[1]
582 if line == 'M0' or line == 'M1':
584 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
585 if self._printSection in self._feedRateModifier:
586 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
587 if ('G0' in line or 'G1' in line) and 'Z' in line:
588 z = float(re.search('Z([0-9\.]*)', line).group(1))
589 if self._currentZ != z:
591 self._callback.mcZChange(z)
593 self._log("Unexpected error: %s" % (getExceptionString()))
594 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
595 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
597 self._callback.mcProgress(self._gcodePos)
599 def sendCommand(self, cmd):
600 cmd = cmd.encode('ascii', 'replace')
601 if self.isPrinting():
602 self._commandQueue.put(cmd)
603 elif self.isOperational():
604 self._sendCommand(cmd)
606 def printGCode(self, gcodeList):
607 if not self.isOperational() or self.isPrinting():
609 self._gcodeList = gcodeList
611 self._printStartTime100 = None
612 self._printSection = 'CUSTOM'
613 self._changeState(self.STATE_PRINTING)
614 self._printStartTime = time.time()
615 for i in xrange(0, 4):
618 def cancelPrint(self):
619 if self.isOperational():
620 self._changeState(self.STATE_OPERATIONAL)
622 def setPause(self, pause):
623 if not pause and self.isPaused():
624 self._changeState(self.STATE_PRINTING)
625 for i in xrange(0, 6):
627 if pause and self.isPrinting():
628 self._changeState(self.STATE_PAUSED)
630 def setFeedrateModifier(self, type, value):
631 self._feedRateModifier[type] = value
633 def getExceptionString():
634 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
635 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])