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)
45 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*")
46 prev = profile.getPreference('serial_port_auto')
49 baselist.insert(0, prev)
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.getPreference('serial_port')
58 if profile.getPreference('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.getPreference('serial_baud_auto') != '':
68 prev = int(profile.getPreference('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.getPreference('serial_port')
162 if profile.getPreference('serial_baud') == 'AUTO':
165 baudrate = int(profile.getPreference('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._baudrateDetectList = baudrateList()
175 self._baudrateDetectRetry = 0
176 self._extruderCount = int(profile.getPreference('extruder_amount'))
177 self._temperatureRequestExtruder = 0
178 self._temp = [0] * self._extruderCount
179 self._targetTemp = [0] * self._extruderCount
181 self._bedTargetTemp = 0
182 self._gcodeList = None
184 self._commandQueue = queue.Queue()
185 self._logQueue = queue.Queue(256)
186 self._feedRateModifier = {}
188 self._heatupWaitStartTime = 0
189 self._heatupWaitTimeLost = 0.0
190 self._printStartTime100 = None
192 self.thread = threading.Thread(target=self._monitor)
193 self.thread.daemon = True
196 def _changeState(self, newState):
197 if self._state == newState:
199 oldState = self.getStateString()
200 self._state = newState
201 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
202 self._callback.mcStateChange(newState)
207 def getStateString(self):
208 if self._state == self.STATE_NONE:
210 if self._state == self.STATE_OPEN_SERIAL:
211 return "Opening serial port"
212 if self._state == self.STATE_DETECT_SERIAL:
213 return "Detecting serial port"
214 if self._state == self.STATE_DETECT_BAUDRATE:
215 return "Detecting baudrate"
216 if self._state == self.STATE_CONNECTING:
218 if self._state == self.STATE_OPERATIONAL:
220 if self._state == self.STATE_PRINTING:
222 if self._state == self.STATE_PAUSED:
224 if self._state == self.STATE_CLOSED:
226 if self._state == self.STATE_ERROR:
227 return "Error: %s" % (self.getShortErrorString())
228 if self._state == self.STATE_CLOSED_WITH_ERROR:
229 return "Error: %s" % (self.getShortErrorString())
230 return "?%d?" % (self._state)
232 def getShortErrorString(self):
233 if len(self._errorValue) < 20:
234 return self._errorValue
235 return self._errorValue[:20] + "..."
237 def getErrorString(self):
238 return self._errorValue
240 def isClosedOrError(self):
241 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
244 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
246 def isOperational(self):
247 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
249 def isPrinting(self):
250 return self._state == self.STATE_PRINTING
253 return self._state == self.STATE_PAUSED
255 def getPrintPos(self):
256 return self._gcodePos
258 def getPrintTime(self):
259 return time.time() - self._printStartTime
261 def getPrintTimeRemainingEstimate(self):
262 if self._printStartTime100 is None or self.getPrintPos() < 200:
264 printTime = (time.time() - self._printStartTime100) / 60
265 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
266 printTimeLeft = printTimeTotal - printTime
272 def getBedTemp(self):
277 while not self._logQueue.empty():
278 ret.append(self._logQueue.get())
280 self._logQueue.put(line, False)
284 #Open the serial port.
285 if self._port == 'AUTO':
286 self._changeState(self.STATE_DETECT_SERIAL)
287 programmer = stk500v2.Stk500v2()
288 self._log("Serial port list: %s" % (str(serialList(True))))
289 for p in serialList(True):
291 self._log("Connecting to: %s" % (p))
292 programmer.connect(p)
293 self._serial = programmer.leaveISP()
294 profile.putPreference('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 elif self._port == 'VIRTUAL':
303 self._changeState(self.STATE_OPEN_SERIAL)
304 self._serial = VirtualPrinter()
306 self._changeState(self.STATE_OPEN_SERIAL)
308 self._log("Connecting to: %s" % (self._port))
309 if self._baudrate == 0:
310 self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
312 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
314 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
315 if self._serial == None:
316 self._log("Failed to open serial port (%s)" % (self._port))
317 self._errorValue = 'Failed to autodetect serial port.'
318 self._changeState(self.STATE_ERROR)
320 self._log("Connected to: %s, starting monitor" % (self._serial))
321 if self._baudrate == 0:
322 self._changeState(self.STATE_DETECT_BAUDRATE)
324 self._changeState(self.STATE_CONNECTING)
326 #Start monitoring the serial port.
327 if self._state == self.STATE_CONNECTING:
328 timeout = time.time() + 15
330 timeout = time.time() + 5
331 tempRequestTimeout = timeout
333 line = self._readline()
337 #No matter the state, if we see an error, goto the error state and store the error for reference.
338 if line.startswith('Error:'):
339 #Oh YEAH, consistency.
340 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
341 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
342 # So we can have an extra newline in the most common case. Awesome work people.
343 if re.match('Error:[0-9]\n', line):
344 line = line.rstrip() + self._readline()
345 #Skip the communication errors, as those get corrected.
346 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:
348 elif not self.isError():
349 self._errorValue = line[6:]
350 self._changeState(self.STATE_ERROR)
351 if ' T:' in line or line.startswith('T:'):
352 self._temp[self._temperatureRequestExtruder] = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
354 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
355 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
356 #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.
357 if not 'ok' in line and self._heatupWaitStartTime != 0:
359 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
360 self._heatupWaitStartTime = t
361 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
362 self._callback.mcMessage(line)
364 if self._state == self.STATE_DETECT_BAUDRATE:
365 if line == '' or time.time() > timeout:
366 if len(self._baudrateDetectList) < 1:
368 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
369 self._changeState(self.STATE_ERROR)
370 elif self._baudrateDetectRetry > 0:
371 self._baudrateDetectRetry -= 1
372 self._serial.write('\n')
373 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
374 self._sendCommand("M105")
375 self._testingBaudrate = True
377 baudrate = self._baudrateDetectList.pop(0)
379 self._serial.baudrate = baudrate
380 self._serial.timeout = 0.5
381 self._log("Trying baudrate: %d" % (baudrate))
382 self._baudrateDetectRetry = 5
383 self._baudrateDetectTestOk = 0
384 timeout = time.time() + 5
385 self._serial.write('\n')
386 self._sendCommand("M105")
387 self._testingBaudrate = True
389 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
391 self._baudrateDetectTestOk += 1
392 if self._baudrateDetectTestOk < 10:
393 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
394 self._sendCommand("M105")
396 self._sendCommand("M999")
397 self._serial.timeout = 2
398 profile.putPreference('serial_baud_auto', self._serial.baudrate)
399 self._changeState(self.STATE_OPERATIONAL)
401 self._testingBaudrate = False
402 elif self._state == self.STATE_CONNECTING:
404 self._sendCommand("M105")
406 self._changeState(self.STATE_OPERATIONAL)
407 if time.time() > timeout:
409 elif self._state == self.STATE_OPERATIONAL:
410 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
412 if self._extruderCount > 0:
413 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
414 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
416 self._sendCommand("M105")
417 tempRequestTimeout = time.time() + 5
418 elif self._state == self.STATE_PRINTING:
419 if line == '' and time.time() > timeout:
420 self._log("Communication timeout during printing, forcing a line")
422 #Even when printing request the temperature every 5 seconds.
423 if time.time() > tempRequestTimeout:
424 if self._extruderCount > 0:
425 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
426 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
428 self._sendCommand("M105")
429 tempRequestTimeout = time.time() + 5
431 timeout = time.time() + 5
432 if not self._commandQueue.empty():
433 self._sendCommand(self._commandQueue.get())
436 elif "resend" in line.lower() or "rs" in line:
438 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
441 self._gcodePos = int(line.split()[1])
442 self._log("Connection closed, closing down monitor")
444 def _log(self, message):
445 self._callback.mcLog(message)
447 self._logQueue.put(message, False)
449 #If the log queue is full, remove the first message and append the new message again
452 self._logQueue.put(message, False)
457 if self._serial == None:
460 ret = self._serial.readline()
462 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
463 self._errorValue = getExceptionString()
467 #self._log("Recv: TIMEOUT")
469 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
472 def close(self, isError = False):
473 if self._serial != None:
476 self._changeState(self.STATE_CLOSED_WITH_ERROR)
478 self._changeState(self.STATE_CLOSED)
484 def _sendCommand(self, cmd):
485 if self._serial is None:
487 if 'M109' in cmd or 'M190' in cmd:
488 self._heatupWaitStartTime = time.time()
489 if 'M104' in cmd or 'M109' in cmd:
493 t = int(re.search('T([0-9]+)', cmd).group(1))
494 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
497 if 'M140' in cmd or 'M190' in cmd:
499 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
502 self._log('Send: %s' % (cmd))
504 self._serial.write(cmd + '\n')
505 except serial.SerialTimeoutException:
506 self._log("Serial timeout while writing to serial port, trying again.")
509 self._serial.write(cmd + '\n')
511 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
512 self._errorValue = getExceptionString()
515 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
516 self._errorValue = getExceptionString()
520 if self._gcodePos >= len(self._gcodeList):
521 self._changeState(self.STATE_OPERATIONAL)
523 if self._gcodePos == 100:
524 self._printStartTime100 = time.time()
525 line = self._gcodeList[self._gcodePos]
526 if type(line) is tuple:
527 self._printSection = line[1]
530 if line == 'M0' or line == 'M1':
532 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
533 if self._printSection in self._feedRateModifier:
534 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
535 if ('G0' in line or 'G1' in line) and 'Z' in line:
536 z = float(re.search('Z([0-9\.]*)', line).group(1))
537 if self._currentZ != z:
539 self._callback.mcZChange(z)
541 self._log("Unexpected error: %s" % (getExceptionString()))
542 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
543 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
545 self._callback.mcProgress(self._gcodePos)
547 def sendCommand(self, cmd):
548 cmd = cmd.encode('ascii', 'replace')
549 if self.isPrinting():
550 self._commandQueue.put(cmd)
551 elif self.isOperational():
552 self._sendCommand(cmd)
554 def printGCode(self, gcodeList):
555 if not self.isOperational() or self.isPrinting():
557 self._gcodeList = gcodeList
559 self._printStartTime100 = None
560 self._printSection = 'CUSTOM'
561 self._changeState(self.STATE_PRINTING)
562 self._printStartTime = time.time()
563 for i in xrange(0, 4):
566 def cancelPrint(self):
567 if self.isOperational():
568 self._changeState(self.STATE_OPERATIONAL)
570 def setPause(self, pause):
571 if not pause and self.isPaused():
572 self._changeState(self.STATE_PRINTING)
573 for i in xrange(0, 6):
575 if pause and self.isPrinting():
576 self._changeState(self.STATE_PAUSED)
578 def setFeedrateModifier(self, type, value):
579 self._feedRateModifier[type] = value
581 def getExceptionString():
582 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
583 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])