def _connect(self): self.changeState(State.OPENING_CONNECTION) if self._port == "VIRTUAL": self._serial = VirtualPrinter(read_timeout=self._readTimeout, write_timeout=self._writeTimeout) self.changeState(State.CONNECTED) self._transport_logger.debug("Connected to %s" % self._serial) else: try: self._serial = serial.Serial(self._port, self._baudrate, timeout=self._readTimeout, writeTimeout=self._writeTimeout) self.changeState(State.CONNECTED) self._transport_logger.debug("Connected to %s" % self._serial) except: self.logError( "Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString())) self.onError( "Failed to open serial port, permissions correct?") return False eventManager().fire(Events.CONNECTED, { "port": self._port, "baudrate": self._baudrate }) return True
def _openSerial(self): 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() if self._serial is None: self._log("Failed to autodetect serial port") self._errorValue = 'Failed to autodetect serial port.' self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) return False 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=settings().getFloat([ "serial", "timeout", "connection" ]), writeTimeout=10000) except: self._log( "Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString())) self._errorValue = "Failed to open serial port, permissions correct?" self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) return False return True
def _connect(self): self.changeState(State.OPENING_CONNECTION) if self._port == "VIRTUAL": self._serial = VirtualPrinter(read_timeout=self._readTimeout, write_timeout=self._writeTimeout) self.changeState(State.CONNECTED) self._transport_logger.debug("Connected to %s" % self._serial) else: try: self._serial = serial.Serial(self._port, self._baudrate, timeout=self._readTimeout, writeTimeout=self._writeTimeout) self.changeState(State.CONNECTED) self._transport_logger.debug("Connected to %s" % self._serial) except: self.logError("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString())) self.onError("Failed to open serial port, permissions correct?") return False eventManager().fire(Events.CONNECTED, {"port": self._port, "baudrate": self._baudrate}) return True
def _openSerial(self): if self._port == 'AUTO': self._changeState(self.STATE_DETECT_SERIAL) programmer = stk500v2.Stk500v2() self._log("Serial port list: %s" % (str(self._callback.serialList()))) for p in self._callback.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() if self._serial is None: self._log("Failed to autodetect serial port") self._errorValue = 'Failed to autodetect serial port.' self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) return False 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=settings().getFloat(["serial", "timeout", "connection"]), writeTimeout=10000) except: self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString())) self._errorValue = "Failed to open serial port, permissions correct?" self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) return False return True
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 STATE_TRANSFERING_FILE = 11 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 = callbackObject.baudrateList() self._baudrateDetectRetry = 0 self._temp = {} self._tempOffset = {} self._bedTemp = None self._bedTempOffset = 0 self._heatingUp = None self._commandQueue = queue.Queue() self._currentZ = None self._currentLayer = None self._lastLayerHeight = None self._heatupWaitStartTime = 0 self._heatupWaitTimeLost = 0.0 self._currentExtruder = 0 self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) self._currentLine = 1 self._resendDelta = None self._lastLines = deque([], 100) # SD status data self._sdAvailable = False self._sdFileList = False self._sdFiles = [] # print job self._currentFile = None self._positionWhenPaused = {} # regexes floatPattern = "[-+]?[0-9]*\.?[0-9]+" positiveFloatPattern = "[+]?[0-9]*\.?[0-9]+" intPattern = "\d+" self._regex_command = re.compile("^\s*([GM]\d+|T)") self._regex_float = re.compile(floatPattern) self._regex_paramZFloat = re.compile("Z(%s)" % floatPattern) self._regex_paramSInt = re.compile("S(%s)" % intPattern) self._regex_paramNInt = re.compile("N(%s)" % intPattern) self._regex_paramTInt = re.compile("T(%s)" % intPattern) self._regex_extrusion = re.compile("E%s" % positiveFloatPattern) self._regex_minMaxError = re.compile("Error:[0-9]\n") self._regex_sdPrintingByte = re.compile("([0-9]*)/([0-9]*)") self._regex_sdFileOpened = re.compile("File opened:\s*(.*?)\s+Size:\s*(%s)" % intPattern) self._regex_M114Response = re.compile("X:(%s)Y:(%s)Z:(%s)E:(%s)" % (floatPattern, floatPattern, floatPattern, floatPattern)) # Regex matching temperature entries in line. Groups will be as follows: # - 1: whole tool designator incl. optional toolNumber ("T", "Tn", "B") # - 2: toolNumber, if given ("", "n", "") # - 3: actual temperature # - 4: whole target substring, if given (e.g. " / 22.0") # - 5: target temperature self._regex_temp = re.compile("(B|T(\d*)):\s*(%s)(\s*\/?\s*(%s))?" % (positiveFloatPattern, positiveFloatPattern)) self._regex_repetierTempExtr = re.compile("TargetExtr([0-9]+):(%s)" % positiveFloatPattern) self._regex_repetierTempBed = re.compile("TargetBed:(%s)" % positiveFloatPattern) # multithreading locks self._sendNextLock = threading.Lock() self._sendingLock = threading.Lock() # monitoring thread self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() def __del__(self): self.close() ##~~ internal state management def _changeState(self, newState): if self._state == newState: return if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR: if settings().get(["feature", "sdSupport"]): self._sdFileList = False self._sdFiles = [] self._callback.mcSdFiles([]) oldState = self.getStateString() self._state = newState self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) self._callback.mcStateChange(newState) def _log(self, message): #Currently we don't want the logs to clogg the notification between box/boxrouter/browser #self._callback.mcLog(message) self._serialLogger.debug(message) def _addToLastLines(self, cmd): self._lastLines.append(cmd) self._logger.debug("Got %d lines of history in memory" % len(self._lastLines)) ##~~ getters 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: if self.isSdFileSelected(): return "Printing from SD" elif self.isStreaming(): return "Sending file to SD" else: 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()) if self._state == self.STATE_TRANSFERING_FILE: return "Transfering file to SD" return "?%d?" % (self._state) def getShortErrorString(self): if len(self._errorValue) < 50: return self._errorValue return self._errorValue[:50] + "..." 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 or self._state == self.STATE_TRANSFERING_FILE def isPrinting(self): return self._state == self.STATE_PRINTING def isSdPrinting(self): return self.isSdFileSelected() and self.isPrinting() def isSdFileSelected(self): return self._currentFile is not None and isinstance(self._currentFile, PrintingSdFileInformation) def isStreaming(self): return self._currentFile is not None and isinstance(self._currentFile, StreamingGcodeFileInformation) def isPaused(self): return self._state == self.STATE_PAUSED def isBusy(self): return self.isPrinting() or self.isPaused() def isSdReady(self): return self._sdAvailable def isHeatingUp(self): return self._heatingUp def getPrintProgress(self): if self._currentFile is None: return None return self._currentFile.getProgress() def getPrintFilepos(self): if self._currentFile is None: return None return self._currentFile.getFilepos() def getPrintTime(self): if self._currentFile is None or self._currentFile.getStartTime() is None: return None else: return time.time() - self._currentFile.getStartTime() def getPrintTimeRemainingEstimate(self): printTime = self.getPrintTime() if printTime is None: return None printTime /= 60 progress = self._currentFile.getProgress() if progress: return printTimeTotal - printTime else: return None def getTemp(self): return self._temp def getBedTemp(self): return self._bedTemp def getOffsets(self): return self._tempOffset, self._bedTempOffset def getConnection(self): return self._port, self._baudrate ##~~ external interface def close(self, isError = False): printing = self.isPrinting() or self.isPaused() if self._serial is not None: try: self._serial.close() except OSError as e: #log it but continue self._logger.error('Error closing serial port: %s' % e) if isError: self._changeState(self.STATE_CLOSED_WITH_ERROR) else: self._changeState(self.STATE_CLOSED) self._serial = None if settings().get(["feature", "sdSupport"]): self._sdFileList = [] if printing: payload = None if self._currentFile is not None: payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() } eventManager().fire(Events.PRINT_FAILED, payload) eventManager().fire(Events.DISCONNECTED) def setTemperatureOffset(self, tool=None, bed=None): if tool is not None: self._tempOffset = tool if bed is not None: self._bedTempOffset = bed def sendCommand(self, cmd): cmd = cmd.encode('ascii', 'replace') if self.isPrinting() and not self.isSdFileSelected(): self._commandQueue.put(cmd) elif self.isOperational(): self._sendCommand(cmd) def startPrint(self): if not self.isOperational() or self.isPrinting(): return if self._currentFile is None: raise ValueError("No file selected for printing") try: self._currentFile.start() self._lastLayerHeight = 0.0; self._currentLayer = 0; #self._currentLayer = 1; #sefl._lastLayerHeight; #self._callback.mcLayerChange(self._tentativeLayer) wasPaused = self.isPaused() self._changeState(self.STATE_PRINTING) eventManager().fire(Events.PRINT_STARTED, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) if self.isSdFileSelected(): if wasPaused: self.sendCommand("M26 S0") self._currentFile.setFilepos(0) self.sendCommand("M24") else: self._sendNext() except: self._errorValue = getExceptionString() self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) def startFileTransfer(self, filename, localFilename, remoteFilename): if not self.isOperational() or self.isBusy(): logging.info("Printer is not operation or busy") return self._currentFile = StreamingGcodeFileInformation(filename, localFilename, remoteFilename) self._currentFile.start() self.sendCommand("M28 %s" % remoteFilename) eventManager().fire(Events.TRANSFER_STARTED, {"local": localFilename, "remote": remoteFilename}) self._callback.mcFileTransferStarted(remoteFilename, self._currentFile.getFilesize()) def selectFile(self, filename, sd): if self.isBusy(): return if sd: if not self.isOperational(): # printer is not connected, can't use SD return self.sendCommand("M23 %s" % filename) else: self._currentFile = PrintingGcodeFileInformation(filename, self.getOffsets) eventManager().fire(Events.FILE_SELECTED, { "file": self._currentFile.getFilename(), "origin": self._currentFile.getFileLocation() }) self._callback.mcFileSelected(filename, self._currentFile.getFilesize(), False) def unselectFile(self): if self.isBusy(): return self._currentFile = None eventManager().fire(Events.FILE_DESELECTED) self._callback.mcFileSelected(None, None, False) def cancelPrint(self): if not self.isOperational() or self.isStreaming(): return self._changeState(self.STATE_OPERATIONAL) if self.isSdFileSelected(): self.sendCommand("M25") # pause print self.sendCommand("M26 S0") # reset position in file to byte 0 eventManager().fire(Events.PRINT_CANCELLED, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) def setPause(self, pause): if self.isStreaming(): return if not pause and self.isPaused(): self._changeState(self.STATE_PRINTING) if self.isSdFileSelected(): self.sendCommand("M24") else: #restore position if self._positionWhenPaused: self._currentZ = self._positionWhenPaused[2] # To avoid miscounting layers #reset extrusion to what it was in case we did some extrusion while paused self._sendCommand("G92 E%.4f" % self._positionWhenPaused[3]) #We need to reset the Z axis first in case they lowered it self._sendCommand("G1 Z%.4f F2000" % ( self._positionWhenPaused[2] )) #Get back to where you were before pausing self._sendCommand("G1 X%.4f Y%.4f E%.4f F9000" % (self._positionWhenPaused[0], self._positionWhenPaused[1], self._positionWhenPaused[3] )) #slow down the speed for the first movement self._sendCommand("G1 F1000") self._sendNext() eventManager().fire(Events.PRINT_RESUMED, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) elif pause and self.isPrinting(): self._changeState(self.STATE_PAUSED) if self.isSdFileSelected(): self.sendCommand("M25") # pause print else: self.sendCommand("M106 S0") #Stop fans self.sendCommand("M114") # Current position is saved at self._positionWhenPaused #the head movement out of the way is done on the M114 reponse. eventManager().fire(Events.PRINT_PAUSED, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) def getSdFiles(self): return self._sdFiles def startSdFileTransfer(self, filename): if not self.isOperational() or self.isBusy(): return self._changeState(self.STATE_TRANSFERING_FILE) self.sendCommand("M28 %s" % filename.lower()) def endSdFileTransfer(self, filename): if not self.isOperational() or self.isBusy(): return self.sendCommand("M29 %s" % filename.lower()) self._changeState(self.STATE_OPERATIONAL) self.refreshSdFiles() def deleteSdFile(self, filename): if not self.isOperational() or (self.isBusy() and isinstance(self._currentFile, PrintingSdFileInformation) and self._currentFile.getFilename() == filename): # do not delete a file from sd we are currently printing from return self.sendCommand("M30 %s" % filename.lower()) self.refreshSdFiles() def refreshSdFiles(self): if not self.isOperational() or self.isBusy(): return self.sendCommand("M20") def initSdCard(self): if not self.isOperational(): return self.sendCommand("M21") if settings().getBoolean(["feature", "sdAlwaysAvailable"]): self._sdAvailable = True self.refreshSdFiles() self._callback.mcSdStateChange(self._sdAvailable) def releaseSdCard(self): if not self.isOperational() or (self.isBusy() and self.isSdFileSelected()): # do not release the sd card if we are currently printing from it return self.sendCommand("M22") self._sdAvailable = False self._sdFiles = [] self._callback.mcSdStateChange(self._sdAvailable) self._callback.mcSdFiles(self._sdFiles) ##~~ communication monitoring and handling def _parseTemperatures(self, line): result = {} maxToolNum = 0 for match in re.finditer(self._regex_temp, line): tool = match.group(1) toolNumber = int(match.group(2)) if match.group(2) and len(match.group(2)) > 0 else None if toolNumber > maxToolNum: maxToolNum = toolNumber try: actual = float(match.group(3)) target = None if match.group(4) and match.group(5): target = float(match.group(5)) result[tool] = (toolNumber, actual, target) except ValueError: # catch conversion issues, we'll rather just not get the temperature update instead of killing the connection pass if "T0" in result.keys() and "T" in result.keys(): del result["T"] return maxToolNum, result def _processTemperatures(self, line): maxToolNum, parsedTemps = self._parseTemperatures(line) # extruder temperatures if not "T0" in parsedTemps.keys() and "T" in parsedTemps.keys(): # only single reporting, "T" is our one and only extruder temperature toolNum, actual, target = parsedTemps["T"] if target is not None: self._temp[0] = (actual, target) elif 0 in self._temp.keys() and self._temp[0] is not None and isinstance(self._temp[0], tuple): (oldActual, oldTarget) = self._temp[0] self._temp[0] = (actual, oldTarget) else: self._temp[0] = (actual, None) elif "T0" in parsedTemps.keys(): for n in range(maxToolNum + 1): tool = "T%d" % n if not tool in parsedTemps.keys(): continue toolNum, actual, target = parsedTemps[tool] if target is not None: self._temp[toolNum] = (actual, target) elif toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple): (oldActual, oldTarget) = self._temp[toolNum] self._temp[toolNum] = (actual, oldTarget) else: self._temp[toolNum] = (actual, None) # bed temperature if "B" in parsedTemps.keys(): toolNum, actual, target = parsedTemps["B"] if target is not None: self._bedTemp = (actual, target) elif self._bedTemp is not None and isinstance(self._bedTemp, tuple): (oldActual, oldTarget) = self._bedTemp self._bedTemp = (actual, oldTarget) else: self._bedTemp = (actual, None) def _monitor(self): feedbackControls = settings().getFeedbackControls() pauseTriggers = settings().getPauseTriggers() feedbackErrors = [] #Open the serial port. if not self._openSerial(): return self._log("Connected to: %s, starting monitor" % self._serial) if self._baudrate == 0: self._log("Starting baud rate detection") self._changeState(self.STATE_DETECT_BAUDRATE) else: self._changeState(self.STATE_CONNECTING) #Start monitoring the serial port. timeout = getNewTimeout("communication") tempRequestTimeout = getNewTimeout("temperature") sdStatusRequestTimeout = getNewTimeout("sdStatus") startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) self._heatingUp = False swallowOk = False supportRepetierTargetTemp = settings().getBoolean(["feature", "repetierTargetTemp"]) while True: try: line = self._readline() if line is None: break if line.strip() is not "" and line.isalnum(): timeout = getNewTimeout("communication") ##~~ Error handling line = self._handleErrors(line) ##~~ SD file list # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing if self._sdFileList and not "End file list" in line: fileinfo = line.strip().split(None, 2) if len(fileinfo) > 1: # we got extended file information here, so let's split filename and size and try to make them a bit nicer filename, size = fileinfo filename = filename.lower() try: size = int(size) except ValueError: # whatever that was, it was not an integer, so we'll just ignore it and set size to None size = None else: # no extended file information, so only the filename is there and we set size to None filename = fileinfo[0].lower() size = None if self._callback.fileManager.isValidFilename(filename): if filterNonAscii(filename): self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename) else: self._sdFiles.append((filename, size)) continue ##~~ Temperature processing if ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:'): self._processTemperatures(line) self._callback.mcTempUpdate(self._temp, self._bedTemp) #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 elif supportRepetierTargetTemp and ('TargetExtr' in line or 'TargetBed' in line): matchExtr = self._regex_repetierTempExtr.match(line) matchBed = self._regex_repetierTempBed.match(line) if matchExtr is not None: toolNum = int(matchExtr.group(1)) try: target = float(matchExtr.group(2)) if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple): (actual, oldTarget) = self._temp[toolNum] self._temp[toolNum] = (actual, target) else: self._temp[toolNum] = (None, target) self._callback.mcTempUpdate(self._temp, self._bedTemp) except ValueError: pass elif matchBed is not None: try: target = float(matchBed.group(1)) if self._bedTemp is not None and isinstance(self._bedTemp, tuple): (actual, oldTarget) = self._bedTemp self._bedTemp = (actual, target) else: self._bedTemp = (None, target) self._callback.mcTempUpdate(self._temp, self._bedTemp) except ValueError: pass ##~~ SD Card handling elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: self._sdAvailable = False self._sdFiles = [] self._callback.mcSdStateChange(self._sdAvailable) elif 'Not SD printing' in line: if self.isSdFileSelected() and self.isPrinting(): # something went wrong, printer is reporting that we actually are not printing right now... self._sdFilePos = 0 self._changeState(self.STATE_OPERATIONAL) elif 'SD card ok' in line and not self._sdAvailable: self._sdAvailable = True self.refreshSdFiles() self._callback.mcSdStateChange(self._sdAvailable) elif 'Begin file list' in line: self._sdFiles = [] self._sdFileList = True elif 'End file list' in line: self._sdFileList = False self._callback.mcSdFiles(self._sdFiles) elif 'SD printing byte' in line: # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" match = self._regex_sdPrintingByte.search(line) self._currentFile.setFilepos(int(match.group(1))) self._callback.mcProgress() elif 'File opened' in line: # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" match = self._regex_sdFileOpened.search(line) self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2))) elif 'File selected' in line: # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" if self._currentFile is not None: self._callback.mcFileSelected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) eventManager().fire(Events.FILE_SELECTED, { "file": self._currentFile.getFilename(), "origin": self._currentFile.getFileLocation() }) elif 'Writing to file' in line: # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" self._printSection = "CUSTOM" self._changeState(self.STATE_PRINTING) line = "ok" elif 'Done printing file' in line: # printer is reporting file finished printing self._sdFilePos = 0 self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire(Events.PRINT_DONE, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), "time": self.getPrintTime(), "layerCount": self._currentLayer }) elif 'Done saving file' in line: self.refreshSdFiles() ##~~ Message handling 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) ##~~ Parsing for feedback commands if feedbackControls: for name, matcher, template in feedbackControls: if name in feedbackErrors: # we previously had an error with that one, so we'll skip it now continue try: match = matcher.search(line) if match is not None: formatFunction = None if isinstance(template, str): formatFunction = str.format elif isinstance(template, unicode): formatFunction = unicode.format if formatFunction is not None: self._callback.mcReceivedRegisteredMessage(name, formatFunction(template, *(match.groups("n/a")))) except: if not name in feedbackErrors: self._logger.info("Something went wrong with feedbackControl \"%s\": " % name, exc_info=True) feedbackErrors.append(name) pass ##~~ Parsing for pause triggers if pauseTriggers and not self.isStreaming(): if "enable" in pauseTriggers.keys() and pauseTriggers["enable"].search(line) is not None: self.setPause(True) elif "disable" in pauseTriggers.keys() and pauseTriggers["disable"].search(line) is not None: self.setPause(False) elif "toggle" in pauseTriggers.keys() and pauseTriggers["toggle"].search(line) is not None: self.setPause(not self.isPaused()) if "ok" in line and self._heatingUp: self._heatingUp = False self._callback.mcHeatingUpUpdate(self._heatingUp) ### 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) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) 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 = settings().getFloat(["serial", "timeout", "detection"]) self._log("Trying baudrate: %d" % (baudrate)) self._baudrateDetectRetry = 5 self._baudrateDetectTestOk = 0 timeout = getNewTimeout("communication") 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 = settings().getFloat(["serial", "timeout", "connection"]) self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() else: self.initSdCard() eventManager().fire(Events.CONNECTED, {"port": self._port, "baudrate": self._baudrate}) else: self._testingBaudrate = False ### Connection attempt elif self._state == self.STATE_CONNECTING: if (line == "" or "wait" in line) and startSeen: if time.time() > timeout: self.close() else: self._sendCommand("M105") elif "start" in line: startSeen = True elif "ok" in line and startSeen: self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() else: self.initSdCard() eventManager().fire(Events.CONNECTED, {"port": self._port, "baudrate": self._baudrate}) 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 = getNewTimeout("temperature") # resend -> start resend procedure from requested line elif line.lower().startswith("resend") or line.lower().startswith("rs"): if settings().get(["feature", "swallowOkAfterResend"]): swallowOk = True self._handleResendRequest(line) else: positionMatch = self._regex_M114Response.search(line) if positionMatch: self._positionWhenPaused = ( float(positionMatch.group(1)), float(positionMatch.group(2)), float(positionMatch.group(3)), float(positionMatch.group(4)) ) if self.isPaused(): self.sendCommand("G1 F9000 X0 Y0 Z%.4f E%.4f" % (self._positionWhenPaused[2] + 5, self._positionWhenPaused[3] - 5)) ### Printing elif self._state == self.STATE_PRINTING: if line == "" and time.time() > timeout: self._log("Communication timeout during printing, forcing a line") line = 'ok' if self.isSdPrinting(): if time.time() > tempRequestTimeout and not self._heatingUp: self._sendCommand("M105") tempRequestTimeout = getNewTimeout("temperature") if time.time() > sdStatusRequestTimeout and not self._heatingUp: self._sendCommand("M27") sdStatusRequestTimeout = getNewTimeout("sdStatus") else: # Even when printing request the temperature every 5 seconds. if time.time() > tempRequestTimeout and not self.isStreaming(): self._commandQueue.put("M105") tempRequestTimeout = getNewTimeout("temperature") if "ok" in line and swallowOk: swallowOk = False elif "ok" in line: if self._resendDelta is not None: self._resendNextCommand() elif not self._commandQueue.empty() and not self.isStreaming(): self._sendCommand(self._commandQueue.get(), True) else: self._sendNext() elif line.lower().startswith("resend") or line.lower().startswith("rs"): if settings().get(["feature", "swallowOkAfterResend"]): swallowOk = True self._handleResendRequest(line) except: self._logger.exception("Something crashed inside the serial connection loop, please report this to AstroPrint:") errorMsg = "See astrobox.log for details" self._log(errorMsg) self._errorValue = errorMsg self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) self._log("Connection closed, closing down monitor") def _openSerial(self): if self._port == 'AUTO': self._changeState(self.STATE_DETECT_SERIAL) programmer = stk500v2.Stk500v2() self._log("Serial port list: %s" % (str(self._callback.serialList()))) for p in self._callback.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() if self._serial is None: self._log("Failed to autodetect serial port") self._errorValue = 'Failed to autodetect serial port.' self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) return False 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=settings().getFloat(["serial", "timeout", "connection"]), writeTimeout=10000) except: self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString())) self._errorValue = "Failed to open serial port, permissions correct?" self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) return False return True def _handleErrors(self, line): # 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 self._regex_minMaxError.match(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) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) return line 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" % sanitizeAscii(ret)) return ret def _sendNext(self): with self._sendNextLock: line = self._currentFile.getNext() if line is None: if self.isStreaming(): self._sendCommand("M29") remote = self._currentFile.getRemoteFilename() payload = { "local": self._currentFile.getLocalFilename(), "remote": remote, "time": self.getPrintTime() } self._currentFile = None self._changeState(self.STATE_OPERATIONAL) self._callback.mcFileTransferDone(remote) eventManager().fire(Events.TRANSFER_DONE, payload) self.refreshSdFiles() else: payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), "time": self.getPrintTime(), "layerCount": self._currentLayer } self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire(Events.PRINT_DONE, payload) return self._sendCommand(line, True) self._callback.mcProgress() 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) or len(self._lastLines) == 0 or self._resendDelta <= 0: self._errorValue = "Printer requested line %d but no sufficient history is available, can't resend [Delta: %d, History: %d]" % (lineToResend, self._resendDelta, len(self._lastLines)) self._logger.warn(self._errorValue) if self.isPrinting(): # abort the print, there's nothing we can do to rescue it now self._callback.disableMotorsAndHeater() self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) else: # reset resend delta, we can't do anything about it self._resendDelta = None else: self._resendNextCommand() 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: if self._serial is None: return if not self.isStreaming(): gcode = self._regex_command.search(cmd) if gcode: gcode = gcode.group(1) if gcode in gcodeToEvent: eventManager().fire(gcodeToEvent[gcode]) gcodeHandler = "_gcode_" + gcode if hasattr(self, gcodeHandler): cmd = getattr(self, gcodeHandler)(cmd) if cmd is not None: self._doSend(cmd, sendChecksum) def _doSend(self, cmd, sendChecksum=False): if sendChecksum or self._alwaysSendChecksum: lineNumber = self._currentLine 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)) commandToSend = "N%d %s" % (lineNumber, cmd) checksum = reduce(lambda x,y:x^y, map(ord, commandToSend)) commandToSend = "%s*%d" % (commandToSend, 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 _gcode_T(self, cmd): toolMatch = self._regex_paramTInt.search(cmd) if toolMatch: self._currentExtruder = int(toolMatch.group(1)) return cmd def _gcode_G0(self, cmd): if 'Z' in cmd: match = self._regex_paramZFloat.search(cmd) if match: try: z = float(match.group(1)) if self._currentZ != z: self._currentZ = z self._callback.mcZChange(z) except ValueError: pass elif self._state == self.STATE_PRINTING and self._currentZ != self._lastLayerHeight and self._regex_extrusion.search(cmd) != None: if self._currentZ > self._lastLayerHeight: self._currentLayer += 1 self._callback.mcLayerChange(self._currentLayer) self._lastLayerHeight = self._currentZ return cmd _gcode_G1 = _gcode_G0 def _gcode_M0(self, cmd): self.setPause(True) return "M105" # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. _gcode_M1 = _gcode_M0 def _gcode_M104(self, cmd): toolNum = self._currentExtruder toolMatch = self._regex_paramTInt.search(cmd) if toolMatch: toolNum = int(toolMatch.group(1)) match = self._regex_paramSInt.search(cmd) if match: try: target = float(match.group(1)) if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple): (actual, oldTarget) = self._temp[toolNum] self._temp[toolNum] = (actual, target) else: self._temp[toolNum] = (None, target) except ValueError: pass return cmd def _gcode_M140(self, cmd): match = self._regex_paramSInt.search(cmd) if match: try: target = float(match.group(1)) if self._bedTemp is not None and isinstance(self._bedTemp, tuple): (actual, oldTarget) = self._bedTemp self._bedTemp = (actual, target) else: self._bedTemp = (None, target) except ValueError: pass return cmd def _gcode_M109(self, cmd): self._heatingUp = True self._callback.mcHeatingUpUpdate(self._heatingUp) self._heatupWaitStartTime = time.time() return self._gcode_M104(cmd) def _gcode_M190(self, cmd): self._heatingUp = True self._callback.mcHeatingUpUpdate(self._heatingUp) self._heatupWaitStartTime = time.time() return self._gcode_M140(cmd) def _gcode_M110(self, cmd): newLineNumber = None match = self._regex_paramNInt.search(cmd) if match: try: newLineNumber = int(match.group(1)) except: pass else: newLineNumber = 0 # send M110 command with new line number self._doSendWithChecksum(cmd, newLineNumber) 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.clear() self._resendDelta = None return None def _gcode_M112(self, cmd): # It's an emergency what todo? Canceling the print should be the minimum self.cancelPrint() return cmd def _gcode_M112(self, cmd): # It's an emergency what todo? Canceling the print should be the minimum self.cancelPrint() return cmd
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 STATE_TRANSFERING_FILE = 11 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 = {} self._tempOffset = {} self._bedTemp = None self._bedTempOffset = 0 self._commandQueue = queue.Queue() self._currentZ = None self._heatupWaitStartTime = 0 self._heatupWaitTimeLost = 0.0 self._currentExtruder = 0 self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) self._currentLine = 1 self._resendDelta = None self._lastLines = deque([], 50) # SD status data self._sdAvailable = False self._sdFileList = False self._sdFiles = [] # print job self._currentFile = None # regexes floatPattern = "[-+]?[0-9]*\.?[0-9]+" positiveFloatPattern = "[+]?[0-9]*\.?[0-9]+" intPattern = "\d+" self._regex_command = re.compile("^\s*([GM]\d+|T)") self._regex_float = re.compile(floatPattern) self._regex_paramZFloat = re.compile("Z(%s)" % floatPattern) self._regex_paramSInt = re.compile("S(%s)" % intPattern) self._regex_paramNInt = re.compile("N(%s)" % intPattern) self._regex_paramTInt = re.compile("T(%s)" % intPattern) self._regex_minMaxError = re.compile("Error:[0-9]\n") self._regex_sdPrintingByte = re.compile("([0-9]*)/([0-9]*)") self._regex_sdFileOpened = re.compile("File opened:\s*(.*?)\s+Size:\s*(%s)" % intPattern) # Regex matching temperature entries in line. Groups will be as follows: # - 1: whole tool designator incl. optional toolNumber ("T", "Tn", "B") # - 2: toolNumber, if given ("", "n", "") # - 3: actual temperature # - 4: whole target substring, if given (e.g. " / 22.0") # - 5: target temperature self._regex_temp = re.compile("(B|T(\d*)):\s*(%s)(\s*\/?\s*(%s))?" % (positiveFloatPattern, positiveFloatPattern)) self._regex_repetierTempExtr = re.compile("TargetExtr([0-9]+):(%s)" % positiveFloatPattern) self._regex_repetierTempBed = re.compile("TargetBed:(%s)" % positiveFloatPattern) # multithreading locks self._sendNextLock = threading.Lock() self._sendingLock = threading.Lock() # monitoring thread self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() def __del__(self): self.close() ##~~ internal state management def _changeState(self, newState): if self._state == newState: return if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR: if settings().get(["feature", "sdSupport"]): self._sdFileList = False self._sdFiles = [] self._callback.mcSdFiles([]) oldState = self.getStateString() self._state = newState self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) self._callback.mcStateChange(newState) def _log(self, message): self._callback.mcLog(message) self._serialLogger.debug(message) def _addToLastLines(self, cmd): self._lastLines.append(cmd) self._logger.debug("Got %d lines of history in memory" % len(self._lastLines)) ##~~ getters 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: if self.isSdFileSelected(): return "Printing from SD" elif self.isStreaming(): return "Sending file to SD" else: 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.getErrorString()) if self._state == self.STATE_CLOSED_WITH_ERROR: return "Error: %s" % (self.getErrorString()) if self._state == self.STATE_TRANSFERING_FILE: return "Transfering file to SD" return "?%d?" % (self._state) 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 or self._state == self.STATE_TRANSFERING_FILE def isPrinting(self): return self._state == self.STATE_PRINTING def isSdPrinting(self): return self.isSdFileSelected() and self.isPrinting() def isSdFileSelected(self): return self._currentFile is not None and isinstance(self._currentFile, PrintingSdFileInformation) def isStreaming(self): return self._currentFile is not None and isinstance(self._currentFile, StreamingGcodeFileInformation) def isPaused(self): return self._state == self.STATE_PAUSED def isBusy(self): return self.isPrinting() or self.isPaused() def isSdReady(self): return self._sdAvailable def getPrintProgress(self): if self._currentFile is None: return None return self._currentFile.getProgress() def getPrintFilepos(self): if self._currentFile is None: return None return self._currentFile.getFilepos() def getPrintTime(self): if self._currentFile is None or self._currentFile.getStartTime() is None: return None else: return time.time() - self._currentFile.getStartTime() def getPrintTimeRemainingEstimate(self): printTime = self.getPrintTime() if printTime is None: return None printTime /= 60 progress = self._currentFile.getProgress() if progress: printTimeTotal = printTime / progress return printTimeTotal - printTime else: return None def getTemp(self): return self._temp def getBedTemp(self): return self._bedTemp def getOffsets(self): return self._tempOffset, self._bedTempOffset def getConnection(self): return self._port, self._baudrate ##~~ external interface def close(self, isError = False): printing = self.isPrinting() or self.isPaused() if self._serial is not None: if isError: self._changeState(self.STATE_CLOSED_WITH_ERROR) else: self._changeState(self.STATE_CLOSED) self._serial.close() self._serial = None if settings().get(["feature", "sdSupport"]): self._sdFileList = [] if printing: payload = None if self._currentFile is not None: payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() } eventManager().fire(Events.PRINT_FAILED, payload) eventManager().fire(Events.DISCONNECTED) def setTemperatureOffset(self, tool=None, bed=None): if tool is not None: self._tempOffset = tool if bed is not None: self._bedTempOffset = bed def sendCommand(self, cmd): cmd = cmd.encode('ascii', 'replace') if self.isPrinting() and not self.isSdFileSelected(): self._commandQueue.put(cmd) elif self.isOperational(): self._sendCommand(cmd) def startPrint(self): if not self.isOperational() or self.isPrinting(): return if self._currentFile is None: raise ValueError("No file selected for printing") try: self._currentFile.start() wasPaused = self.isPaused() self._changeState(self.STATE_PRINTING) eventManager().fire(Events.PRINT_STARTED, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) if self.isSdFileSelected(): if wasPaused: self.sendCommand("M26 S0") self._currentFile.setFilepos(0) self.sendCommand("M24") else: self._sendNext() except: self._errorValue = getExceptionString() self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) def startFileTransfer(self, filename, localFilename, remoteFilename): if not self.isOperational() or self.isBusy(): logging.info("Printer is not operation or busy") return self._currentFile = StreamingGcodeFileInformation(filename, localFilename, remoteFilename) self._currentFile.start() self.sendCommand("M28 %s" % remoteFilename) eventManager().fire(Events.TRANSFER_STARTED, {"local": localFilename, "remote": remoteFilename}) self._callback.mcFileTransferStarted(remoteFilename, self._currentFile.getFilesize()) def selectFile(self, filename, sd): if self.isBusy(): return if sd: if not self.isOperational(): # printer is not connected, can't use SD return self.sendCommand("M23 %s" % filename) else: self._currentFile = PrintingGcodeFileInformation(filename, self.getOffsets) eventManager().fire(Events.FILE_SELECTED, { "file": self._currentFile.getFilename(), "origin": self._currentFile.getFileLocation() }) self._callback.mcFileSelected(filename, self._currentFile.getFilesize(), False) def unselectFile(self): if self.isBusy(): return self._currentFile = None eventManager().fire(Events.FILE_DESELECTED) self._callback.mcFileSelected(None, None, False) def cancelPrint(self): if not self.isOperational() or self.isStreaming(): return self._changeState(self.STATE_OPERATIONAL) if self.isSdFileSelected(): self.sendCommand("M25") # pause print self.sendCommand("M26 S0") # reset position in file to byte 0 eventManager().fire(Events.PRINT_CANCELLED, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) def setPause(self, pause): if self.isStreaming(): return if not pause and self.isPaused(): self._changeState(self.STATE_PRINTING) if self.isSdFileSelected(): self.sendCommand("M24") else: self._sendNext() eventManager().fire(Events.PRINT_RESUMED, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) elif pause and self.isPrinting(): self._changeState(self.STATE_PAUSED) if self.isSdFileSelected(): self.sendCommand("M25") # pause print eventManager().fire(Events.PRINT_PAUSED, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) def getSdFiles(self): return self._sdFiles def startSdFileTransfer(self, filename): if not self.isOperational() or self.isBusy(): return self._changeState(self.STATE_TRANSFERING_FILE) self.sendCommand("M28 %s" % filename.lower()) def endSdFileTransfer(self, filename): if not self.isOperational() or self.isBusy(): return self.sendCommand("M29 %s" % filename.lower()) self._changeState(self.STATE_OPERATIONAL) self.refreshSdFiles() def deleteSdFile(self, filename): if not self.isOperational() or (self.isBusy() and isinstance(self._currentFile, PrintingSdFileInformation) and self._currentFile.getFilename() == filename): # do not delete a file from sd we are currently printing from return self.sendCommand("M30 %s" % filename.lower()) self.refreshSdFiles() def refreshSdFiles(self): if not self.isOperational() or self.isBusy(): return self.sendCommand("M20") def initSdCard(self): if not self.isOperational(): return self.sendCommand("M21") if settings().getBoolean(["feature", "sdAlwaysAvailable"]): self._sdAvailable = True self.refreshSdFiles() self._callback.mcSdStateChange(self._sdAvailable) def releaseSdCard(self): if not self.isOperational() or (self.isBusy() and self.isSdFileSelected()): # do not release the sd card if we are currently printing from it return self.sendCommand("M22") self._sdAvailable = False self._sdFiles = [] self._callback.mcSdStateChange(self._sdAvailable) self._callback.mcSdFiles(self._sdFiles) ##~~ communication monitoring and handling def _parseTemperatures(self, line): result = {} maxToolNum = 0 for match in re.finditer(self._regex_temp, line): tool = match.group(1) toolNumber = int(match.group(2)) if match.group(2) and len(match.group(2)) > 0 else None if toolNumber > maxToolNum: maxToolNum = toolNumber try: actual = float(match.group(3)) target = None if match.group(4) and match.group(5): target = float(match.group(5)) result[tool] = (toolNumber, actual, target) except ValueError: # catch conversion issues, we'll rather just not get the temperature update instead of killing the connection pass if "T0" in result.keys() and "T" in result.keys(): del result["T"] return maxToolNum, result def _processTemperatures(self, line): maxToolNum, parsedTemps = self._parseTemperatures(line) # extruder temperatures if not "T0" in parsedTemps.keys() and "T" in parsedTemps.keys(): # only single reporting, "T" is our one and only extruder temperature toolNum, actual, target = parsedTemps["T"] if target is not None: self._temp[0] = (actual, target) elif 0 in self._temp.keys() and self._temp[0] is not None and isinstance(self._temp[0], tuple): (oldActual, oldTarget) = self._temp[0] self._temp[0] = (actual, oldTarget) else: self._temp[0] = (actual, None) elif "T0" in parsedTemps.keys(): for n in range(maxToolNum + 1): tool = "T%d" % n if not tool in parsedTemps.keys(): continue toolNum, actual, target = parsedTemps[tool] if target is not None: self._temp[toolNum] = (actual, target) elif toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple): (oldActual, oldTarget) = self._temp[toolNum] self._temp[toolNum] = (actual, oldTarget) else: self._temp[toolNum] = (actual, None) # bed temperature if "B" in parsedTemps.keys(): toolNum, actual, target = parsedTemps["B"] if target is not None: self._bedTemp = (actual, target) elif self._bedTemp is not None and isinstance(self._bedTemp, tuple): (oldActual, oldTarget) = self._bedTemp self._bedTemp = (actual, oldTarget) else: self._bedTemp = (actual, None) def _monitor(self): feedbackControls = settings().getFeedbackControls() pauseTriggers = settings().getPauseTriggers() feedbackErrors = [] #Open the serial port. if not self._openSerial(): return self._log("Connected to: %s, starting monitor" % self._serial) if self._baudrate == 0: self._log("Starting baud rate detection") self._changeState(self.STATE_DETECT_BAUDRATE) else: self._changeState(self.STATE_CONNECTING) #Start monitoring the serial port. timeout = getNewTimeout("communication") tempRequestTimeout = getNewTimeout("temperature") sdStatusRequestTimeout = getNewTimeout("sdStatus") startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) heatingUp = False swallowOk = False supportRepetierTargetTemp = settings().getBoolean(["feature", "repetierTargetTemp"]) while True: try: line = self._readline() if line is None: break if line.strip() is not "": timeout = getNewTimeout("communication") ##~~ Error handling line = self._handleErrors(line) ##~~ SD file list # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing if self._sdFileList and isGcodeFileName(line.strip().lower()) and not 'End file list' in line: filename = line.strip().lower() if filterNonAscii(filename): self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename) else: self._sdFiles.append(filename) continue ##~~ Temperature processing if ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:'): self._processTemperatures(line) self._callback.mcTempUpdate(self._temp, self._bedTemp) #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: heatingUp = True if self._heatupWaitStartTime != 0: t = time.time() self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitStartTime = t elif supportRepetierTargetTemp: matchExtr = self._regex_repetierTempExtr.match(line) matchBed = self._regex_repetierTempBed.match(line) if matchExtr is not None: toolNum = int(matchExtr.group(1)) try: target = float(matchExtr.group(2)) if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple): (actual, oldTarget) = self._temp[toolNum] self._temp[toolNum] = (actual, target) else: self._temp[toolNum] = (None, target) self._callback.mcTempUpdate(self._temp, self._bedTemp) except ValueError: pass elif matchBed is not None: try: target = float(matchBed.group(1)) if self._bedTemp is not None and isinstance(self._bedTemp, tuple): (actual, oldTarget) = self._bedTemp self._bedTemp = (actual, target) else: self._bedTemp = (None, target) self._callback.mcTempUpdate(self._temp, self._bedTemp) except ValueError: pass ##~~ SD Card handling elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: self._sdAvailable = False self._sdFiles = [] self._callback.mcSdStateChange(self._sdAvailable) elif 'Not SD printing' in line: if self.isSdFileSelected() and self.isPrinting(): # something went wrong, printer is reporting that we actually are not printing right now... self._sdFilePos = 0 self._changeState(self.STATE_OPERATIONAL) elif 'SD card ok' in line and not self._sdAvailable: self._sdAvailable = True self.refreshSdFiles() self._callback.mcSdStateChange(self._sdAvailable) elif 'Begin file list' in line: self._sdFiles = [] self._sdFileList = True elif 'End file list' in line: self._sdFileList = False self._callback.mcSdFiles(self._sdFiles) elif 'SD printing byte' in line: # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" match = self._regex_sdPrintingByte.search(line) self._currentFile.setFilepos(int(match.group(1))) self._callback.mcProgress() elif 'File opened' in line: # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" match = self._regex_sdFileOpened.search(line) self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2))) elif 'File selected' in line: # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" if self._currentFile is not None: self._callback.mcFileSelected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) eventManager().fire(Events.FILE_SELECTED, { "file": self._currentFile.getFilename(), "origin": self._currentFile.getFileLocation() }) elif 'Writing to file' in line: # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" self._printSection = "CUSTOM" self._changeState(self.STATE_PRINTING) line = "ok" elif 'Done printing file' in line: # printer is reporting file finished printing self._sdFilePos = 0 self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire(Events.PRINT_DONE, { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), "time": self.getPrintTime() }) elif 'Done saving file' in line: self.refreshSdFiles() ##~~ Message handling 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) ##~~ Parsing for feedback commands if feedbackControls: for name, matcher, template in feedbackControls: if name in feedbackErrors: # we previously had an error with that one, so we'll skip it now continue try: match = matcher.search(line) if match is not None: formatFunction = None if isinstance(template, str): formatFunction = str.format elif isinstance(template, unicode): formatFunction = unicode.format if formatFunction is not None: self._callback.mcReceivedRegisteredMessage(name, formatFunction(template, *(match.groups("n/a")))) except: if not name in feedbackErrors: self._logger.info("Something went wrong with feedbackControl \"%s\": " % name, exc_info=True) feedbackErrors.append(name) pass ##~~ Parsing for pause triggers if pauseTriggers and not self.isStreaming(): if "enable" in pauseTriggers.keys() and pauseTriggers["enable"].search(line) is not None: self.setPause(True) elif "disable" in pauseTriggers.keys() and pauseTriggers["disable"].search(line) is not None: self.setPause(False) elif "toggle" in pauseTriggers.keys() and pauseTriggers["toggle"].search(line) is not None: self.setPause(not self.isPaused()) if "ok" in line and heatingUp: heatingUp = False ### 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) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) 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 = settings().getFloat(["serial", "timeout", "detection"]) self._log("Trying baudrate: %d" % (baudrate)) self._baudrateDetectRetry = 5 self._baudrateDetectTestOk = 0 timeout = getNewTimeout("communication") 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 = settings().getFloat(["serial", "timeout", "connection"]) self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() else: self.initSdCard() eventManager().fire(Events.CONNECTED, {"port": self._port, "baudrate": self._baudrate}) else: self._testingBaudrate = False ### Connection attempt elif self._state == self.STATE_CONNECTING: if (line == "" or "wait" in line) and startSeen: self._sendCommand("M105") elif "start" in line: startSeen = True elif "ok" in line and startSeen: self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() else: self.initSdCard() eventManager().fire(Events.CONNECTED, {"port": self._port, "baudrate": self._baudrate}) 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 = getNewTimeout("temperature") # resend -> start resend procedure from requested line elif line.lower().startswith("resend") or line.lower().startswith("rs"): if settings().get(["feature", "swallowOkAfterResend"]): swallowOk = True 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' if self.isSdPrinting(): if time.time() > tempRequestTimeout and not heatingUp: self._sendCommand("M105") tempRequestTimeout = getNewTimeout("temperature") if time.time() > sdStatusRequestTimeout and not heatingUp: self._sendCommand("M27") sdStatusRequestTimeout = getNewTimeout("sdStatus") else: # Even when printing request the temperature every 5 seconds. if time.time() > tempRequestTimeout and not self.isStreaming(): self._commandQueue.put("M105") tempRequestTimeout = getNewTimeout("temperature") if "ok" in line and swallowOk: swallowOk = False elif "ok" in line: if self._resendDelta is not None: self._resendNextCommand() elif not self._commandQueue.empty() and not self.isStreaming(): self._sendCommand(self._commandQueue.get()) else: self._sendNext() elif line.lower().startswith("resend") or line.lower().startswith("rs"): if settings().get(["feature", "swallowOkAfterResend"]): swallowOk = True self._handleResendRequest(line) except: self._logger.exception("Something crashed inside the serial connection loop, please report this in OctoPrint's bug tracker:") errorMsg = "See octoprint.log for details" self._log(errorMsg) self._errorValue = errorMsg self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) self._log("Connection closed, closing down monitor") def _openSerial(self): 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() if self._serial is None: self._log("Failed to autodetect serial port") self._errorValue = 'Failed to autodetect serial port.' self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) return False 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=settings().getFloat(["serial", "timeout", "connection"]), writeTimeout=10000) except: self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString())) self._errorValue = "Failed to open serial port, permissions correct?" self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) return False return True def _handleErrors(self, line): # 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 self._regex_minMaxError.match(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) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) return line 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" % sanitizeAscii(ret)) return ret def _sendNext(self): with self._sendNextLock: line = self._currentFile.getNext() if line is None: if self.isStreaming(): self._sendCommand("M29") filename = self._currentFile.getFilename() payload = { "local": self._currentFile.getLocalFilename(), "remote": self._currentFile.getRemoteFilename(), "time": self.getPrintTime() } self._currentFile = None self._changeState(self.STATE_OPERATIONAL) self._callback.mcFileTransferDone(filename) eventManager().fire(Events.TRANSFER_DONE, payload) self.refreshSdFiles() else: payload = { "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), "time": self.getPrintTime() } self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire(Events.PRINT_DONE, payload) return self._sendCommand(line, True) self._callback.mcProgress() 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) or len(self._lastLines) == 0 or self._resendDelta <= 0: self._errorValue = "Printer requested line %d but no sufficient history is available, can't resend" % lineToResend self._logger.warn(self._errorValue) if self.isPrinting(): # abort the print, there's nothing we can do to rescue it now self._changeState(self.STATE_ERROR) eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) else: # reset resend delta, we can't do anything about it self._resendDelta = None else: self._resendNextCommand() 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: if self._serial is None: return if not self.isStreaming(): gcode = self._regex_command.search(cmd) if gcode: gcode = gcode.group(1) if gcode in gcodeToEvent: eventManager().fire(gcodeToEvent[gcode]) gcodeHandler = "_gcode_" + gcode if hasattr(self, gcodeHandler): cmd = getattr(self, gcodeHandler)(cmd) if cmd is not None: self._doSend(cmd, sendChecksum) def _doSend(self, cmd, sendChecksum=False): if sendChecksum or self._alwaysSendChecksum: lineNumber = self._currentLine 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)) commandToSend = "N%d %s" % (lineNumber, cmd) checksum = reduce(lambda x,y:x^y, map(ord, commandToSend)) commandToSend = "%s*%d" % (commandToSend, 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 _gcode_T(self, cmd): toolMatch = self._regex_paramTInt.search(cmd) if toolMatch: self._currentExtruder = int(toolMatch.group(1)) return cmd def _gcode_G0(self, cmd): if 'Z' in cmd: match = self._regex_paramZFloat.search(cmd) if match: try: z = float(match.group(1)) if self._currentZ != z: self._currentZ = z self._callback.mcZChange(z) except ValueError: pass return cmd _gcode_G1 = _gcode_G0 def _gcode_M0(self, cmd): self.setPause(True) return "M105" # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. _gcode_M1 = _gcode_M0 def _gcode_M104(self, cmd): toolNum = self._currentExtruder toolMatch = self._regex_paramTInt.search(cmd) if toolMatch: toolNum = int(toolMatch.group(1)) match = self._regex_paramSInt.search(cmd) if match: try: target = float(match.group(1)) if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple): (actual, oldTarget) = self._temp[toolNum] self._temp[toolNum] = (actual, target) else: self._temp[toolNum] = (None, target) except ValueError: pass return cmd def _gcode_M140(self, cmd): match = self._regex_paramSInt.search(cmd) if match: try: target = float(match.group(1)) if self._bedTemp is not None and isinstance(self._bedTemp, tuple): (actual, oldTarget) = self._bedTemp self._bedTemp = (actual, target) else: self._bedTemp = (None, target) except ValueError: pass return cmd def _gcode_M109(self, cmd): self._heatupWaitStartTime = time.time() return self._gcode_M104(cmd) def _gcode_M190(self, cmd): self._heatupWaitStartTime = time.time() return self._gcode_M140(cmd) def _gcode_M110(self, cmd): newLineNumber = None match = self._regex_paramNInt.search(cmd) if match: try: newLineNumber = int(match.group(1)) except: pass else: newLineNumber = 0 # send M110 command with new line number self._doSendWithChecksum(cmd, newLineNumber) 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.clear() self._resendDelta = None return None def _gcode_M112(self, cmd): # It's an emergency what todo? Canceling the print should be the minimum self.cancelPrint() return cmd
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 STATE_TRANSFERING_FILE = 11 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._tempOffset = 0 self._bedTempOffset = 0 self._commandQueue = queue.Queue() self._currentZ = None self._heatupWaitStartTime = 0 self._heatupWaitTimeLost = 0.0 self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) self._currentLine = 1 self._resendDelta = None self._lastLines = deque([], 50) # multithreading locks self._sendNextLock = threading.Lock() self._sendingLock = threading.Lock() # monitoring thread self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() # SD status data self._sdAvailable = False self._sdFileList = False self._sdFiles = [] # print job self._currentFile = None # regexes floatPattern = "[-+]?[0-9]*\.?[0-9]+" intPattern = "[0-9]+" self._regex_command = re.compile("^\s*([GM]\d+)") self._regex_float = re.compile(floatPattern) self._regex_paramZFloat = re.compile("Z(%s)" % floatPattern) self._regex_paramSInt = re.compile("S(%s)" % intPattern) self._regex_paramNInt = re.compile("N(%s)" % intPattern) self._regex_minMaxError = re.compile("Error:[0-9]\n") self._regex_sdPrintingByte = re.compile("([0-9]*)/([0-9]*)") self._regex_sdFileOpened = re.compile("File opened:\s*(.*?)\s+Size:\s*([0-9]*)") def __del__(self): self.close() ##~~ internal state management def _changeState(self, newState): if self._state == newState: return if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR: if settings().get(["feature", "sdSupport"]): self._sdFileList = False self._sdFiles = [] self._callback.mcSdFiles([]) oldState = self.getStateString() self._state = newState self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) self._callback.mcStateChange(newState) def _log(self, message): self._callback.mcLog(message) self._serialLogger.debug(message) def _addToLastLines(self, cmd): self._lastLines.append(cmd) self._logger.debug("Got %d lines of history in memory" % len(self._lastLines)) ##~~ getters 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: if self.isSdFileSelected(): return "Printing from SD" elif self.isStreaming(): return "Sending file to SD" else: 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()) if self._state == self.STATE_TRANSFERING_FILE: return "Transfering file to SD" 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 or self._state == self.STATE_TRANSFERING_FILE def isPrinting(self): return self._state == self.STATE_PRINTING def isSdPrinting(self): return self.isSdFileSelected() and self.isPrinting() def isSdFileSelected(self): return self._currentFile is not None and isinstance(self._currentFile, PrintingSdFileInformation) def isStreaming(self): return self._currentFile is not None and isinstance(self._currentFile, StreamingGcodeFileInformation) def isPaused(self): return self._state == self.STATE_PAUSED def isBusy(self): return self.isPrinting() or self.isPaused() def isSdReady(self): return self._sdAvailable def getPrintProgress(self): if self._currentFile is None: return None return self._currentFile.getProgress() def getPrintFilepos(self): if self._currentFile is None: return None return self._currentFile.getFilepos() def getPrintTime(self): if self._currentFile is None or self._currentFile.getStartTime() is None: return None else: return time.time() - self._currentFile.getStartTime() def getPrintTimeRemainingEstimate(self): printTime = self.getPrintTime() if printTime is None: return None printTime /= 60 progress = self._currentFile.getProgress() if progress: printTimeTotal = printTime / progress return printTimeTotal - printTime else: return None def getTemp(self): return self._temp def getBedTemp(self): return self._bedTemp def getOffsets(self): return (self._tempOffset, self._bedTempOffset) ##~~ external interface def close(self, isError = False): printing = self.isPrinting() or self.isPaused() if self._serial is not None: self._serial.close() if isError: self._changeState(self.STATE_CLOSED_WITH_ERROR) else: self._changeState(self.STATE_CLOSED) self._serial = None if settings().get(["feature", "sdSupport"]): self._sdFileList = [] if printing: eventManager().fire("PrintFailed") eventManager().fire("Disconnected") def setTemperatureOffset(self, extruder=None, bed=None): if extruder is not None: self._tempOffset = extruder if bed is not None: self._bedTempOffset = bed def sendCommand(self, cmd): cmd = cmd.encode('ascii', 'replace') if self.isPrinting() and not self.isSdFileSelected(): self._commandQueue.put(cmd) elif self.isOperational(): self._sendCommand(cmd) def startPrint(self): if not self.isOperational() or self.isPrinting(): return if self._currentFile is None: raise ValueError("No file selected for printing") wasPaused = self.isPaused() try: self._currentFile.start() self._changeState(self.STATE_PRINTING) eventManager().fire("PrintStarted", self._currentFile.getFilename()) if self.isSdFileSelected(): if wasPaused: self.sendCommand("M26 S0") self._currentFile.setFilepos(0) self.sendCommand("M24") else: self._sendNext() except: self._errorValue = getExceptionString() self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) def startFileTransfer(self, filename, remoteFilename): if not self.isOperational() or self.isBusy(): logging.info("Printer is not operation or busy") return self._currentFile = StreamingGcodeFileInformation(filename) self._currentFile.start() self.sendCommand("M28 %s" % remoteFilename) eventManager().fire("TransferStart", remoteFilename) self._callback.mcFileTransferStarted(remoteFilename, self._currentFile.getFilesize()) def selectFile(self, filename, sd): if self.isBusy(): return if sd: if not self.isOperational(): # printer is not connected, can't use SD return self.sendCommand("M23 %s" % filename) else: self._currentFile = PrintingGcodeFileInformation(filename, self.getOffsets) eventManager().fire("FileSelected", filename) self._callback.mcFileSelected(filename, self._currentFile.getFilesize(), False) def unselectFile(self): if self.isBusy(): return self._currentFile = None eventManager().fire("FileSelected", None) self._callback.mcFileSelected(None, None, False) def cancelPrint(self): if not self.isOperational() or self.isStreaming(): return self._changeState(self.STATE_OPERATIONAL) if self.isSdFileSelected(): self.sendCommand("M25") # pause print self.sendCommand("M26 S0") # reset position in file to byte 0 eventManager().fire("PrintCancelled") def setPause(self, pause): if self.isStreaming(): return if not pause and self.isPaused(): self._changeState(self.STATE_PRINTING) if self.isSdFileSelected(): self.sendCommand("M24") else: self._sendNext() eventManager().fire("PrintResumed", self._currentFile.getFilename()) if pause and self.isPrinting(): self._changeState(self.STATE_PAUSED) if self.isSdFileSelected(): self.sendCommand("M25") # pause print eventManager().fire("Paused") def getSdFiles(self): return self._sdFiles def startSdFileTransfer(self, filename): if not self.isOperational() or self.isBusy(): return self._changeState(self.STATE_TRANSFERING_FILE) self.sendCommand("M28 %s" % filename.lower()) def endSdFileTransfer(self, filename): if not self.isOperational() or self.isBusy(): return self.sendCommand("M29 %s" % filename.lower()) self._changeState(self.STATE_OPERATIONAL) self.refreshSdFiles() def deleteSdFile(self, filename): if not self.isOperational() or (self.isBusy() and isinstance(self._currentFile, PrintingSdFileInformation) and self._currentFile.getFilename() == filename): # do not delete a file from sd we are currently printing from return self.sendCommand("M30 %s" % filename.lower()) self.refreshSdFiles() def refreshSdFiles(self): if not self.isOperational() or self.isBusy(): return self.sendCommand("M20") def initSdCard(self): if not self.isOperational(): return self.sendCommand("M21") def releaseSdCard(self): if not self.isOperational() or (self.isBusy() and self.isSdFileSelected()): # do not release the sd card if we are currently printing from it return self.sendCommand("M22") self._sdAvailable = False self._sdFiles = [] self._callback.mcSdStateChange(self._sdAvailable) self._callback.mcSdFiles(self._sdFiles) ##~~ communication monitoring and handling def _monitor(self): feedbackControls = settings().getFeedbackControls() pauseTriggers = settings().getPauseTriggers() feedbackErrors = [] #Open the serial port. if not self._openSerial(): return self._log("Connected to: %s, starting monitor" % self._serial) if self._baudrate == 0: self._log("Starting baud rate detection") self._changeState(self.STATE_DETECT_BAUDRATE) else: self._changeState(self.STATE_CONNECTING) #Start monitoring the serial port. timeout = getNewTimeout("communication") tempRequestTimeout = timeout sdStatusRequestTimeout = timeout startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) heatingUp = False swallowOk = False while True: try: line = self._readline() if line is None: break ##~~ Error handling line = self._handleErrors(line) ##~~ SD file list # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing if self._sdFileList and isGcodeFileName(line.strip().lower()) and not 'End file list' in line: filename = line.strip().lower() if filterNonAscii(filename): self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename) else: self._sdFiles.append(filename) continue ##~~ Temperature processing if ' T:' in line or line.startswith('T:'): try: self._temp = float(self._regex_float.search(line.split('T:')[1]).group(0)) if ' B:' in line: self._bedTemp = float(self._regex_float.search(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: heatingUp = True if self._heatupWaitStartTime != 0: t = time.time() self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitStartTime = t ##~~ SD Card handling elif 'SD init fail' in line or 'volume.init failed' in line or 'openRoot failed' in line: self._sdAvailable = False self._sdFiles = [] self._callback.mcSdStateChange(self._sdAvailable) elif 'Not SD printing' in line: if self.isSdFileSelected() and self.isPrinting(): # something went wrong, printer is reporting that we actually are not printing right now... self._sdFilePos = 0 self._changeState(self.STATE_OPERATIONAL) elif 'SD card ok' in line: self._sdAvailable = True self.refreshSdFiles() self._callback.mcSdStateChange(self._sdAvailable) elif 'Begin file list' in line: self._sdFiles = [] self._sdFileList = True elif 'End file list' in line: self._sdFileList = False self._callback.mcSdFiles(self._sdFiles) elif 'SD printing byte' in line: # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" match = self._regex_sdPrintingByte.search(line) self._currentFile.setFilepos(int(match.group(1))) self._callback.mcProgress() elif 'File opened' in line: # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" match = self._regex_sdFileOpened.search(line) self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2))) elif 'File selected' in line: # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" if self._currentFile is not None: self._callback.mcFileSelected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) eventManager().fire("FileSelected", self._currentFile.getFilename()) elif 'Writing to file' in line: # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" self._changeState(self.STATE_PRINTING) line = "ok" elif 'Done printing file' in line: # printer is reporting file finished printing self._sdFilePos = 0 self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire("PrintDone") elif 'Done saving file' in line: self.refreshSdFiles() ##~~ Message handling 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) ##~~ Parsing for feedback commands if feedbackControls: for name, matcher, template in feedbackControls: if name in feedbackErrors: # we previously had an error with that one, so we'll skip it now continue try: match = matcher.search(line) if match is not None: formatFunction = None if isinstance(template, str): formatFunction = str.format elif isinstance(template, unicode): formatFunction = unicode.format if formatFunction is not None: self._callback.mcReceivedRegisteredMessage(name, formatFunction(template, *(match.groups("n/a")))) except: if not name in feedbackErrors: self._logger.info("Something went wrong with feedbackControl \"%s\": " % name, exc_info=True) feedbackErrors.append(name) pass ##~~ Parsing for pause triggers if pauseTriggers and not self.isStreaming(): if "enable" in pauseTriggers.keys() and pauseTriggers["enable"].search(line) is not None: self.setPause(True) elif "disable" in pauseTriggers.keys() and pauseTriggers["disable"].search(line) is not None: self.setPause(False) elif "toggle" in pauseTriggers.keys() and pauseTriggers["toggle"].search(line) is not None: self.setPause(not self.isPaused()) if "ok" in line and heatingUp: heatingUp = False ### 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) eventManager().fire("Error", self.getErrorString()) 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 = settings().getFloat(["serial", "timeout", "detection"]) self._log("Trying baudrate: %d" % (baudrate)) self._baudrateDetectRetry = 5 self._baudrateDetectTestOk = 0 timeout = getNewTimeout("communication") 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 = settings().getFloat(["serial", "timeout", "connection"]) self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate)) else: self._testingBaudrate = False ### Connection attempt elif self._state == self.STATE_CONNECTING: if (line == "" or "wait" in line) and startSeen: self._sendCommand("M105") elif "start" in line: startSeen = True elif "ok" in line and startSeen: self._changeState(self.STATE_OPERATIONAL) if not self._sdAvailable: self.initSdCard() eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate)) 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 = getNewTimeout("communication") # resend -> start resend procedure from requested line elif line.lower().startswith("resend") or line.lower().startswith("rs"): if settings().get(["feature", "swallowOkAfterResend"]): swallowOk = True 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' if self.isSdPrinting(): if time.time() > tempRequestTimeout and not heatingUp: self._sendCommand("M105") tempRequestTimeout = getNewTimeout("communication") if time.time() > sdStatusRequestTimeout and not heatingUp: self._sendCommand("M27") sdStatusRequestTimeout = time.time() + 1 if 'ok' or 'SD printing byte' in line: timeout = getNewTimeout("communication") else: # Even when printing request the temperature every 5 seconds. if time.time() > tempRequestTimeout and not self.isStreaming(): self._commandQueue.put("M105") tempRequestTimeout = getNewTimeout("communication") if "ok" in line and swallowOk: swallowOk = False elif "ok" in line: timeout = getNewTimeout("communication") if self._resendDelta is not None: self._resendNextCommand() elif not self._commandQueue.empty() and not self.isStreaming(): self._sendCommand(self._commandQueue.get()) else: self._sendNext() elif line.lower().startswith("resend") or line.lower().startswith("rs"): if settings().get(["feature", "swallowOkAfterResend"]): swallowOk = True self._handleResendRequest(line) except: self._logger.exception("Something crashed inside the serial connection loop, please report this in OctoPrint's bug tracker:") errorMsg = "See octoprint.log for details" self._log(errorMsg) self._errorValue = errorMsg self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) self._log("Connection closed, closing down monitor") def _openSerial(self): 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() if self._serial is None: self._log("Failed to autodetect serial port") self._errorValue = 'Failed to autodetect serial port.' self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) return False 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=settings().getFloat(["serial", "timeout", "connection"]), writeTimeout=10000) except: self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString())) self._errorValue = "Failed to open serial port, permissions correct?" self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) return False return True def _handleErrors(self, line): # 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 self._regex_minMaxError.match(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) eventManager().fire("Error", self.getErrorString()) return line 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" % sanitizeAscii(ret)) return ret def _sendNext(self): with self._sendNextLock: line = self._currentFile.getNext() if line is None: if self.isStreaming(): self._sendCommand("M29") filename = self._currentFile.getFilename() self._currentFile = None self._callback.mcFileTransferDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire("TransferDone", filename) self.refreshSdFiles() else: self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire("PrintDone", self._currentFile.getFilename()) return self._sendCommand(line, True) self._callback.mcProgress() 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) or len(self._lastLines) == 0 or self._resendDelta <= 0: self._errorValue = "Printer requested line %d but no sufficient history is available, can't resend" % lineToResend self._logger.warn(self._errorValue) if self.isPrinting(): # abort the print, there's nothing we can do to rescue it now self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) else: # reset resend delta, we can't do anything about it self._resendDelta = None else: self._resendNextCommand() 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: if self._serial is None: return if not self.isStreaming(): gcode = self._regex_command.search(cmd) if gcode: gcode = gcode.group(1) if gcode in gcodeToEvent: eventManager().fire(gcodeToEvent[gcode]) gcodeHandler = "_gcode_" + gcode if hasattr(self, gcodeHandler): cmd = getattr(self, gcodeHandler)(cmd) if cmd is not None: self._doSend(cmd, sendChecksum) def _doSend(self, cmd, sendChecksum=False): if sendChecksum or self._alwaysSendChecksum: lineNumber = self._currentLine 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)) commandToSend = "N%d %s" % (lineNumber, cmd) checksum = reduce(lambda x,y:x^y, map(ord, commandToSend)) commandToSend = "%s*%d" % (commandToSend, 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 _gcode_G0(self, cmd): if 'Z' in cmd: match = self._regex_paramZFloat.search(cmd) if match: try: z = float(match.group(1)) if self._currentZ != z: self._currentZ = z self._callback.mcZChange(z) except ValueError: pass return cmd _gcode_G1 = _gcode_G0 def _gcode_M0(self, cmd): self.setPause(True) return "M105" # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. _gcode_M1 = _gcode_M0 def _gcode_M104(self, cmd): match = self._regex_paramSInt.search(cmd) if match: try: self._targetTemp = float(match.group(1)) except ValueError: pass return cmd def _gcode_M140(self, cmd): match = self._regex_paramSInt.search(cmd) if match: try: self._bedTargetTemp = float(match.group(1)) except ValueError: pass return cmd def _gcode_M109(self, cmd): self._heatupWaitStartTime = time.time() return self._gcode_M104(cmd) def _gcode_M190(self, cmd): self._heatupWaitStartTime = time.time() return self._gcode_M140(cmd) def _gcode_M110(self, cmd): newLineNumber = None match = self._regex_paramNInt.search(cmd) if match: try: newLineNumber = int(match.group(1)) except: pass else: newLineNumber = 0 # send M110 command with new line number self._doSendWithChecksum(cmd, newLineNumber) 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.clear() self._resendDelta = None return None
def _monitor(self): feedbackControls = settings().getFeedbackControls() pauseTriggers = settings().getPauseTriggers() feedbackErrors = [] #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=settings().getFloat(["serial", "timeout", "connection"]), 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) eventManager().fire("Error", self.getErrorString()) 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 = getNewTimeout("communication") tempRequestTimeout = timeout sdStatusRequestTimeout = timeout startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) heatingUp = False swallowOk = False while True: try: line = self._readline() if line == None: break ##~~ Error handling # 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) eventManager().fire("Error", self.getErrorString()) ##~~ SD file list # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing if self._sdFileList and not 'End file list' in line: self._sdFiles.append(line.strip().lower()) continue ##~~ Temperature processing 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: heatingUp = True if self._heatupWaitStartTime != 0: t = time.time() self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitStartTime = t ##~~ SD Card handling elif 'SD init fail' in line: self._sdAvailable = False self._sdFiles = [] self._callback.mcSdStateChange(self._sdAvailable) elif 'Not SD printing' in line: if self.isSdFileSelected() and self.isPrinting(): # something went wrong, printer is reporting that we actually are not printing right now... self._sdFilePos = 0 self._changeState(self.STATE_OPERATIONAL) elif 'SD card ok' in line: self._sdAvailable = True self.refreshSdFiles() self._callback.mcSdStateChange(self._sdAvailable) elif 'Begin file list' in line: self._sdFiles = [] self._sdFileList = True elif 'End file list' in line: self._sdFileList = False self._callback.mcSdFiles(self._sdFiles) elif 'SD printing byte' in line: # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" match = re.search("([0-9]*)/([0-9]*)", line) self._currentFile.setFilepos(int(match.group(1))) self._callback.mcProgress() elif 'File opened' in line: # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" match = re.search("File opened:\s*(.*?)\s+Size:\s*([0-9]*)", line) self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2))) elif 'File selected' in line: # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" self._callback.mcFileSelected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) eventManager().fire("FileSelected", self._currentFile.getFilename()) elif 'Writing to file' in line: # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" self._printSection = "CUSTOM" self._changeState(self.STATE_PRINTING) elif 'Done printing file' in line: # printer is reporting file finished printing self._sdFilePos = 0 self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire("PrintDone") ##~~ Message handling 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) ##~~ Parsing for feedback commands if feedbackControls: for name, matcher, template in feedbackControls: try: match = matcher.search(line) if match is not None: format = None if isinstance(template, str): format = str.format elif isinstance(template, unicode): format = unicode.format if format is not None: self._callback.mcReceivedRegisteredMessage(name, format(template, *(match.groups("n/a")))) except: if not name in feedbackErrors: self._logger.info("Something went wrong with feedbackControl \"%s\": " % name, exc_info=True) feedbackErrors.append(name) pass ##~~ Parsing for pause triggers if pauseTriggers: if "enable" in pauseTriggers.keys() and pauseTriggers["enable"].search(line) is not None: self.setPause(True) elif "disable" in pauseTriggers.keys() and pauseTriggers["disable"].search(line) is not None: self.setPause(False) elif "toggle" in pauseTriggers.keys() and pauseTriggers["toggle"].search(line) is not None: self.setPause(not self.isPaused()) if "ok" in line and heatingUp: heatingUp = False ### 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) eventManager().fire("Error", self.getErrorString()) 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 = settings().getFloat(["serial", "timeout", "detection"]) self._log("Trying baudrate: %d" % (baudrate)) self._baudrateDetectRetry = 5 self._baudrateDetectTestOk = 0 timeout = getNewTimeout("communication") 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 = settings().getFloat(["serial", "timeout", "connection"]) self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate)) else: self._testingBaudrate = False ### Connection attempt elif self._state == self.STATE_CONNECTING: if (line == "" or "wait" in line) and startSeen: self._sendCommand("M105") elif "start" in line: startSeen = True elif "ok" in line and startSeen: self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate)) 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 = getNewTimeout("communication") # resend -> start resend procedure from requested line elif line.lower().startswith("resend") or line.lower().startswith("rs"): if settings().get(["feature", "swallowOkAfterResend"]): swallowOk = True 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' if self.isSdPrinting(): if time.time() > tempRequestTimeout and not heatingUp: self._sendCommand("M105") tempRequestTimeout = getNewTimeout("communication") if time.time() > sdStatusRequestTimeout and not heatingUp: self._sendCommand("M27") sdStatusRequestTimeout = time.time() + 1 if 'ok' or 'SD printing byte' in line: timeout = getNewTimeout("communication") else: # Even when printing request the temperature every 5 seconds. if time.time() > tempRequestTimeout and not self.isStreaming(): self._commandQueue.put("M105") tempRequestTimeout = getNewTimeout("communication") if "ok" in line and swallowOk: swallowOk = False elif "ok" in line: timeout = getNewTimeout("communication") if self._resendDelta is not None: self._resendNextCommand() elif not self._commandQueue.empty() and not self.isStreaming(): self._sendCommand(self._commandQueue.get()) else: self._sendNext() elif line.lower().startswith("resend") or line.lower().startswith("rs"): if settings().get(["feature", "swallowOkAfterResend"]): swallowOk = True self._handleResendRequest(line) except: self._logger.exception("Something crashed inside the serial connection loop, please report this in OctoPrint's bug tracker:") errorMsg = "See octoprint.log for details" self._log(errorMsg) self._errorValue = errorMsg self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) self._log("Connection closed, closing down monitor")
def _monitor(self): feedbackControls = settings().getFeedbackControls() pauseTriggers = settings().getPauseTriggers() feedbackErrors = [] #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=settings().getFloat([ "serial", "timeout", "connection" ]), 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) eventManager().fire("Error", self.getErrorString()) 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 = getNewTimeout("communication") tempRequestTimeout = timeout sdStatusRequestTimeout = timeout startSeen = not settings().getBoolean( ["feature", "waitForStartOnConnect"]) heatingUp = False swallowOk = False while True: try: line = self._readline() if line == None: break ##~~ Error handling # 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 self._regex_minMaxError.match(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) eventManager().fire("Error", self.getErrorString()) ##~~ SD file list # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing if self._sdFileList and not 'End file list' in line: self._sdFiles.append(line.strip().lower()) continue ##~~ Temperature processing if ' T:' in line or line.startswith('T:'): try: self._temp = float( self._regex_float.search( line.split('T:')[1]).group(0)) if ' B:' in line: self._bedTemp = float( self._regex_float.search( 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: heatingUp = True if self._heatupWaitStartTime != 0: t = time.time() self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitStartTime = t ##~~ SD Card handling elif 'SD init fail' in line: self._sdAvailable = False self._sdFiles = [] self._callback.mcSdStateChange(self._sdAvailable) elif 'Not SD printing' in line: if self.isSdFileSelected() and self.isPrinting(): # something went wrong, printer is reporting that we actually are not printing right now... self._sdFilePos = 0 self._changeState(self.STATE_OPERATIONAL) elif 'SD card ok' in line: self._sdAvailable = True self.refreshSdFiles() self._callback.mcSdStateChange(self._sdAvailable) elif 'Begin file list' in line: self._sdFiles = [] self._sdFileList = True elif 'End file list' in line: self._sdFileList = False self._callback.mcSdFiles(self._sdFiles) elif 'SD printing byte' in line: # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" match = self._regex_sdPrintingByte.search( "([0-9]*)/([0-9]*)", line) self._currentFile.setFilepos(int(match.group(1))) self._callback.mcProgress() elif 'File opened' in line: # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" match = self._regex_sdFileOpened.search(line) self._currentFile = PrintingSdFileInformation( match.group(1), int(match.group(2))) elif 'File selected' in line: # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" self._callback.mcFileSelected( self._currentFile.getFilename(), self._currentFile.getFilesize(), True) eventManager().fire("FileSelected", self._currentFile.getFilename()) elif 'Writing to file' in line: # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" self._changeState(self.STATE_PRINTING) line = "ok" elif 'Done printing file' in line: # printer is reporting file finished printing self._sdFilePos = 0 self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire("PrintDone") ##~~ Message handling 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) ##~~ Parsing for feedback commands if feedbackControls: for name, matcher, template in feedbackControls: try: match = matcher.search(line) if match is not None: format = None if isinstance(template, str): format = str.format elif isinstance(template, unicode): format = unicode.format if format is not None: self._callback.mcReceivedRegisteredMessage( name, format(template, *(match.groups("n/a")))) except: if not name in feedbackErrors: self._logger.info( "Something went wrong with feedbackControl \"%s\": " % name, exc_info=True) feedbackErrors.append(name) pass ##~~ Parsing for pause triggers if pauseTriggers and not self.isStreaming(): if "enable" in pauseTriggers.keys( ) and pauseTriggers["enable"].search(line) is not None: self.setPause(True) elif "disable" in pauseTriggers.keys( ) and pauseTriggers["disable"].search(line) is not None: self.setPause(False) elif "toggle" in pauseTriggers.keys( ) and pauseTriggers["toggle"].search(line) is not None: self.setPause(not self.isPaused()) if "ok" in line and heatingUp: heatingUp = False ### 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) eventManager().fire("Error", self.getErrorString()) 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 = settings().getFloat( ["serial", "timeout", "detection"]) self._log("Trying baudrate: %d" % (baudrate)) self._baudrateDetectRetry = 5 self._baudrateDetectTestOk = 0 timeout = getNewTimeout("communication") 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 = settings().getFloat( ["serial", "timeout", "connection"]) self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() eventManager().fire( "Connected", "%s at %s baud" % (self._port, self._baudrate)) else: self._testingBaudrate = False ### Connection attempt elif self._state == self.STATE_CONNECTING: if (line == "" or "wait" in line) and startSeen: self._sendCommand("M105") elif "start" in line: startSeen = True elif "ok" in line and startSeen: self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() eventManager().fire( "Connected", "%s at %s baud" % (self._port, self._baudrate)) 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 = getNewTimeout("communication") # resend -> start resend procedure from requested line elif line.lower().startswith( "resend") or line.lower().startswith("rs"): if settings().get(["feature", "swallowOkAfterResend"]): swallowOk = True 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' if self.isSdPrinting(): if time.time() > tempRequestTimeout and not heatingUp: self._sendCommand("M105") tempRequestTimeout = getNewTimeout("communication") if time.time( ) > sdStatusRequestTimeout and not heatingUp: self._sendCommand("M27") sdStatusRequestTimeout = time.time() + 1 if 'ok' or 'SD printing byte' in line: timeout = getNewTimeout("communication") else: # Even when printing request the temperature every 5 seconds. if time.time( ) > tempRequestTimeout and not self.isStreaming(): self._commandQueue.put("M105") tempRequestTimeout = getNewTimeout("communication") if "ok" in line and swallowOk: swallowOk = False elif "ok" in line: timeout = getNewTimeout("communication") if self._resendDelta is not None: self._resendNextCommand() elif not self._commandQueue.empty( ) and not self.isStreaming(): self._sendCommand(self._commandQueue.get()) else: self._sendNext() elif line.lower().startswith( "resend") or line.lower().startswith("rs"): if settings().get( ["feature", "swallowOkAfterResend"]): swallowOk = True self._handleResendRequest(line) except: self._logger.exception( "Something crashed inside the serial connection loop, please report this in OctoPrint's bug tracker:" ) errorMsg = "See octoprint.log for details" self._log(errorMsg) self._errorValue = errorMsg self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) self._log("Connection closed, closing down monitor")
class SerialTransport(Transport): __transportinfo__ = ("serial", "Serial", False) def __init__(self, messageReceiver, stateReceiver, logReceiver): Transport.__init__(self, messageReceiver, stateReceiver, logReceiver) self._serial = None self._port = None self._baudrate = None self._connectionTimeout = None self._writeTimeout = None self._readTimeout = None self._timeoutCounter = 0 self._maxTimeouts = 20 self._thread = None def get_properties(self): return {TransportProperties.FLOWCONTROL: False} def get_connection_options(self): return { "port": self.__getSerialList(), "baudrate": self.__getBaudrateList() } def connect(self, opt): Transport.connect(self, opt) self._port = opt["port"] if "port" in opt else None self._baudrate = opt["baudrate"] if "baudrate" in opt else None self._readTimeout = opt["timeout"][ "read"] if "timeout" in opt and "read" in opt["timeout"] else 5.0 self._writeTimeout = opt["timeout"][ "write"] if "timeout" in opt and "write" in opt["timeout"] else 0.5 if self._connect(): self._thread = threading.Thread(target=self._monitor, name="SerialTransportMonitor") self._thread.daemon = True self._thread.start() def disconnect(self, onError=False): try: if self._serial is not None: self._serial.close() finally: self._serial = None self._thread = None Transport.disconnect(self, onError) def send(self, command): commandToSend = command + "\n" try: self._serial.write(commandToSend) self._transport_logger.info("Send: %s" % command) self.logTx(command) except serial.SerialTimeoutException: self._transport_logger.warn("Timeout while sending: %s" % command) self.logError( "Serial timeout while writing to serial port, try again later." ) raise SendTimeout() except: exceptionString = getExceptionString() self.logError("Unexpected error while writing serial port: %s" % exceptionString) self.onError(exceptionString) self.disconnect(True) raise TransportError() def receive(self): return self._readline() def _monitor(self): error = None while True: line = self._readline() if line is None: error = "Serial connection closed unexpectedly" break if line == "": self._timeoutCounter += 1 self.onTimeout() if self._maxTimeouts and self._timeoutCounter > self._maxTimeouts: error = "Printer did not respond at all over %d retries, considering it dead" % self._maxTimeouts break else: self._timeoutCounter = 0 self.onMessageReceived(line.strip()) if error is not None: self._transport_logger.error(error) self.logError(error) self.onError(error) # TODO further error handling def _connect(self): self.changeState(State.OPENING_CONNECTION) if self._port == "VIRTUAL": self._serial = VirtualPrinter(read_timeout=self._readTimeout, write_timeout=self._writeTimeout) self.changeState(State.CONNECTED) self._transport_logger.debug("Connected to %s" % self._serial) else: try: self._serial = serial.Serial(self._port, self._baudrate, timeout=self._readTimeout, writeTimeout=self._writeTimeout) self.changeState(State.CONNECTED) self._transport_logger.debug("Connected to %s" % self._serial) except: self.logError( "Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString())) self.onError( "Failed to open serial port, permissions correct?") return False eventManager().fire(Events.CONNECTED, { "port": self._port, "baudrate": self._baudrate }) return True def _readline(self): if self._serial is None: return None try: line = self._serial.readline() except: exceptionString = getExceptionString() self.logError("Unexpected error while reading serial port: %s" % exceptionString) self.onError(exceptionString) self.disconnect() return None if line != "": loggable_line = unicode(line, "ascii", "replace").encode("ascii", "replace").rstrip() self._transport_logger.debug("Recv: %s" % loggable_line) self.logRx(loggable_line) return line def __getSerialList(self): 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/ttyAMA*") \ + glob.glob("/dev/tty.usb*") \ + glob.glob("/dev/cu.*") \ + glob.glob("/dev/rfcomm*") additionalPorts = settings().get(["serial", "additionalPorts"]) for additional in additionalPorts: baselist += glob.glob(additional) prev = settings().get(["serial", "port"]) if prev in baselist: baselist.remove(prev) baselist.insert(0, prev) if settings().getBoolean(["devel", "virtualPrinter", "enabled"]): baselist.append("VIRTUAL") return baselist def __getBaudrateList(self): 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 SerialTransport(Transport): __transportinfo__ = ("serial", "Serial", False) def __init__(self, messageReceiver, stateReceiver, logReceiver): Transport.__init__(self, messageReceiver, stateReceiver, logReceiver) self._serial = None self._port = None self._baudrate = None self._connectionTimeout = None self._writeTimeout = None self._readTimeout = None self._timeoutCounter = 0 self._maxTimeouts = 20 self._thread = None def get_properties(self): return { TransportProperties.FLOWCONTROL: False } def get_connection_options(self): return { "port": self.__getSerialList(), "baudrate": self.__getBaudrateList() } def connect(self, opt): Transport.connect(self, opt) self._port = opt["port"] if "port" in opt else None self._baudrate = opt["baudrate"] if "baudrate" in opt else None self._readTimeout = opt["timeout"]["read"] if "timeout" in opt and "read" in opt["timeout"] else 5.0 self._writeTimeout = opt["timeout"]["write"] if "timeout" in opt and "write" in opt["timeout"] else 0.5 if self._connect(): self._thread = threading.Thread(target=self._monitor, name="SerialTransportMonitor") self._thread.daemon = True self._thread.start() def disconnect(self, onError=False): try: if self._serial is not None: self._serial.close() finally: self._serial = None self._thread = None Transport.disconnect(self, onError) def send(self, command): commandToSend = command + "\n" try: self._serial.write(commandToSend) self._transport_logger.info("Send: %s" % command) self.logTx(command) except serial.SerialTimeoutException: self._transport_logger.warn("Timeout while sending: %s" % command) self.logError("Serial timeout while writing to serial port, try again later.") raise SendTimeout() except: exceptionString = getExceptionString() self.logError("Unexpected error while writing serial port: %s" % exceptionString) self.onError(exceptionString) self.disconnect(True) raise TransportError() def receive(self): return self._readline() def _monitor(self): error = None while True: line = self._readline() if line is None: error = "Serial connection closed unexpectedly" break if line == "": self._timeoutCounter += 1 self.onTimeout() if self._maxTimeouts and self._timeoutCounter > self._maxTimeouts: error = "Printer did not respond at all over %d retries, considering it dead" % self._maxTimeouts break else: self._timeoutCounter = 0 self.onMessageReceived(line.strip()) if error is not None: self._transport_logger.error(error) self.logError(error) self.onError(error) # TODO further error handling def _connect(self): self.changeState(State.OPENING_CONNECTION) if self._port == "VIRTUAL": self._serial = VirtualPrinter(read_timeout=self._readTimeout, write_timeout=self._writeTimeout) self.changeState(State.CONNECTED) self._transport_logger.debug("Connected to %s" % self._serial) else: try: self._serial = serial.Serial(self._port, self._baudrate, timeout=self._readTimeout, writeTimeout=self._writeTimeout) self.changeState(State.CONNECTED) self._transport_logger.debug("Connected to %s" % self._serial) except: self.logError("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString())) self.onError("Failed to open serial port, permissions correct?") return False eventManager().fire(Events.CONNECTED, {"port": self._port, "baudrate": self._baudrate}) return True def _readline(self): if self._serial is None: return None try: line = self._serial.readline() except: exceptionString = getExceptionString() self.logError("Unexpected error while reading serial port: %s" % exceptionString) self.onError(exceptionString) self.disconnect() return None if line != "": loggable_line = unicode(line, "ascii", "replace").encode("ascii", "replace").rstrip() self._transport_logger.debug("Recv: %s" % loggable_line) self.logRx(loggable_line) return line def __getSerialList(self): 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/ttyAMA*") \ + glob.glob("/dev/tty.usb*") \ + glob.glob("/dev/cu.*") \ + glob.glob("/dev/rfcomm*") additionalPorts = settings().get(["serial", "additionalPorts"]) for additional in additionalPorts: baselist += glob.glob(additional) prev = settings().get(["serial", "port"]) if prev in baselist: baselist.remove(prev) baselist.insert(0, prev) if settings().getBoolean(["devel", "virtualPrinter", "enabled"]): baselist.append("VIRTUAL") return baselist def __getBaudrateList(self): 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 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 STATE_TRANSFERING_FILE = 11 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._commandQueue = queue.Queue() self._currentZ = None self._heatupWaitStartTime = 0 self._heatupWaitTimeLost = 0.0 self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) self._currentLine = 0 self._resendDelta = None self._lastLines = deque([], 50) # multithreading locks self._sendNextLock = threading.Lock() self._sendingLock = threading.Lock() # monitoring thread self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() # SD status data self._sdAvailable = False self._sdFileList = False self._sdFiles = [] # print job self._currentFile = None def __del__(self): self.close() ##~~ internal state management def _changeState(self, newState): if self._state == newState: return if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR: if settings().get(["feature", "sdSupport"]): self._sdFileList = False self._sdFiles = [] self._callback.mcSdFiles([]) oldState = self.getStateString() self._state = newState self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) self._callback.mcStateChange(newState) def _log(self, message): self._callback.mcLog(message) self._serialLogger.debug(message) def _addToLastLines(self, cmd): self._lastLines.append(cmd) self._logger.debug("Got %d lines of history in memory" % len(self._lastLines)) ##~~ getters 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: if self.isSdFileSelected(): return "Printing from SD" elif self.isStreaming(): return "Sending file to SD" else: 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()) if self._state == self.STATE_TRANSFERING_FILE: return "Transfering file to SD" 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 or self._state == self.STATE_TRANSFERING_FILE def isPrinting(self): return self._state == self.STATE_PRINTING def isSdPrinting(self): return self.isSdFileSelected() and self.isPrinting() def isSdFileSelected(self): return self._currentFile is not None and isinstance(self._currentFile, PrintingSdFileInformation) def isStreaming(self): return self._currentFile is not None and isinstance(self._currentFile, StreamingGcodeFileInformation) def isPaused(self): return self._state == self.STATE_PAUSED def isBusy(self): return self.isPrinting() or self.isPaused() def isSdReady(self): return self._sdAvailable def getPrintProgress(self): if self._currentFile is None: return None return self._currentFile.getProgress() def getPrintFilepos(self): if self._currentFile is None: return None return self._currentFile.getFilepos() def getPrintTime(self): if self._currentFile is None or self._currentFile.getStartTime() is None: return None else: return time.time() - self._currentFile.getStartTime() def getPrintTimeRemainingEstimate(self): printTime = self.getPrintTime() if printTime is None: return None printTime /= 60 progress = self._currentFile.getProgress() if progress: printTimeTotal = printTime / progress return printTimeTotal - printTime else: return None def getTemp(self): return self._temp def getBedTemp(self): return self._bedTemp ##~~ external interface def close(self, isError = False): printing = self.isPrinting() or self.isPaused() if self._serial is not None: self._serial.close() if isError: self._changeState(self.STATE_CLOSED_WITH_ERROR) else: self._changeState(self.STATE_CLOSED) self._serial = None if settings().get(["feature", "sdSupport"]): self._sdFileList = [] if printing: eventManager().fire("PrintFailed") eventManager().fire("Disconnected") def sendCommand(self, cmd): cmd = cmd.encode('ascii', 'replace') if self.isPrinting() and not self.isSdFileSelected(): self._commandQueue.put(cmd) elif self.isOperational(): self._sendCommand(cmd) def startPrint(self): if not self.isOperational() or self.isPrinting(): return if self._currentFile is None: raise ValueError("No file selected for printing") self._printSection = "CUSTOM" self._changeState(self.STATE_PRINTING) eventManager().fire("PrintStarted", self._currentFile.getFilename()) try: self._currentFile.start() if self.isSdFileSelected(): if self.isPaused(): self.sendCommand("M26 S0") self._currentFile.setFilepos(0) self.sendCommand("M24") else: self._sendNext() except: self._errorValue = getExceptionString() self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) def startFileTransfer(self, filename, remoteFilename): if not self.isOperational() or self.isBusy(): return self._currentFile = StreamingGcodeFileInformation(filename) self._currentFile.start() self.sendCommand("M28 %s" % remoteFilename) eventManager().fire("TransferStart", remoteFilename) self._callback.mcFileTransferStarted(remoteFilename, self._currentFile.getFilesize()) def selectFile(self, filename, sd): if self.isBusy(): return if sd: if not self.isOperational(): # printer is not connected, can't use SD return self.sendCommand("M23 %s" % filename) else: self._currentFile = PrintingGcodeFileInformation(filename) eventManager().fire("FileSelected", filename) self._callback.mcFileSelected(filename, self._currentFile.getFilesize(), False) def unselectFile(self): if self.isBusy(): return self._currentFile = None eventManager().fire("FileSelected", None) self._callback.mcFileSelected(None, None, False) def cancelPrint(self): if not self.isOperational() or self.isStreaming(): return self._changeState(self.STATE_OPERATIONAL) if self.isSdFileSelected(): self.sendCommand("M25") # pause print self.sendCommand("M26 S0") # reset position in file to byte 0 eventManager().fire("PrintCancelled") def setPause(self, pause): if self.isStreaming(): return if not pause and self.isPaused(): self._changeState(self.STATE_PRINTING) if self.isSdFileSelected(): self.sendCommand("M24") else: self._sendNext() if pause and self.isPrinting(): self._changeState(self.STATE_PAUSED) if self.isSdFileSelected(): self.sendCommand("M25") # pause print eventManager().fire("Paused") def getSdFiles(self): return self._sdFiles def startSdFileTransfer(self, filename): if not self.isOperational() or self.isBusy(): return self._changeState(self.STATE_TRANSFERING_FILE) self.sendCommand("M28 %s" % filename.lower()) def endSdFileTransfer(self, filename): if not self.isOperational() or self.isBusy(): return self.sendCommand("M29 %s" % filename.lower()) self._changeState(self.STATE_OPERATIONAL) self.refreshSdFiles() def deleteSdFile(self, filename): if not self.isOperational() or (self.isBusy() and isinstance(self._currentFile, PrintingSdFileInformation) and self._currentFile.getFilename() == filename): # do not delete a file from sd we are currently printing from return self.sendCommand("M30 %s" % filename.lower()) self.refreshSdFiles() def refreshSdFiles(self): if not self.isOperational() or self.isBusy(): return self.sendCommand("M20") def initSdCard(self): if not self.isOperational(): return self.sendCommand("M21") def releaseSdCard(self): if not self.isOperational() or (self.isBusy() and self.isSdFileSelected()): # do not release the sd card if we are currently printing from it return self.sendCommand("M22") self._sdAvailable = False self._sdFiles = [] self._callback.mcSdStateChange(self._sdAvailable) self._callback.mcSdFiles(self._sdFiles) ##~~ communication monitoring and handling def _monitor(self): feedbackControls = settings().getFeedbackControls() pauseTriggers = settings().getPauseTriggers() #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=settings().getFloat(["serial", "timeout", "connection"]), 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) eventManager().fire("Error", self.getErrorString()) 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 = getNewTimeout("communication") tempRequestTimeout = timeout sdStatusRequestTimeout = timeout startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) heatingUp = False while True: try: line = self._readline() if line == None: break ##~~ Error handling # 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) eventManager().fire("Error", self.getErrorString()) ##~~ SD file list # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing if self._sdFileList and not 'End file list' in line: self._sdFiles.append(line.strip().lower()) continue ##~~ Temperature processing 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: heatingUp = True if self._heatupWaitStartTime != 0: t = time.time() self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitStartTime = t ##~~ SD Card handling elif 'SD init fail' in line: self._sdAvailable = False self._sdFiles = [] self._callback.mcSdStateChange(self._sdAvailable) elif 'Not SD printing' in line: if self.isSdFileSelected() and self.isPrinting(): # something went wrong, printer is reporting that we actually are not printing right now... self._sdFilePos = 0 self._changeState(self.STATE_OPERATIONAL) elif 'SD card ok' in line: self._sdAvailable = True self.refreshSdFiles() self._callback.mcSdStateChange(self._sdAvailable) elif 'Begin file list' in line: self._sdFiles = [] self._sdFileList = True elif 'End file list' in line: self._sdFileList = False self._callback.mcSdFiles(self._sdFiles) elif 'SD printing byte' in line: # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" match = re.search("([0-9]*)/([0-9]*)", line) self._currentFile.setFilepos(int(match.group(1))) self._callback.mcProgress() elif 'File opened' in line: # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d" match = re.search("File opened:\s*(.*?)\s+Size:\s*([0-9]*)", line) self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2))) elif 'File selected' in line: # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" self._callback.mcFileSelected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) eventManager().fire("FileSelected", self._currentFile.getFilename()) elif 'Writing to file' in line: # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" self._printSection = "CUSTOM" self._changeState(self.STATE_PRINTING) elif 'Done printing file' in line: # printer is reporting file finished printing self._sdFilePos = 0 self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire("PrintDone") ##~~ Message handling 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) ##~~ Parsing for feedback commands if feedbackControls: for name, matcher, template in feedbackControls: try: match = matcher.search(line) if match is not None: self._callback.mcReceivedRegisteredMessage(name, str.format(template, *(match.groups("n/a")))) except: # ignored on purpose pass ##~~ Parsing for pause triggers if pauseTriggers: if "enable" in pauseTriggers.keys() and pauseTriggers["enable"].search(line) is not None: self.setPause(True) elif "disable" in pauseTriggers.keys() and pauseTriggers["disable"].search(line) is not None: self.setPause(False) elif "toggle" in pauseTriggers.keys() and pauseTriggers["toggle"].search(line) is not None: self.setPause(not self.isPaused()) if "ok" in line and heatingUp: heatingUp = False ### 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) eventManager().fire("Error", self.getErrorString()) 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 = settings().getFloat(["serial", "timeout", "detection"]) self._log("Trying baudrate: %d" % (baudrate)) self._baudrateDetectRetry = 5 self._baudrateDetectTestOk = 0 timeout = getNewTimeout("communication") 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 = settings().getFloat(["serial", "timeout", "connection"]) self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate)) else: self._testingBaudrate = False ### Connection attempt elif self._state == self.STATE_CONNECTING: if (line == "" or "wait" in line) and startSeen: self._sendCommand("M105") elif "start" in line: startSeen = True elif "ok" in line and startSeen: self._changeState(self.STATE_OPERATIONAL) if self._sdAvailable: self.refreshSdFiles() eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate)) 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 = getNewTimeout("communication") # resend -> start resend procedure from requested line elif line.lower().startswith("resend") or line.lower().startswith("rs"): 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' if self.isSdPrinting(): if time.time() > tempRequestTimeout and not heatingUp: self._sendCommand("M105") tempRequestTimeout = getNewTimeout("communication") if time.time() > sdStatusRequestTimeout and not heatingUp: self._sendCommand("M27") sdStatusRequestTimeout = time.time() + 1 if 'ok' or 'SD printing byte' in line: timeout = getNewTimeout("communication") else: # Even when printing request the temperature every 5 seconds. if time.time() > tempRequestTimeout and not self.isStreaming(): self._commandQueue.put("M105") tempRequestTimeout = getNewTimeout("communication") if 'ok' in line: timeout = getNewTimeout("communication") if self._resendDelta is not None: self._resendNextCommand() elif not self._commandQueue.empty() and not self.isStreaming(): self._sendCommand(self._commandQueue.get()) else: self._sendNext() elif line.lower().startswith("resend") or line.lower().startswith("rs"): self._handleResendRequest(line) except: self._logger.exception("Something crashed inside the serial connection loop, please report this in OctoPrint's bug tracker:") errorMsg = "See octoprint.log for details" self._log(errorMsg) self._errorValue = errorMsg self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) self._log("Connection closed, closing down monitor") 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 _sendNext(self): with self._sendNextLock: line = self._currentFile.getNext() if line is None: if self.isStreaming(): self._sendCommand("M29") filename = self._currentFile.getFilename() self._currentFile = None self._callback.mcFileTransferDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire("TransferDone", filename) else: self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) eventManager().fire("PrintDone", self._currentFile.getFilename()) return if type(line) is tuple: self._printSection = line[1] line = line[0] self._sendCommand(line, True) self._callback.mcProgress() 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) or len(self._lastLines) == 0: self._errorValue = "Printer requested line %d but no sufficient history is available, can't resend" % lineToResend self._logger.warn(self._errorValue) if self.isPrinting(): # abort the print, there's nothing we can do to rescue it now self._changeState(self.STATE_ERROR) eventManager().fire("Error", self.getErrorString()) else: # reset resend delta, we can't do anything about it self._resendDelta = None else: self._resendNextCommand() 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: if self._serial is None: return if not self.isStreaming(): gcode = re.search("^\s*([GM]\d+)", cmd) if gcode: gcode = gcode.group(1) if gcode in gcodeToEvent: eventManager().fire(gcodeToEvent[gcode]) gcodeHandler = "_gcode_" + gcode if hasattr(self, gcodeHandler): cmd = getattr(self, gcodeHandler)(cmd) if cmd is not None: self._doSend(cmd, sendChecksum) def _doSend(self, cmd, sendChecksum=False): if sendChecksum or self._alwaysSendChecksum: lineNumber = self._currentLine 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)) commandToSend = "N%d %s" % (lineNumber, cmd) checksum = reduce(lambda x,y:x^y, map(ord, commandToSend)) commandToSend = "%s*%d" % (commandToSend, 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 _gcode_G0(self, cmd): if 'Z' in cmd: try: z = float(re.search('Z([0-9\.]*)', cmd).group(1)) if self._currentZ != z: self._currentZ = z self._callback.mcZChange(z) except ValueError: pass return cmd _gcode_G1 = _gcode_G0 def _gcode_M0(self, cmd): self.setPause(True) return "M105" # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. _gcode_M1 = _gcode_M0 def _gcode_M104(self, cmd): match = re.search('S([0-9]+)', cmd) if match: try: self._targetTemp = float(match.group(1)) except ValueError: pass return cmd def _gcode_M140(self, cmd): match = re.search('S([0-9]+)', cmd) if match: try: self._bedTargetTemp = float(match.group(1)) except ValueError: pass return cmd def _gcode_M109(self, cmd): self._heatupWaitStartTime = time.time() return self._gcode_M104(cmd) def _gcode_M190(self, cmd): self._heatupWaitStartTime = time.time() return self._gcode_M140(cmd) def _gcode_M110(self, cmd): newLineNumber = None match = re.search("N([0-9]+)", cmd) if match: try: newLineNumber = int(match.group(1)) except: pass else: newLineNumber = 0 # send M110 command with new line number self._doSendWithChecksum(cmd, newLineNumber) 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.clear() self._resendDelta = None return None