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/tty.usb*") + 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/tty.usb*") + glob.glob("/dev/cu.*") + 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 port = profile.getPreference('serial_port')
57 return len(serialList(True)) > 0
58 if platform.system() == "Windows":
59 return port in serialList(True)
60 return os.path.isfile(port)
63 ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
64 if profile.getPreference('serial_baud_auto') != '':
65 prev = int(profile.getPreference('serial_baud_auto'))
71 class VirtualPrinter():
73 self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
76 self.lastTempAt = time.time()
78 self.bedTargetTemp = 1.0
80 def write(self, data):
81 if self.readList is None:
83 #print "Send: %s" % (data.rstrip())
84 if 'M104' in data or 'M109' in data:
86 self.targetTemp = float(re.search('S([0-9]+)', data).group(1))
89 if 'M140' in data or 'M190' in data:
91 self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
95 self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
96 elif len(data.strip()) > 0:
97 self.readList.append("ok\n")
100 if self.readList is None:
103 timeDiff = self.lastTempAt - time.time()
104 self.lastTempAt = time.time()
105 if abs(self.temp - self.targetTemp) > 1:
106 self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp)
107 if abs(self.bedTemp - self.bedTargetTemp) > 1:
108 self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
109 while len(self.readList) < 1:
114 if self.readList is None:
117 #print "Recv: %s" % (self.readList[0].rstrip())
118 return self.readList.pop(0)
123 class MachineComPrintCallback(object):
124 def mcLog(self, message):
127 def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
130 def mcStateChange(self, state):
133 def mcMessage(self, message):
136 def mcProgress(self, lineNr):
139 def mcZChange(self, newZ):
142 class MachineCom(object):
144 STATE_OPEN_SERIAL = 1
145 STATE_DETECT_SERIAL = 2
146 STATE_DETECT_BAUDRATE = 3
148 STATE_OPERATIONAL = 5
153 STATE_CLOSED_WITH_ERROR = 10
155 def __init__(self, port = None, baudrate = None, callbackObject = None):
157 port = profile.getPreference('serial_port')
159 if profile.getPreference('serial_baud') == 'AUTO':
162 baudrate = int(profile.getPreference('serial_baud'))
163 if callbackObject is None:
164 callbackObject = MachineComPrintCallback()
167 self._baudrate = baudrate
168 self._callback = callbackObject
169 self._state = self.STATE_NONE
171 self._baudrateDetectList = baudrateList()
172 self._baudrateDetectRetry = 0
173 self._extruderCount = int(profile.getPreference('extruder_amount'))
174 self._temperatureRequestExtruder = 0
175 self._temp = [0] * self._extruderCount
176 self._targetTemp = [0] * self._extruderCount
178 self._bedTargetTemp = 0
179 self._gcodeList = None
181 self._commandQueue = queue.Queue()
182 self._logQueue = queue.Queue(256)
183 self._feedRateModifier = {}
185 self._heatupWaitStartTime = 0
186 self._heatupWaitTimeLost = 0.0
187 self._printStartTime100 = None
189 self.thread = threading.Thread(target=self._monitor)
190 self.thread.daemon = True
193 def _changeState(self, newState):
194 if self._state == newState:
196 oldState = self.getStateString()
197 self._state = newState
198 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
199 self._callback.mcStateChange(newState)
204 def getStateString(self):
205 if self._state == self.STATE_NONE:
207 if self._state == self.STATE_OPEN_SERIAL:
208 return "Opening serial port"
209 if self._state == self.STATE_DETECT_SERIAL:
210 return "Detecting serial port"
211 if self._state == self.STATE_DETECT_BAUDRATE:
212 return "Detecting baudrate"
213 if self._state == self.STATE_CONNECTING:
215 if self._state == self.STATE_OPERATIONAL:
217 if self._state == self.STATE_PRINTING:
219 if self._state == self.STATE_PAUSED:
221 if self._state == self.STATE_CLOSED:
223 if self._state == self.STATE_ERROR:
224 return "Error: %s" % (self.getShortErrorString())
225 if self._state == self.STATE_CLOSED_WITH_ERROR:
226 return "Error: %s" % (self.getShortErrorString())
227 return "?%d?" % (self._state)
229 def getShortErrorString(self):
230 if len(self._errorValue) < 20:
231 return self._errorValue
232 return self._errorValue[:20] + "..."
234 def getErrorString(self):
235 return self._errorValue
237 def isClosedOrError(self):
238 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
241 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
243 def isOperational(self):
244 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
246 def isPrinting(self):
247 return self._state == self.STATE_PRINTING
250 return self._state == self.STATE_PAUSED
252 def getPrintPos(self):
253 return self._gcodePos
255 def getPrintTime(self):
256 return time.time() - self._printStartTime
258 def getPrintTimeRemainingEstimate(self):
259 if self._printStartTime100 == None or self.getPrintPos() < 200:
261 printTime = (time.time() - self._printStartTime100) / 60
262 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
263 printTimeLeft = printTimeTotal - printTime
269 def getBedTemp(self):
274 while not self._logQueue.empty():
275 ret.append(self._logQueue.get())
277 self._logQueue.put(line, False)
281 #Open the serial port.
282 if self._port == 'AUTO':
283 self._changeState(self.STATE_DETECT_SERIAL)
284 programmer = stk500v2.Stk500v2()
285 self._log("Serial port list: %s" % (str(serialList(True))))
286 for p in serialList(True):
288 self._log("Connecting to: %s" % (p))
289 programmer.connect(p)
290 self._serial = programmer.leaveISP()
291 profile.putPreference('serial_port_auto', p)
293 except ispBase.IspError as (e):
294 self._log("Error while connecting to %s: %s" % (p, str(e)))
297 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
299 elif self._port == 'VIRTUAL':
300 self._changeState(self.STATE_OPEN_SERIAL)
301 self._serial = VirtualPrinter()
303 self._changeState(self.STATE_OPEN_SERIAL)
305 self._log("Connecting to: %s" % (self._port))
306 if self._baudrate == 0:
307 self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
309 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
311 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
312 if self._serial == None:
313 self._log("Failed to open serial port (%s)" % (self._port))
314 self._errorValue = 'Failed to autodetect serial port.'
315 self._changeState(self.STATE_ERROR)
317 self._log("Connected to: %s, starting monitor" % (self._serial))
318 if self._baudrate == 0:
319 self._changeState(self.STATE_DETECT_BAUDRATE)
321 self._changeState(self.STATE_CONNECTING)
323 #Start monitoring the serial port.
324 if self._state == self.STATE_CONNECTING:
325 timeout = time.time() + 15
327 timeout = time.time() + 5
328 tempRequestTimeout = timeout
330 line = self._readline()
334 #No matter the state, if we see an error, goto the error state and store the error for reference.
335 if line.startswith('Error:'):
336 #Oh YEAH, consistency.
337 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
338 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
339 # So we can have an extra newline in the most common case. Awesome work people.
340 if re.match('Error:[0-9]\n', line):
341 line = line.rstrip() + self._readline()
342 #Skip the communication errors, as those get corrected.
343 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:
345 elif not self.isError():
346 self._errorValue = line[6:]
347 self._changeState(self.STATE_ERROR)
348 if ' T:' in line or line.startswith('T:'):
349 self._temp[self._temperatureRequestExtruder] = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
351 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
352 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
353 #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.
354 if not 'ok' in line and self._heatupWaitStartTime != 0:
356 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
357 self._heatupWaitStartTime = t
358 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
359 self._callback.mcMessage(line)
361 if self._state == self.STATE_DETECT_BAUDRATE:
362 if line == '' or time.time() > timeout:
363 if len(self._baudrateDetectList) < 1:
365 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
366 self._changeState(self.STATE_ERROR)
367 elif self._baudrateDetectRetry > 0:
368 self._baudrateDetectRetry -= 1
369 self._serial.write('\n')
370 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
371 self._sendCommand("M105")
372 self._testingBaudrate = True
374 baudrate = self._baudrateDetectList.pop(0)
376 self._serial.baudrate = baudrate
377 self._serial.timeout = 0.5
378 self._log("Trying baudrate: %d" % (baudrate))
379 self._baudrateDetectRetry = 5
380 self._baudrateDetectTestOk = 0
381 timeout = time.time() + 5
382 self._serial.write('\n')
383 self._sendCommand("M105")
384 self._testingBaudrate = True
386 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
387 elif 'ok' in line and 'T:' in line:
388 self._baudrateDetectTestOk += 1
389 if self._baudrateDetectTestOk < 10:
390 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
391 self._sendCommand("M105")
393 self._sendCommand("M999")
394 self._serial.timeout = 2
395 profile.putPreference('serial_baud_auto', self._serial.baudrate)
396 self._changeState(self.STATE_OPERATIONAL)
398 self._testingBaudrate = False
399 elif self._state == self.STATE_CONNECTING:
401 self._sendCommand("M105")
403 self._changeState(self.STATE_OPERATIONAL)
404 if time.time() > timeout:
406 elif self._state == self.STATE_OPERATIONAL:
407 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
409 if self._extruderCount > 0:
410 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
411 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
413 self._sendCommand("M105")
414 tempRequestTimeout = time.time() + 5
415 elif self._state == self.STATE_PRINTING:
416 if line == '' and time.time() > timeout:
417 self._log("Communication timeout during printing, forcing a line")
419 #Even when printing request the temperature every 5 seconds.
420 if time.time() > tempRequestTimeout:
421 if self._extruderCount > 0:
422 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
423 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
425 self._sendCommand("M105")
426 tempRequestTimeout = time.time() + 5
428 timeout = time.time() + 5
429 if not self._commandQueue.empty():
430 self._sendCommand(self._commandQueue.get())
433 elif "resend" in line.lower() or "rs" in line:
435 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
438 self._gcodePos = int(line.split()[1])
439 self._log("Connection closed, closing down monitor")
441 def _log(self, message):
442 self._callback.mcLog(message)
444 self._logQueue.put(message, False)
446 #If the log queue is full, remove the first message and append the new message again
449 self._logQueue.put(message, False)
454 if self._serial == None:
457 ret = self._serial.readline()
459 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
460 self._errorValue = getExceptionString()
464 #self._log("Recv: TIMEOUT")
466 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
469 def close(self, isError = False):
470 if self._serial != None:
473 self._changeState(self.STATE_CLOSED_WITH_ERROR)
475 self._changeState(self.STATE_CLOSED)
481 def _sendCommand(self, cmd):
482 if self._serial is None:
484 if 'M109' in cmd or 'M190' in cmd:
485 self._heatupWaitStartTime = time.time()
486 if 'M104' in cmd or 'M109' in cmd:
490 t = int(re.search('T([0-9]+)', cmd).group(1))
491 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
494 if 'M140' in cmd or 'M190' in cmd:
496 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
499 self._log('Send: %s' % (cmd))
501 self._serial.write(cmd + '\n')
502 except serial.SerialTimeoutException:
503 self._log("Serial timeout while writing to serial port, trying again.")
506 self._serial.write(cmd + '\n')
508 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
509 self._errorValue = getExceptionString()
512 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
513 self._errorValue = getExceptionString()
517 if self._gcodePos >= len(self._gcodeList):
518 self._changeState(self.STATE_OPERATIONAL)
520 if self._gcodePos == 100:
521 self._printStartTime100 = time.time()
522 line = self._gcodeList[self._gcodePos]
523 if type(line) is tuple:
524 self._printSection = line[1]
527 if line == 'M0' or line == 'M1':
529 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
530 if self._printSection in self._feedRateModifier:
531 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
532 if ('G0' in line or 'G1' in line) and 'Z' in line:
533 z = float(re.search('Z([0-9\.]*)', line).group(1))
534 if self._currentZ != z:
536 self._callback.mcZChange(z)
538 self._log("Unexpected error: %s" % (getExceptionString()))
539 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
540 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
542 self._callback.mcProgress(self._gcodePos)
544 def sendCommand(self, cmd):
545 cmd = cmd.encode('ascii', 'replace')
546 if self.isPrinting():
547 self._commandQueue.put(cmd)
548 elif self.isOperational():
549 self._sendCommand(cmd)
551 def printGCode(self, gcodeList):
552 if not self.isOperational() or self.isPrinting():
554 self._gcodeList = gcodeList
556 self._printStartTime100 = None
557 self._printSection = 'CUSTOM'
558 self._changeState(self.STATE_PRINTING)
559 self._printStartTime = time.time()
560 for i in xrange(0, 4):
563 def cancelPrint(self):
564 if self.isOperational():
565 self._changeState(self.STATE_OPERATIONAL)
567 def setPause(self, pause):
568 if not pause and self.isPaused():
569 self._changeState(self.STATE_PRINTING)
570 for i in xrange(0, 6):
572 if pause and self.isPrinting():
573 self._changeState(self.STATE_PAUSED)
575 def setFeedrateModifier(self, type, value):
576 self._feedRateModifier[type] = value
578 def getExceptionString():
579 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
580 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])