1 from __future__ import absolute_import
4 import os, glob, sys, time, math, re, traceback, threading
9 from avr_isp import stk500v2
10 from avr_isp import ispBase
11 from avr_isp import intelHex
13 from util import profile
14 from util import version
25 key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
28 baselist+=[_winreg.EnumValue(key,i)[1]]
32 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*") + glob.glob("/dev/rfcomm*")
33 prev = profile.getPreference('serial_port_auto')
36 baselist.insert(0, prev)
37 if version.isDevVersion():
38 baselist.append('VIRTUAL')
42 ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
43 if profile.getPreference('serial_baud_auto') != '':
44 prev = int(profile.getPreference('serial_baud_auto'))
50 class VirtualPrinter():
52 self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
55 self.lastTempAt = time.time()
57 self.bedTargetTemp = 1.0
59 def write(self, data):
60 if self.readList == None:
62 #print "Send: %s" % (data.rstrip())
63 if 'M104' in data or 'M109' in data:
65 self.targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
68 if 'M140' in data or 'M190' in data:
70 self.bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
74 self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
75 elif len(data.strip()) > 0:
76 self.readList.append("ok\n")
79 if self.readList == None:
82 timeDiff = self.lastTempAt - time.time()
83 self.lastTempAt = time.time()
84 if abs(self.temp - self.targetTemp) > 1:
85 self.temp += math.copysign(timeDiff, self.targetTemp - self.temp)
86 if abs(self.bedTemp - self.bedTargetTemp) > 1:
87 self.bedTemp += math.copysign(timeDiff, self.bedTargetTemp - self.bedTemp)
88 while len(self.readList) < 1:
93 if self.readList == None:
96 #print "Recv: %s" % (self.readList[0].rstrip())
97 return self.readList.pop(0)
102 class MachineComPrintCallback(object):
103 def mcLog(self, message):
106 def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
109 def mcStateChange(self, state):
112 def mcMessage(self, message):
115 def mcProgress(self, lineNr):
118 def mcZChange(self, newZ):
121 class MachineCom(object):
123 STATE_OPEN_SERIAL = 1
124 STATE_DETECT_SERIAL = 2
125 STATE_DETECT_BAUDRATE = 3
127 STATE_OPERATIONAL = 5
132 STATE_CLOSED_WITH_ERROR = 10
134 def __init__(self, port = None, baudrate = None, callbackObject = None):
136 port = profile.getPreference('serial_port')
138 if profile.getPreference('serial_baud') == 'AUTO':
141 baudrate = int(profile.getPreference('serial_baud'))
142 if callbackObject == None:
143 callbackObject = MachineComPrintCallback()
146 self._baudrate = baudrate
147 self._callback = callbackObject
148 self._state = self.STATE_NONE
150 self._baudrateDetectList = baudrateList()
151 self._baudrateDetectRetry = 0
155 self._bedTargetTemp = 0
156 self._gcodeList = None
158 self._commandQueue = queue.Queue()
159 self._logQueue = queue.Queue(256)
160 self._feedRateModifier = {}
162 self._heatupWaitStartTime = 0
163 self._heatupWaitTimeLost = 0.0
164 self._printStartTime100 = None
166 self.thread = threading.Thread(target=self._monitor)
167 self.thread.daemon = True
170 def _changeState(self, newState):
171 if self._state == newState:
173 oldState = self.getStateString()
174 self._state = newState
175 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
176 self._callback.mcStateChange(newState)
181 def getStateString(self):
182 if self._state == self.STATE_NONE:
184 if self._state == self.STATE_OPEN_SERIAL:
185 return "Opening serial port"
186 if self._state == self.STATE_DETECT_SERIAL:
187 return "Detecting serial port"
188 if self._state == self.STATE_DETECT_BAUDRATE:
189 return "Detecting baudrate"
190 if self._state == self.STATE_CONNECTING:
192 if self._state == self.STATE_OPERATIONAL:
194 if self._state == self.STATE_PRINTING:
196 if self._state == self.STATE_PAUSED:
198 if self._state == self.STATE_CLOSED:
200 if self._state == self.STATE_ERROR:
201 return "Error: %s" % (self.getShortErrorString())
202 if self._state == self.STATE_CLOSED_WITH_ERROR:
203 return "Error: %s" % (self.getShortErrorString())
204 return "?%d?" % (self._state)
206 def getShortErrorString(self):
207 if len(self._errorValue) < 20:
208 return self._errorValue
209 return self._errorValue[:20] + "..."
211 def isClosedOrError(self):
212 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
215 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
217 def isOperational(self):
218 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
220 def isPrinting(self):
221 return self._state == self.STATE_PRINTING
224 return self._state == self.STATE_PAUSED
226 def getPrintPos(self):
227 return self._gcodePos
229 def getPrintTime(self):
230 return time.time() - self._printStartTime
232 def getPrintTimeRemainingEstimate(self):
233 if self._printStartTime100 == None or self.getPrintPos() < 200:
235 printTime = (time.time() - self._printStartTime100) / 60
236 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
237 printTimeLeft = printTimeTotal - printTime
243 def getBedTemp(self):
248 while not self._logQueue.empty():
249 ret.append(self._logQueue.get())
251 self._logQueue.put(line, False)
255 #Open the serial port.
256 if self._port == 'AUTO':
257 self._changeState(self.STATE_DETECT_SERIAL)
258 programmer = stk500v2.Stk500v2()
259 self._log("Serial port list: %s" % (str(serialList())))
260 for p in serialList():
262 self._log("Connecting to: %s" % (p))
263 programmer.connect(p)
264 self._serial = programmer.leaveISP()
265 profile.putPreference('serial_port_auto', p)
267 except ispBase.IspError as (e):
268 self._log("Error while connecting to %s: %s" % (p, str(e)))
271 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
273 elif self._port == 'VIRTUAL':
274 self._changeState(self.STATE_OPEN_SERIAL)
275 self._serial = VirtualPrinter()
277 self._changeState(self.STATE_OPEN_SERIAL)
279 self._log("Connecting to: %s" % (self._port))
280 if self._baudrate == 0:
281 self._serial = serial.Serial(self._port, 115200, timeout=0.1, writeTimeout=10000)
283 self._serial = serial.Serial(self._port, self._baudrate, timeout=2, writeTimeout=10000)
285 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
286 if self._serial == None:
287 self._log("Failed to open serial port (%s)" % (self._port))
288 self._errorValue = 'Failed to autodetect serial port.'
289 self._changeState(self.STATE_ERROR)
291 self._log("Connected to: %s, starting monitor" % (self._serial))
292 if self._baudrate == 0:
293 self._changeState(self.STATE_DETECT_BAUDRATE)
295 self._changeState(self.STATE_CONNECTING)
297 #Start monitoring the serial port.
298 timeout = time.time() + 5
299 tempRequestTimeout = timeout
301 line = self._readline()
305 #No matter the state, if we see an error, goto the error state and store the error for reference.
306 if line.startswith('Error:'):
307 #Oh YEAH, consistency.
308 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
309 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
310 # So we can have an extra newline in the most common case. Awesome work people.
311 if re.match('Error:[0-9]\n', line):
312 line = line.rstrip() + self._readline()
313 #Skip the communication errors, as those get corrected.
314 if 'checksum mismatch' in line or 'Line Number is not Last Line Number' in line or 'No Line Number with checksum' in line:
316 elif not self.isError():
317 self._errorValue = line[6:]
318 self._changeState(self.STATE_ERROR)
319 if ' T:' in line or line.startswith('T:'):
320 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
322 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
323 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
324 #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.
325 if not 'ok' in line and self._heatupWaitStartTime != 0:
327 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
328 self._heatupWaitStartTime = t
329 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
330 self._callback.mcMessage(line)
332 if self._state == self.STATE_DETECT_BAUDRATE:
333 if line == '' or time.time() > timeout:
334 if len(self._baudrateDetectList) < 1:
336 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
337 self._changeState(self.STATE_ERROR)
338 elif self._baudrateDetectRetry > 0:
339 self._baudrateDetectRetry -= 1
340 self._serial.write('\n')
341 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
342 self._sendCommand("M105")
343 self._testingBaudrate = True
345 baudrate = self._baudrateDetectList.pop(0)
347 self._serial.baudrate = baudrate
348 self._serial.timeout = 0.5
349 self._log("Trying baudrate: %d" % (baudrate))
350 self._baudrateDetectRetry = 5
351 self._baudrateDetectTestOk = 0
352 timeout = time.time() + 5
353 self._serial.write('\n')
354 self._sendCommand("M105")
355 self._testingBaudrate = True
357 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
358 elif 'ok' in line and 'T:' in line:
359 self._baudrateDetectTestOk += 1
360 if self._baudrateDetectTestOk < 10:
361 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
362 self._sendCommand("M105")
364 self._sendCommand("M999")
365 self._serial.timeout = 2
366 profile.putPreference('serial_baud_auto', self._serial.baudrate)
367 self._changeState(self.STATE_OPERATIONAL)
369 self._testingBaudrate = False
370 elif self._state == self.STATE_CONNECTING:
372 self._sendCommand("M105")
374 self._changeState(self.STATE_OPERATIONAL)
375 if time.time() > timeout:
377 elif self._state == self.STATE_OPERATIONAL:
378 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
380 self._sendCommand("M105")
381 tempRequestTimeout = time.time() + 5
382 elif self._state == self.STATE_PRINTING:
383 if line == '' and time.time() > timeout:
384 self._log("Communication timeout during printing, forcing a line")
386 #Even when printing request the temperture every 5 seconds.
387 if time.time() > tempRequestTimeout:
388 self._commandQueue.put("M105")
389 tempRequestTimeout = time.time() + 5
391 timeout = time.time() + 5
392 if not self._commandQueue.empty():
393 self._sendCommand(self._commandQueue.get())
396 elif "resend" in line.lower() or "rs" in line:
398 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
401 self._gcodePos = int(line.split()[1])
402 self._log("Connection closed, closing down monitor")
404 def _log(self, message):
405 self._callback.mcLog(message)
407 self._logQueue.put(message, False)
409 #If the log queue is full, remove the first message and append the new message again
412 self._logQueue.put(message, False)
417 if self._serial == None:
420 ret = self._serial.readline()
422 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
423 self._errorValue = getExceptionString()
427 #self._log("Recv: TIMEOUT")
429 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
432 def close(self, isError = False):
433 if self._serial != None:
436 self._changeState(self.STATE_CLOSED_WITH_ERROR)
438 self._changeState(self.STATE_CLOSED)
444 def _sendCommand(self, cmd):
445 if self._serial == None:
447 if 'M109' in cmd or 'M190' in cmd:
448 self._heatupWaitStartTime = time.time()
449 if 'M104' in cmd or 'M109' in cmd:
451 self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
454 if 'M140' in cmd or 'M190' in cmd:
456 self._bedTargetTemp = float(re.search('S([0-9]+)').group(1))
459 self._log('Send: %s' % (cmd))
461 self._serial.write(cmd + '\n')
462 except serial.SerialTimeoutException:
463 self._log("Serial timeout while writing to serial port, trying again.")
465 self._serial.write(cmd + '\n')
467 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
468 self._errorValue = getExceptionString()
471 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
472 self._errorValue = getExceptionString()
476 if self._gcodePos >= len(self._gcodeList):
477 self._changeState(self.STATE_OPERATIONAL)
479 if self._gcodePos == 100:
480 self._printStartTime100 = time.time()
481 line = self._gcodeList[self._gcodePos]
482 if type(line) is tuple:
483 self._printSection = line[1]
486 if line == 'M0' or line == 'M1':
488 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
489 if self._printSection in self._feedRateModifier:
490 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
491 if ('G0' in line or 'G1' in line) and 'Z' in line:
492 z = float(re.search('Z([0-9\.]*)', line).group(1))
493 if self._currentZ != z:
495 self._callback.mcZChange(z)
497 self._log("Unexpected error: %s" % (getExceptionString()))
498 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
499 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
501 self._callback.mcProgress(self._gcodePos)
503 def sendCommand(self, cmd):
504 cmd = cmd.encode('ascii', 'replace')
505 if self.isPrinting():
506 self._commandQueue.put(cmd)
507 elif self.isOperational():
508 self._sendCommand(cmd)
510 def printGCode(self, gcodeList):
511 if not self.isOperational() or self.isPrinting():
513 self._gcodeList = gcodeList
515 self._printStartTime100 = None
516 self._printSection = 'CUSTOM'
517 self._changeState(self.STATE_PRINTING)
518 self._printStartTime = time.time()
519 for i in xrange(0, 6):
522 def cancelPrint(self):
523 if self.isOperational():
524 self._changeState(self.STATE_OPERATIONAL)
526 def setPause(self, pause):
527 if not pause and self.isPaused():
528 self._changeState(self.STATE_PRINTING)
529 for i in xrange(0, 6):
531 if pause and self.isPrinting():
532 self._changeState(self.STATE_PAUSED)
534 def setFeedrateModifier(self, type, value):
535 self._feedRateModifier[type] = value
537 def getExceptionString():
538 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
539 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])