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(data[data.find('S')+1:])
68 if 'M140' in data or 'M190' in data:
70 self.bedTargetTemp = float(data[data.find('S')+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):
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
154 self._gcodeList = None
156 self._commandQueue = queue.Queue()
157 self._logQueue = queue.Queue(256)
158 self._feedRateModifier = {}
160 self._heatupWaitStartTime = 0
161 self._heatupWaitTimeLost = 0.0
163 self.thread = threading.Thread(target=self._monitor)
164 self.thread.daemon = True
167 def _changeState(self, newState):
168 if self._state == newState:
170 oldState = self.getStateString()
171 self._state = newState
172 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
173 self._callback.mcStateChange(newState)
178 def getStateString(self):
179 if self._state == self.STATE_NONE:
181 if self._state == self.STATE_OPEN_SERIAL:
182 return "Opening serial port"
183 if self._state == self.STATE_DETECT_SERIAL:
184 return "Detecting serial port"
185 if self._state == self.STATE_DETECT_BAUDRATE:
186 return "Detecting baudrate"
187 if self._state == self.STATE_CONNECTING:
189 if self._state == self.STATE_OPERATIONAL:
191 if self._state == self.STATE_PRINTING:
193 if self._state == self.STATE_PAUSED:
195 if self._state == self.STATE_CLOSED:
197 if self._state == self.STATE_ERROR:
198 return "Error: %s" % (self._errorValue)
199 if self._state == self.STATE_CLOSED_WITH_ERROR:
200 return "Error: %s" % (self._errorValue)
201 return "?%d?" % (self._state)
203 def isClosedOrError(self):
204 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
207 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
209 def isOperational(self):
210 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
212 def isPrinting(self):
213 return self._state == self.STATE_PRINTING
215 def getPrintPos(self):
216 return self._gcodePos
218 def getPrintTime(self):
219 return time.time() - self._printStartTime - self._heatupWaitTimeLost
222 return self._state == self.STATE_PAUSED
227 def getBedTemp(self):
232 while not self._logQueue.empty():
233 ret.append(self._logQueue.get())
235 self._logQueue.put(line, False)
239 #Open the serial port.
240 if self._port == 'AUTO':
241 self._changeState(self.STATE_DETECT_SERIAL)
242 programmer = stk500v2.Stk500v2()
243 self._log("Serial port list: %s" % (str(serialList())))
244 for p in serialList():
246 self._log("Connecting to: %s" % (p))
247 programmer.connect(p)
248 self._serial = programmer.leaveISP()
249 profile.putPreference('serial_port_auto', p)
251 except ispBase.IspError as (e):
252 self._log("Error while connecting to %s: %s" % (p, str(e)))
255 self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
257 elif self._port == 'VIRTUAL':
258 self._changeState(self.STATE_OPEN_SERIAL)
259 self._serial = VirtualPrinter()
261 self._changeState(self.STATE_OPEN_SERIAL)
263 self._log("Connecting to: %s" % (self._port))
264 if self._baudrate == 0:
265 self._serial = Serial(self._port, 115200, timeout=0.1, writeTimeout=10000)
267 self._serial = Serial(self._port, self._baudrate, timeout=2, writeTimeout=10000)
269 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
270 if self._serial == None:
271 self._log("Failed to open serial port (%s)" % (self._port))
272 self._errorValue = 'Failed to autodetect serial port.'
273 self._changeState(self.STATE_ERROR)
275 self._log("Connected to: %s, starting monitor" % (self._serial))
276 if self._baudrate == 0:
277 self._changeState(self.STATE_DETECT_BAUDRATE)
279 self._changeState(self.STATE_CONNECTING)
281 #Start monitoring the serial port.
282 timeout = time.time() + 5
283 tempRequestTimeout = timeout
285 line = self._readline()
289 #No matter the state, if we see an error, goto the error state and store the error for reference.
290 if line.startswith('Error: '):
291 #Oh YEAH, consistency.
292 # Marlin reports an MIN/MAX temp error as "Error: x\n: Extruder switched off. MAXTEMP triggered !\n"
293 # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
294 # So we can have an extra newline in the most common case. Awesome work people.
295 if re.match('Error: [0-9]\n', line):
296 line = line.rstrip() + self._readline()
297 self._errorValue = line
298 self._changeState(self.STATE_ERROR)
299 if ' T:' in line or line.startswith('T:'):
300 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
302 self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
303 self._callback.mcTempUpdate(self._temp, self._bedTemp)
304 #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.
305 if not 'ok' in line and self._heatupWaitStartTime != 0:
307 self._heatupWaitTimeLost = t - self._heatupWaitStartTime
308 self._heatupWaitStartTime = t
309 elif line.strip() != 'ok' and self.isOperational():
310 self._callback.mcMessage(line)
312 if self._state == self.STATE_DETECT_BAUDRATE:
313 if line == '' or time.time() > timeout:
314 if len(self._baudrateDetectList) < 1:
315 self._log("No more baudrates to test, and no suitable baudrate found.")
317 elif self._baudrateDetectRetry > 0:
318 self._baudrateDetectRetry -= 1
319 self._serial.write('\n')
320 self._sendCommand("M105")
322 baudrate = self._baudrateDetectList.pop(0)
324 self._serial.baudrate = baudrate
325 self._serial.timeout = 0.5
326 self._log("Trying baudrate: %d" % (baudrate))
327 self._baudrateDetectRetry = 5
328 timeout = time.time() + 5
329 self._serial.write('\n')
330 self._sendCommand("M105")
332 self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
334 self._serial.timeout = 2
335 profile.putPreference('serial_baud_auto', self._serial.baudrate)
336 self._changeState(self.STATE_OPERATIONAL)
337 elif self._state == self.STATE_CONNECTING:
339 self._sendCommand("M105")
341 self._changeState(self.STATE_OPERATIONAL)
342 if time.time() > timeout:
344 elif self._state == self.STATE_OPERATIONAL:
345 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
347 self._sendCommand("M105")
348 tempRequestTimeout = time.time() + 5
349 elif self._state == self.STATE_PRINTING:
350 if line == '' and time.time() > timeout:
351 self._log("Communication timeout during printing, forcing a line")
353 #Even when printing request the temperture every 5 seconds.
354 if time.time() > tempRequestTimeout:
355 self._commandQueue.put("M105")
356 tempRequestTimeout = time.time() + 5
358 timeout = time.time() + 5
359 if not self._commandQueue.empty():
360 self._sendCommand(self._commandQueue.get())
363 elif "resend" in line.lower() or "rs" in line:
365 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
368 self._gcodePos = int(line.split()[1])
369 self._log("Connection closed, closing down monitor")
371 def _log(self, message):
372 self._callback.mcLog(message)
374 self._logQueue.put(message, False)
376 #If the log queue is full, remove the first message and append the new message again
378 self._logQueue.put(message, False)
381 if self._serial == None:
384 ret = self._serial.readline()
386 self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
387 self._errorValue = getExceptionString()
391 #self._log("Recv: TIMEOUT")
393 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
396 def close(self, isError = False):
397 if self._serial != None:
400 self._changeState(self.STATE_CLOSED_WITH_ERROR)
402 self._changeState(self.STATE_CLOSED)
408 def _sendCommand(self, cmd):
409 if self._serial == None:
411 if 'M109' in cmd or 'M190' in cmd:
412 self._heatupWaitStartTime = time.time()
413 self._log('Send: %s' % (cmd))
415 #TODO: This can throw a write timeout exception, but we do not want timeout on writes. Find a fix for this.
416 # Oddly enough, the write timeout is not even set and thus we should not get a write timeout.
417 self._serial.write(cmd + '\n')
419 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
420 self._errorValue = getExceptionString()
424 if self._gcodePos >= len(self._gcodeList):
425 self._changeState(self.STATE_OPERATIONAL)
427 line = self._gcodeList[self._gcodePos]
428 if type(line) is tuple:
429 self._printSection = line[1]
432 if line == 'M0' or line == 'M1':
434 line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
435 if self._printSection in self._feedRateModifier:
436 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
437 if ('G0' in line or 'G1' in line) and 'Z' in line:
438 z = float(re.search('Z([0-9\.]*)', line).group(1))
439 if self._currentZ != z:
441 self._callback.mcZChange(z)
443 self._log("Unexpected error: %s" % (getExceptionString()))
444 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
445 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
447 self._callback.mcProgress(self._gcodePos)
449 def sendCommand(self, cmd):
450 cmd = cmd.encode('ascii', 'replace')
451 if self.isPrinting():
452 self._commandQueue.put(cmd)
453 elif self.isOperational():
454 self._sendCommand(cmd)
456 def printGCode(self, gcodeList):
457 if not self.isOperational() or self.isPrinting():
459 self._gcodeList = gcodeList
461 self._printSection = 'CUSTOM'
462 self._changeState(self.STATE_PRINTING)
463 self._printStartTime = time.time()
464 for i in xrange(0, 6):
467 def cancelPrint(self):
468 if self.isOperational():
469 self._changeState(self.STATE_OPERATIONAL)
471 def setPause(self, pause):
472 if not pause and self.isPaused():
473 self._changeState(self.STATE_PRINTING)
474 for i in xrange(0, 6):
476 if pause and self.isPrinting():
477 self._changeState(self.STATE_PAUSED)
479 def setFeedrateModifier(self, type, value):
480 self._feedRateModifier[type] = value
482 def getExceptionString():
483 locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
484 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])