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
30 key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
33 baselist+=[_winreg.EnumValue(key,i)[1]]
37 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*") + glob.glob("/dev/rfcomm*")
38 prev = profile.getPreference('serial_port_auto')
41 baselist.insert(0, prev)
42 if version.isDevVersion():
43 baselist.append('VIRTUAL')
47 ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
48 if profile.getPreference('serial_baud_auto') != '':
49 prev = int(profile.getPreference('serial_baud_auto'))
55 class VirtualPrinter():
57 self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
60 self.lastTempAt = time.time()
62 self.bedTargetTemp = 1.0
64 def write(self, data):
65 if self.readList is None:
67 #print "Send: %s" % (data.rstrip())
68 if 'M104' in data or 'M109' in data:
70 self.targetTemp = float(re.search('S([0-9]+)', data).group(1))
73 if 'M140' in data or 'M190' in data:
75 self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
79 self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
80 elif len(data.strip()) > 0:
81 self.readList.append("ok\n")
84 if self.readList is None:
87 timeDiff = self.lastTempAt - time.time()
88 self.lastTempAt = time.time()
89 if abs(self.temp - self.targetTemp) > 1:
90 self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp)
91 if abs(self.bedTemp - self.bedTargetTemp) > 1:
92 self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
93 while len(self.readList) < 1:
98 if self.readList is None:
101 #print "Recv: %s" % (self.readList[0].rstrip())
102 return self.readList.pop(0)
107 class MachineComPrintCallback(object):
108 def mcLog(self, message):
111 def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
114 def mcStateChange(self, state):
117 def mcMessage(self, message):
120 def mcProgress(self, lineNr):
123 def mcZChange(self, newZ):
126 class MachineCom(object):
128 STATE_OPEN_SERIAL = 1
129 STATE_DETECT_SERIAL = 2
130 STATE_DETECT_BAUDRATE = 3
132 STATE_OPERATIONAL = 5
137 STATE_CLOSED_WITH_ERROR = 10
139 def __init__(self, port = None, baudrate = None, callbackObject = None):
141 port = profile.getPreference('serial_port')
143 if profile.getPreference('serial_baud') == 'AUTO':
146 baudrate = int(profile.getPreference('serial_baud'))
147 if callbackObject == None:
148 callbackObject = MachineComPrintCallback()
151 self._baudrate = baudrate
152 self._callback = callbackObject
153 self._state = self.STATE_NONE
155 self._baudrateDetectList = baudrateList()
156 self._baudrateDetectRetry = 0
160 self._bedTargetTemp = 0
161 self._gcodeList = None
163 self._commandQueue = queue.Queue()
164 self._logQueue = queue.Queue(256)
165 self._feedRateModifier = {}
167 self._heatupWaitStartTime = 0
168 self._heatupWaitTimeLost = 0.0
169 self._printStartTime100 = None
171 self.thread = threading.Thread(target=self._monitor)
172 self.thread.daemon = True
175 def _changeState(self, newState):
176 if self._state == newState:
178 oldState = self.getStateString()
179 self._state = newState
180 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
181 self._callback.mcStateChange(newState)
186 def getStateString(self):
187 if self._state == self.STATE_NONE:
189 if self._state == self.STATE_OPEN_SERIAL:
190 return "Opening serial port"
191 if self._state == self.STATE_DETECT_SERIAL:
192 return "Detecting serial port"
193 if self._state == self.STATE_DETECT_BAUDRATE:
194 return "Detecting baudrate"
195 if self._state == self.STATE_CONNECTING:
197 if self._state == self.STATE_OPERATIONAL:
199 if self._state == self.STATE_PRINTING:
201 if self._state == self.STATE_PAUSED:
203 if self._state == self.STATE_CLOSED:
205 if self._state == self.STATE_ERROR:
206 return "Error: %s" % (self.getShortErrorString())
207 if self._state == self.STATE_CLOSED_WITH_ERROR:
208 return "Error: %s" % (self.getShortErrorString())
209 return "?%d?" % (self._state)
211 def getShortErrorString(self):
212 if len(self._errorValue) < 20:
213 return self._errorValue
214 return self._errorValue[:20] + "..."
216 def getErrorString(self):
217 return self._errorValue
219 def isClosedOrError(self):
220 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
223 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
225 def isOperational(self):
226 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
228 def isPrinting(self):
229 return self._state == self.STATE_PRINTING
232 return self._state == self.STATE_PAUSED
234 def getPrintPos(self):
235 return self._gcodePos
237 def getPrintTime(self):
238 return time.time() - self._printStartTime
240 def getPrintTimeRemainingEstimate(self):
241 if self._printStartTime100 == None or self.getPrintPos() < 200:
243 printTime = (time.time() - self._printStartTime100) / 60
244 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
245 printTimeLeft = printTimeTotal - printTime
251 def getBedTemp(self):
256 while not self._logQueue.empty():
257 ret.append(self._logQueue.get())
259 self._logQueue.put(line, False)
263 #Open the serial port.
264 if self._port == 'AUTO':
265 self._changeState(self.STATE_DETECT_SERIAL)
266 programmer = stk500v2.Stk500v2()
267 self._log("Serial port list: %s" % (str(serialList())))
268 for p in serialList():
270 self._log("Connecting to: %s" % (p))
271 programmer.connect(p)
272 self._serial = programmer.leaveISP()
273 profile.putPreference('serial_port_auto', p)
275 except ispBase.IspError as (e):
276 self._log("Error while connecting to %s: %s" % (p, str(e)))
279 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
281 elif self._port == 'VIRTUAL':
282 self._changeState(self.STATE_OPEN_SERIAL)
283 self._serial = VirtualPrinter()
285 self._changeState(self.STATE_OPEN_SERIAL)
287 self._log("Connecting to: %s" % (self._port))
288 if self._baudrate == 0:
289 self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
291 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
293 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
294 if self._serial == None:
295 self._log("Failed to open serial port (%s)" % (self._port))
296 self._errorValue = 'Failed to autodetect serial port.'
297 self._changeState(self.STATE_ERROR)
299 self._log("Connected to: %s, starting monitor" % (self._serial))
300 if self._baudrate == 0:
301 self._changeState(self.STATE_DETECT_BAUDRATE)
303 self._changeState(self.STATE_CONNECTING)
305 #Start monitoring the serial port.
306 timeout = time.time() + 5
307 tempRequestTimeout = timeout
309 line = self._readline()
313 #No matter the state, if we see an error, goto the error state and store the error for reference.
314 if line.startswith('Error:'):
315 #Oh YEAH, consistency.
316 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
317 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
318 # So we can have an extra newline in the most common case. Awesome work people.
319 if re.match('Error:[0-9]\n', line):
320 line = line.rstrip() + self._readline()
321 #Skip the communication errors, as those get corrected.
322 if 'checksum mismatch' in line or 'Line Number is not Last Line Number' in line or 'No Line Number with checksum' in line:
324 elif not self.isError():
325 self._errorValue = line[6:]
326 self._changeState(self.STATE_ERROR)
327 if ' T:' in line or line.startswith('T:'):
328 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
330 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
331 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
332 #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.
333 if not 'ok' in line and self._heatupWaitStartTime != 0:
335 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
336 self._heatupWaitStartTime = t
337 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
338 self._callback.mcMessage(line)
340 if self._state == self.STATE_DETECT_BAUDRATE:
341 if line == '' or time.time() > timeout:
342 if len(self._baudrateDetectList) < 1:
344 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
345 self._changeState(self.STATE_ERROR)
346 elif self._baudrateDetectRetry > 0:
347 self._baudrateDetectRetry -= 1
348 self._serial.write('\n')
349 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
350 self._sendCommand("M105")
351 self._testingBaudrate = True
353 baudrate = self._baudrateDetectList.pop(0)
355 self._serial.baudrate = baudrate
356 self._serial.timeout = 0.5
357 self._log("Trying baudrate: %d" % (baudrate))
358 self._baudrateDetectRetry = 5
359 self._baudrateDetectTestOk = 0
360 timeout = time.time() + 5
361 self._serial.write('\n')
362 self._sendCommand("M105")
363 self._testingBaudrate = True
365 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
366 elif 'ok' in line and 'T:' in line:
367 self._baudrateDetectTestOk += 1
368 if self._baudrateDetectTestOk < 10:
369 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
370 self._sendCommand("M105")
372 self._sendCommand("M999")
373 self._serial.timeout = 2
374 profile.putPreference('serial_baud_auto', self._serial.baudrate)
375 self._changeState(self.STATE_OPERATIONAL)
377 self._testingBaudrate = False
378 elif self._state == self.STATE_CONNECTING:
380 self._sendCommand("M105")
382 self._changeState(self.STATE_OPERATIONAL)
383 if time.time() > timeout:
385 elif self._state == self.STATE_OPERATIONAL:
386 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
388 self._sendCommand("M105")
389 tempRequestTimeout = time.time() + 5
390 elif self._state == self.STATE_PRINTING:
391 if line == '' and time.time() > timeout:
392 self._log("Communication timeout during printing, forcing a line")
394 #Even when printing request the temperture every 5 seconds.
395 if time.time() > tempRequestTimeout:
396 self._commandQueue.put("M105")
397 tempRequestTimeout = time.time() + 5
399 timeout = time.time() + 5
400 if not self._commandQueue.empty():
401 self._sendCommand(self._commandQueue.get())
404 elif "resend" in line.lower() or "rs" in line:
406 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
409 self._gcodePos = int(line.split()[1])
410 self._log("Connection closed, closing down monitor")
412 def _log(self, message):
413 self._callback.mcLog(message)
415 self._logQueue.put(message, False)
417 #If the log queue is full, remove the first message and append the new message again
420 self._logQueue.put(message, False)
425 if self._serial == None:
428 ret = self._serial.readline()
430 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
431 self._errorValue = getExceptionString()
435 #self._log("Recv: TIMEOUT")
437 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
440 def close(self, isError = False):
441 if self._serial != None:
444 self._changeState(self.STATE_CLOSED_WITH_ERROR)
446 self._changeState(self.STATE_CLOSED)
452 def _sendCommand(self, cmd):
453 if self._serial is None:
455 if 'M109' in cmd or 'M190' in cmd:
456 self._heatupWaitStartTime = time.time()
457 if 'M104' in cmd or 'M109' in cmd:
459 self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
462 if 'M140' in cmd or 'M190' in cmd:
464 self._bedTargetTemp = float(re.search('S([0-9]+)').group(1))
467 self._log('Send: %s' % (cmd))
469 self._serial.write(cmd + '\n')
470 except serial.SerialTimeoutException:
471 self._log("Serial timeout while writing to serial port, trying again.")
473 self._serial.write(cmd + '\n')
475 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
476 self._errorValue = getExceptionString()
479 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
480 self._errorValue = getExceptionString()
484 if self._gcodePos >= len(self._gcodeList):
485 self._changeState(self.STATE_OPERATIONAL)
487 if self._gcodePos == 100:
488 self._printStartTime100 = time.time()
489 line = self._gcodeList[self._gcodePos]
490 if type(line) is tuple:
491 self._printSection = line[1]
494 if line == 'M0' or line == 'M1':
496 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
497 if self._printSection in self._feedRateModifier:
498 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
499 if ('G0' in line or 'G1' in line) and 'Z' in line:
500 z = float(re.search('Z([0-9\.]*)', line).group(1))
501 if self._currentZ != z:
503 self._callback.mcZChange(z)
505 self._log("Unexpected error: %s" % (getExceptionString()))
506 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
507 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
509 self._callback.mcProgress(self._gcodePos)
511 def sendCommand(self, cmd):
512 cmd = cmd.encode('ascii', 'replace')
513 if self.isPrinting():
514 self._commandQueue.put(cmd)
515 elif self.isOperational():
516 self._sendCommand(cmd)
518 def printGCode(self, gcodeList):
519 if not self.isOperational() or self.isPrinting():
521 self._gcodeList = gcodeList
523 self._printStartTime100 = None
524 self._printSection = 'CUSTOM'
525 self._changeState(self.STATE_PRINTING)
526 self._printStartTime = time.time()
527 for i in xrange(0, 6):
530 def cancelPrint(self):
531 if self.isOperational():
532 self._changeState(self.STATE_OPERATIONAL)
534 def setPause(self, pause):
535 if not pause and self.isPaused():
536 self._changeState(self.STATE_PRINTING)
537 for i in xrange(0, 6):
539 if pause and self.isPrinting():
540 self._changeState(self.STATE_PAUSED)
542 def setFeedrateModifier(self, type, value):
543 self._feedRateModifier[type] = value
545 def getExceptionString():
546 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
547 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])