1 from __future__ import absolute_import
4 import os, glob, sys, time, math, re, traceback, threading
7 from serial import Serial
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._errorValue)
201 if self._state == self.STATE_CLOSED_WITH_ERROR:
202 return "Error: %s" % (self._errorValue)
203 return "?%d?" % (self._state)
205 def isClosedOrError(self):
206 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
209 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
211 def isOperational(self):
212 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
214 def isPrinting(self):
215 return self._state == self.STATE_PRINTING
218 return self._state == self.STATE_PAUSED
220 def getPrintPos(self):
221 return self._gcodePos
223 def getPrintTime(self):
224 return time.time() - self._printStartTime - self._heatupWaitTimeLost
229 def getBedTemp(self):
234 while not self._logQueue.empty():
235 ret.append(self._logQueue.get())
237 self._logQueue.put(line, False)
241 #Open the serial port.
242 if self._port == 'AUTO':
243 self._changeState(self.STATE_DETECT_SERIAL)
244 programmer = stk500v2.Stk500v2()
245 self._log("Serial port list: %s" % (str(serialList())))
246 for p in serialList():
248 self._log("Connecting to: %s" % (p))
249 programmer.connect(p)
250 self._serial = programmer.leaveISP()
251 profile.putPreference('serial_port_auto', p)
253 except ispBase.IspError as (e):
254 self._log("Error while connecting to %s: %s" % (p, str(e)))
257 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
259 elif self._port == 'VIRTUAL':
260 self._changeState(self.STATE_OPEN_SERIAL)
261 self._serial = VirtualPrinter()
263 self._changeState(self.STATE_OPEN_SERIAL)
265 self._log("Connecting to: %s" % (self._port))
266 if self._baudrate == 0:
267 self._serial = Serial(self._port, 115200, timeout=0.1, writeTimeout=10000)
269 self._serial = Serial(self._port, self._baudrate, timeout=2, writeTimeout=10000)
271 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
272 if self._serial == None:
273 self._log("Failed to open serial port (%s)" % (self._port))
274 self._errorValue = 'Failed to autodetect serial port.'
275 self._changeState(self.STATE_ERROR)
277 self._log("Connected to: %s, starting monitor" % (self._serial))
278 if self._baudrate == 0:
279 self._changeState(self.STATE_DETECT_BAUDRATE)
281 self._changeState(self.STATE_CONNECTING)
283 #Start monitoring the serial port.
284 timeout = time.time() + 5
285 tempRequestTimeout = timeout
287 line = self._readline()
291 #No matter the state, if we see an error, goto the error state and store the error for reference.
292 if line.startswith('Error:'):
293 #Oh YEAH, consistency.
294 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
295 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
296 # So we can have an extra newline in the most common case. Awesome work people.
297 if re.match('Error:[0-9]\n', line):
298 line = line.rstrip() + self._readline()
299 #Skip the communication errors, as those get corrected.
300 if 'checksum mismatch' in line or 'Line Number is not Last Line Number' in line or 'No Line Number with checksum' in line:
303 self._errorValue = line[6:]
304 self._changeState(self.STATE_ERROR)
305 if ' T:' in line or line.startswith('T:'):
306 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
308 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
309 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
310 #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.
311 if not 'ok' in line and self._heatupWaitStartTime != 0:
313 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
314 self._heatupWaitStartTime = t
315 elif line.strip() != '' and line.strip() != 'ok' and self.isOperational():
316 self._callback.mcMessage(line)
318 if self._state == self.STATE_DETECT_BAUDRATE:
319 if line == '' or time.time() > timeout:
320 if len(self._baudrateDetectList) < 1:
321 self._log("No more baudrates to test, and no suitable baudrate found.")
323 elif self._baudrateDetectRetry > 0:
324 self._baudrateDetectRetry -= 1
325 self._serial.write('\n')
326 self._sendCommand("M105")
328 baudrate = self._baudrateDetectList.pop(0)
330 self._serial.baudrate = baudrate
331 self._serial.timeout = 0.5
332 self._log("Trying baudrate: %d" % (baudrate))
333 self._baudrateDetectRetry = 5
334 timeout = time.time() + 5
335 self._serial.write('\n')
336 self._sendCommand("M105")
338 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
340 self._sendCommand("M999")
341 self._serial.timeout = 2
342 profile.putPreference('serial_baud_auto', self._serial.baudrate)
343 self._changeState(self.STATE_OPERATIONAL)
344 elif self._state == self.STATE_CONNECTING:
346 self._sendCommand("M105")
348 self._changeState(self.STATE_OPERATIONAL)
349 if time.time() > timeout:
351 elif self._state == self.STATE_OPERATIONAL:
352 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
354 self._sendCommand("M105")
355 tempRequestTimeout = time.time() + 5
356 elif self._state == self.STATE_PRINTING:
357 if line == '' and time.time() > timeout:
358 self._log("Communication timeout during printing, forcing a line")
360 #Even when printing request the temperture every 5 seconds.
361 if time.time() > tempRequestTimeout:
362 self._commandQueue.put("M105")
363 tempRequestTimeout = time.time() + 5
365 timeout = time.time() + 5
366 if not self._commandQueue.empty():
367 self._sendCommand(self._commandQueue.get())
370 elif "resend" in line.lower() or "rs" in line:
372 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
375 self._gcodePos = int(line.split()[1])
376 self._log("Connection closed, closing down monitor")
378 def _log(self, message):
379 self._callback.mcLog(message)
381 self._logQueue.put(message, False)
383 #If the log queue is full, remove the first message and append the new message again
385 self._logQueue.put(message, False)
388 if self._serial == None:
391 ret = self._serial.readline()
393 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
394 self._errorValue = getExceptionString()
398 #self._log("Recv: TIMEOUT")
400 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
403 def close(self, isError = False):
404 if self._serial != None:
407 self._changeState(self.STATE_CLOSED_WITH_ERROR)
409 self._changeState(self.STATE_CLOSED)
415 def _sendCommand(self, cmd):
416 if self._serial == None:
418 if 'M109' in cmd or 'M190' in cmd:
419 self._heatupWaitStartTime = time.time()
420 if 'M104' in cmd or 'M109' in cmd:
422 self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
425 if 'M140' in cmd or 'M190' in cmd:
427 self._bedTargetTemp = float(re.search('S([0-9]+)').group(1))
430 self._log('Send: %s' % (cmd))
432 self._serial.write(cmd + '\n')
433 except SerialTimeoutException:
434 self._log("Serial timeout while writing to serial port, trying again.")
436 self._serial.write(cmd + '\n')
438 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
439 self._errorValue = getExceptionString()
442 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
443 self._errorValue = getExceptionString()
447 if self._gcodePos >= len(self._gcodeList):
448 self._changeState(self.STATE_OPERATIONAL)
450 line = self._gcodeList[self._gcodePos]
451 if type(line) is tuple:
452 self._printSection = line[1]
455 if line == 'M0' or line == 'M1':
457 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
458 if self._printSection in self._feedRateModifier:
459 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
460 if ('G0' in line or 'G1' in line) and 'Z' in line:
461 z = float(re.search('Z([0-9\.]*)', line).group(1))
462 if self._currentZ != z:
464 self._callback.mcZChange(z)
466 self._log("Unexpected error: %s" % (getExceptionString()))
467 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
468 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
470 self._callback.mcProgress(self._gcodePos)
472 def sendCommand(self, cmd):
473 cmd = cmd.encode('ascii', 'replace')
474 if self.isPrinting():
475 self._commandQueue.put(cmd)
476 elif self.isOperational():
477 self._sendCommand(cmd)
479 def printGCode(self, gcodeList):
480 if not self.isOperational() or self.isPrinting():
482 self._gcodeList = gcodeList
484 self._printSection = 'CUSTOM'
485 self._changeState(self.STATE_PRINTING)
486 self._printStartTime = time.time()
487 for i in xrange(0, 6):
490 def cancelPrint(self):
491 if self.isOperational():
492 self._changeState(self.STATE_OPERATIONAL)
494 def setPause(self, pause):
495 if not pause and self.isPaused():
496 self._changeState(self.STATE_PRINTING)
497 for i in xrange(0, 6):
499 if pause and self.isPrinting():
500 self._changeState(self.STATE_PAUSED)
502 def setFeedrateModifier(self, type, value):
503 self._feedRateModifier[type] = value
505 def getExceptionString():
506 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
507 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])