1 from __future__ import absolute_import
15 from Cura.avr_isp import stk500v2
16 from Cura.avr_isp import ispBase
18 from Cura.util import profile
19 from Cura.util import version
26 def serialList(forAutoDetect=False):
30 key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
33 values = _winreg.EnumValue(key, i)
34 if not forAutoDetect or 'USBSER' in values[0]:
35 baselist+=[_winreg.EnumValue(key,i)[1]]
40 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*")
41 baselist = filter(lambda s: not 'Bluetooth' in s, baselist)
43 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*") + glob.glob("/dev/rfcomm*")
44 prev = profile.getPreference('serial_port_auto')
47 baselist.insert(0, prev)
48 if version.isDevVersion():
49 baselist.append('VIRTUAL')
53 ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
54 if profile.getPreference('serial_baud_auto') != '':
55 prev = int(profile.getPreference('serial_baud_auto'))
61 class VirtualPrinter():
63 self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
66 self.lastTempAt = time.time()
68 self.bedTargetTemp = 1.0
70 def write(self, data):
71 if self.readList is None:
73 #print "Send: %s" % (data.rstrip())
74 if 'M104' in data or 'M109' in data:
76 self.targetTemp = float(re.search('S([0-9]+)', data).group(1))
79 if 'M140' in data or 'M190' in data:
81 self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
85 self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
86 elif len(data.strip()) > 0:
87 self.readList.append("ok\n")
90 if self.readList is None:
93 timeDiff = self.lastTempAt - time.time()
94 self.lastTempAt = time.time()
95 if abs(self.temp - self.targetTemp) > 1:
96 self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp)
97 if abs(self.bedTemp - self.bedTargetTemp) > 1:
98 self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
99 while len(self.readList) < 1:
104 if self.readList is None:
107 #print "Recv: %s" % (self.readList[0].rstrip())
108 return self.readList.pop(0)
113 class MachineComPrintCallback(object):
114 def mcLog(self, message):
117 def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
120 def mcStateChange(self, state):
123 def mcMessage(self, message):
126 def mcProgress(self, lineNr):
129 def mcZChange(self, newZ):
132 class MachineCom(object):
134 STATE_OPEN_SERIAL = 1
135 STATE_DETECT_SERIAL = 2
136 STATE_DETECT_BAUDRATE = 3
138 STATE_OPERATIONAL = 5
143 STATE_CLOSED_WITH_ERROR = 10
145 def __init__(self, port = None, baudrate = None, callbackObject = None):
147 port = profile.getPreference('serial_port')
149 if profile.getPreference('serial_baud') == 'AUTO':
152 baudrate = int(profile.getPreference('serial_baud'))
153 if callbackObject == None:
154 callbackObject = MachineComPrintCallback()
157 self._baudrate = baudrate
158 self._callback = callbackObject
159 self._state = self.STATE_NONE
161 self._baudrateDetectList = baudrateList()
162 self._baudrateDetectRetry = 0
166 self._bedTargetTemp = 0
167 self._gcodeList = None
169 self._commandQueue = queue.Queue()
170 self._logQueue = queue.Queue(256)
171 self._feedRateModifier = {}
173 self._heatupWaitStartTime = 0
174 self._heatupWaitTimeLost = 0.0
175 self._printStartTime100 = None
177 self.thread = threading.Thread(target=self._monitor)
178 self.thread.daemon = True
181 def _changeState(self, newState):
182 if self._state == newState:
184 oldState = self.getStateString()
185 self._state = newState
186 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
187 self._callback.mcStateChange(newState)
192 def getStateString(self):
193 if self._state == self.STATE_NONE:
195 if self._state == self.STATE_OPEN_SERIAL:
196 return "Opening serial port"
197 if self._state == self.STATE_DETECT_SERIAL:
198 return "Detecting serial port"
199 if self._state == self.STATE_DETECT_BAUDRATE:
200 return "Detecting baudrate"
201 if self._state == self.STATE_CONNECTING:
203 if self._state == self.STATE_OPERATIONAL:
205 if self._state == self.STATE_PRINTING:
207 if self._state == self.STATE_PAUSED:
209 if self._state == self.STATE_CLOSED:
211 if self._state == self.STATE_ERROR:
212 return "Error: %s" % (self.getShortErrorString())
213 if self._state == self.STATE_CLOSED_WITH_ERROR:
214 return "Error: %s" % (self.getShortErrorString())
215 return "?%d?" % (self._state)
217 def getShortErrorString(self):
218 if len(self._errorValue) < 20:
219 return self._errorValue
220 return self._errorValue[:20] + "..."
222 def getErrorString(self):
223 return self._errorValue
225 def isClosedOrError(self):
226 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
229 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
231 def isOperational(self):
232 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
234 def isPrinting(self):
235 return self._state == self.STATE_PRINTING
238 return self._state == self.STATE_PAUSED
240 def getPrintPos(self):
241 return self._gcodePos
243 def getPrintTime(self):
244 return time.time() - self._printStartTime
246 def getPrintTimeRemainingEstimate(self):
247 if self._printStartTime100 == None or self.getPrintPos() < 200:
249 printTime = (time.time() - self._printStartTime100) / 60
250 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
251 printTimeLeft = printTimeTotal - printTime
257 def getBedTemp(self):
262 while not self._logQueue.empty():
263 ret.append(self._logQueue.get())
265 self._logQueue.put(line, False)
269 #Open the serial port.
270 if self._port == 'AUTO':
271 self._changeState(self.STATE_DETECT_SERIAL)
272 programmer = stk500v2.Stk500v2()
273 self._log("Serial port list: %s" % (str(serialList(True))))
274 for p in serialList(True):
276 self._log("Connecting to: %s" % (p))
277 programmer.connect(p)
278 self._serial = programmer.leaveISP()
279 profile.putPreference('serial_port_auto', p)
281 except ispBase.IspError as (e):
282 self._log("Error while connecting to %s: %s" % (p, str(e)))
285 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
287 elif self._port == 'VIRTUAL':
288 self._changeState(self.STATE_OPEN_SERIAL)
289 self._serial = VirtualPrinter()
291 self._changeState(self.STATE_OPEN_SERIAL)
293 self._log("Connecting to: %s" % (self._port))
294 if self._baudrate == 0:
295 self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
297 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
299 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
300 if self._serial == None:
301 self._log("Failed to open serial port (%s)" % (self._port))
302 self._errorValue = 'Failed to autodetect serial port.'
303 self._changeState(self.STATE_ERROR)
305 self._log("Connected to: %s, starting monitor" % (self._serial))
306 if self._baudrate == 0:
307 self._changeState(self.STATE_DETECT_BAUDRATE)
309 self._changeState(self.STATE_CONNECTING)
311 #Start monitoring the serial port.
312 if self._state == self.STATE_CONNECTING:
313 timeout = time.time() + 15
315 timeout = time.time() + 5
316 tempRequestTimeout = timeout
318 line = self._readline()
322 #No matter the state, if we see an error, goto the error state and store the error for reference.
323 if line.startswith('Error:'):
324 #Oh YEAH, consistency.
325 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
326 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
327 # So we can have an extra newline in the most common case. Awesome work people.
328 if re.match('Error:[0-9]\n', line):
329 line = line.rstrip() + self._readline()
330 #Skip the communication errors, as those get corrected.
331 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:
333 elif not self.isError():
334 self._errorValue = line[6:]
335 self._changeState(self.STATE_ERROR)
336 if ' T:' in line or line.startswith('T:'):
337 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
339 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
340 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
341 #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.
342 if not 'ok' in line and self._heatupWaitStartTime != 0:
344 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
345 self._heatupWaitStartTime = t
346 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
347 self._callback.mcMessage(line)
349 if self._state == self.STATE_DETECT_BAUDRATE:
350 if line == '' or time.time() > timeout:
351 if len(self._baudrateDetectList) < 1:
353 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
354 self._changeState(self.STATE_ERROR)
355 elif self._baudrateDetectRetry > 0:
356 self._baudrateDetectRetry -= 1
357 self._serial.write('\n')
358 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
359 self._sendCommand("M105")
360 self._testingBaudrate = True
362 baudrate = self._baudrateDetectList.pop(0)
364 self._serial.baudrate = baudrate
365 self._serial.timeout = 0.5
366 self._log("Trying baudrate: %d" % (baudrate))
367 self._baudrateDetectRetry = 5
368 self._baudrateDetectTestOk = 0
369 timeout = time.time() + 5
370 self._serial.write('\n')
371 self._sendCommand("M105")
372 self._testingBaudrate = True
374 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
375 elif 'ok' in line and 'T:' in line:
376 self._baudrateDetectTestOk += 1
377 if self._baudrateDetectTestOk < 10:
378 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
379 self._sendCommand("M105")
381 self._sendCommand("M999")
382 self._serial.timeout = 2
383 profile.putPreference('serial_baud_auto', self._serial.baudrate)
384 self._changeState(self.STATE_OPERATIONAL)
386 self._testingBaudrate = False
387 elif self._state == self.STATE_CONNECTING:
389 self._sendCommand("M105")
391 self._changeState(self.STATE_OPERATIONAL)
392 if time.time() > timeout:
394 elif self._state == self.STATE_OPERATIONAL:
395 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
397 self._sendCommand("M105")
398 tempRequestTimeout = time.time() + 5
399 elif self._state == self.STATE_PRINTING:
400 if line == '' and time.time() > timeout:
401 self._log("Communication timeout during printing, forcing a line")
403 #Even when printing request the temperture every 5 seconds.
404 if time.time() > tempRequestTimeout:
405 self._commandQueue.put("M105")
406 tempRequestTimeout = time.time() + 5
408 timeout = time.time() + 5
409 if not self._commandQueue.empty():
410 self._sendCommand(self._commandQueue.get())
413 elif "resend" in line.lower() or "rs" in line:
415 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
418 self._gcodePos = int(line.split()[1])
419 self._log("Connection closed, closing down monitor")
421 def _log(self, message):
422 self._callback.mcLog(message)
424 self._logQueue.put(message, False)
426 #If the log queue is full, remove the first message and append the new message again
429 self._logQueue.put(message, False)
434 if self._serial == None:
437 ret = self._serial.readline()
439 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
440 self._errorValue = getExceptionString()
444 #self._log("Recv: TIMEOUT")
446 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
449 def close(self, isError = False):
450 if self._serial != None:
453 self._changeState(self.STATE_CLOSED_WITH_ERROR)
455 self._changeState(self.STATE_CLOSED)
461 def _sendCommand(self, cmd):
462 if self._serial is None:
464 if 'M109' in cmd or 'M190' in cmd:
465 self._heatupWaitStartTime = time.time()
466 if 'M104' in cmd or 'M109' in cmd:
468 self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
471 if 'M140' in cmd or 'M190' in cmd:
473 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
476 self._log('Send: %s' % (cmd))
478 self._serial.write(cmd + '\n')
479 except serial.SerialTimeoutException:
480 self._log("Serial timeout while writing to serial port, trying again.")
482 self._serial.write(cmd + '\n')
484 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
485 self._errorValue = getExceptionString()
488 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
489 self._errorValue = getExceptionString()
493 if self._gcodePos >= len(self._gcodeList):
494 self._changeState(self.STATE_OPERATIONAL)
496 if self._gcodePos == 100:
497 self._printStartTime100 = time.time()
498 line = self._gcodeList[self._gcodePos]
499 if type(line) is tuple:
500 self._printSection = line[1]
503 if line == 'M0' or line == 'M1':
505 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
506 if self._printSection in self._feedRateModifier:
507 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
508 if ('G0' in line or 'G1' in line) and 'Z' in line:
509 z = float(re.search('Z([0-9\.]*)', line).group(1))
510 if self._currentZ != z:
512 self._callback.mcZChange(z)
514 self._log("Unexpected error: %s" % (getExceptionString()))
515 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
516 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
518 self._callback.mcProgress(self._gcodePos)
520 def sendCommand(self, cmd):
521 cmd = cmd.encode('ascii', 'replace')
522 if self.isPrinting():
523 self._commandQueue.put(cmd)
524 elif self.isOperational():
525 self._sendCommand(cmd)
527 def printGCode(self, gcodeList):
528 if not self.isOperational() or self.isPrinting():
530 self._gcodeList = gcodeList
532 self._printStartTime100 = None
533 self._printSection = 'CUSTOM'
534 self._changeState(self.STATE_PRINTING)
535 self._printStartTime = time.time()
536 for i in xrange(0, 6):
539 def cancelPrint(self):
540 if self.isOperational():
541 self._changeState(self.STATE_OPERATIONAL)
543 def setPause(self, pause):
544 if not pause and self.isPaused():
545 self._changeState(self.STATE_PRINTING)
546 for i in xrange(0, 6):
548 if pause and self.isPrinting():
549 self._changeState(self.STATE_PAUSED)
551 def setFeedrateModifier(self, type, value):
552 self._feedRateModifier[type] = value
554 def getExceptionString():
555 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
556 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])