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
165 self.thread = threading.Thread(target=self._monitor)
166 self.thread.daemon = True
169 def _changeState(self, newState):
170 if self._state == newState:
172 oldState = self.getStateString()
173 self._state = newState
174 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
175 self._callback.mcStateChange(newState)
180 def getStateString(self):
181 if self._state == self.STATE_NONE:
183 if self._state == self.STATE_OPEN_SERIAL:
184 return "Opening serial port"
185 if self._state == self.STATE_DETECT_SERIAL:
186 return "Detecting serial port"
187 if self._state == self.STATE_DETECT_BAUDRATE:
188 return "Detecting baudrate"
189 if self._state == self.STATE_CONNECTING:
191 if self._state == self.STATE_OPERATIONAL:
193 if self._state == self.STATE_PRINTING:
195 if self._state == self.STATE_PAUSED:
197 if self._state == self.STATE_CLOSED:
199 if self._state == self.STATE_ERROR:
200 return "Error: %s" % (self.getShortErrorString())
201 if self._state == self.STATE_CLOSED_WITH_ERROR:
202 return "Error: %s" % (self.getShortErrorString())
203 return "?%d?" % (self._state)
205 def getShortErrorString(self):
206 if len(self._errorValue) < 20:
207 return self._errorValue
208 return self._errorValue[:20] + "..."
210 def isClosedOrError(self):
211 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
214 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
216 def isOperational(self):
217 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
219 def isPrinting(self):
220 return self._state == self.STATE_PRINTING
223 return self._state == self.STATE_PAUSED
225 def getPrintPos(self):
226 return self._gcodePos
228 def getPrintTime(self):
229 return time.time() - self._printStartTime - self._heatupWaitTimeLost
234 def getBedTemp(self):
239 while not self._logQueue.empty():
240 ret.append(self._logQueue.get())
242 self._logQueue.put(line, False)
246 #Open the serial port.
247 if self._port == 'AUTO':
248 self._changeState(self.STATE_DETECT_SERIAL)
249 programmer = stk500v2.Stk500v2()
250 self._log("Serial port list: %s" % (str(serialList())))
251 for p in serialList():
253 self._log("Connecting to: %s" % (p))
254 programmer.connect(p)
255 self._serial = programmer.leaveISP()
256 profile.putPreference('serial_port_auto', p)
258 except ispBase.IspError as (e):
259 self._log("Error while connecting to %s: %s" % (p, str(e)))
262 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
264 elif self._port == 'VIRTUAL':
265 self._changeState(self.STATE_OPEN_SERIAL)
266 self._serial = VirtualPrinter()
268 self._changeState(self.STATE_OPEN_SERIAL)
270 self._log("Connecting to: %s" % (self._port))
271 if self._baudrate == 0:
272 self._serial = serial.Serial(self._port, 115200, timeout=0.1, writeTimeout=10000)
274 self._serial = serial.Serial(self._port, self._baudrate, timeout=2, writeTimeout=10000)
276 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
277 if self._serial == None:
278 self._log("Failed to open serial port (%s)" % (self._port))
279 self._errorValue = 'Failed to autodetect serial port.'
280 self._changeState(self.STATE_ERROR)
282 self._log("Connected to: %s, starting monitor" % (self._serial))
283 if self._baudrate == 0:
284 self._changeState(self.STATE_DETECT_BAUDRATE)
286 self._changeState(self.STATE_CONNECTING)
288 #Start monitoring the serial port.
289 timeout = time.time() + 5
290 tempRequestTimeout = timeout
292 line = self._readline()
296 #No matter the state, if we see an error, goto the error state and store the error for reference.
297 if line.startswith('Error:'):
298 #Oh YEAH, consistency.
299 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
300 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
301 # So we can have an extra newline in the most common case. Awesome work people.
302 if re.match('Error:[0-9]\n', line):
303 line = line.rstrip() + self._readline()
304 #Skip the communication errors, as those get corrected.
305 if 'checksum mismatch' in line or 'Line Number is not Last Line Number' in line or 'No Line Number with checksum' in line:
307 elif not self.isError():
308 self._errorValue = line[6:]
309 self._changeState(self.STATE_ERROR)
310 if ' T:' in line or line.startswith('T:'):
311 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
313 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
314 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
315 #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.
316 if not 'ok' in line and self._heatupWaitStartTime != 0:
318 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
319 self._heatupWaitStartTime = t
320 elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
321 self._callback.mcMessage(line)
323 if self._state == self.STATE_DETECT_BAUDRATE:
324 if line == '' or time.time() > timeout:
325 if len(self._baudrateDetectList) < 1:
327 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
328 self._changeState(self.STATE_ERROR)
329 elif self._baudrateDetectRetry > 0:
330 self._baudrateDetectRetry -= 1
331 self._serial.write('\n')
332 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
333 self._sendCommand("M105")
334 self._testingBaudrate = True
336 baudrate = self._baudrateDetectList.pop(0)
338 self._serial.baudrate = baudrate
339 self._serial.timeout = 0.5
340 self._log("Trying baudrate: %d" % (baudrate))
341 self._baudrateDetectRetry = 5
342 self._baudrateDetectTestOk = 0
343 timeout = time.time() + 5
344 self._serial.write('\n')
345 self._sendCommand("M105")
346 self._testingBaudrate = True
348 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
349 elif 'ok' in line and 'T:' in line:
350 self._baudrateDetectTestOk += 1
351 if self._baudrateDetectTestOk < 10:
352 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
353 self._sendCommand("M105")
355 self._sendCommand("M999")
356 self._serial.timeout = 2
357 profile.putPreference('serial_baud_auto', self._serial.baudrate)
358 self._changeState(self.STATE_OPERATIONAL)
360 self._testingBaudrate = False
361 elif self._state == self.STATE_CONNECTING:
363 self._sendCommand("M105")
365 self._changeState(self.STATE_OPERATIONAL)
366 if time.time() > timeout:
368 elif self._state == self.STATE_OPERATIONAL:
369 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
371 self._sendCommand("M105")
372 tempRequestTimeout = time.time() + 5
373 elif self._state == self.STATE_PRINTING:
374 if line == '' and time.time() > timeout:
375 self._log("Communication timeout during printing, forcing a line")
377 #Even when printing request the temperture every 5 seconds.
378 if time.time() > tempRequestTimeout:
379 self._commandQueue.put("M105")
380 tempRequestTimeout = time.time() + 5
382 timeout = time.time() + 5
383 if not self._commandQueue.empty():
384 self._sendCommand(self._commandQueue.get())
387 elif "resend" in line.lower() or "rs" in line:
389 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
392 self._gcodePos = int(line.split()[1])
393 self._log("Connection closed, closing down monitor")
395 def _log(self, message):
396 self._callback.mcLog(message)
398 self._logQueue.put(message, False)
400 #If the log queue is full, remove the first message and append the new message again
402 self._logQueue.put(message, False)
405 if self._serial == None:
408 ret = self._serial.readline()
410 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
411 self._errorValue = getExceptionString()
415 #self._log("Recv: TIMEOUT")
417 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
420 def close(self, isError = False):
421 if self._serial != None:
424 self._changeState(self.STATE_CLOSED_WITH_ERROR)
426 self._changeState(self.STATE_CLOSED)
432 def _sendCommand(self, cmd):
433 if self._serial == None:
435 if 'M109' in cmd or 'M190' in cmd:
436 self._heatupWaitStartTime = time.time()
437 if 'M104' in cmd or 'M109' in cmd:
439 self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
442 if 'M140' in cmd or 'M190' in cmd:
444 self._bedTargetTemp = float(re.search('S([0-9]+)').group(1))
447 self._log('Send: %s' % (cmd))
449 self._serial.write(cmd + '\n')
450 except serial.SerialTimeoutException:
451 self._log("Serial timeout while writing to serial port, trying again.")
453 self._serial.write(cmd + '\n')
455 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
456 self._errorValue = getExceptionString()
459 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
460 self._errorValue = getExceptionString()
464 if self._gcodePos >= len(self._gcodeList):
465 self._changeState(self.STATE_OPERATIONAL)
467 line = self._gcodeList[self._gcodePos]
468 if type(line) is tuple:
469 self._printSection = line[1]
472 if line == 'M0' or line == 'M1':
474 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
475 if self._printSection in self._feedRateModifier:
476 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
477 if ('G0' in line or 'G1' in line) and 'Z' in line:
478 z = float(re.search('Z([0-9\.]*)', line).group(1))
479 if self._currentZ != z:
481 self._callback.mcZChange(z)
483 self._log("Unexpected error: %s" % (getExceptionString()))
484 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
485 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
487 self._callback.mcProgress(self._gcodePos)
489 def sendCommand(self, cmd):
490 cmd = cmd.encode('ascii', 'replace')
491 if self.isPrinting():
492 self._commandQueue.put(cmd)
493 elif self.isOperational():
494 self._sendCommand(cmd)
496 def printGCode(self, gcodeList):
497 if not self.isOperational() or self.isPrinting():
499 self._gcodeList = gcodeList
501 self._printSection = 'CUSTOM'
502 self._changeState(self.STATE_PRINTING)
503 self._printStartTime = time.time()
504 for i in xrange(0, 6):
507 def cancelPrint(self):
508 if self.isOperational():
509 self._changeState(self.STATE_OPERATIONAL)
511 def setPause(self, pause):
512 if not pause and self.isPaused():
513 self._changeState(self.STATE_PRINTING)
514 for i in xrange(0, 6):
516 if pause and self.isPrinting():
517 self._changeState(self.STATE_PAUSED)
519 def setFeedrateModifier(self, type, value):
520 self._feedRateModifier[type] = value
522 def getExceptionString():
523 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
524 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])