1 from __future__ import absolute_import
16 from Cura.avr_isp import stk500v2
17 from Cura.avr_isp import ispBase
19 from Cura.util import profile
20 from Cura.util import version
27 def serialList(forAutoDetect=False):
29 if platform.system() == "Windows":
31 key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
34 values = _winreg.EnumValue(key, i)
35 if not forAutoDetect or 'USBSER' in values[0]:
41 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*")
42 baselist = filter(lambda s: not 'Bluetooth' in s, baselist)
44 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*") + glob.glob("/dev/rfcomm*")
45 prev = profile.getPreference('serial_port_auto')
48 baselist.insert(0, prev)
49 if version.isDevVersion() and not forAutoDetect:
50 baselist.append('VIRTUAL')
53 def machineIsConnected():
54 port = profile.getPreference('serial_port')
56 return len(serialList(True)) > 0
57 if platform.system() == "Windows":
58 return port in serialList(True)
59 return os.path.isfile(port)
62 ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
63 if profile.getPreference('serial_baud_auto') != '':
64 prev = int(profile.getPreference('serial_baud_auto'))
70 class VirtualPrinter():
72 self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
75 self.lastTempAt = time.time()
77 self.bedTargetTemp = 1.0
79 def write(self, data):
80 if self.readList is None:
82 #print "Send: %s" % (data.rstrip())
83 if 'M104' in data or 'M109' in data:
85 self.targetTemp = float(re.search('S([0-9]+)', data).group(1))
88 if 'M140' in data or 'M190' in data:
90 self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
94 self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
95 elif len(data.strip()) > 0:
96 self.readList.append("ok\n")
99 if self.readList is None:
102 timeDiff = self.lastTempAt - time.time()
103 self.lastTempAt = time.time()
104 if abs(self.temp - self.targetTemp) > 1:
105 self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp)
106 if abs(self.bedTemp - self.bedTargetTemp) > 1:
107 self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
108 while len(self.readList) < 1:
113 if self.readList is None:
116 #print "Recv: %s" % (self.readList[0].rstrip())
117 return self.readList.pop(0)
122 class MachineComPrintCallback(object):
123 def mcLog(self, message):
126 def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
129 def mcStateChange(self, state):
132 def mcMessage(self, message):
135 def mcProgress(self, lineNr):
138 def mcZChange(self, newZ):
141 class MachineCom(object):
143 STATE_OPEN_SERIAL = 1
144 STATE_DETECT_SERIAL = 2
145 STATE_DETECT_BAUDRATE = 3
147 STATE_OPERATIONAL = 5
152 STATE_CLOSED_WITH_ERROR = 10
154 def __init__(self, port = None, baudrate = None, callbackObject = None):
156 port = profile.getPreference('serial_port')
158 if profile.getPreference('serial_baud') == 'AUTO':
161 baudrate = int(profile.getPreference('serial_baud'))
162 if callbackObject is None:
163 callbackObject = MachineComPrintCallback()
166 self._baudrate = baudrate
167 self._callback = callbackObject
168 self._state = self.STATE_NONE
170 self._baudrateDetectList = baudrateList()
171 self._baudrateDetectRetry = 0
175 self._bedTargetTemp = 0
176 self._gcodeList = None
178 self._commandQueue = queue.Queue()
179 self._logQueue = queue.Queue(256)
180 self._feedRateModifier = {}
182 self._heatupWaitStartTime = 0
183 self._heatupWaitTimeLost = 0.0
184 self._printStartTime100 = None
186 self.thread = threading.Thread(target=self._monitor)
187 self.thread.daemon = True
190 def _changeState(self, newState):
191 if self._state == newState:
193 oldState = self.getStateString()
194 self._state = newState
195 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
196 self._callback.mcStateChange(newState)
201 def getStateString(self):
202 if self._state == self.STATE_NONE:
204 if self._state == self.STATE_OPEN_SERIAL:
205 return "Opening serial port"
206 if self._state == self.STATE_DETECT_SERIAL:
207 return "Detecting serial port"
208 if self._state == self.STATE_DETECT_BAUDRATE:
209 return "Detecting baudrate"
210 if self._state == self.STATE_CONNECTING:
212 if self._state == self.STATE_OPERATIONAL:
214 if self._state == self.STATE_PRINTING:
216 if self._state == self.STATE_PAUSED:
218 if self._state == self.STATE_CLOSED:
220 if self._state == self.STATE_ERROR:
221 return "Error: %s" % (self.getShortErrorString())
222 if self._state == self.STATE_CLOSED_WITH_ERROR:
223 return "Error: %s" % (self.getShortErrorString())
224 return "?%d?" % (self._state)
226 def getShortErrorString(self):
227 if len(self._errorValue) < 20:
228 return self._errorValue
229 return self._errorValue[:20] + "..."
231 def getErrorString(self):
232 return self._errorValue
234 def isClosedOrError(self):
235 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
238 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
240 def isOperational(self):
241 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
243 def isPrinting(self):
244 return self._state == self.STATE_PRINTING
247 return self._state == self.STATE_PAUSED
249 def getPrintPos(self):
250 return self._gcodePos
252 def getPrintTime(self):
253 return time.time() - self._printStartTime
255 def getPrintTimeRemainingEstimate(self):
256 if self._printStartTime100 == None or self.getPrintPos() < 200:
258 printTime = (time.time() - self._printStartTime100) / 60
259 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
260 printTimeLeft = printTimeTotal - printTime
266 def getBedTemp(self):
271 while not self._logQueue.empty():
272 ret.append(self._logQueue.get())
274 self._logQueue.put(line, False)
278 #Open the serial port.
279 if self._port == 'AUTO':
280 self._changeState(self.STATE_DETECT_SERIAL)
281 programmer = stk500v2.Stk500v2()
282 self._log("Serial port list: %s" % (str(serialList(True))))
283 for p in serialList(True):
285 self._log("Connecting to: %s" % (p))
286 programmer.connect(p)
287 self._serial = programmer.leaveISP()
288 profile.putPreference('serial_port_auto', p)
290 except ispBase.IspError as (e):
291 self._log("Error while connecting to %s: %s" % (p, str(e)))
294 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
296 elif self._port == 'VIRTUAL':
297 self._changeState(self.STATE_OPEN_SERIAL)
298 self._serial = VirtualPrinter()
300 self._changeState(self.STATE_OPEN_SERIAL)
302 self._log("Connecting to: %s" % (self._port))
303 if self._baudrate == 0:
304 self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
306 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
308 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
309 if self._serial == None:
310 self._log("Failed to open serial port (%s)" % (self._port))
311 self._errorValue = 'Failed to autodetect serial port.'
312 self._changeState(self.STATE_ERROR)
314 self._log("Connected to: %s, starting monitor" % (self._serial))
315 if self._baudrate == 0:
316 self._changeState(self.STATE_DETECT_BAUDRATE)
318 self._changeState(self.STATE_CONNECTING)
320 #Start monitoring the serial port.
321 if self._state == self.STATE_CONNECTING:
322 timeout = time.time() + 15
324 timeout = time.time() + 5
325 tempRequestTimeout = timeout
327 line = self._readline()
331 #No matter the state, if we see an error, goto the error state and store the error for reference.
332 if line.startswith('Error:'):
333 #Oh YEAH, consistency.
334 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
335 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
336 # So we can have an extra newline in the most common case. Awesome work people.
337 if re.match('Error:[0-9]\n', line):
338 line = line.rstrip() + self._readline()
339 #Skip the communication errors, as those get corrected.
340 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:
342 elif not self.isError():
343 self._errorValue = line[6:]
344 self._changeState(self.STATE_ERROR)
345 if ' T:' in line or line.startswith('T:'):
346 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
348 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
349 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
350 #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.
351 if not 'ok' in line and self._heatupWaitStartTime != 0:
353 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
354 self._heatupWaitStartTime = t
355 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
356 self._callback.mcMessage(line)
358 if self._state == self.STATE_DETECT_BAUDRATE:
359 if line == '' or time.time() > timeout:
360 if len(self._baudrateDetectList) < 1:
362 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
363 self._changeState(self.STATE_ERROR)
364 elif self._baudrateDetectRetry > 0:
365 self._baudrateDetectRetry -= 1
366 self._serial.write('\n')
367 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
368 self._sendCommand("M105")
369 self._testingBaudrate = True
371 baudrate = self._baudrateDetectList.pop(0)
373 self._serial.baudrate = baudrate
374 self._serial.timeout = 0.5
375 self._log("Trying baudrate: %d" % (baudrate))
376 self._baudrateDetectRetry = 5
377 self._baudrateDetectTestOk = 0
378 timeout = time.time() + 5
379 self._serial.write('\n')
380 self._sendCommand("M105")
381 self._testingBaudrate = True
383 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
384 elif 'ok' in line and 'T:' in line:
385 self._baudrateDetectTestOk += 1
386 if self._baudrateDetectTestOk < 10:
387 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
388 self._sendCommand("M105")
390 self._sendCommand("M999")
391 self._serial.timeout = 2
392 profile.putPreference('serial_baud_auto', self._serial.baudrate)
393 self._changeState(self.STATE_OPERATIONAL)
395 self._testingBaudrate = False
396 elif self._state == self.STATE_CONNECTING:
398 self._sendCommand("M105")
400 self._changeState(self.STATE_OPERATIONAL)
401 if time.time() > timeout:
403 elif self._state == self.STATE_OPERATIONAL:
404 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
406 self._sendCommand("M105")
407 tempRequestTimeout = time.time() + 5
408 elif self._state == self.STATE_PRINTING:
409 if line == '' and time.time() > timeout:
410 self._log("Communication timeout during printing, forcing a line")
412 #Even when printing request the temperture every 5 seconds.
413 if time.time() > tempRequestTimeout:
414 self._commandQueue.put("M105")
415 tempRequestTimeout = time.time() + 5
417 timeout = time.time() + 5
418 if not self._commandQueue.empty():
419 self._sendCommand(self._commandQueue.get())
422 elif "resend" in line.lower() or "rs" in line:
424 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
427 self._gcodePos = int(line.split()[1])
428 self._log("Connection closed, closing down monitor")
430 def _log(self, message):
431 self._callback.mcLog(message)
433 self._logQueue.put(message, False)
435 #If the log queue is full, remove the first message and append the new message again
438 self._logQueue.put(message, False)
443 if self._serial == None:
446 ret = self._serial.readline()
448 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
449 self._errorValue = getExceptionString()
453 #self._log("Recv: TIMEOUT")
455 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
458 def close(self, isError = False):
459 if self._serial != None:
462 self._changeState(self.STATE_CLOSED_WITH_ERROR)
464 self._changeState(self.STATE_CLOSED)
470 def _sendCommand(self, cmd):
471 if self._serial is None:
473 if 'M109' in cmd or 'M190' in cmd:
474 self._heatupWaitStartTime = time.time()
475 if 'M104' in cmd or 'M109' in cmd:
477 self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
480 if 'M140' in cmd or 'M190' in cmd:
482 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
485 self._log('Send: %s' % (cmd))
487 self._serial.write(cmd + '\n')
488 except serial.SerialTimeoutException:
489 self._log("Serial timeout while writing to serial port, trying again.")
491 self._serial.write(cmd + '\n')
493 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
494 self._errorValue = getExceptionString()
497 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
498 self._errorValue = getExceptionString()
502 if self._gcodePos >= len(self._gcodeList):
503 self._changeState(self.STATE_OPERATIONAL)
505 if self._gcodePos == 100:
506 self._printStartTime100 = time.time()
507 line = self._gcodeList[self._gcodePos]
508 if type(line) is tuple:
509 self._printSection = line[1]
512 if line == 'M0' or line == 'M1':
514 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
515 if self._printSection in self._feedRateModifier:
516 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
517 if ('G0' in line or 'G1' in line) and 'Z' in line:
518 z = float(re.search('Z([0-9\.]*)', line).group(1))
519 if self._currentZ != z:
521 self._callback.mcZChange(z)
523 self._log("Unexpected error: %s" % (getExceptionString()))
524 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
525 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
527 self._callback.mcProgress(self._gcodePos)
529 def sendCommand(self, cmd):
530 cmd = cmd.encode('ascii', 'replace')
531 if self.isPrinting():
532 self._commandQueue.put(cmd)
533 elif self.isOperational():
534 self._sendCommand(cmd)
536 def printGCode(self, gcodeList):
537 if not self.isOperational() or self.isPrinting():
539 self._gcodeList = gcodeList
541 self._printStartTime100 = None
542 self._printSection = 'CUSTOM'
543 self._changeState(self.STATE_PRINTING)
544 self._printStartTime = time.time()
545 for i in xrange(0, 4):
548 def cancelPrint(self):
549 if self.isOperational():
550 self._changeState(self.STATE_OPERATIONAL)
552 def setPause(self, pause):
553 if not pause and self.isPaused():
554 self._changeState(self.STATE_PRINTING)
555 for i in xrange(0, 6):
557 if pause and self.isPrinting():
558 self._changeState(self.STATE_PAUSED)
560 def setFeedrateModifier(self, type, value):
561 self._feedRateModifier[type] = value
563 def getExceptionString():
564 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
565 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])