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 getErrorString(self):
212 return self._errorValue
214 def isClosedOrError(self):
215 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
218 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
220 def isOperational(self):
221 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
223 def isPrinting(self):
224 return self._state == self.STATE_PRINTING
227 return self._state == self.STATE_PAUSED
229 def getPrintPos(self):
230 return self._gcodePos
232 def getPrintTime(self):
233 return time.time() - self._printStartTime
235 def getPrintTimeRemainingEstimate(self):
236 if self._printStartTime100 == None or self.getPrintPos() < 200:
238 printTime = (time.time() - self._printStartTime100) / 60
239 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
240 printTimeLeft = printTimeTotal - printTime
246 def getBedTemp(self):
251 while not self._logQueue.empty():
252 ret.append(self._logQueue.get())
254 self._logQueue.put(line, False)
258 #Open the serial port.
259 if self._port == 'AUTO':
260 self._changeState(self.STATE_DETECT_SERIAL)
261 programmer = stk500v2.Stk500v2()
262 self._log("Serial port list: %s" % (str(serialList())))
263 for p in serialList():
265 self._log("Connecting to: %s" % (p))
266 programmer.connect(p)
267 self._serial = programmer.leaveISP()
268 profile.putPreference('serial_port_auto', p)
270 except ispBase.IspError as (e):
271 self._log("Error while connecting to %s: %s" % (p, str(e)))
274 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
276 elif self._port == 'VIRTUAL':
277 self._changeState(self.STATE_OPEN_SERIAL)
278 self._serial = VirtualPrinter()
280 self._changeState(self.STATE_OPEN_SERIAL)
282 self._log("Connecting to: %s" % (self._port))
283 if self._baudrate == 0:
284 self._serial = serial.Serial(self._port, 115200, timeout=0.1, writeTimeout=10000)
286 self._serial = serial.Serial(self._port, self._baudrate, timeout=2, writeTimeout=10000)
288 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
289 if self._serial == None:
290 self._log("Failed to open serial port (%s)" % (self._port))
291 self._errorValue = 'Failed to autodetect serial port.'
292 self._changeState(self.STATE_ERROR)
294 self._log("Connected to: %s, starting monitor" % (self._serial))
295 if self._baudrate == 0:
296 self._changeState(self.STATE_DETECT_BAUDRATE)
298 self._changeState(self.STATE_CONNECTING)
300 #Start monitoring the serial port.
301 timeout = time.time() + 5
302 tempRequestTimeout = timeout
304 line = self._readline()
308 #No matter the state, if we see an error, goto the error state and store the error for reference.
309 if line.startswith('Error:'):
310 #Oh YEAH, consistency.
311 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
312 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
313 # So we can have an extra newline in the most common case. Awesome work people.
314 if re.match('Error:[0-9]\n', line):
315 line = line.rstrip() + self._readline()
316 #Skip the communication errors, as those get corrected.
317 if 'checksum mismatch' in line or 'Line Number is not Last Line Number' in line or 'No Line Number with checksum' in line:
319 elif not self.isError():
320 self._errorValue = line[6:]
321 self._changeState(self.STATE_ERROR)
322 if ' T:' in line or line.startswith('T:'):
323 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
325 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
326 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
327 #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.
328 if not 'ok' in line and self._heatupWaitStartTime != 0:
330 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
331 self._heatupWaitStartTime = t
332 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
333 self._callback.mcMessage(line)
335 if self._state == self.STATE_DETECT_BAUDRATE:
336 if line == '' or time.time() > timeout:
337 if len(self._baudrateDetectList) < 1:
339 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
340 self._changeState(self.STATE_ERROR)
341 elif self._baudrateDetectRetry > 0:
342 self._baudrateDetectRetry -= 1
343 self._serial.write('\n')
344 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
345 self._sendCommand("M105")
346 self._testingBaudrate = True
348 baudrate = self._baudrateDetectList.pop(0)
350 self._serial.baudrate = baudrate
351 self._serial.timeout = 0.5
352 self._log("Trying baudrate: %d" % (baudrate))
353 self._baudrateDetectRetry = 5
354 self._baudrateDetectTestOk = 0
355 timeout = time.time() + 5
356 self._serial.write('\n')
357 self._sendCommand("M105")
358 self._testingBaudrate = True
360 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
361 elif 'ok' in line and 'T:' in line:
362 self._baudrateDetectTestOk += 1
363 if self._baudrateDetectTestOk < 10:
364 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
365 self._sendCommand("M105")
367 self._sendCommand("M999")
368 self._serial.timeout = 2
369 profile.putPreference('serial_baud_auto', self._serial.baudrate)
370 self._changeState(self.STATE_OPERATIONAL)
372 self._testingBaudrate = False
373 elif self._state == self.STATE_CONNECTING:
375 self._sendCommand("M105")
377 self._changeState(self.STATE_OPERATIONAL)
378 if time.time() > timeout:
380 elif self._state == self.STATE_OPERATIONAL:
381 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
383 self._sendCommand("M105")
384 tempRequestTimeout = time.time() + 5
385 elif self._state == self.STATE_PRINTING:
386 if line == '' and time.time() > timeout:
387 self._log("Communication timeout during printing, forcing a line")
389 #Even when printing request the temperture every 5 seconds.
390 if time.time() > tempRequestTimeout:
391 self._commandQueue.put("M105")
392 tempRequestTimeout = time.time() + 5
394 timeout = time.time() + 5
395 if not self._commandQueue.empty():
396 self._sendCommand(self._commandQueue.get())
399 elif "resend" in line.lower() or "rs" in line:
401 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
404 self._gcodePos = int(line.split()[1])
405 self._log("Connection closed, closing down monitor")
407 def _log(self, message):
408 self._callback.mcLog(message)
410 self._logQueue.put(message, False)
412 #If the log queue is full, remove the first message and append the new message again
415 self._logQueue.put(message, False)
420 if self._serial == None:
423 ret = self._serial.readline()
425 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
426 self._errorValue = getExceptionString()
430 #self._log("Recv: TIMEOUT")
432 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
435 def close(self, isError = False):
436 if self._serial != None:
439 self._changeState(self.STATE_CLOSED_WITH_ERROR)
441 self._changeState(self.STATE_CLOSED)
447 def _sendCommand(self, cmd):
448 if self._serial == None:
450 if 'M109' in cmd or 'M190' in cmd:
451 self._heatupWaitStartTime = time.time()
452 if 'M104' in cmd or 'M109' in cmd:
454 self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
457 if 'M140' in cmd or 'M190' in cmd:
459 self._bedTargetTemp = float(re.search('S([0-9]+)').group(1))
462 self._log('Send: %s' % (cmd))
464 self._serial.write(cmd + '\n')
465 except serial.SerialTimeoutException:
466 self._log("Serial timeout while writing to serial port, trying again.")
468 self._serial.write(cmd + '\n')
470 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
471 self._errorValue = getExceptionString()
474 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
475 self._errorValue = getExceptionString()
479 if self._gcodePos >= len(self._gcodeList):
480 self._changeState(self.STATE_OPERATIONAL)
482 if self._gcodePos == 100:
483 self._printStartTime100 = time.time()
484 line = self._gcodeList[self._gcodePos]
485 if type(line) is tuple:
486 self._printSection = line[1]
489 if line == 'M0' or line == 'M1':
491 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
492 if self._printSection in self._feedRateModifier:
493 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
494 if ('G0' in line or 'G1' in line) and 'Z' in line:
495 z = float(re.search('Z([0-9\.]*)', line).group(1))
496 if self._currentZ != z:
498 self._callback.mcZChange(z)
500 self._log("Unexpected error: %s" % (getExceptionString()))
501 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
502 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
504 self._callback.mcProgress(self._gcodePos)
506 def sendCommand(self, cmd):
507 cmd = cmd.encode('ascii', 'replace')
508 if self.isPrinting():
509 self._commandQueue.put(cmd)
510 elif self.isOperational():
511 self._sendCommand(cmd)
513 def printGCode(self, gcodeList):
514 if not self.isOperational() or self.isPrinting():
516 self._gcodeList = gcodeList
518 self._printStartTime100 = None
519 self._printSection = 'CUSTOM'
520 self._changeState(self.STATE_PRINTING)
521 self._printStartTime = time.time()
522 for i in xrange(0, 6):
525 def cancelPrint(self):
526 if self.isOperational():
527 self._changeState(self.STATE_OPERATIONAL)
529 def setPause(self, pause):
530 if not pause and self.isPaused():
531 self._changeState(self.STATE_PRINTING)
532 for i in xrange(0, 6):
534 if pause and self.isPrinting():
535 self._changeState(self.STATE_PAUSED)
537 def setFeedrateModifier(self, type, value):
538 self._feedRateModifier[type] = value
540 def getExceptionString():
541 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
542 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])