from __future__ import absolute_import import os import glob import sys import time import math import re import traceback import threading import Queue as queue import logging import serial from octoprint.util.avr_isp import stk500v2 from octoprint.util.avr_isp import ispBase from octoprint.settings import settings try: import _winreg except: pass def isDevVersion(): gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../.git")) return os.path.exists(gitPath) def serialList(): baselist=[] if os.name=="nt": try: key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM") i=0 while(1): baselist+=[_winreg.EnumValue(key,i)[1]] i+=1 except: pass baselist = baselist + glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/cu.*") + glob.glob("/dev/rfcomm*") prev = settings().get(["serial", "port"]) if prev in baselist: baselist.remove(prev) baselist.insert(0, prev) if isDevVersion(): baselist.append("VIRTUAL") return baselist def baudrateList(): ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600] prev = settings().getInt(["serial", "baudrate"]) if prev in ret: ret.remove(prev) ret.insert(0, prev) return ret class VirtualPrinter(): def __init__(self): self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n'] self.temp = 0.0 self.targetTemp = 0.0 self.lastTempAt = time.time() self.bedTemp = 1.0 self.bedTargetTemp = 1.0 self.currentLine = 0 waitThread = threading.Thread(target=self._sendWaitAfterTimeout) waitThread.start() def write(self, data): if self.readList is None: return #print "Send: %s" % (data.rstrip()) if 'M104' in data or 'M109' in data: try: self.targetTemp = float(re.search('S([0-9]+)', data).group(1)) except: pass if 'M140' in data or 'M190' in data: try: self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1)) except: pass if 'M105' in data: # send simulated temperature data self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp)) elif "M110" in data: # reset current line self.currentLine = int(re.search('N([0-9]+)', data).group(1)) self.readList.append("ok\n") elif self.currentLine == 100: # simulate a resend at line 100 of the last 5 lines self.readList.append("Error: Line Number is not Last Line Number\n") self.readList.append("rs %d\n" % (self.currentLine - 5)) elif len(data.strip()) > 0: self.readList.append("ok\n") if "*" in data: self.currentLine += 1 def readline(self): if self.readList is None: return '' n = 0 timeDiff = self.lastTempAt - time.time() self.lastTempAt = time.time() if abs(self.temp - self.targetTemp) > 1: self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp) if self.temp < 0: self.temp = 0 if abs(self.bedTemp - self.bedTargetTemp) > 1: self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp) if self.bedTemp < 0: self.bedTemp = 0 while len(self.readList) < 1: time.sleep(0.1) n += 1 if n == 20: return '' if self.readList is None: return '' time.sleep(0.001) #print "Recv: %s" % (self.readList[0].rstrip()) return self.readList.pop(0) def close(self): self.readList = None def _sendWaitAfterTimeout(self, timeout=5): time.sleep(timeout) self.readList.append("wait") class MachineComPrintCallback(object): def mcLog(self, message): pass def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp): pass def mcStateChange(self, state): pass def mcMessage(self, message): pass def mcProgress(self, lineNr): pass def mcZChange(self, newZ): pass class MachineCom(object): STATE_NONE = 0 STATE_OPEN_SERIAL = 1 STATE_DETECT_SERIAL = 2 STATE_DETECT_BAUDRATE = 3 STATE_CONNECTING = 4 STATE_OPERATIONAL = 5 STATE_PRINTING = 6 STATE_PAUSED = 7 STATE_CLOSED = 8 STATE_ERROR = 9 STATE_CLOSED_WITH_ERROR = 10 def __init__(self, port = None, baudrate = None, callbackObject = None): self._logger = logging.getLogger(__name__) self._serialLogger = logging.getLogger("SERIAL") if port == None: port = settings().get(["serial", "port"]) if baudrate == None: settingsBaudrate = settings().getInt(["serial", "baudrate"]) if settingsBaudrate is None: baudrate = 0 else: baudrate = settingsBaudrate if callbackObject == None: callbackObject = MachineComPrintCallback() self._port = port self._baudrate = baudrate self._callback = callbackObject self._state = self.STATE_NONE self._serial = None self._baudrateDetectList = baudrateList() self._baudrateDetectRetry = 0 self._temp = 0 self._bedTemp = 0 self._targetTemp = 0 self._bedTargetTemp = 0 self._gcodeList = None self._gcodePos = 0 self._commandQueue = queue.Queue() self._logQueue = queue.Queue(256) self._feedRateModifier = {} self._currentZ = -1 self._heatupWaitStartTime = 0 self._heatupWaitTimeLost = 0.0 self._printStartTime100 = None self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) self._currentLine = 1 self._resendDelta = None self._lastLines = [] self._sendingLock = threading.Lock() self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() def _changeState(self, newState): if self._state == newState: return oldState = self.getStateString() self._state = newState self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) self._callback.mcStateChange(newState) def getState(self): return self._state def getStateString(self): if self._state == self.STATE_NONE: return "Offline" if self._state == self.STATE_OPEN_SERIAL: return "Opening serial port" if self._state == self.STATE_DETECT_SERIAL: return "Detecting serial port" if self._state == self.STATE_DETECT_BAUDRATE: return "Detecting baudrate" if self._state == self.STATE_CONNECTING: return "Connecting" if self._state == self.STATE_OPERATIONAL: return "Operational" if self._state == self.STATE_PRINTING: return "Printing" if self._state == self.STATE_PAUSED: return "Paused" if self._state == self.STATE_CLOSED: return "Closed" if self._state == self.STATE_ERROR: return "Error: %s" % (self.getShortErrorString()) if self._state == self.STATE_CLOSED_WITH_ERROR: return "Error: %s" % (self.getShortErrorString()) return "?%d?" % (self._state) def getShortErrorString(self): if len(self._errorValue) < 20: return self._errorValue return self._errorValue[:20] + "..." def getErrorString(self): return self._errorValue def isClosedOrError(self): return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED def isError(self): return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR def isOperational(self): return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED def isPrinting(self): return self._state == self.STATE_PRINTING def isPaused(self): return self._state == self.STATE_PAUSED def getPrintPos(self): return self._gcodePos def getPrintTime(self): if self._printStartTime100 == None: return 0 else: return time.time() - self._printStartTime100 def getPrintTimeRemainingEstimate(self): if self._printStartTime100 == None or self.getPrintPos() < 200: return None printTime = (time.time() - self._printStartTime100) / 60 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100) printTimeLeft = printTimeTotal - printTime return printTimeLeft def getTemp(self): return self._temp def getBedTemp(self): return self._bedTemp def getLog(self): ret = [] while not self._logQueue.empty(): ret.append(self._logQueue.get()) for line in ret: self._logQueue.put(line, False) return ret def _monitor(self): #Open the serial port. if self._port == 'AUTO': self._changeState(self.STATE_DETECT_SERIAL) programmer = stk500v2.Stk500v2() self._log("Serial port list: %s" % (str(serialList()))) for p in serialList(): try: self._log("Connecting to: %s" % (p)) programmer.connect(p) self._serial = programmer.leaveISP() break except ispBase.IspError as (e): self._log("Error while connecting to %s: %s" % (p, str(e))) pass except: self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString())) programmer.close() elif self._port == 'VIRTUAL': self._changeState(self.STATE_OPEN_SERIAL) self._serial = VirtualPrinter() else: self._changeState(self.STATE_OPEN_SERIAL) try: self._log("Connecting to: %s" % (self._port)) if self._baudrate == 0: self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000) else: self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000) except: self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString())) if self._serial == None: self._log("Failed to open serial port (%s)" % (self._port)) self._errorValue = 'Failed to autodetect serial port.' self._changeState(self.STATE_ERROR) return self._log("Connected to: %s, starting monitor" % (self._serial)) if self._baudrate == 0: self._changeState(self.STATE_DETECT_BAUDRATE) else: self._changeState(self.STATE_CONNECTING) #Start monitoring the serial port. timeout = time.time() + 5 tempRequestTimeout = timeout startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) waitSeen = not settings().getBoolean(["feature", "waitForWaitOnConnect"]) while True: line = self._readline() if line == None: break ### Error detection #No matter the state, if we see an error, goto the error state and store the error for reference. if line.startswith('Error:'): #Oh YEAH, consistency. # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!" # So we can have an extra newline in the most common case. Awesome work people. if re.match('Error:[0-9]\n', line): line = line.rstrip() + self._readline() #Skip the communication errors, as those get corrected. if 'checksum mismatch' in line \ or 'Wrong checksum' in line \ or 'Line Number is not Last Line Number' in line \ or 'expected line' in line \ or 'No Line Number with checksum' in line \ or 'No Checksum with line number' in line \ or 'Missing checksum' in line: pass elif not self.isError(): self._errorValue = line[6:] self._changeState(self.STATE_ERROR) ### Evaluate temperature status messages if ' T:' in line or line.startswith('T:'): try: self._temp = float(re.search("-?[0-9\.]*", line.split('T:')[1]).group(0)) if ' B:' in line: self._bedTemp = float(re.search("-?[0-9\.]*", line.split(' B:')[1]).group(0)) self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp) except ValueError: # catch conversion issues, we'll rather just not get the temperature update instead of killing the connection pass #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. if not 'ok' in line and self._heatupWaitStartTime != 0: t = time.time() self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitStartTime = t ### Forward messages from the firmware elif line.strip() != '' and line.strip() != 'ok' and not line.startswith("wait") and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational(): self._callback.mcMessage(line) ### Baudrate detection if self._state == self.STATE_DETECT_BAUDRATE: if line == '' or time.time() > timeout: if len(self._baudrateDetectList) < 1: self.close() self._errorValue = "No more baudrates to test, and no suitable baudrate found." self._changeState(self.STATE_ERROR) elif self._baudrateDetectRetry > 0: self._baudrateDetectRetry -= 1 self._serial.write('\n') self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry)) self._sendCommand("M105") self._testingBaudrate = True else: baudrate = self._baudrateDetectList.pop(0) try: self._serial.baudrate = baudrate self._serial.timeout = 0.5 self._log("Trying baudrate: %d" % (baudrate)) self._baudrateDetectRetry = 5 self._baudrateDetectTestOk = 0 timeout = time.time() + 5 self._serial.write('\n') self._sendCommand("M105") self._testingBaudrate = True except: self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString())) elif 'ok' in line and 'T:' in line: self._baudrateDetectTestOk += 1 if self._baudrateDetectTestOk < 10: self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk)) self._sendCommand("M105") else: self._sendCommand("M999") self._serial.timeout = 2 self._changeState(self.STATE_OPERATIONAL) else: self._testingBaudrate = False ### Connection attempt elif self._state == self.STATE_CONNECTING: if line == "" and startSeen and waitSeen: self._sendCommand("M105") elif "start" in line: startSeen = True elif "wait" in line: waitSeen = True elif "ok" in line and startSeen and waitSeen: self._changeState(self.STATE_OPERATIONAL) elif time.time() > timeout: self.close() ### Operational elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: #Request the temperature on comm timeout (every 5 seconds) when we are not printing. if line == "" or "wait" in line: if self._resendDelta is not None: self._resendNextCommand() elif not self._commandQueue.empty(): self._sendCommand(self._commandQueue.get()) else: self._sendCommand("M105") tempRequestTimeout = time.time() + 5 # resend -> start resend procedure from requested line elif "resend" in line.lower() or "rs" in line: self._handleResendRequest(line) ### Printing elif self._state == self.STATE_PRINTING: if line == "" and time.time() > timeout: self._log("Communication timeout during printing, forcing a line") line = "ok" #Even when printing request the temperature every 5 seconds. if time.time() > tempRequestTimeout: self._commandQueue.put("M105") tempRequestTimeout = time.time() + 5 # ok -> send next command if "ok" in line: timeout = time.time() + 5 if self._resendDelta is not None: self._resendNextCommand() elif not self._commandQueue.empty(): self._sendCommand(self._commandQueue.get()) else: self._sendNext() # resend -> start resend procedure from requested line elif "resend" in line.lower() or "rs" in line: self._handleResendRequest(line) self._log("Connection closed, closing down monitor") def _handleResendRequest(self, line): lineToResend = None try: lineToResend = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1]) except: if "rs" in line: lineToResend = int(line.split()[1]) if lineToResend is not None: self._resendDelta = self._currentLine - lineToResend if self._resendDelta > len(self._lastLines): self._errorValue = "Printer requested line %d but history is only available up to line %d" % (lineToResend, self._currentLine - len(self._lastLines)) self._changeState(self.STATE_ERROR) self._logger.warn(self._errorValue) else: self._resendNextCommand() def _log(self, message): self._callback.mcLog(message) self._serialLogger.debug(message) try: self._logQueue.put(message, False) except: #If the log queue is full, remove the first message and append the new message again self._logQueue.get() try: self._logQueue.put(message, False) except: pass def _readline(self): if self._serial == None: return None try: ret = self._serial.readline() except: self._log("Unexpected error while reading serial port: %s" % (getExceptionString())) self._errorValue = getExceptionString() self.close(True) return None if ret == '': #self._log("Recv: TIMEOUT") return '' self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip())) return ret def close(self, isError = False): if self._serial != None: self._serial.close() if isError: self._changeState(self.STATE_CLOSED_WITH_ERROR) else: self._changeState(self.STATE_CLOSED) self._serial = None def __del__(self): self.close() def _resendNextCommand(self): # Make sure we are only handling one sending job at a time with self._sendingLock: self._logger.debug("Resending line %d, delta is %d, history log is %s items strong" % (self._currentLine - self._resendDelta, self._resendDelta, len(self._lastLines))) cmd = self._lastLines[-self._resendDelta] lineNumber = self._currentLine - self._resendDelta self._doSendWithChecksum(cmd, lineNumber) self._resendDelta -= 1 if self._resendDelta <= 0: self._resendDelta = None def _sendCommand(self, cmd, sendChecksum=False): # Make sure we are only handling one sending job at a time with self._sendingLock: cmd = cmd.upper() if self._serial is None: return if 'M109' in cmd or 'M190' in cmd: self._heatupWaitStartTime = time.time() if 'M104' in cmd or 'M109' in cmd: try: self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1)) except: pass if 'M140' in cmd or 'M190' in cmd: try: self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1)) except: pass if "M110" in cmd: newLineNumber = None if " N" in cmd: try: newLineNumber = int(re.search("N([0-9]+)", cmd).group(1)) except: pass else: newLineNumber = 0 if settings().getBoolean(["feature", "resetLineNumbersWithPrefixedN"]) and newLineNumber is not None: # let's rewrite the M110 command to fit repetier syntax self._addToLastLines(cmd) self._doSendWithChecksum("M110", newLineNumber) else: self._doSend(cmd, sendChecksum) if newLineNumber is not None: self._currentLine = newLineNumber + 1 # after a reset of the line number we have no way to determine what line exactly the printer now wants self._lastLines = [] self._resendDelta = None else: self._doSend(cmd, sendChecksum) def _addToLastLines(self, cmd): self._lastLines.append(cmd) if len(self._lastLines) > 50: self._lastLines = self._lastLines[-50:] # only keep the last 50 lines in memory self._logger.debug("Got %d lines of history in memory" % len(self._lastLines)) def _doSend(self, cmd, sendChecksum=False): if sendChecksum or self._alwaysSendChecksum: if self._alwaysSendChecksum: lineNumber = self._currentLine else: lineNumber = self._gcodePos self._addToLastLines(cmd) self._currentLine += 1 self._doSendWithChecksum(cmd, lineNumber) else: self._doSendWithoutChecksum(cmd) def _doSendWithChecksum(self, cmd, lineNumber): self._logger.debug("Sending cmd '%s' with lineNumber %r" % (cmd, lineNumber)) checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (lineNumber, cmd))) commandToSend = "N%d%s*%d" % (lineNumber, cmd, checksum) self._doSendWithoutChecksum(commandToSend) def _doSendWithoutChecksum(self, cmd): self._log("Send: %s" % cmd) try: self._serial.write(cmd + '\n') except serial.SerialTimeoutException: self._log("Serial timeout while writing to serial port, trying again.") try: self._serial.write(cmd + '\n') except: self._log("Unexpected error while writing serial port: %s" % (getExceptionString())) self._errorValue = getExceptionString() self.close(True) except: self._log("Unexpected error while writing serial port: %s" % (getExceptionString())) self._errorValue = getExceptionString() self.close(True) def _sendNext(self): if self._gcodePos >= len(self._gcodeList): self._changeState(self.STATE_OPERATIONAL) return if self._gcodePos == 100: self._printStartTime100 = time.time() line = self._gcodeList[self._gcodePos] if type(line) is tuple: self._printSection = line[1] line = line[0] try: if line == 'M0' or line == 'M1': self.setPause(True) line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. if self._printSection in self._feedRateModifier: line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line) if ('G0' in line or 'G1' in line) and 'Z' in line: z = float(re.search('Z([0-9\.]*)', line).group(1)) if self._currentZ != z: self._currentZ = z self._callback.mcZChange(z) except: self._log("Unexpected error: %s" % (getExceptionString())) self._sendCommand(line, True) self._gcodePos += 1 self._callback.mcProgress(self._gcodePos) def sendCommand(self, cmd): cmd = cmd.encode('ascii', 'replace') if self.isPrinting(): self._commandQueue.put(cmd) elif self.isOperational(): self._sendCommand(cmd) def printGCode(self, gcodeList): if not self.isOperational() or self.isPrinting(): return self._gcodeList = gcodeList self._gcodePos = 0 self._printStartTime100 = None self._printSection = 'CUSTOM' self._changeState(self.STATE_PRINTING) self._printStartTime = time.time() for i in xrange(0, 6): self._sendNext() def cancelPrint(self): if self.isOperational(): self._changeState(self.STATE_OPERATIONAL) def setPause(self, pause): if not pause and self.isPaused(): self._changeState(self.STATE_PRINTING) for i in xrange(0, 6): self._sendNext() if pause and self.isPrinting(): self._changeState(self.STATE_PAUSED) def setFeedrateModifier(self, type, value): self._feedRateModifier[type] = value def getFeedrateModifiers(self): result = {} result.update(self._feedRateModifier) return result def getExceptionString(): locationInfo = traceback.extract_tb(sys.exc_info()[2])[0] 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])