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 if self._state == self.STATE_CONNECTING:
307 timeout = time.time() + 15
309 timeout = time.time() + 5
310 tempRequestTimeout = timeout
312 line = self._readline()
316 #No matter the state, if we see an error, goto the error state and store the error for reference.
317 if line.startswith('Error:'):
318 #Oh YEAH, consistency.
319 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
320 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
321 # So we can have an extra newline in the most common case. Awesome work people.
322 if re.match('Error:[0-9]\n', line):
323 line = line.rstrip() + self._readline()
324 #Skip the communication errors, as those get corrected.
325 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:
327 elif not self.isError():
328 self._errorValue = line[6:]
329 self._changeState(self.STATE_ERROR)
330 if ' T:' in line or line.startswith('T:'):
331 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
333 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
334 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
335 #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.
336 if not 'ok' in line and self._heatupWaitStartTime != 0:
338 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
339 self._heatupWaitStartTime = t
340 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
341 self._callback.mcMessage(line)
343 if self._state == self.STATE_DETECT_BAUDRATE:
344 if line == '' or time.time() > timeout:
345 if len(self._baudrateDetectList) < 1:
347 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
348 self._changeState(self.STATE_ERROR)
349 elif self._baudrateDetectRetry > 0:
350 self._baudrateDetectRetry -= 1
351 self._serial.write('\n')
352 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
353 self._sendCommand("M105")
354 self._testingBaudrate = True
356 baudrate = self._baudrateDetectList.pop(0)
358 self._serial.baudrate = baudrate
359 self._serial.timeout = 0.5
360 self._log("Trying baudrate: %d" % (baudrate))
361 self._baudrateDetectRetry = 5
362 self._baudrateDetectTestOk = 0
363 timeout = time.time() + 5
364 self._serial.write('\n')
365 self._sendCommand("M105")
366 self._testingBaudrate = True
368 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
369 elif 'ok' in line and 'T:' in line:
370 self._baudrateDetectTestOk += 1
371 if self._baudrateDetectTestOk < 10:
372 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
373 self._sendCommand("M105")
375 self._sendCommand("M999")
376 self._serial.timeout = 2
377 profile.putPreference('serial_baud_auto', self._serial.baudrate)
378 self._changeState(self.STATE_OPERATIONAL)
380 self._testingBaudrate = False
381 elif self._state == self.STATE_CONNECTING:
383 self._sendCommand("M105")
385 self._changeState(self.STATE_OPERATIONAL)
386 if time.time() > timeout:
388 elif self._state == self.STATE_OPERATIONAL:
389 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
391 self._sendCommand("M105")
392 tempRequestTimeout = time.time() + 5
393 elif self._state == self.STATE_PRINTING:
394 if line == '' and time.time() > timeout:
395 self._log("Communication timeout during printing, forcing a line")
397 #Even when printing request the temperture every 5 seconds.
398 if time.time() > tempRequestTimeout:
399 self._commandQueue.put("M105")
400 tempRequestTimeout = time.time() + 5
402 timeout = time.time() + 5
403 if not self._commandQueue.empty():
404 self._sendCommand(self._commandQueue.get())
407 elif "resend" in line.lower() or "rs" in line:
409 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
412 self._gcodePos = int(line.split()[1])
413 self._log("Connection closed, closing down monitor")
415 def _log(self, message):
416 self._callback.mcLog(message)
418 self._logQueue.put(message, False)
420 #If the log queue is full, remove the first message and append the new message again
423 self._logQueue.put(message, False)
428 if self._serial == None:
431 ret = self._serial.readline()
433 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
434 self._errorValue = getExceptionString()
438 #self._log("Recv: TIMEOUT")
440 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
443 def close(self, isError = False):
444 if self._serial != None:
447 self._changeState(self.STATE_CLOSED_WITH_ERROR)
449 self._changeState(self.STATE_CLOSED)
455 def _sendCommand(self, cmd):
456 if self._serial is None:
458 if 'M109' in cmd or 'M190' in cmd:
459 self._heatupWaitStartTime = time.time()
460 if 'M104' in cmd or 'M109' in cmd:
462 self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
465 if 'M140' in cmd or 'M190' in cmd:
467 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
470 self._log('Send: %s' % (cmd))
472 self._serial.write(cmd + '\n')
473 except serial.SerialTimeoutException:
474 self._log("Serial timeout while writing to serial port, trying again.")
476 self._serial.write(cmd + '\n')
478 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
479 self._errorValue = getExceptionString()
482 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
483 self._errorValue = getExceptionString()
487 if self._gcodePos >= len(self._gcodeList):
488 self._changeState(self.STATE_OPERATIONAL)
490 if self._gcodePos == 100:
491 self._printStartTime100 = time.time()
492 line = self._gcodeList[self._gcodePos]
493 if type(line) is tuple:
494 self._printSection = line[1]
497 if line == 'M0' or line == 'M1':
499 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
500 if self._printSection in self._feedRateModifier:
501 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
502 if ('G0' in line or 'G1' in line) and 'Z' in line:
503 z = float(re.search('Z([0-9\.]*)', line).group(1))
504 if self._currentZ != z:
506 self._callback.mcZChange(z)
508 self._log("Unexpected error: %s" % (getExceptionString()))
509 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
510 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
512 self._callback.mcProgress(self._gcodePos)
514 def sendCommand(self, cmd):
515 cmd = cmd.encode('ascii', 'replace')
516 if self.isPrinting():
517 self._commandQueue.put(cmd)
518 elif self.isOperational():
519 self._sendCommand(cmd)
521 def printGCode(self, gcodeList):
522 if not self.isOperational() or self.isPrinting():
524 self._gcodeList = gcodeList
526 self._printStartTime100 = None
527 self._printSection = 'CUSTOM'
528 self._changeState(self.STATE_PRINTING)
529 self._printStartTime = time.time()
530 for i in xrange(0, 6):
533 def cancelPrint(self):
534 if self.isOperational():
535 self._changeState(self.STATE_OPERATIONAL)
537 def setPause(self, pause):
538 if not pause and self.isPaused():
539 self._changeState(self.STATE_PRINTING)
540 for i in xrange(0, 6):
542 if pause and self.isPrinting():
543 self._changeState(self.STATE_PAUSED)
545 def setFeedrateModifier(self, type, value):
546 self._feedRateModifier[type] = value
548 def getExceptionString():
549 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
550 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])