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.usb*")
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
172 self._extruderCount = int(profile.getPreference('extruder_amount'))
173 self._temperatureRequestExtruder = 0
174 self._temp = [0] * self._extruderCount
175 self._targetTemp = [0] * self._extruderCount
177 self._bedTargetTemp = 0
178 self._gcodeList = None
180 self._commandQueue = queue.Queue()
181 self._logQueue = queue.Queue(256)
182 self._feedRateModifier = {}
184 self._heatupWaitStartTime = 0
185 self._heatupWaitTimeLost = 0.0
186 self._printStartTime100 = None
188 self.thread = threading.Thread(target=self._monitor)
189 self.thread.daemon = True
192 def _changeState(self, newState):
193 if self._state == newState:
195 oldState = self.getStateString()
196 self._state = newState
197 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
198 self._callback.mcStateChange(newState)
203 def getStateString(self):
204 if self._state == self.STATE_NONE:
206 if self._state == self.STATE_OPEN_SERIAL:
207 return "Opening serial port"
208 if self._state == self.STATE_DETECT_SERIAL:
209 return "Detecting serial port"
210 if self._state == self.STATE_DETECT_BAUDRATE:
211 return "Detecting baudrate"
212 if self._state == self.STATE_CONNECTING:
214 if self._state == self.STATE_OPERATIONAL:
216 if self._state == self.STATE_PRINTING:
218 if self._state == self.STATE_PAUSED:
220 if self._state == self.STATE_CLOSED:
222 if self._state == self.STATE_ERROR:
223 return "Error: %s" % (self.getShortErrorString())
224 if self._state == self.STATE_CLOSED_WITH_ERROR:
225 return "Error: %s" % (self.getShortErrorString())
226 return "?%d?" % (self._state)
228 def getShortErrorString(self):
229 if len(self._errorValue) < 20:
230 return self._errorValue
231 return self._errorValue[:20] + "..."
233 def getErrorString(self):
234 return self._errorValue
236 def isClosedOrError(self):
237 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
240 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
242 def isOperational(self):
243 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
245 def isPrinting(self):
246 return self._state == self.STATE_PRINTING
249 return self._state == self.STATE_PAUSED
251 def getPrintPos(self):
252 return self._gcodePos
254 def getPrintTime(self):
255 return time.time() - self._printStartTime
257 def getPrintTimeRemainingEstimate(self):
258 if self._printStartTime100 == None or self.getPrintPos() < 200:
260 printTime = (time.time() - self._printStartTime100) / 60
261 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
262 printTimeLeft = printTimeTotal - printTime
268 def getBedTemp(self):
273 while not self._logQueue.empty():
274 ret.append(self._logQueue.get())
276 self._logQueue.put(line, False)
280 #Open the serial port.
281 if self._port == 'AUTO':
282 self._changeState(self.STATE_DETECT_SERIAL)
283 programmer = stk500v2.Stk500v2()
284 self._log("Serial port list: %s" % (str(serialList(True))))
285 for p in serialList(True):
287 self._log("Connecting to: %s" % (p))
288 programmer.connect(p)
289 self._serial = programmer.leaveISP()
290 profile.putPreference('serial_port_auto', p)
292 except ispBase.IspError as (e):
293 self._log("Error while connecting to %s: %s" % (p, str(e)))
296 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
298 elif self._port == 'VIRTUAL':
299 self._changeState(self.STATE_OPEN_SERIAL)
300 self._serial = VirtualPrinter()
302 self._changeState(self.STATE_OPEN_SERIAL)
304 self._log("Connecting to: %s" % (self._port))
305 if self._baudrate == 0:
306 self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
308 self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
310 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
311 if self._serial == None:
312 self._log("Failed to open serial port (%s)" % (self._port))
313 self._errorValue = 'Failed to autodetect serial port.'
314 self._changeState(self.STATE_ERROR)
316 self._log("Connected to: %s, starting monitor" % (self._serial))
317 if self._baudrate == 0:
318 self._changeState(self.STATE_DETECT_BAUDRATE)
320 self._changeState(self.STATE_CONNECTING)
322 #Start monitoring the serial port.
323 if self._state == self.STATE_CONNECTING:
324 timeout = time.time() + 15
326 timeout = time.time() + 5
327 tempRequestTimeout = timeout
329 line = self._readline()
333 #No matter the state, if we see an error, goto the error state and store the error for reference.
334 if line.startswith('Error:'):
335 #Oh YEAH, consistency.
336 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
337 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
338 # So we can have an extra newline in the most common case. Awesome work people.
339 if re.match('Error:[0-9]\n', line):
340 line = line.rstrip() + self._readline()
341 #Skip the communication errors, as those get corrected.
342 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:
344 elif not self.isError():
345 self._errorValue = line[6:]
346 self._changeState(self.STATE_ERROR)
347 if ' T:' in line or line.startswith('T:'):
348 self._temp[self._temperatureRequestExtruder] = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
350 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
351 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
352 #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.
353 if not 'ok' in line and self._heatupWaitStartTime != 0:
355 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
356 self._heatupWaitStartTime = t
357 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
358 self._callback.mcMessage(line)
360 if self._state == self.STATE_DETECT_BAUDRATE:
361 if line == '' or time.time() > timeout:
362 if len(self._baudrateDetectList) < 1:
364 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
365 self._changeState(self.STATE_ERROR)
366 elif self._baudrateDetectRetry > 0:
367 self._baudrateDetectRetry -= 1
368 self._serial.write('\n')
369 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
370 self._sendCommand("M105")
371 self._testingBaudrate = True
373 baudrate = self._baudrateDetectList.pop(0)
375 self._serial.baudrate = baudrate
376 self._serial.timeout = 0.5
377 self._log("Trying baudrate: %d" % (baudrate))
378 self._baudrateDetectRetry = 5
379 self._baudrateDetectTestOk = 0
380 timeout = time.time() + 5
381 self._serial.write('\n')
382 self._sendCommand("M105")
383 self._testingBaudrate = True
385 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
386 elif 'ok' in line and 'T:' in line:
387 self._baudrateDetectTestOk += 1
388 if self._baudrateDetectTestOk < 10:
389 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
390 self._sendCommand("M105")
392 self._sendCommand("M999")
393 self._serial.timeout = 2
394 profile.putPreference('serial_baud_auto', self._serial.baudrate)
395 self._changeState(self.STATE_OPERATIONAL)
397 self._testingBaudrate = False
398 elif self._state == self.STATE_CONNECTING:
400 self._sendCommand("M105")
402 self._changeState(self.STATE_OPERATIONAL)
403 if time.time() > timeout:
405 elif self._state == self.STATE_OPERATIONAL:
406 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
408 if self._extruderCount > 0:
409 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
410 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
412 self._sendCommand("M105")
413 tempRequestTimeout = time.time() + 5
414 elif self._state == self.STATE_PRINTING:
415 if line == '' and time.time() > timeout:
416 self._log("Communication timeout during printing, forcing a line")
418 #Even when printing request the temperature every 5 seconds.
419 if time.time() > tempRequestTimeout:
420 if self._extruderCount > 0:
421 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
422 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
424 self._sendCommand("M105")
425 tempRequestTimeout = time.time() + 5
427 timeout = time.time() + 5
428 if not self._commandQueue.empty():
429 self._sendCommand(self._commandQueue.get())
432 elif "resend" in line.lower() or "rs" in line:
434 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
437 self._gcodePos = int(line.split()[1])
438 self._log("Connection closed, closing down monitor")
440 def _log(self, message):
441 self._callback.mcLog(message)
443 self._logQueue.put(message, False)
445 #If the log queue is full, remove the first message and append the new message again
448 self._logQueue.put(message, False)
453 if self._serial == None:
456 ret = self._serial.readline()
458 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
459 self._errorValue = getExceptionString()
463 #self._log("Recv: TIMEOUT")
465 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
468 def close(self, isError = False):
469 if self._serial != None:
472 self._changeState(self.STATE_CLOSED_WITH_ERROR)
474 self._changeState(self.STATE_CLOSED)
480 def _sendCommand(self, cmd):
481 if self._serial is None:
483 if 'M109' in cmd or 'M190' in cmd:
484 self._heatupWaitStartTime = time.time()
485 if 'M104' in cmd or 'M109' in cmd:
489 t = int(re.search('T([0-9]+)', cmd).group(1))
490 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
493 if 'M140' in cmd or 'M190' in cmd:
495 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
498 self._log('Send: %s' % (cmd))
500 self._serial.write(cmd + '\n')
501 except serial.SerialTimeoutException:
502 self._log("Serial timeout while writing to serial port, trying again.")
505 self._serial.write(cmd + '\n')
507 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
508 self._errorValue = getExceptionString()
511 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
512 self._errorValue = getExceptionString()
516 if self._gcodePos >= len(self._gcodeList):
517 self._changeState(self.STATE_OPERATIONAL)
519 if self._gcodePos == 100:
520 self._printStartTime100 = time.time()
521 line = self._gcodeList[self._gcodePos]
522 if type(line) is tuple:
523 self._printSection = line[1]
526 if line == 'M0' or line == 'M1':
528 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
529 if self._printSection in self._feedRateModifier:
530 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
531 if ('G0' in line or 'G1' in line) and 'Z' in line:
532 z = float(re.search('Z([0-9\.]*)', line).group(1))
533 if self._currentZ != z:
535 self._callback.mcZChange(z)
537 self._log("Unexpected error: %s" % (getExceptionString()))
538 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
539 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
541 self._callback.mcProgress(self._gcodePos)
543 def sendCommand(self, cmd):
544 cmd = cmd.encode('ascii', 'replace')
545 if self.isPrinting():
546 self._commandQueue.put(cmd)
547 elif self.isOperational():
548 self._sendCommand(cmd)
550 def printGCode(self, gcodeList):
551 if not self.isOperational() or self.isPrinting():
553 self._gcodeList = gcodeList
555 self._printStartTime100 = None
556 self._printSection = 'CUSTOM'
557 self._changeState(self.STATE_PRINTING)
558 self._printStartTime = time.time()
559 for i in xrange(0, 4):
562 def cancelPrint(self):
563 if self.isOperational():
564 self._changeState(self.STATE_OPERATIONAL)
566 def setPause(self, pause):
567 if not pause and self.isPaused():
568 self._changeState(self.STATE_PRINTING)
569 for i in xrange(0, 6):
571 if pause and self.isPrinting():
572 self._changeState(self.STATE_PAUSED)
574 def setFeedrateModifier(self, type, value):
575 self._feedRateModifier[type] = value
577 def getExceptionString():
578 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
579 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])