From 3b66f63ff2d586898bb69e19d69ad08bf2991f88 Mon Sep 17 00:00:00 2001 From: Lars Norpchen Date: Thu, 9 May 2013 14:05:48 -0700 Subject: [PATCH 1/7] Added external system commands support for events Print start, print end, print cancel, and Z height change can now trigger external system commands. These are set in config.yaml and take _FILE_, _PERCENT_, _LINES_ and _ZHEIGHT_ tokens to be passed to external commands. system_commands: cancelled: echo cancelled _FILE_ at _PROGRESS_ percent done. print_done: echo done with _FILE_ print_started: echo starting _FILE_ z_change: echo _LINE_ _PROGRESS_ _ZHEIGHT_ --- octoprint/printer.py | 1112 +++++++++++++++++++++-------------------- octoprint/server.py | 14 + octoprint/settings.py | 10 +- 3 files changed, 602 insertions(+), 534 deletions(-) diff --git a/octoprint/printer.py b/octoprint/printer.py index be080b2..2d49df2 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -7,6 +7,9 @@ import datetime import threading import copy import os +import subprocess +import re +import logging, logging.config import octoprint.util.comm as comm import octoprint.util as util @@ -14,587 +17,632 @@ import octoprint.util as util from octoprint.settings import settings def getConnectionOptions(): - """ - Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer. - """ - return { - "ports": comm.serialList(), - "baudrates": comm.baudrateList(), - "portPreference": settings().get(["serial", "port"]), - "baudratePreference": settings().getInt(["serial", "baudrate"]) - } + """ + Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer. + """ + return { + "ports": comm.serialList(), + "baudrates": comm.baudrateList(), + "portPreference": settings().get(["serial", "port"]), + "baudratePreference": settings().getInt(["serial", "baudrate"]) + } class Printer(): - def __init__(self, gcodeManager): - self._gcodeManager = gcodeManager + def __init__(self, gcodeManager): + self._gcodeManager = gcodeManager - # state - self._temp = None - self._bedTemp = None - self._targetTemp = None - self._targetBedTemp = None - self._temps = { - "actual": [], - "target": [], - "actualBed": [], - "targetBed": [] - } - self._tempBacklog = [] + # state + self._temp = None + self._bedTemp = None + self._targetTemp = None + self._targetBedTemp = None + self._temps = { + "actual": [], + "target": [], + "actualBed": [], + "targetBed": [] + } + self._tempBacklog = [] - self._latestMessage = None - self._messages = [] - self._messageBacklog = [] + self._latestMessage = None + self._messages = [] + self._messageBacklog = [] - self._latestLog = None - self._log = [] - self._logBacklog = [] + self._latestLog = None + self._log = [] + self._logBacklog = [] - self._state = None + self._state = None - self._currentZ = None + self._currentZ = None - self._progress = None - self._printTime = None - self._printTimeLeft = None + self.peakZ = -1 + self._progress = None + self._printTime = None + self._printTimeLeft = None - # gcode handling - self._gcodeList = None - self._filename = None - self._gcodeLoader = None + # gcode handling + self._gcodeList = None + self._filename = None + self._gcodeLoader = None - # feedrate - self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"} + # feedrate + self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"} - # timelapse - self._timelapse = None + # timelapse + self._timelapse = None - # comm - self._comm = None + # comm + self._comm = None - # callbacks - self._callbacks = [] - self._lastProgressReport = None + # callbacks + self._callbacks = [] + self._lastProgressReport = None - self._stateMonitor = StateMonitor( - ratelimit=0.5, - updateCallback=self._sendCurrentDataCallbacks, - addTemperatureCallback=self._sendAddTemperatureCallbacks, - addLogCallback=self._sendAddLogCallbacks, - addMessageCallback=self._sendAddMessageCallbacks - ) - self._stateMonitor.reset( - state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()}, - jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None}, - gcodeData={"filename": None, "progress": None}, - progress={"progress": None, "printTime": None, "printTimeLeft": None}, - currentZ=None - ) + self._stateMonitor = StateMonitor( + ratelimit=0.5, + updateCallback=self._sendCurrentDataCallbacks, + addTemperatureCallback=self._sendAddTemperatureCallbacks, + addLogCallback=self._sendAddLogCallbacks, + addMessageCallback=self._sendAddMessageCallbacks + ) + self._stateMonitor.reset( + state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()}, + jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None}, + gcodeData={"filename": None, "progress": None}, + progress={"progress": None, "printTime": None, "printTimeLeft": None}, + currentZ=None + ) - #~~ callback handling + self.sys_command= { "z_change": settings().get(["system_commands", "z_change"]), "cancelled" : settings().get(["system_commands", "cancelled"]), "print_done" :settings().get(["system_commands", "print_done"]), "print_started": settings().get(["system_commands", "print_started"])}; + - def registerCallback(self, callback): - self._callbacks.append(callback) - self._sendInitialStateUpdate(callback) + #~~ callback handling - def unregisterCallback(self, callback): - if callback in self._callbacks: - self._callbacks.remove(callback) + def registerCallback(self, callback): + self._callbacks.append(callback) + self._sendInitialStateUpdate(callback) - def _sendAddTemperatureCallbacks(self, data): - for callback in self._callbacks: - try: callback.addTemperature(data) - except: pass + def unregisterCallback(self, callback): + if callback in self._callbacks: + self._callbacks.remove(callback) - def _sendAddLogCallbacks(self, data): - for callback in self._callbacks: - try: callback.addLog(data) - except: pass + def _sendAddTemperatureCallbacks(self, data): + for callback in self._callbacks: + try: callback.addTemperature(data) + except: pass - def _sendAddMessageCallbacks(self, data): - for callback in self._callbacks: - try: callback.addMessage(data) - except: pass + def _sendAddLogCallbacks(self, data): + for callback in self._callbacks: + try: callback.addLog(data) + except: pass - def _sendCurrentDataCallbacks(self, data): - for callback in self._callbacks: - try: callback.sendCurrentData(copy.deepcopy(data)) - except: pass + def _sendAddMessageCallbacks(self, data): + for callback in self._callbacks: + try: callback.addMessage(data) + except: pass + + def _sendCurrentDataCallbacks(self, data): + for callback in self._callbacks: + try: callback.sendCurrentData(copy.deepcopy(data)) + except: pass #~~ printer commands - def connect(self, port=None, baudrate=None): - """ - Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection - will be attempted. - """ - if self._comm is not None: - self._comm.close() - self._comm = comm.MachineCom(port, baudrate, callbackObject=self) - - def disconnect(self): - """ - Closes the connection to the printer. - """ - if self._comm is not None: - self._comm.close() - self._comm = None - - def command(self, command): - """ - Sends a single gcode command to the printer. - """ - self.commands([command]) - - def commands(self, commands): - """ - Sends multiple gcode commands (provided as a list) to the printer. - """ - for command in commands: - self._comm.sendCommand(command) - - def setFeedrateModifier(self, structure, percentage): - if (not self._feedrateModifierMapping.has_key(structure)) or percentage < 0: - return - - self._comm.setFeedrateModifier(self._feedrateModifierMapping[structure], percentage / 100.0) - - def loadGcode(self, file, printAfterLoading=False): - """ - Loads the gcode from the given file as the new print job. - Aborts if the printer is currently printing or another gcode file is currently being loaded. - """ - if (self._comm is not None and self._comm.isPrinting()) or (self._gcodeLoader is not None): - return - - self._setJobData(None, None) - - onGcodeLoadedCallback = self._onGcodeLoaded - if printAfterLoading: - onGcodeLoadedCallback = self._onGcodeLoadedToPrint - - self._gcodeLoader = GcodeLoader(file, self._onGcodeLoadingProgress, onGcodeLoadedCallback) - self._gcodeLoader.start() - - self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) - - def startPrint(self): - """ - Starts the currently loaded print job. - Only starts if the printer is connected and operational, not currently printing and a printjob is loaded - """ - if self._comm is None or not self._comm.isOperational(): - return - if self._gcodeList is None: - return - if self._comm.isPrinting(): - return - - self._setCurrentZ(-1) - self._comm.printGCode(self._gcodeList) - - def togglePausePrint(self): - """ - Pause the current printjob. - """ - if self._comm is None: - return - self._comm.setPause(not self._comm.isPaused()) - - def cancelPrint(self, disableMotorsAndHeater=True): - """ - Cancel the current printjob. - """ - if self._comm is None: - return - self._comm.cancelPrint() - if disableMotorsAndHeater: - self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan - - # reset line, height, print time - self._setCurrentZ(None) - self._setProgressData(None, None, None) - - # mark print as failure - self._gcodeManager.printFailed(self._filename) - - #~~ state monitoring - - def setTimelapse(self, timelapse): - if self._timelapse is not None and self.isPrinting(): - self._timelapse.onPrintjobStopped() - del self._timelapse - self._timelapse = timelapse - - def getTimelapse(self): - return self._timelapse - - def _setCurrentZ(self, currentZ): - self._currentZ = currentZ - - formattedCurrentZ = None - if self._currentZ: - formattedCurrentZ = "%.2f mm" % (self._currentZ) - self._stateMonitor.setCurrentZ(formattedCurrentZ) - - def _setState(self, state): - self._state = state - self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) - - def _addLog(self, log): - self._log.append(log) - self._log = self._log[-300:] - self._stateMonitor.addLog(log) - - def _addMessage(self, message): - self._messages.append(message) - self._messages = self._messages[-300:] - self._stateMonitor.addMessage(message) - - def _setProgressData(self, progress, printTime, printTimeLeft): - self._progress = progress - self._printTime = printTime - self._printTimeLeft = printTimeLeft - - formattedPrintTime = None - if (self._printTime): - formattedPrintTime = util.getFormattedTimeDelta(datetime.timedelta(seconds=self._printTime)) - - formattedPrintTimeLeft = None - if (self._printTimeLeft): - formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft)) - - self._stateMonitor.setProgress({"progress": self._progress, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft}) - - def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp): - currentTimeUtc = int(time.time() * 1000) - - self._temps["actual"].append((currentTimeUtc, temp)) - self._temps["actual"] = self._temps["actual"][-300:] - - self._temps["target"].append((currentTimeUtc, targetTemp)) - self._temps["target"] = self._temps["target"][-300:] - - self._temps["actualBed"].append((currentTimeUtc, bedTemp)) - self._temps["actualBed"] = self._temps["actualBed"][-300:] - - self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp)) - self._temps["targetBed"] = self._temps["targetBed"][-300:] - - self._temp = temp - self._bedTemp = bedTemp - self._targetTemp = targetTemp - self._targetBedTemp = bedTargetTemp - - self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp}) - - def _setJobData(self, filename, gcodeList): - self._filename = filename - self._gcodeList = gcodeList - - lines = None - if self._gcodeList: - lines = len(self._gcodeList) - - formattedFilename = None - estimatedPrintTime = None - filament = None - if self._filename: - formattedFilename = os.path.basename(self._filename) - - fileData = self._gcodeManager.getFileData(filename) - if fileData is not None and "gcodeAnalysis" in fileData.keys(): - if "estimatedPrintTime" in fileData["gcodeAnalysis"].keys(): - estimatedPrintTime = fileData["gcodeAnalysis"]["estimatedPrintTime"] - if "filament" in fileData["gcodeAnalysis"].keys(): - filament = fileData["gcodeAnalysis"]["filament"] - - self._stateMonitor.setJobData({"filename": formattedFilename, "lines": lines, "estimatedPrintTime": estimatedPrintTime, "filament": filament}) - - def _sendInitialStateUpdate(self, callback): - try: - data = self._stateMonitor.getCurrentData() - data.update({ - "temperatureHistory": self._temps, - "logHistory": self._log, - "messageHistory": self._messages - }) - callback.sendHistoryData(data) - except Exception, err: - import sys - sys.stderr.write("ERROR: %s\n" % str(err)) - pass - - def _getStateFlags(self): - return { - "operational": self.isOperational(), - "printing": self.isPrinting(), - "closedOrError": self.isClosedOrError(), - "error": self.isError(), - "loading": self.isLoading(), - "paused": self.isPaused(), - "ready": self.isReady() - } - - #~~ callbacks triggered from self._comm - - def mcLog(self, message): - """ - Callback method for the comm object, called upon log output. - """ - self._addLog(message) - - def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp): - self._addTemperatureData(temp, bedTemp, targetTemp, bedTargetTemp) - - def mcStateChange(self, state): - """ - Callback method for the comm object, called if the connection state changes. - """ - oldState = self._state - - # forward relevant state changes to timelapse - if self._timelapse is not None: - if oldState == self._comm.STATE_PRINTING and state != self._comm.STATE_PAUSED: - self._timelapse.onPrintjobStopped() - elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED: - self._timelapse.onPrintjobStarted(self._filename) - - # forward relevant state changes to gcode manager - if self._comm is not None and oldState == self._comm.STATE_PRINTING: - if state == self._comm.STATE_OPERATIONAL: - self._gcodeManager.printSucceeded(self._filename) - elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: - self._gcodeManager.printFailed(self._filename) - self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing - elif self._comm is not None and state == self._comm.STATE_PRINTING: - self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use - - self._setState(state) - - - def mcMessage(self, message): - """ - Callback method for the comm object, called upon message exchanges via serial. - Stores the message in the message buffer, truncates buffer to the last 300 lines. - """ - self._addMessage(message) - - def mcProgress(self, lineNr): - """ - Callback method for the comm object, called upon any change in progress of the printjob. - Triggers storage of new values for printTime, printTimeLeft and the current line. - """ - oldProgress = self._progress - - if self._timelapse is not None: - try: self._timelapse.onPrintjobProgress(oldProgress, self._progress, int(round(self._progress * 100 / len(self._gcodeList)))) - except: pass - - self._setProgressData(self._comm.getPrintPos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate()) - - def mcZChange(self, newZ): - """ - Callback method for the comm object, called upon change of the z-layer. - """ - oldZ = self._currentZ - if self._timelapse is not None: - self._timelapse.onZChange(oldZ, newZ) - - self._setCurrentZ(newZ) - - #~~ callbacks triggered by gcodeLoader - - def _onGcodeLoadingProgress(self, filename, progress, mode): - formattedFilename = None - if filename is not None: - formattedFilename = os.path.basename(filename) - - self._stateMonitor.setGcodeData({"filename": formattedFilename, "progress": progress, "mode": mode}) - - def _onGcodeLoaded(self, filename, gcodeList): - self._setJobData(filename, gcodeList) - self._setCurrentZ(None) - self._setProgressData(None, None, None) - self._gcodeLoader = None - - self._stateMonitor.setGcodeData({"filename": None, "progress": None}) - self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) - - def _onGcodeLoadedToPrint(self, filename, gcodeList): - self._onGcodeLoaded(filename, gcodeList) - self.startPrint() - - #~~ state reports - - def feedrateState(self): - if self._comm is not None: - feedrateModifiers = self._comm.getFeedrateModifiers() - result = {} - for structure in self._feedrateModifierMapping.keys(): - if (feedrateModifiers.has_key(self._feedrateModifierMapping[structure])): - result[structure] = int(round(feedrateModifiers[self._feedrateModifierMapping[structure]] * 100)) - else: - result[structure] = 100 - return result - else: - return None - - def getStateString(self): - """ - Returns a human readable string corresponding to the current communication state. - """ - if self._comm is None: - return "Offline" - else: - return self._comm.getStateString() - - def isClosedOrError(self): - return self._comm is None or self._comm.isClosedOrError() - - def isOperational(self): - return self._comm is not None and self._comm.isOperational() - - def isPrinting(self): - return self._comm is not None and self._comm.isPrinting() - - def isPaused(self): - return self._comm is not None and self._comm.isPaused() - - def isError(self): - return self._comm is not None and self._comm.isError() - - def isReady(self): - return self._gcodeLoader is None and self._gcodeList and len(self._gcodeList) > 0 - - def isLoading(self): - return self._gcodeLoader is not None + def connect(self, port=None, baudrate=None): + """ + Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection + will be attempted. + """ + if self._comm is not None: + self._comm.close() + self._comm = comm.MachineCom(port, baudrate, callbackObject=self) + + def disconnect(self): + """ + Closes the connection to the printer. + """ + if self._comm is not None: + self._comm.close() + self._comm = None + + def command(self, command): + """ + Sends a single gcode command to the printer. + """ + self.commands([command]) + + def commands(self, commands): + """ + Sends multiple gcode commands (provided as a list) to the printer. + """ + for command in commands: + self._comm.sendCommand(command) + + def setFeedrateModifier(self, structure, percentage): + if (not self._feedrateModifierMapping.has_key(structure)) or percentage < 0: + return + + self._comm.setFeedrateModifier(self._feedrateModifierMapping[structure], percentage / 100.0) + + def loadGcode(self, file, printAfterLoading=False): + """ + Loads the gcode from the given file as the new print job. + Aborts if the printer is currently printing or another gcode file is currently being loaded. + """ + if (self._comm is not None and self._comm.isPrinting()) or (self._gcodeLoader is not None): + return + + self._setJobData(None, None) + + onGcodeLoadedCallback = self._onGcodeLoaded + if printAfterLoading: + onGcodeLoadedCallback = self._onGcodeLoadedToPrint + + self._gcodeLoader = GcodeLoader(file, self._onGcodeLoadingProgress, onGcodeLoadedCallback) + self._gcodeLoader.start() + + self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) + + def startPrint(self): + """ + Starts the currently loaded print job. + Only starts if the printer is connected and operational, not currently printing and a printjob is loaded + """ + if self._comm is None or not self._comm.isOperational(): + return + if self._gcodeList is None: + return + if self._comm.isPrinting(): + return + + self._setCurrentZ(-1) + + self.executeSystemCommand(self.sys_command['print_started']) + + self._comm.printGCode(self._gcodeList) + + def togglePausePrint(self): + """ + Pause the current printjob. + """ + if self._comm is None: + return + self._comm.setPause(not self._comm.isPaused()) + + def cancelPrint(self, disableMotorsAndHeater=True): + """ + Cancel the current printjob. + """ + if self._comm is None: + return + self._comm.cancelPrint() + if disableMotorsAndHeater: + self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan + + # reset line, height, print time + self._setCurrentZ(None) + self._setProgressData(None, None, None) + + # mark print as failure + self._gcodeManager.printFailed(self._filename) + self.executeSystemCommand(self.sys_command['cancelled']) + + #~~ state monitoring + + def setTimelapse(self, timelapse): + if self._timelapse is not None and self.isPrinting(): + self._timelapse.onPrintjobStopped() + del self._timelapse + self._timelapse = timelapse + + def getTimelapse(self): + return self._timelapse + + def _setCurrentZ(self, currentZ): + self._currentZ = currentZ + + formattedCurrentZ = None + if self._currentZ: + formattedCurrentZ = "%.2f mm" % (self._currentZ) + self._stateMonitor.setCurrentZ(formattedCurrentZ) + + def _setState(self, state): + self._state = state + self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) + + def _addLog(self, log): + self._log.append(log) + self._log = self._log[-300:] + self._stateMonitor.addLog(log) + + def _addMessage(self, message): + self._messages.append(message) + self._messages = self._messages[-300:] + self._stateMonitor.addMessage(message) + + def _setProgressData(self, progress, printTime, printTimeLeft): + self._progress = progress + self._printTime = printTime + self._printTimeLeft = printTimeLeft + + formattedPrintTime = None + if (self._printTime): + formattedPrintTime = util.getFormattedTimeDelta(datetime.timedelta(seconds=self._printTime)) + + formattedPrintTimeLeft = None + if (self._printTimeLeft): + formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft)) + + self._stateMonitor.setProgress({"progress": self._progress, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft}) + + def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp): + currentTimeUtc = int(time.time() * 1000) + + self._temps["actual"].append((currentTimeUtc, temp)) + self._temps["actual"] = self._temps["actual"][-300:] + + self._temps["target"].append((currentTimeUtc, targetTemp)) + self._temps["target"] = self._temps["target"][-300:] + + self._temps["actualBed"].append((currentTimeUtc, bedTemp)) + self._temps["actualBed"] = self._temps["actualBed"][-300:] + + self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp)) + self._temps["targetBed"] = self._temps["targetBed"][-300:] + + self._temp = temp + self._bedTemp = bedTemp + self._targetTemp = targetTemp + self._targetBedTemp = bedTargetTemp + + self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp}) + + def _setJobData(self, filename, gcodeList): + self._filename = filename + self._gcodeList = gcodeList + + lines = None + if self._gcodeList: + lines = len(self._gcodeList) + + formattedFilename = None + estimatedPrintTime = None + filament = None + if self._filename: + formattedFilename = os.path.basename(self._filename) + + fileData = self._gcodeManager.getFileData(filename) + if fileData is not None and "gcodeAnalysis" in fileData.keys(): + if "estimatedPrintTime" in fileData["gcodeAnalysis"].keys(): + estimatedPrintTime = fileData["gcodeAnalysis"]["estimatedPrintTime"] + if "filament" in fileData["gcodeAnalysis"].keys(): + filament = fileData["gcodeAnalysis"]["filament"] + + self._stateMonitor.setJobData({"filename": formattedFilename, "lines": lines, "estimatedPrintTime": estimatedPrintTime, "filament": filament}) + + def _sendInitialStateUpdate(self, callback): + try: + data = self._stateMonitor.getCurrentData() + data.update({ + "temperatureHistory": self._temps, + "logHistory": self._log, + "messageHistory": self._messages + }) + callback.sendHistoryData(data) + except Exception, err: + import sys + sys.stderr.write("ERROR: %s\n" % str(err)) + pass + + def _getStateFlags(self): + return { + "operational": self.isOperational(), + "printing": self.isPrinting(), + "closedOrError": self.isClosedOrError(), + "error": self.isError(), + "loading": self.isLoading(), + "paused": self.isPaused(), + "ready": self.isReady() + } + + #~~ callbacks triggered from self._comm + + def mcLog(self, message): + """ + Callback method for the comm object, called upon log output. + """ + self._addLog(message) + + def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp): + self._addTemperatureData(temp, bedTemp, targetTemp, bedTargetTemp) + + def mcStateChange(self, state): + """ + Callback method for the comm object, called if the connection state changes. + """ + oldState = self._state + + # forward relevant state changes to timelapse + if self._timelapse is not None: + if oldState == self._comm.STATE_PRINTING and state != self._comm.STATE_PAUSED: + self._timelapse.onPrintjobStopped() + elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED: + self._timelapse.onPrintjobStarted(self._filename) + + # forward relevant state changes to gcode manager + if self._comm is not None and oldState == self._comm.STATE_PRINTING: + if state == self._comm.STATE_OPERATIONAL: + self._gcodeManager.printSucceeded(self._filename) + #hrm....we seem to hit this state and THEN the next failed state on a cancel request? + # oh well, add a check to see if we're really done before sending the success event external command + if self._printTimeLeft < 1: + self.executeSystemCommand(self.sys_command['print_done']) + elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: + self._gcodeManager.printFailed(self._filename) + self.executeSystemCommand(self.sys_command['cancelled']) + self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing + elif self._comm is not None and state == self._comm.STATE_PRINTING: + self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use + + self._setState(state) + + + def mcMessage(self, message): + """ + Callback method for the comm object, called upon message exchanges via serial. + Stores the message in the message buffer, truncates buffer to the last 300 lines. + """ + self._addMessage(message) + + def mcProgress(self, lineNr): + """ + Callback method for the comm object, called upon any change in progress of the printjob. + Triggers storage of new values for printTime, printTimeLeft and the current line. + """ + oldProgress = self._progress + + if self._timelapse is not None: + try: self._timelapse.onPrintjobProgress(oldProgress, self._progress, int(round(self._progress * 100 / len(self._gcodeList)))) + except: pass + + self._setProgressData(self._comm.getPrintPos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate()) + + def mcZChange(self, newZ): + """ + Callback method for the comm object, called upon change of the z-layer. + """ + oldZ = self._currentZ + # only do this if we hit a new Z peak level. Some slicers do a Z-lift when retracting / moving without printing + # and some do ananti-backlash up-then-down movement when advancing layers + if newZ > self.peakZ: + self.peakZ = newZ + if self._timelapse is not None: + self._timelapse.onZChange(oldZ, newZ) + self.executeSystemCommand(self.sys_command['z_change']) + + self._setCurrentZ(newZ) + + #~~ callbacks triggered by gcodeLoader + + def _onGcodeLoadingProgress(self, filename, progress, mode): + formattedFilename = None + if filename is not None: + formattedFilename = os.path.basename(filename) + + self._stateMonitor.setGcodeData({"filename": formattedFilename, "progress": progress, "mode": mode}) + + def _onGcodeLoaded(self, filename, gcodeList): + self._setJobData(filename, gcodeList) + self._setCurrentZ(None) + self._setProgressData(None, None, None) + self._gcodeLoader = None + + self._stateMonitor.setGcodeData({"filename": None, "progress": None}) + self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) + + def _onGcodeLoadedToPrint(self, filename, gcodeList): + self._onGcodeLoaded(filename, gcodeList) + self.startPrint() + + #~~ state reports + + def feedrateState(self): + if self._comm is not None: + feedrateModifiers = self._comm.getFeedrateModifiers() + result = {} + for structure in self._feedrateModifierMapping.keys(): + if (feedrateModifiers.has_key(self._feedrateModifierMapping[structure])): + result[structure] = int(round(feedrateModifiers[self._feedrateModifierMapping[structure]] * 100)) + else: + result[structure] = 100 + return result + else: + return None + + def getStateString(self): + """ + Returns a human readable string corresponding to the current communication state. + """ + if self._comm is None: + return "Offline" + else: + return self._comm.getStateString() + + def isClosedOrError(self): + return self._comm is None or self._comm.isClosedOrError() + + def isOperational(self): + return self._comm is not None and self._comm.isOperational() + + def isPrinting(self): + return self._comm is not None and self._comm.isPrinting() + + def isPaused(self): + return self._comm is not None and self._comm.isPaused() + + def isError(self): + return self._comm is not None and self._comm.isError() + + def isReady(self): + return self._gcodeLoader is None and self._gcodeList and len(self._gcodeList) > 0 + + def isLoading(self): + return self._gcodeLoader is not None + + def executeSystemCommand(self,command_string): + if command_string is None: + return + logger = logging.getLogger(__name__) + try: + # handle a few regex substs for job data passed to external apps + cmd_string_with_params = command_string + cmd_string_with_params = re.sub("_ZHEIGHT_",str(self._currentZ), cmd_string_with_params) + cmd_string_with_params = re.sub("_FILE_",os.path.basename(self._filename), cmd_string_with_params) + # cut down to 2 decimal places, forcing through an int to avoid the 10.320000000001 floating point thing... + if self._gcodeList and self._progress: + prog = int(10000.0 * self._progress / len(self._gcodeList))/100.0 + else: + prog = 0.0 + cmd_string_with_params = re.sub("_PROGRESS_",str(prog), cmd_string_with_params) + cmd_string_with_params = re.sub("_LINE_",str(self._comm._gcodePos), cmd_string_with_params) + logger.info ("Executing system command: %s " % cmd_string_with_params) + #use Popen here since it won't wait for the shell to return...and we send some of these + # commands during a print job, we don't want to block! + subprocess.Popen(cmd_string_with_params,shell = True) + except subprocess.CalledProcessError, e: + logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message)) + except Exception, ex: + logger.exception("Command failed") + class GcodeLoader(threading.Thread): - """ - The GcodeLoader takes care of loading a gcode-File from disk and parsing it into a gcode object in a separate - thread while constantly notifying interested listeners about the current progress. - The progress is returned as a float value between 0 and 1 which is to be interpreted as the percentage of completion. - """ + """ + The GcodeLoader takes care of loading a gcode-File from disk and parsing it into a gcode object in a separate + thread while constantly notifying interested listeners about the current progress. + The progress is returned as a float value between 0 and 1 which is to be interpreted as the percentage of completion. + """ - def __init__(self, filename, progressCallback, loadedCallback): - threading.Thread.__init__(self) + def __init__(self, filename, progressCallback, loadedCallback): + threading.Thread.__init__(self) - self._progressCallback = progressCallback - self._loadedCallback = loadedCallback + self._progressCallback = progressCallback + self._loadedCallback = loadedCallback - self._filename = filename - self._gcodeList = None + self._filename = filename + self._gcodeList = None - def run(self): - #Send an initial M110 to reset the line counter to zero. - prevLineType = lineType = "CUSTOM" - gcodeList = ["M110"] - filesize = os.stat(self._filename).st_size - with open(self._filename, "r") as file: - for line in file: - if line.startswith(";TYPE:"): - lineType = line[6:].strip() - if ";" in line: - line = line[0:line.find(";")] - line = line.strip() - if len(line) > 0: - if prevLineType != lineType: - gcodeList.append((line, lineType, )) - else: - gcodeList.append(line) - prevLineType = lineType - self._onLoadingProgress(float(file.tell()) / float(filesize)) + def run(self): + #Send an initial M110 to reset the line counter to zero. + prevLineType = lineType = "CUSTOM" + gcodeList = ["M110"] + filesize = os.stat(self._filename).st_size + with open(self._filename, "r") as file: + for line in file: + if line.startswith(";TYPE:"): + lineType = line[6:].strip() + if ";" in line: + line = line[0:line.find(";")] + line = line.strip() + if len(line) > 0: + if prevLineType != lineType: + gcodeList.append((line, lineType, )) + else: + gcodeList.append(line) + prevLineType = lineType + self._onLoadingProgress(float(file.tell()) / float(filesize)) - self._gcodeList = gcodeList - self._loadedCallback(self._filename, self._gcodeList) + self._gcodeList = gcodeList + self._loadedCallback(self._filename, self._gcodeList) - def _onLoadingProgress(self, progress): - self._progressCallback(self._filename, progress, "loading") + def _onLoadingProgress(self, progress): + self._progressCallback(self._filename, progress, "loading") - def _onParsingProgress(self, progress): - self._progressCallback(self._filename, progress, "parsing") + def _onParsingProgress(self, progress): + self._progressCallback(self._filename, progress, "parsing") class StateMonitor(object): - def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback): - self._ratelimit = ratelimit - self._updateCallback = updateCallback - self._addTemperatureCallback = addTemperatureCallback - self._addLogCallback = addLogCallback - self._addMessageCallback = addMessageCallback + def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback): + self._ratelimit = ratelimit + self._updateCallback = updateCallback + self._addTemperatureCallback = addTemperatureCallback + self._addLogCallback = addLogCallback + self._addMessageCallback = addMessageCallback - self._state = None - self._jobData = None - self._gcodeData = None - self._currentZ = None - self._progress = None + self._state = None + self._jobData = None + self._gcodeData = None + self._currentZ = None + self._peakZ = -1 + self._progress = None - self._changeEvent = threading.Event() + self._changeEvent = threading.Event() - self._lastUpdate = time.time() - self._worker = threading.Thread(target=self._work) - self._worker.daemon = True - self._worker.start() + self._lastUpdate = time.time() + self._worker = threading.Thread(target=self._work) + self._worker.daemon = True + self._worker.start() - def reset(self, state=None, jobData=None, gcodeData=None, progress=None, currentZ=None): - self.setState(state) - self.setJobData(jobData) - self.setGcodeData(gcodeData) - self.setProgress(progress) - self.setCurrentZ(currentZ) + def reset(self, state=None, jobData=None, gcodeData=None, progress=None, currentZ=None): + self.setState(state) + self.setJobData(jobData) + self.setGcodeData(gcodeData) + self.setProgress(progress) + self.setCurrentZ(currentZ) - def addTemperature(self, temperature): - self._addTemperatureCallback(temperature) - self._changeEvent.set() + def addTemperature(self, temperature): + self._addTemperatureCallback(temperature) + self._changeEvent.set() - def addLog(self, log): - self._addLogCallback(log) - self._changeEvent.set() + def addLog(self, log): + self._addLogCallback(log) + self._changeEvent.set() - def addMessage(self, message): - self._addMessageCallback(message) - self._changeEvent.set() + def addMessage(self, message): + self._addMessageCallback(message) + self._changeEvent.set() - def setCurrentZ(self, currentZ): - self._currentZ = currentZ - self._changeEvent.set() + def setCurrentZ(self, currentZ): + self._currentZ = currentZ + self._changeEvent.set() - def setState(self, state): - self._state = state - self._changeEvent.set() + def setState(self, state): + self._state = state + self._changeEvent.set() - def setJobData(self, jobData): - self._jobData = jobData - self._changeEvent.set() + def setJobData(self, jobData): + self._jobData = jobData + self._changeEvent.set() - def setGcodeData(self, gcodeData): - self._gcodeData = gcodeData - self._changeEvent.set() + def setGcodeData(self, gcodeData): + self._gcodeData = gcodeData + self._changeEvent.set() - def setProgress(self, progress): - self._progress = progress - self._changeEvent.set() + def setProgress(self, progress): + self._progress = progress + self._changeEvent.set() - def _work(self): - while True: - self._changeEvent.wait() + def _work(self): + while True: + self._changeEvent.wait() - now = time.time() - delta = now - self._lastUpdate - additionalWaitTime = self._ratelimit - delta - if additionalWaitTime > 0: - time.sleep(additionalWaitTime) + now = time.time() + delta = now - self._lastUpdate + additionalWaitTime = self._ratelimit - delta + if additionalWaitTime > 0: + time.sleep(additionalWaitTime) - data = self.getCurrentData() - self._updateCallback(data) - self._lastUpdate = time.time() - self._changeEvent.clear() + data = self.getCurrentData() + self._updateCallback(data) + self._lastUpdate = time.time() + self._changeEvent.clear() - def getCurrentData(self): - return { - "state": self._state, - "job": self._jobData, - "gcode": self._gcodeData, - "currentZ": self._currentZ, - "progress": self._progress - } + def getCurrentData(self): + return { + "state": self._state, + "job": self._jobData, + "gcode": self._gcodeData, + "currentZ": self._currentZ, + "progress": self._progress + } diff --git a/octoprint/server.py b/octoprint/server.py index aab4393..feaae8e 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -375,7 +375,15 @@ def getSettings(): }, "system": { "actions": s.get(["system", "actions"]) + }, + "system_commands": { + "print_done": s.get(["system_commands", "print_done"]), + "cancelled": s.get(["system_commands", "cancelled"]), + "print_started": s.get(["system_commands", "print_started"]), + "z_change": s.get(["system_commands", "z_change"]) + } + }) @app.route(BASEURL + "settings", methods=["POST"]) @@ -417,6 +425,12 @@ def setSettings(): if "system" in data.keys(): if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"]) + if "system_commands" in data.keys(): + if "z_change" in data["system_commands"].keys(): s.set(["system_commands", "z_change"], data["system_commands"]["z_change"]) + if "print_started" in data["system_commands"].keys(): s.set(["system_commands", "print_started"], data["system_commands"]["print_started"]) + if "cancelled" in data["system_commands"].keys(): s.set(["system_commands", "cancelled"], data["system_commands"]["cancelled"]) + if "print_done" in data["system_commands"].keys(): s.set(["system_commands", "print_done"], data["system_commands"]["print_done"]) + s.save() return getSettings() diff --git a/octoprint/settings.py b/octoprint/settings.py index 0c7dfad..b655f49 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -69,8 +69,14 @@ default_settings = { "controls": [], "system": { "actions": [] - } -} + }, + "system_commands": { + "z_change":None, + "print_started":None, + "cancelled":None, + "print_done":None + } +} valid_boolean_trues = ["true", "yes", "y", "1"] From b609123d8a570b9448b0fed0fbd0835a02824361 Mon Sep 17 00:00:00 2001 From: Lars Norpchen Date: Thu, 9 May 2013 14:22:40 -0700 Subject: [PATCH 2/7] Revert "Added external system commands support for events" This reverts commit 3b66f63ff2d586898bb69e19d69ad08bf2991f88. --- octoprint/printer.py | 1112 ++++++++++++++++++++--------------------- octoprint/server.py | 14 - octoprint/settings.py | 10 +- 3 files changed, 534 insertions(+), 602 deletions(-) diff --git a/octoprint/printer.py b/octoprint/printer.py index 2d49df2..be080b2 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -7,9 +7,6 @@ import datetime import threading import copy import os -import subprocess -import re -import logging, logging.config import octoprint.util.comm as comm import octoprint.util as util @@ -17,632 +14,587 @@ import octoprint.util as util from octoprint.settings import settings def getConnectionOptions(): - """ - Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer. - """ - return { - "ports": comm.serialList(), - "baudrates": comm.baudrateList(), - "portPreference": settings().get(["serial", "port"]), - "baudratePreference": settings().getInt(["serial", "baudrate"]) - } + """ + Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer. + """ + return { + "ports": comm.serialList(), + "baudrates": comm.baudrateList(), + "portPreference": settings().get(["serial", "port"]), + "baudratePreference": settings().getInt(["serial", "baudrate"]) + } class Printer(): - def __init__(self, gcodeManager): - self._gcodeManager = gcodeManager + def __init__(self, gcodeManager): + self._gcodeManager = gcodeManager - # state - self._temp = None - self._bedTemp = None - self._targetTemp = None - self._targetBedTemp = None - self._temps = { - "actual": [], - "target": [], - "actualBed": [], - "targetBed": [] - } - self._tempBacklog = [] + # state + self._temp = None + self._bedTemp = None + self._targetTemp = None + self._targetBedTemp = None + self._temps = { + "actual": [], + "target": [], + "actualBed": [], + "targetBed": [] + } + self._tempBacklog = [] - self._latestMessage = None - self._messages = [] - self._messageBacklog = [] + self._latestMessage = None + self._messages = [] + self._messageBacklog = [] - self._latestLog = None - self._log = [] - self._logBacklog = [] + self._latestLog = None + self._log = [] + self._logBacklog = [] - self._state = None + self._state = None - self._currentZ = None + self._currentZ = None - self.peakZ = -1 - self._progress = None - self._printTime = None - self._printTimeLeft = None + self._progress = None + self._printTime = None + self._printTimeLeft = None - # gcode handling - self._gcodeList = None - self._filename = None - self._gcodeLoader = None + # gcode handling + self._gcodeList = None + self._filename = None + self._gcodeLoader = None - # feedrate - self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"} + # feedrate + self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"} - # timelapse - self._timelapse = None + # timelapse + self._timelapse = None - # comm - self._comm = None + # comm + self._comm = None - # callbacks - self._callbacks = [] - self._lastProgressReport = None + # callbacks + self._callbacks = [] + self._lastProgressReport = None - self._stateMonitor = StateMonitor( - ratelimit=0.5, - updateCallback=self._sendCurrentDataCallbacks, - addTemperatureCallback=self._sendAddTemperatureCallbacks, - addLogCallback=self._sendAddLogCallbacks, - addMessageCallback=self._sendAddMessageCallbacks - ) - self._stateMonitor.reset( - state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()}, - jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None}, - gcodeData={"filename": None, "progress": None}, - progress={"progress": None, "printTime": None, "printTimeLeft": None}, - currentZ=None - ) + self._stateMonitor = StateMonitor( + ratelimit=0.5, + updateCallback=self._sendCurrentDataCallbacks, + addTemperatureCallback=self._sendAddTemperatureCallbacks, + addLogCallback=self._sendAddLogCallbacks, + addMessageCallback=self._sendAddMessageCallbacks + ) + self._stateMonitor.reset( + state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()}, + jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None}, + gcodeData={"filename": None, "progress": None}, + progress={"progress": None, "printTime": None, "printTimeLeft": None}, + currentZ=None + ) - self.sys_command= { "z_change": settings().get(["system_commands", "z_change"]), "cancelled" : settings().get(["system_commands", "cancelled"]), "print_done" :settings().get(["system_commands", "print_done"]), "print_started": settings().get(["system_commands", "print_started"])}; - + #~~ callback handling - #~~ callback handling + def registerCallback(self, callback): + self._callbacks.append(callback) + self._sendInitialStateUpdate(callback) - def registerCallback(self, callback): - self._callbacks.append(callback) - self._sendInitialStateUpdate(callback) + def unregisterCallback(self, callback): + if callback in self._callbacks: + self._callbacks.remove(callback) - def unregisterCallback(self, callback): - if callback in self._callbacks: - self._callbacks.remove(callback) + def _sendAddTemperatureCallbacks(self, data): + for callback in self._callbacks: + try: callback.addTemperature(data) + except: pass - def _sendAddTemperatureCallbacks(self, data): - for callback in self._callbacks: - try: callback.addTemperature(data) - except: pass + def _sendAddLogCallbacks(self, data): + for callback in self._callbacks: + try: callback.addLog(data) + except: pass - def _sendAddLogCallbacks(self, data): - for callback in self._callbacks: - try: callback.addLog(data) - except: pass + def _sendAddMessageCallbacks(self, data): + for callback in self._callbacks: + try: callback.addMessage(data) + except: pass - def _sendAddMessageCallbacks(self, data): - for callback in self._callbacks: - try: callback.addMessage(data) - except: pass - - def _sendCurrentDataCallbacks(self, data): - for callback in self._callbacks: - try: callback.sendCurrentData(copy.deepcopy(data)) - except: pass + def _sendCurrentDataCallbacks(self, data): + for callback in self._callbacks: + try: callback.sendCurrentData(copy.deepcopy(data)) + except: pass #~~ printer commands - def connect(self, port=None, baudrate=None): - """ - Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection - will be attempted. - """ - if self._comm is not None: - self._comm.close() - self._comm = comm.MachineCom(port, baudrate, callbackObject=self) - - def disconnect(self): - """ - Closes the connection to the printer. - """ - if self._comm is not None: - self._comm.close() - self._comm = None - - def command(self, command): - """ - Sends a single gcode command to the printer. - """ - self.commands([command]) - - def commands(self, commands): - """ - Sends multiple gcode commands (provided as a list) to the printer. - """ - for command in commands: - self._comm.sendCommand(command) - - def setFeedrateModifier(self, structure, percentage): - if (not self._feedrateModifierMapping.has_key(structure)) or percentage < 0: - return - - self._comm.setFeedrateModifier(self._feedrateModifierMapping[structure], percentage / 100.0) - - def loadGcode(self, file, printAfterLoading=False): - """ - Loads the gcode from the given file as the new print job. - Aborts if the printer is currently printing or another gcode file is currently being loaded. - """ - if (self._comm is not None and self._comm.isPrinting()) or (self._gcodeLoader is not None): - return - - self._setJobData(None, None) - - onGcodeLoadedCallback = self._onGcodeLoaded - if printAfterLoading: - onGcodeLoadedCallback = self._onGcodeLoadedToPrint - - self._gcodeLoader = GcodeLoader(file, self._onGcodeLoadingProgress, onGcodeLoadedCallback) - self._gcodeLoader.start() - - self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) - - def startPrint(self): - """ - Starts the currently loaded print job. - Only starts if the printer is connected and operational, not currently printing and a printjob is loaded - """ - if self._comm is None or not self._comm.isOperational(): - return - if self._gcodeList is None: - return - if self._comm.isPrinting(): - return - - self._setCurrentZ(-1) - - self.executeSystemCommand(self.sys_command['print_started']) - - self._comm.printGCode(self._gcodeList) - - def togglePausePrint(self): - """ - Pause the current printjob. - """ - if self._comm is None: - return - self._comm.setPause(not self._comm.isPaused()) - - def cancelPrint(self, disableMotorsAndHeater=True): - """ - Cancel the current printjob. - """ - if self._comm is None: - return - self._comm.cancelPrint() - if disableMotorsAndHeater: - self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan - - # reset line, height, print time - self._setCurrentZ(None) - self._setProgressData(None, None, None) - - # mark print as failure - self._gcodeManager.printFailed(self._filename) - self.executeSystemCommand(self.sys_command['cancelled']) - - #~~ state monitoring - - def setTimelapse(self, timelapse): - if self._timelapse is not None and self.isPrinting(): - self._timelapse.onPrintjobStopped() - del self._timelapse - self._timelapse = timelapse - - def getTimelapse(self): - return self._timelapse - - def _setCurrentZ(self, currentZ): - self._currentZ = currentZ - - formattedCurrentZ = None - if self._currentZ: - formattedCurrentZ = "%.2f mm" % (self._currentZ) - self._stateMonitor.setCurrentZ(formattedCurrentZ) - - def _setState(self, state): - self._state = state - self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) - - def _addLog(self, log): - self._log.append(log) - self._log = self._log[-300:] - self._stateMonitor.addLog(log) - - def _addMessage(self, message): - self._messages.append(message) - self._messages = self._messages[-300:] - self._stateMonitor.addMessage(message) - - def _setProgressData(self, progress, printTime, printTimeLeft): - self._progress = progress - self._printTime = printTime - self._printTimeLeft = printTimeLeft - - formattedPrintTime = None - if (self._printTime): - formattedPrintTime = util.getFormattedTimeDelta(datetime.timedelta(seconds=self._printTime)) - - formattedPrintTimeLeft = None - if (self._printTimeLeft): - formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft)) - - self._stateMonitor.setProgress({"progress": self._progress, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft}) - - def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp): - currentTimeUtc = int(time.time() * 1000) - - self._temps["actual"].append((currentTimeUtc, temp)) - self._temps["actual"] = self._temps["actual"][-300:] - - self._temps["target"].append((currentTimeUtc, targetTemp)) - self._temps["target"] = self._temps["target"][-300:] - - self._temps["actualBed"].append((currentTimeUtc, bedTemp)) - self._temps["actualBed"] = self._temps["actualBed"][-300:] - - self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp)) - self._temps["targetBed"] = self._temps["targetBed"][-300:] - - self._temp = temp - self._bedTemp = bedTemp - self._targetTemp = targetTemp - self._targetBedTemp = bedTargetTemp - - self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp}) - - def _setJobData(self, filename, gcodeList): - self._filename = filename - self._gcodeList = gcodeList - - lines = None - if self._gcodeList: - lines = len(self._gcodeList) - - formattedFilename = None - estimatedPrintTime = None - filament = None - if self._filename: - formattedFilename = os.path.basename(self._filename) - - fileData = self._gcodeManager.getFileData(filename) - if fileData is not None and "gcodeAnalysis" in fileData.keys(): - if "estimatedPrintTime" in fileData["gcodeAnalysis"].keys(): - estimatedPrintTime = fileData["gcodeAnalysis"]["estimatedPrintTime"] - if "filament" in fileData["gcodeAnalysis"].keys(): - filament = fileData["gcodeAnalysis"]["filament"] - - self._stateMonitor.setJobData({"filename": formattedFilename, "lines": lines, "estimatedPrintTime": estimatedPrintTime, "filament": filament}) - - def _sendInitialStateUpdate(self, callback): - try: - data = self._stateMonitor.getCurrentData() - data.update({ - "temperatureHistory": self._temps, - "logHistory": self._log, - "messageHistory": self._messages - }) - callback.sendHistoryData(data) - except Exception, err: - import sys - sys.stderr.write("ERROR: %s\n" % str(err)) - pass - - def _getStateFlags(self): - return { - "operational": self.isOperational(), - "printing": self.isPrinting(), - "closedOrError": self.isClosedOrError(), - "error": self.isError(), - "loading": self.isLoading(), - "paused": self.isPaused(), - "ready": self.isReady() - } - - #~~ callbacks triggered from self._comm - - def mcLog(self, message): - """ - Callback method for the comm object, called upon log output. - """ - self._addLog(message) - - def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp): - self._addTemperatureData(temp, bedTemp, targetTemp, bedTargetTemp) - - def mcStateChange(self, state): - """ - Callback method for the comm object, called if the connection state changes. - """ - oldState = self._state - - # forward relevant state changes to timelapse - if self._timelapse is not None: - if oldState == self._comm.STATE_PRINTING and state != self._comm.STATE_PAUSED: - self._timelapse.onPrintjobStopped() - elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED: - self._timelapse.onPrintjobStarted(self._filename) - - # forward relevant state changes to gcode manager - if self._comm is not None and oldState == self._comm.STATE_PRINTING: - if state == self._comm.STATE_OPERATIONAL: - self._gcodeManager.printSucceeded(self._filename) - #hrm....we seem to hit this state and THEN the next failed state on a cancel request? - # oh well, add a check to see if we're really done before sending the success event external command - if self._printTimeLeft < 1: - self.executeSystemCommand(self.sys_command['print_done']) - elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: - self._gcodeManager.printFailed(self._filename) - self.executeSystemCommand(self.sys_command['cancelled']) - self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing - elif self._comm is not None and state == self._comm.STATE_PRINTING: - self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use - - self._setState(state) - - - def mcMessage(self, message): - """ - Callback method for the comm object, called upon message exchanges via serial. - Stores the message in the message buffer, truncates buffer to the last 300 lines. - """ - self._addMessage(message) - - def mcProgress(self, lineNr): - """ - Callback method for the comm object, called upon any change in progress of the printjob. - Triggers storage of new values for printTime, printTimeLeft and the current line. - """ - oldProgress = self._progress - - if self._timelapse is not None: - try: self._timelapse.onPrintjobProgress(oldProgress, self._progress, int(round(self._progress * 100 / len(self._gcodeList)))) - except: pass - - self._setProgressData(self._comm.getPrintPos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate()) - - def mcZChange(self, newZ): - """ - Callback method for the comm object, called upon change of the z-layer. - """ - oldZ = self._currentZ - # only do this if we hit a new Z peak level. Some slicers do a Z-lift when retracting / moving without printing - # and some do ananti-backlash up-then-down movement when advancing layers - if newZ > self.peakZ: - self.peakZ = newZ - if self._timelapse is not None: - self._timelapse.onZChange(oldZ, newZ) - self.executeSystemCommand(self.sys_command['z_change']) - - self._setCurrentZ(newZ) - - #~~ callbacks triggered by gcodeLoader - - def _onGcodeLoadingProgress(self, filename, progress, mode): - formattedFilename = None - if filename is not None: - formattedFilename = os.path.basename(filename) - - self._stateMonitor.setGcodeData({"filename": formattedFilename, "progress": progress, "mode": mode}) - - def _onGcodeLoaded(self, filename, gcodeList): - self._setJobData(filename, gcodeList) - self._setCurrentZ(None) - self._setProgressData(None, None, None) - self._gcodeLoader = None - - self._stateMonitor.setGcodeData({"filename": None, "progress": None}) - self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) - - def _onGcodeLoadedToPrint(self, filename, gcodeList): - self._onGcodeLoaded(filename, gcodeList) - self.startPrint() - - #~~ state reports - - def feedrateState(self): - if self._comm is not None: - feedrateModifiers = self._comm.getFeedrateModifiers() - result = {} - for structure in self._feedrateModifierMapping.keys(): - if (feedrateModifiers.has_key(self._feedrateModifierMapping[structure])): - result[structure] = int(round(feedrateModifiers[self._feedrateModifierMapping[structure]] * 100)) - else: - result[structure] = 100 - return result - else: - return None - - def getStateString(self): - """ - Returns a human readable string corresponding to the current communication state. - """ - if self._comm is None: - return "Offline" - else: - return self._comm.getStateString() - - def isClosedOrError(self): - return self._comm is None or self._comm.isClosedOrError() - - def isOperational(self): - return self._comm is not None and self._comm.isOperational() - - def isPrinting(self): - return self._comm is not None and self._comm.isPrinting() - - def isPaused(self): - return self._comm is not None and self._comm.isPaused() - - def isError(self): - return self._comm is not None and self._comm.isError() - - def isReady(self): - return self._gcodeLoader is None and self._gcodeList and len(self._gcodeList) > 0 - - def isLoading(self): - return self._gcodeLoader is not None - - def executeSystemCommand(self,command_string): - if command_string is None: - return - logger = logging.getLogger(__name__) - try: - # handle a few regex substs for job data passed to external apps - cmd_string_with_params = command_string - cmd_string_with_params = re.sub("_ZHEIGHT_",str(self._currentZ), cmd_string_with_params) - cmd_string_with_params = re.sub("_FILE_",os.path.basename(self._filename), cmd_string_with_params) - # cut down to 2 decimal places, forcing through an int to avoid the 10.320000000001 floating point thing... - if self._gcodeList and self._progress: - prog = int(10000.0 * self._progress / len(self._gcodeList))/100.0 - else: - prog = 0.0 - cmd_string_with_params = re.sub("_PROGRESS_",str(prog), cmd_string_with_params) - cmd_string_with_params = re.sub("_LINE_",str(self._comm._gcodePos), cmd_string_with_params) - logger.info ("Executing system command: %s " % cmd_string_with_params) - #use Popen here since it won't wait for the shell to return...and we send some of these - # commands during a print job, we don't want to block! - subprocess.Popen(cmd_string_with_params,shell = True) - except subprocess.CalledProcessError, e: - logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message)) - except Exception, ex: - logger.exception("Command failed") - + def connect(self, port=None, baudrate=None): + """ + Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection + will be attempted. + """ + if self._comm is not None: + self._comm.close() + self._comm = comm.MachineCom(port, baudrate, callbackObject=self) + + def disconnect(self): + """ + Closes the connection to the printer. + """ + if self._comm is not None: + self._comm.close() + self._comm = None + + def command(self, command): + """ + Sends a single gcode command to the printer. + """ + self.commands([command]) + + def commands(self, commands): + """ + Sends multiple gcode commands (provided as a list) to the printer. + """ + for command in commands: + self._comm.sendCommand(command) + + def setFeedrateModifier(self, structure, percentage): + if (not self._feedrateModifierMapping.has_key(structure)) or percentage < 0: + return + + self._comm.setFeedrateModifier(self._feedrateModifierMapping[structure], percentage / 100.0) + + def loadGcode(self, file, printAfterLoading=False): + """ + Loads the gcode from the given file as the new print job. + Aborts if the printer is currently printing or another gcode file is currently being loaded. + """ + if (self._comm is not None and self._comm.isPrinting()) or (self._gcodeLoader is not None): + return + + self._setJobData(None, None) + + onGcodeLoadedCallback = self._onGcodeLoaded + if printAfterLoading: + onGcodeLoadedCallback = self._onGcodeLoadedToPrint + + self._gcodeLoader = GcodeLoader(file, self._onGcodeLoadingProgress, onGcodeLoadedCallback) + self._gcodeLoader.start() + + self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) + + def startPrint(self): + """ + Starts the currently loaded print job. + Only starts if the printer is connected and operational, not currently printing and a printjob is loaded + """ + if self._comm is None or not self._comm.isOperational(): + return + if self._gcodeList is None: + return + if self._comm.isPrinting(): + return + + self._setCurrentZ(-1) + self._comm.printGCode(self._gcodeList) + + def togglePausePrint(self): + """ + Pause the current printjob. + """ + if self._comm is None: + return + self._comm.setPause(not self._comm.isPaused()) + + def cancelPrint(self, disableMotorsAndHeater=True): + """ + Cancel the current printjob. + """ + if self._comm is None: + return + self._comm.cancelPrint() + if disableMotorsAndHeater: + self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan + + # reset line, height, print time + self._setCurrentZ(None) + self._setProgressData(None, None, None) + + # mark print as failure + self._gcodeManager.printFailed(self._filename) + + #~~ state monitoring + + def setTimelapse(self, timelapse): + if self._timelapse is not None and self.isPrinting(): + self._timelapse.onPrintjobStopped() + del self._timelapse + self._timelapse = timelapse + + def getTimelapse(self): + return self._timelapse + + def _setCurrentZ(self, currentZ): + self._currentZ = currentZ + + formattedCurrentZ = None + if self._currentZ: + formattedCurrentZ = "%.2f mm" % (self._currentZ) + self._stateMonitor.setCurrentZ(formattedCurrentZ) + + def _setState(self, state): + self._state = state + self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) + + def _addLog(self, log): + self._log.append(log) + self._log = self._log[-300:] + self._stateMonitor.addLog(log) + + def _addMessage(self, message): + self._messages.append(message) + self._messages = self._messages[-300:] + self._stateMonitor.addMessage(message) + + def _setProgressData(self, progress, printTime, printTimeLeft): + self._progress = progress + self._printTime = printTime + self._printTimeLeft = printTimeLeft + + formattedPrintTime = None + if (self._printTime): + formattedPrintTime = util.getFormattedTimeDelta(datetime.timedelta(seconds=self._printTime)) + + formattedPrintTimeLeft = None + if (self._printTimeLeft): + formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft)) + + self._stateMonitor.setProgress({"progress": self._progress, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft}) + + def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp): + currentTimeUtc = int(time.time() * 1000) + + self._temps["actual"].append((currentTimeUtc, temp)) + self._temps["actual"] = self._temps["actual"][-300:] + + self._temps["target"].append((currentTimeUtc, targetTemp)) + self._temps["target"] = self._temps["target"][-300:] + + self._temps["actualBed"].append((currentTimeUtc, bedTemp)) + self._temps["actualBed"] = self._temps["actualBed"][-300:] + + self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp)) + self._temps["targetBed"] = self._temps["targetBed"][-300:] + + self._temp = temp + self._bedTemp = bedTemp + self._targetTemp = targetTemp + self._targetBedTemp = bedTargetTemp + + self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp}) + + def _setJobData(self, filename, gcodeList): + self._filename = filename + self._gcodeList = gcodeList + + lines = None + if self._gcodeList: + lines = len(self._gcodeList) + + formattedFilename = None + estimatedPrintTime = None + filament = None + if self._filename: + formattedFilename = os.path.basename(self._filename) + + fileData = self._gcodeManager.getFileData(filename) + if fileData is not None and "gcodeAnalysis" in fileData.keys(): + if "estimatedPrintTime" in fileData["gcodeAnalysis"].keys(): + estimatedPrintTime = fileData["gcodeAnalysis"]["estimatedPrintTime"] + if "filament" in fileData["gcodeAnalysis"].keys(): + filament = fileData["gcodeAnalysis"]["filament"] + + self._stateMonitor.setJobData({"filename": formattedFilename, "lines": lines, "estimatedPrintTime": estimatedPrintTime, "filament": filament}) + + def _sendInitialStateUpdate(self, callback): + try: + data = self._stateMonitor.getCurrentData() + data.update({ + "temperatureHistory": self._temps, + "logHistory": self._log, + "messageHistory": self._messages + }) + callback.sendHistoryData(data) + except Exception, err: + import sys + sys.stderr.write("ERROR: %s\n" % str(err)) + pass + + def _getStateFlags(self): + return { + "operational": self.isOperational(), + "printing": self.isPrinting(), + "closedOrError": self.isClosedOrError(), + "error": self.isError(), + "loading": self.isLoading(), + "paused": self.isPaused(), + "ready": self.isReady() + } + + #~~ callbacks triggered from self._comm + + def mcLog(self, message): + """ + Callback method for the comm object, called upon log output. + """ + self._addLog(message) + + def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp): + self._addTemperatureData(temp, bedTemp, targetTemp, bedTargetTemp) + + def mcStateChange(self, state): + """ + Callback method for the comm object, called if the connection state changes. + """ + oldState = self._state + + # forward relevant state changes to timelapse + if self._timelapse is not None: + if oldState == self._comm.STATE_PRINTING and state != self._comm.STATE_PAUSED: + self._timelapse.onPrintjobStopped() + elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED: + self._timelapse.onPrintjobStarted(self._filename) + + # forward relevant state changes to gcode manager + if self._comm is not None and oldState == self._comm.STATE_PRINTING: + if state == self._comm.STATE_OPERATIONAL: + self._gcodeManager.printSucceeded(self._filename) + elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: + self._gcodeManager.printFailed(self._filename) + self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing + elif self._comm is not None and state == self._comm.STATE_PRINTING: + self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use + + self._setState(state) + + + def mcMessage(self, message): + """ + Callback method for the comm object, called upon message exchanges via serial. + Stores the message in the message buffer, truncates buffer to the last 300 lines. + """ + self._addMessage(message) + + def mcProgress(self, lineNr): + """ + Callback method for the comm object, called upon any change in progress of the printjob. + Triggers storage of new values for printTime, printTimeLeft and the current line. + """ + oldProgress = self._progress + + if self._timelapse is not None: + try: self._timelapse.onPrintjobProgress(oldProgress, self._progress, int(round(self._progress * 100 / len(self._gcodeList)))) + except: pass + + self._setProgressData(self._comm.getPrintPos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate()) + + def mcZChange(self, newZ): + """ + Callback method for the comm object, called upon change of the z-layer. + """ + oldZ = self._currentZ + if self._timelapse is not None: + self._timelapse.onZChange(oldZ, newZ) + + self._setCurrentZ(newZ) + + #~~ callbacks triggered by gcodeLoader + + def _onGcodeLoadingProgress(self, filename, progress, mode): + formattedFilename = None + if filename is not None: + formattedFilename = os.path.basename(filename) + + self._stateMonitor.setGcodeData({"filename": formattedFilename, "progress": progress, "mode": mode}) + + def _onGcodeLoaded(self, filename, gcodeList): + self._setJobData(filename, gcodeList) + self._setCurrentZ(None) + self._setProgressData(None, None, None) + self._gcodeLoader = None + + self._stateMonitor.setGcodeData({"filename": None, "progress": None}) + self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) + + def _onGcodeLoadedToPrint(self, filename, gcodeList): + self._onGcodeLoaded(filename, gcodeList) + self.startPrint() + + #~~ state reports + + def feedrateState(self): + if self._comm is not None: + feedrateModifiers = self._comm.getFeedrateModifiers() + result = {} + for structure in self._feedrateModifierMapping.keys(): + if (feedrateModifiers.has_key(self._feedrateModifierMapping[structure])): + result[structure] = int(round(feedrateModifiers[self._feedrateModifierMapping[structure]] * 100)) + else: + result[structure] = 100 + return result + else: + return None + + def getStateString(self): + """ + Returns a human readable string corresponding to the current communication state. + """ + if self._comm is None: + return "Offline" + else: + return self._comm.getStateString() + + def isClosedOrError(self): + return self._comm is None or self._comm.isClosedOrError() + + def isOperational(self): + return self._comm is not None and self._comm.isOperational() + + def isPrinting(self): + return self._comm is not None and self._comm.isPrinting() + + def isPaused(self): + return self._comm is not None and self._comm.isPaused() + + def isError(self): + return self._comm is not None and self._comm.isError() + + def isReady(self): + return self._gcodeLoader is None and self._gcodeList and len(self._gcodeList) > 0 + + def isLoading(self): + return self._gcodeLoader is not None class GcodeLoader(threading.Thread): - """ - The GcodeLoader takes care of loading a gcode-File from disk and parsing it into a gcode object in a separate - thread while constantly notifying interested listeners about the current progress. - The progress is returned as a float value between 0 and 1 which is to be interpreted as the percentage of completion. - """ + """ + The GcodeLoader takes care of loading a gcode-File from disk and parsing it into a gcode object in a separate + thread while constantly notifying interested listeners about the current progress. + The progress is returned as a float value between 0 and 1 which is to be interpreted as the percentage of completion. + """ - def __init__(self, filename, progressCallback, loadedCallback): - threading.Thread.__init__(self) + def __init__(self, filename, progressCallback, loadedCallback): + threading.Thread.__init__(self) - self._progressCallback = progressCallback - self._loadedCallback = loadedCallback + self._progressCallback = progressCallback + self._loadedCallback = loadedCallback - self._filename = filename - self._gcodeList = None + self._filename = filename + self._gcodeList = None - def run(self): - #Send an initial M110 to reset the line counter to zero. - prevLineType = lineType = "CUSTOM" - gcodeList = ["M110"] - filesize = os.stat(self._filename).st_size - with open(self._filename, "r") as file: - for line in file: - if line.startswith(";TYPE:"): - lineType = line[6:].strip() - if ";" in line: - line = line[0:line.find(";")] - line = line.strip() - if len(line) > 0: - if prevLineType != lineType: - gcodeList.append((line, lineType, )) - else: - gcodeList.append(line) - prevLineType = lineType - self._onLoadingProgress(float(file.tell()) / float(filesize)) + def run(self): + #Send an initial M110 to reset the line counter to zero. + prevLineType = lineType = "CUSTOM" + gcodeList = ["M110"] + filesize = os.stat(self._filename).st_size + with open(self._filename, "r") as file: + for line in file: + if line.startswith(";TYPE:"): + lineType = line[6:].strip() + if ";" in line: + line = line[0:line.find(";")] + line = line.strip() + if len(line) > 0: + if prevLineType != lineType: + gcodeList.append((line, lineType, )) + else: + gcodeList.append(line) + prevLineType = lineType + self._onLoadingProgress(float(file.tell()) / float(filesize)) - self._gcodeList = gcodeList - self._loadedCallback(self._filename, self._gcodeList) + self._gcodeList = gcodeList + self._loadedCallback(self._filename, self._gcodeList) - def _onLoadingProgress(self, progress): - self._progressCallback(self._filename, progress, "loading") + def _onLoadingProgress(self, progress): + self._progressCallback(self._filename, progress, "loading") - def _onParsingProgress(self, progress): - self._progressCallback(self._filename, progress, "parsing") + def _onParsingProgress(self, progress): + self._progressCallback(self._filename, progress, "parsing") class StateMonitor(object): - def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback): - self._ratelimit = ratelimit - self._updateCallback = updateCallback - self._addTemperatureCallback = addTemperatureCallback - self._addLogCallback = addLogCallback - self._addMessageCallback = addMessageCallback + def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback): + self._ratelimit = ratelimit + self._updateCallback = updateCallback + self._addTemperatureCallback = addTemperatureCallback + self._addLogCallback = addLogCallback + self._addMessageCallback = addMessageCallback - self._state = None - self._jobData = None - self._gcodeData = None - self._currentZ = None - self._peakZ = -1 - self._progress = None + self._state = None + self._jobData = None + self._gcodeData = None + self._currentZ = None + self._progress = None - self._changeEvent = threading.Event() + self._changeEvent = threading.Event() - self._lastUpdate = time.time() - self._worker = threading.Thread(target=self._work) - self._worker.daemon = True - self._worker.start() + self._lastUpdate = time.time() + self._worker = threading.Thread(target=self._work) + self._worker.daemon = True + self._worker.start() - def reset(self, state=None, jobData=None, gcodeData=None, progress=None, currentZ=None): - self.setState(state) - self.setJobData(jobData) - self.setGcodeData(gcodeData) - self.setProgress(progress) - self.setCurrentZ(currentZ) + def reset(self, state=None, jobData=None, gcodeData=None, progress=None, currentZ=None): + self.setState(state) + self.setJobData(jobData) + self.setGcodeData(gcodeData) + self.setProgress(progress) + self.setCurrentZ(currentZ) - def addTemperature(self, temperature): - self._addTemperatureCallback(temperature) - self._changeEvent.set() + def addTemperature(self, temperature): + self._addTemperatureCallback(temperature) + self._changeEvent.set() - def addLog(self, log): - self._addLogCallback(log) - self._changeEvent.set() + def addLog(self, log): + self._addLogCallback(log) + self._changeEvent.set() - def addMessage(self, message): - self._addMessageCallback(message) - self._changeEvent.set() + def addMessage(self, message): + self._addMessageCallback(message) + self._changeEvent.set() - def setCurrentZ(self, currentZ): - self._currentZ = currentZ - self._changeEvent.set() + def setCurrentZ(self, currentZ): + self._currentZ = currentZ + self._changeEvent.set() - def setState(self, state): - self._state = state - self._changeEvent.set() + def setState(self, state): + self._state = state + self._changeEvent.set() - def setJobData(self, jobData): - self._jobData = jobData - self._changeEvent.set() + def setJobData(self, jobData): + self._jobData = jobData + self._changeEvent.set() - def setGcodeData(self, gcodeData): - self._gcodeData = gcodeData - self._changeEvent.set() + def setGcodeData(self, gcodeData): + self._gcodeData = gcodeData + self._changeEvent.set() - def setProgress(self, progress): - self._progress = progress - self._changeEvent.set() + def setProgress(self, progress): + self._progress = progress + self._changeEvent.set() - def _work(self): - while True: - self._changeEvent.wait() + def _work(self): + while True: + self._changeEvent.wait() - now = time.time() - delta = now - self._lastUpdate - additionalWaitTime = self._ratelimit - delta - if additionalWaitTime > 0: - time.sleep(additionalWaitTime) + now = time.time() + delta = now - self._lastUpdate + additionalWaitTime = self._ratelimit - delta + if additionalWaitTime > 0: + time.sleep(additionalWaitTime) - data = self.getCurrentData() - self._updateCallback(data) - self._lastUpdate = time.time() - self._changeEvent.clear() + data = self.getCurrentData() + self._updateCallback(data) + self._lastUpdate = time.time() + self._changeEvent.clear() - def getCurrentData(self): - return { - "state": self._state, - "job": self._jobData, - "gcode": self._gcodeData, - "currentZ": self._currentZ, - "progress": self._progress - } + def getCurrentData(self): + return { + "state": self._state, + "job": self._jobData, + "gcode": self._gcodeData, + "currentZ": self._currentZ, + "progress": self._progress + } diff --git a/octoprint/server.py b/octoprint/server.py index feaae8e..aab4393 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -375,15 +375,7 @@ def getSettings(): }, "system": { "actions": s.get(["system", "actions"]) - }, - "system_commands": { - "print_done": s.get(["system_commands", "print_done"]), - "cancelled": s.get(["system_commands", "cancelled"]), - "print_started": s.get(["system_commands", "print_started"]), - "z_change": s.get(["system_commands", "z_change"]) - } - }) @app.route(BASEURL + "settings", methods=["POST"]) @@ -425,12 +417,6 @@ def setSettings(): if "system" in data.keys(): if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"]) - if "system_commands" in data.keys(): - if "z_change" in data["system_commands"].keys(): s.set(["system_commands", "z_change"], data["system_commands"]["z_change"]) - if "print_started" in data["system_commands"].keys(): s.set(["system_commands", "print_started"], data["system_commands"]["print_started"]) - if "cancelled" in data["system_commands"].keys(): s.set(["system_commands", "cancelled"], data["system_commands"]["cancelled"]) - if "print_done" in data["system_commands"].keys(): s.set(["system_commands", "print_done"], data["system_commands"]["print_done"]) - s.save() return getSettings() diff --git a/octoprint/settings.py b/octoprint/settings.py index b655f49..0c7dfad 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -69,14 +69,8 @@ default_settings = { "controls": [], "system": { "actions": [] - }, - "system_commands": { - "z_change":None, - "print_started":None, - "cancelled":None, - "print_done":None - } -} + } +} valid_boolean_trues = ["true", "yes", "y", "1"] From c6363ea0461e2390ab16ffab22b1a69247662840 Mon Sep 17 00:00:00 2001 From: Lars Norpchen Date: Thu, 9 May 2013 14:31:54 -0700 Subject: [PATCH 3/7] External commands on events These changes address issues 87 and 22 by adding the ability to trigger external commands on print start, done, cancel and z-height change. --- README.md | 13 ++++++++++ octoprint/printer.py | 56 +++++++++++++++++++++++++++++++++++++++---- octoprint/server.py | 14 +++++++++++ octoprint/settings.py | 10 ++++++-- 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7dd80ab..b6d6535 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +Added support for system commands at print start, print end, print cancelled and z-height change. Add the commands to run to the config.yaml file: + +system_commands: + cancelled: echo cancelled _FILE_ at _PROGRESS_ percent done. + print_done: growlnotify "done with _FILE_" + print_started: echo starting _FILE_ + z_change: echo _LINE_ _PROGRESS_ _ZHEIGHT_ + +These commands take the tokens take _FILE_, _PERCENT_, _LINES_ and _ZHEIGHT_ which will be passed to external commands. + + + + OctoPrint ========= diff --git a/octoprint/printer.py b/octoprint/printer.py index be080b2..afd5d36 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -7,6 +7,9 @@ import datetime import threading import copy import os +import subprocess +import re +import logging, logging.config import octoprint.util.comm as comm import octoprint.util as util @@ -53,6 +56,7 @@ class Printer(): self._currentZ = None + self.peakZ = -1 self._progress = None self._printTime = None self._printTimeLeft = None @@ -90,6 +94,9 @@ class Printer(): currentZ=None ) + self.sys_command= { "z_change": settings().get(["system_commands", "z_change"]), "cancelled" : settings().get(["system_commands", "cancelled"]), "print_done" :settings().get(["system_commands", "print_done"]), "print_started": settings().get(["system_commands", "print_started"])}; + + #~~ callback handling def registerCallback(self, callback): @@ -190,6 +197,9 @@ class Printer(): return self._setCurrentZ(-1) + + self.executeSystemCommand(self.sys_command['print_started']) + self._comm.printGCode(self._gcodeList) def togglePausePrint(self): @@ -216,7 +226,8 @@ class Printer(): # mark print as failure self._gcodeManager.printFailed(self._filename) - + self.executeSystemCommand(self.sys_command['cancelled']) + #~~ state monitoring def setTimelapse(self, timelapse): @@ -363,8 +374,13 @@ class Printer(): if self._comm is not None and oldState == self._comm.STATE_PRINTING: if state == self._comm.STATE_OPERATIONAL: self._gcodeManager.printSucceeded(self._filename) + #hrm....we seem to hit this state and THEN the next failed state on a cancel request? + # oh well, add a check to see if we're really done before sending the success event external command + if self._printTimeLeft < 1: + self.executeSystemCommand(self.sys_command['print_done']) elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: self._gcodeManager.printFailed(self._filename) + self.executeSystemCommand(self.sys_command['cancelled']) self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing elif self._comm is not None and state == self._comm.STATE_PRINTING: self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use @@ -397,9 +413,14 @@ class Printer(): Callback method for the comm object, called upon change of the z-layer. """ oldZ = self._currentZ - if self._timelapse is not None: - self._timelapse.onZChange(oldZ, newZ) - + # only do this if we hit a new Z peak level. Some slicers do a Z-lift when retracting / moving without printing + # and some do ananti-backlash up-then-down movement when advancing layers + if newZ > self.peakZ: + self.peakZ = newZ + if self._timelapse is not None: + self._timelapse.onZChange(oldZ, newZ) + self.executeSystemCommand(self.sys_command['z_change']) + self._setCurrentZ(newZ) #~~ callbacks triggered by gcodeLoader @@ -468,6 +489,32 @@ class Printer(): def isLoading(self): return self._gcodeLoader is not None + + def executeSystemCommand(self,command_string): + if command_string is None: + return + logger = logging.getLogger(__name__) + try: + # handle a few regex substs for job data passed to external apps + cmd_string_with_params = command_string + cmd_string_with_params = re.sub("_ZHEIGHT_",str(self._currentZ), cmd_string_with_params) + cmd_string_with_params = re.sub("_FILE_",os.path.basename(self._filename), cmd_string_with_params) + # cut down to 2 decimal places, forcing through an int to avoid the 10.320000000001 floating point thing... + if self._gcodeList and self._progress: + prog = int(10000.0 * self._progress / len(self._gcodeList))/100.0 + else: + prog = 0.0 + cmd_string_with_params = re.sub("_PROGRESS_",str(prog), cmd_string_with_params) + cmd_string_with_params = re.sub("_LINE_",str(self._comm._gcodePos), cmd_string_with_params) + logger.info ("Executing system command: %s " % cmd_string_with_params) + #use Popen here since it won't wait for the shell to return...and we send some of these + # commands during a print job, we don't want to block! + subprocess.Popen(cmd_string_with_params,shell = True) + except subprocess.CalledProcessError, e: + logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message)) + except Exception, ex: + logger.exception("Command failed") + class GcodeLoader(threading.Thread): """ @@ -526,6 +573,7 @@ class StateMonitor(object): self._jobData = None self._gcodeData = None self._currentZ = None + self._peakZ = -1 self._progress = None self._changeEvent = threading.Event() diff --git a/octoprint/server.py b/octoprint/server.py index aab4393..feaae8e 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -375,7 +375,15 @@ def getSettings(): }, "system": { "actions": s.get(["system", "actions"]) + }, + "system_commands": { + "print_done": s.get(["system_commands", "print_done"]), + "cancelled": s.get(["system_commands", "cancelled"]), + "print_started": s.get(["system_commands", "print_started"]), + "z_change": s.get(["system_commands", "z_change"]) + } + }) @app.route(BASEURL + "settings", methods=["POST"]) @@ -417,6 +425,12 @@ def setSettings(): if "system" in data.keys(): if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"]) + if "system_commands" in data.keys(): + if "z_change" in data["system_commands"].keys(): s.set(["system_commands", "z_change"], data["system_commands"]["z_change"]) + if "print_started" in data["system_commands"].keys(): s.set(["system_commands", "print_started"], data["system_commands"]["print_started"]) + if "cancelled" in data["system_commands"].keys(): s.set(["system_commands", "cancelled"], data["system_commands"]["cancelled"]) + if "print_done" in data["system_commands"].keys(): s.set(["system_commands", "print_done"], data["system_commands"]["print_done"]) + s.save() return getSettings() diff --git a/octoprint/settings.py b/octoprint/settings.py index 0c7dfad..b655f49 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -69,8 +69,14 @@ default_settings = { "controls": [], "system": { "actions": [] - } -} + }, + "system_commands": { + "z_change":None, + "print_started":None, + "cancelled":None, + "print_done":None + } +} valid_boolean_trues = ["true", "yes", "y", "1"] From 905c3dbea681b29b8dea90b0bb988fb857e7cde5 Mon Sep 17 00:00:00 2001 From: Lars Norpchen Date: Thu, 9 May 2013 15:38:03 -0600 Subject: [PATCH 4/7] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b6d6535..4ac8159 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ Added support for system commands at print start, print end, print cancelled and z-height change. Add the commands to run to the config.yaml file: -system_commands: - cancelled: echo cancelled _FILE_ at _PROGRESS_ percent done. - print_done: growlnotify "done with _FILE_" - print_started: echo starting _FILE_ - z_change: echo _LINE_ _PROGRESS_ _ZHEIGHT_ + system_commands: + cancelled: echo cancelled _FILE_ at _PROGRESS_ percent done. + print_done: growlnotify "done with _FILE_" + print_started: echo starting _FILE_ + z_change: echo _LINE_ _PROGRESS_ _ZHEIGHT_ These commands take the tokens take _FILE_, _PERCENT_, _LINES_ and _ZHEIGHT_ which will be passed to external commands. From 7b214d3f1684caa79344da58e648a492f85e2546 Mon Sep 17 00:00:00 2001 From: Lars Norpchen Date: Thu, 9 May 2013 15:39:57 -0600 Subject: [PATCH 5/7] Update README.md --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ac8159..5876963 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ -Added support for system commands at print start, print end, print cancelled and z-height change. Add the commands to run to the config.yaml file: +Added support for system commands at print start, print end, print cancelled and z-height change. Add the commands as strings as a new section to the config.yaml file: + appearance {} + feature: {} + serial: + baudrate: 115200 + port: /dev/ttyACM0 + system: {} + temperature {} system_commands: cancelled: echo cancelled _FILE_ at _PROGRESS_ percent done. print_done: growlnotify "done with _FILE_" From 525d73a20b49fd4f95c3de95727a3f7ae189c3bc Mon Sep 17 00:00:00 2001 From: Lars Norpchen Date: Mon, 20 May 2013 20:04:21 -0700 Subject: [PATCH 6/7] Event Manager Added event manager to trigger system and gcode commands. --- octoprint/events.py | 150 +++++++++++++++++++++++++++++++++++++++++ octoprint/events.txt | 63 +++++++++++++++++ octoprint/printer.py | 61 ++++++----------- octoprint/server.py | 63 ++++++++++------- octoprint/settings.py | 12 ++-- octoprint/timelapse.py | 17 +++-- octoprint/util/comm.py | 61 +++++++++++++++-- 7 files changed, 343 insertions(+), 84 deletions(-) create mode 100644 octoprint/events.py create mode 100644 octoprint/events.txt diff --git a/octoprint/events.py b/octoprint/events.py new file mode 100644 index 0000000..01a07a9 --- /dev/null +++ b/octoprint/events.py @@ -0,0 +1,150 @@ +import sys +import datetime +import time +import math +import re +import logging, logging.config +import subprocess +import octoprint.printer as printer +import os + +# right now we're logging a lot of extra information for testing +# we might want to comment out some of the logging eventually + +class event_record(object): + what = None + who = None + action = None + + + # object that handles receiving events and dispatching them to listeners + # +class EventManager(object): + def __init__(self): + self.registered_events = [] + self.logger = logging.getLogger(__name__) + + # Fire an event to anyone listening + # any object can generate an event and any object can listen + # pass in the event_name as a string (arbitrary, but case sensitive) + # and any extra data that may pertain to the event + def FireEvent (self,event_name,extra_data=None): + self.logger.info ( "Firing event: " + event_name + " (" +str (extra_data)+")") + for ev in self.registered_events: + if event_name == ev.what: + self.logger.info ( "Sending action to " + str(ev.who)) + if ev.action != None : + ev.action (event_name,extra_data) +# else: +# self.logger.info ( "events don't match " + str(ev.what)+ " and " + event_name) + + + # register a listener to an event -- pass in + # the event name (as a string), the target object + # and the function to call + def Register (self,event_name, target, action): + new_ev =event_record() + new_ev.what = event_name + new_ev.who = target + new_ev.action= action + self.registered_events=self.registered_events+[new_ev] + self.logger.info ("Registered event '"+new_ev.what+"' to invoke '"+str(new_ev.action)+"' on "+str(new_ev.who) ) + + + def unRegister (self,event_name, target, action): + self.registered_events[:] = [e for e in self.registered_events if event_name != e.what or e.action != action or e.who!=target] + + + #sample event receiver + # def event_rec(self,event_name,extra_data): + # print str(self) + " Receieved event ", event_name ," (", str (extra_data),")" + + # and registering it: + # eventManager.Register("Startup",self,self.event_rec) + + +class event_dispatch(object): + type = None + event_string = None + command_data = None + +# object that hooks the event manager to system events, gcode, etc. +# creates listeners to any events defined in the config.yaml settings +class EventResponse(object): + + def __init__(self, eventManager,printer): + self.registered_responses= [] + self._eventManager = eventManager + self._printer = printer + self.logger = logging.getLogger(__name__) + self._event_data = "" + + def setupEvents(self,s): + availableEvents = s.get(["system", "events"]) + for ev in availableEvents: + event = event_dispatch() + event.type = ev["type"].strip() + event.event_string = ev["event"].strip() + event.command_data = ev["command"] + self._eventManager.Register ( event.event_string ,self,self.eventRec) + self.registered_responses = self.registered_responses+[event] + self.logger.info ("Registered "+event.type +" event '"+event.event_string+"' to execute '"+event.command_data+"'" ) + self.logger.info ( "Registered "+ str(len(self.registered_responses))+" events") + + def eventRec (self,event_name, event_data): + self.logger.info ( "Receieved event: " + event_name + " (" + str(event_data) + ")") + self._event_data = event_data + for ev in self.registered_responses: + if ev.event_string == event_name: + if ev.type == "system": + self.executeSystemCommand (ev.command_data) + if ev.type == "gcode": + self.executeGCode(ev.command_data) + + # handle a few regex substs for job data passed to external apps + def doStringProcessing (self, command_string): + cmd_string_with_params = command_string + cmd_string_with_params = re.sub("_ZHEIGHT_",str(self._printer._currentZ), cmd_string_with_params) + if self._printer._filename: + cmd_string_with_params = re.sub("_FILE_",os.path.basename(self._printer._filename), cmd_string_with_params) + else: + cmd_string_with_params = re.sub("_FILE_","NO FILE", cmd_string_with_params) + # cut down to 2 decimal places, forcing through an int to avoid the 10.320000000001 floating point thing... + if self._printer._gcodeList and self._printer._progress: + prog = int(10000.0 * self._printer._progress / len(self._printer._gcodeList))/100.0 + else: + prog = 0.0 + cmd_string_with_params = re.sub("_PROGRESS_",str(prog), cmd_string_with_params) + if self._printer._comm: + cmd_string_with_params = re.sub("_LINE_",str(self._printer._comm._gcodePos), cmd_string_with_params) + else: + cmd_string_with_params = re.sub("_LINE_","0", cmd_string_with_params) + if self._event_data: + cmd_string_with_params = re.sub("_DATA_",str(self._event_data), cmd_string_with_params) + else: + cmd_string_with_params = re.sub("_DATA_","", cmd_string_with_params) + cmd_string_with_params = re.sub("_NOW_",str(datetime.datetime.now()), cmd_string_with_params) + return cmd_string_with_params + + + def executeGCode(self,command_string): + command_string = self.doStringProcessing(command_string) + self.logger.info ("GCode command: " + command_string) + self._printer.commands(command_string.split(',')) + + def executeSystemCommand(self,command_string): + if command_string is None: + return + try: + command_string = self.doStringProcessing(command_string) + self.logger.info ("Executing system command: "+ command_string) + #use Popen here since it won't wait for the shell to return...and we send some of these + # commands during a print job, we don't want to block! + subprocess.Popen(command_string,shell = True) + except subprocess.CalledProcessError, e: + self.logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message)) + except Exception, ex: + self.logger.exception("Command failed") + + + \ No newline at end of file diff --git a/octoprint/events.txt b/octoprint/events.txt new file mode 100644 index 0000000..d6801e2 --- /dev/null +++ b/octoprint/events.txt @@ -0,0 +1,63 @@ +Add to the "system:" section of config.yaml + +There are two types of event handlers at the moment: + system: invokes an external command without waiting for the result + gcode: sends some gcode to the printer. Separate multiple commands with a comma + +Example: + system: + events: + - event: Connected + type: gcode + command: M115,M17 printer connected!,G28 + - event: Disconnected + type: system + command: python ~/growl.py -t mygrowlserver -d "Lost connection to printer" -a OctoPrint -i http://rasppi:8080/Octoprint_logo.png + - event: PrintStarted + type: system + command: python ~/growl.py -t mygrowlserver -d "Starting _FILE_" -a OctoPrint -i http://rasppi:8080/Octoprint_logo.png + - event: PrintDone + type: system + command: python ~/growl.py -t mygrowlserver -d "Completed _FILE_" -a OctoPrint -i http://rasppi:8080/Octoprint_logo.png + +command values support the following dynamic tokens: + _DATA_ - the data associated with the event (not all events have data, when they do, it's often a filename) + _FILE_ - filename of the current print (not always the same as _DATA_ filename) + _LINE_ - the current GCode line + _PROGRESS_ - the percent complete + _ZHEIGHT_ - the current Z position of the head + _NOW_ - the date and time of the event + + +Available Events: + + Startup -- the server has started + Connected -- the server has connected to the printer (data is port and baudrate) + Disconnected -- the server has disconnected from the printer + ClientOpen -- a client has connected to the web server + ClientClosed -- a client has disconnected from the web server + PowerOn -- the GCode has turned on the printer power via M80 + PowerOff -- the GCode has turned on the printer power via M81 + Upload -- a gcode file upload has been uploaded (data is filename) + LoadStart -- a gcode file load has started (data is filename) + LoadDone -- a gcode file load has finished (data is filename) + PrintStarted -- a print has started (data is filename) + PrintFailed -- a print failed (data is filename) + PrintDone -- a print completed successfully (data is filename) + Cancelled -- the print has been cancelled via the cancel button (data is filename) + Home -- the head has gone home via G28 + ZChange -- the printer's Z-Height has changed (new layer) + Paused -- the print has been paused + Waiting -- the print is paused due to a gcode wait command + Cooling -- the GCode has enabled the platform cooler via M245 + Alert -- the GCode has issued a user alert (beep) via M300 + Conveyor -- the GCode has enabled the conveyor belt via M240 + Eject -- the GCode has enabled the part ejector via M40 + CaptureStart -- a timelapse image is starting to be captured (data is image filename) + CaptureDone -- a timelapse image has completed being captured (data is image filename) + MovieDone -- the timelapse movie is completed (data is movie filename) + EStop -- the GCode has issued a panic stop via M112 + Error -- an error has occurred (data is error string) + + + \ No newline at end of file diff --git a/octoprint/printer.py b/octoprint/printer.py index afd5d36..53ae43e 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -7,9 +7,8 @@ import datetime import threading import copy import os -import subprocess -import re -import logging, logging.config + +#import logging, logging.config import octoprint.util.comm as comm import octoprint.util as util @@ -28,8 +27,9 @@ def getConnectionOptions(): } class Printer(): - def __init__(self, gcodeManager): + def __init__(self, gcodeManager,eventManager): self._gcodeManager = gcodeManager + self._eventManager = eventManager # state self._temp = None @@ -94,9 +94,6 @@ class Printer(): currentZ=None ) - self.sys_command= { "z_change": settings().get(["system_commands", "z_change"]), "cancelled" : settings().get(["system_commands", "cancelled"]), "print_done" :settings().get(["system_commands", "print_done"]), "print_started": settings().get(["system_commands", "print_started"])}; - - #~~ callback handling def registerCallback(self, callback): @@ -137,6 +134,7 @@ class Printer(): if self._comm is not None: self._comm.close() self._comm = comm.MachineCom(port, baudrate, callbackObject=self) + self._comm.setEventManager(self._eventManager) def disconnect(self): """ @@ -198,7 +196,7 @@ class Printer(): self._setCurrentZ(-1) - self.executeSystemCommand(self.sys_command['print_started']) + self._eventManager.FireEvent ('PrintStarted',filename) self._comm.printGCode(self._gcodeList) @@ -225,9 +223,9 @@ class Printer(): self._setProgressData(None, None, None) # mark print as failure - self._gcodeManager.printFailed(self._filename) - self.executeSystemCommand(self.sys_command['cancelled']) - + if self._filename: + self._gcodeManager.printFailed(self._filename) + #~~ state monitoring def setTimelapse(self, timelapse): @@ -370,6 +368,13 @@ class Printer(): elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED: self._timelapse.onPrintjobStarted(self._filename) + if state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED: + self._eventManager.FireEvent ('PrintStarted',filename) + if state == self._comm.STATE_OPERATIONAL and (oldState <= self._comm.STATE_CONNECTING or oldState >=self._comm.STATE_CLOSED): + self._eventManager.FireEvent ('Connected',self._comm._port+" at " +self._comm._baudrate) + if state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: + self._eventManager.FireEvent ('Error',self._comm.getErrorString()) + # forward relevant state changes to gcode manager if self._comm is not None and oldState == self._comm.STATE_PRINTING: if state == self._comm.STATE_OPERATIONAL: @@ -377,10 +382,10 @@ class Printer(): #hrm....we seem to hit this state and THEN the next failed state on a cancel request? # oh well, add a check to see if we're really done before sending the success event external command if self._printTimeLeft < 1: - self.executeSystemCommand(self.sys_command['print_done']) + self._eventManager.FireEvent ('PrintDone',filename) elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: self._gcodeManager.printFailed(self._filename) - self.executeSystemCommand(self.sys_command['cancelled']) + self._eventManager.FireEvent ('PrintFailed',filename) self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing elif self._comm is not None and state == self._comm.STATE_PRINTING: self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use @@ -419,7 +424,7 @@ class Printer(): self.peakZ = newZ if self._timelapse is not None: self._timelapse.onZChange(oldZ, newZ) - self.executeSystemCommand(self.sys_command['z_change']) + self._eventManager.FireEvent ('ZChange',newZ) self._setCurrentZ(newZ) @@ -437,7 +442,7 @@ class Printer(): self._setCurrentZ(None) self._setProgressData(None, None, None) self._gcodeLoader = None - + self.eventManager.FireEvent("LoadDone",filename) self._stateMonitor.setGcodeData({"filename": None, "progress": None}) self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) @@ -489,31 +494,7 @@ class Printer(): def isLoading(self): return self._gcodeLoader is not None - - def executeSystemCommand(self,command_string): - if command_string is None: - return - logger = logging.getLogger(__name__) - try: - # handle a few regex substs for job data passed to external apps - cmd_string_with_params = command_string - cmd_string_with_params = re.sub("_ZHEIGHT_",str(self._currentZ), cmd_string_with_params) - cmd_string_with_params = re.sub("_FILE_",os.path.basename(self._filename), cmd_string_with_params) - # cut down to 2 decimal places, forcing through an int to avoid the 10.320000000001 floating point thing... - if self._gcodeList and self._progress: - prog = int(10000.0 * self._progress / len(self._gcodeList))/100.0 - else: - prog = 0.0 - cmd_string_with_params = re.sub("_PROGRESS_",str(prog), cmd_string_with_params) - cmd_string_with_params = re.sub("_LINE_",str(self._comm._gcodePos), cmd_string_with_params) - logger.info ("Executing system command: %s " % cmd_string_with_params) - #use Popen here since it won't wait for the shell to return...and we send some of these - # commands during a print job, we don't want to block! - subprocess.Popen(cmd_string_with_params,shell = True) - except subprocess.CalledProcessError, e: - logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message)) - except Exception, ex: - logger.exception("Command failed") + class GcodeLoader(threading.Thread): diff --git a/octoprint/server.py b/octoprint/server.py index feaae8e..d79fefa 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -17,6 +17,9 @@ import octoprint.timelapse as timelapse import octoprint.gcodefiles as gcodefiles import octoprint.util as util + +import octoprint.events as events + SUCCESS = {} BASEURL = "/ajax/" app = Flask("octoprint") @@ -24,6 +27,7 @@ app = Flask("octoprint") # In order that threads don't start too early when running as a Daemon printer = None gcodeManager = None +eventManager =None #~~ Printer state @@ -41,12 +45,16 @@ class PrinterStateConnection(tornadio2.SocketConnection): self._messageBacklogMutex = threading.Lock() def on_open(self, info): + global eventManager + eventManager.FireEvent("ClientOpen") self._logger.info("New connection from client") # Use of global here is smelly printer.registerCallback(self) gcodeManager.registerCallback(self) def on_close(self): + global eventManager + eventManager.FireEvent("ClientClosed") self._logger.info("Closed client connection") # Use of global here is smelly printer.unregisterCallback(self) @@ -103,7 +111,7 @@ def index(): webcamStream=settings().get(["webcam", "stream"]), enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]), - enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0 + enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0 ) #~~ Printer control @@ -129,7 +137,9 @@ def connect(): @app.route(BASEURL + "control/disconnect", methods=["POST"]) def disconnect(): + global eventManager printer.disconnect() + eventManager.FireEvent("Disconnected") return jsonify(state="Offline") @app.route(BASEURL + "control/command", methods=["POST"]) @@ -162,11 +172,15 @@ def printGcode(): @app.route(BASEURL + "control/pause", methods=["POST"]) def pausePrint(): + global eventManager + eventManager.FireEvent("Paused") printer.togglePausePrint() return jsonify(SUCCESS) @app.route(BASEURL + "control/cancel", methods=["POST"]) def cancelPrint(): + global eventManager + eventManager.FireEvent("Cancelled") printer.cancelPrint() return jsonify(SUCCESS) @@ -256,6 +270,8 @@ def uploadGcodeFile(): if "gcode_file" in request.files.keys(): file = request.files["gcode_file"] filename = gcodeManager.addFile(file) + global eventManager + eventManager.FireEvent("Upload",filename) return jsonify(files=gcodeManager.getAllFileData(), filename=filename) @app.route(BASEURL + "gcodefiles/load", methods=["POST"]) @@ -267,6 +283,8 @@ def loadGcodeFile(): filename = gcodeManager.getAbsolutePath(request.values["filename"]) if filename is not None: printer.loadGcode(filename, printAfterLoading) + global eventManager + eventManager.FireEvent("LoadStart",filename) return jsonify(SUCCESS) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) @@ -317,11 +335,12 @@ def deleteTimelapse(filename): @app.route(BASEURL + "timelapse/config", methods=["POST"]) def setTimelapseConfig(): + global eventManager if request.values.has_key("type"): type = request.values["type"] lapse = None if "zchange" == type: - lapse = timelapse.ZTimelapse() + lapse = timelapse.ZTimelapse(eventManager) elif "timed" == type: interval = 10 if request.values.has_key("interval"): @@ -329,7 +348,7 @@ def setTimelapseConfig(): interval = int(request.values["interval"]) except ValueError: pass - lapse = timelapse.TimedTimelapse(interval) + lapse = timelapse.TimedTimelapse( eventManager,interval) printer.setTimelapse(lapse) return getTimelapseData() @@ -374,16 +393,9 @@ def getSettings(): "profiles": s.get(["temperature", "profiles"]) }, "system": { - "actions": s.get(["system", "actions"]) - }, - "system_commands": { - "print_done": s.get(["system_commands", "print_done"]), - "cancelled": s.get(["system_commands", "cancelled"]), - "print_started": s.get(["system_commands", "print_started"]), - "z_change": s.get(["system_commands", "z_change"]) - - } - + "actions": s.get(["system", "actions"]), + "events": s.get(["system", "events"]) + } }) @app.route(BASEURL + "settings", methods=["POST"]) @@ -424,13 +436,7 @@ def setSettings(): if "system" in data.keys(): if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"]) - - if "system_commands" in data.keys(): - if "z_change" in data["system_commands"].keys(): s.set(["system_commands", "z_change"], data["system_commands"]["z_change"]) - if "print_started" in data["system_commands"].keys(): s.set(["system_commands", "print_started"], data["system_commands"]["print_started"]) - if "cancelled" in data["system_commands"].keys(): s.set(["system_commands", "cancelled"], data["system_commands"]["cancelled"]) - if "print_done" in data["system_commands"].keys(): s.set(["system_commands", "print_done"], data["system_commands"]["print_done"]) - + if "events" in data["system"].keys(): s.set(["system", "events"], data["system"]["events"]) s.save() return getSettings() @@ -465,11 +471,13 @@ class Server(): self._port = port self._debug = debug + def run(self): # Global as I can't work out a way to get it into PrinterStateConnection global printer global gcodeManager - + global eventManager + from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop @@ -481,14 +489,21 @@ class Server(): # then initialize logging self._initLogging(self._debug) + eventManager = events.EventManager() gcodeManager = gcodefiles.GcodeManager() - printer = Printer(gcodeManager) + printer = Printer(gcodeManager, eventManager) + self.event_dispatcher = events.EventResponse (eventManager,printer) + self.event_dispatcher.setupEvents(settings()) +# a few test commands to test the event manager is working... + # eventManager.Register("Startup",self,self.event_rec) + # eventManager.unRegister("Startup",self,self.event_rec) + # eventManager.FireEvent("Startup") + if self._host is None: self._host = settings().get(["server", "host"]) if self._port is None: self._port = settings().getInt(["server", "port"]) - logging.getLogger(__name__).info("Listening on http://%s:%d" % (self._host, self._port)) app.debug = self._debug @@ -499,6 +514,8 @@ class Server(): ]) self._server = HTTPServer(self._tornado_app) self._server.listen(self._port, address=self._host) + + eventManager.FireEvent("Startup") IOLoop.instance().start() def _initSettings(self, configfile, basedir): diff --git a/octoprint/settings.py b/octoprint/settings.py index b655f49..947466a 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -68,14 +68,9 @@ default_settings = { }, "controls": [], "system": { - "actions": [] + "actions": [], + "events": [] }, - "system_commands": { - "z_change":None, - "print_started":None, - "cancelled":None, - "print_done":None - } } valid_boolean_trues = ["true", "yes", "y", "1"] @@ -116,7 +111,8 @@ class Settings(object): if os.path.exists(self._configfile) and os.path.isfile(self._configfile): with open(self._configfile, "r") as f: self._config = yaml.safe_load(f) - else: + # chamged from else to handle cases where the file exists, but is empty / 0 bytes + if not self._config: self._config = {} def save(self, force=False): diff --git a/octoprint/timelapse.py b/octoprint/timelapse.py index 4283323..9b5c546 100644 --- a/octoprint/timelapse.py +++ b/octoprint/timelapse.py @@ -14,6 +14,7 @@ import time import subprocess import fnmatch import datetime +import octoprint.events as events import sys @@ -33,9 +34,9 @@ def getFinishedTimelapses(): return files class Timelapse(object): - def __init__(self): + def __init__(self,ev): self._logger = logging.getLogger(__name__) - + self._eventManager = ev self._imageNumber = None self._inTimelapse = False self._gcodeFile = None @@ -85,7 +86,7 @@ class Timelapse(object): filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber)) self._imageNumber += 1; self._logger.debug("Capturing image to %s" % filename) - + self._eventManager.FireEvent("CaptureStart",filename); captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename}) captureThread.daemon = True captureThread.start() @@ -93,6 +94,7 @@ class Timelapse(object): def _captureWorker(self, filename): urllib.urlretrieve(self._snapshotUrl, filename) self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl)) + self._eventManager.FireEvent("CaptureDone",filename); def _createMovie(self): ffmpeg = settings().get(["webcam", "ffmpeg"]) @@ -122,6 +124,7 @@ class Timelapse(object): command.append(output) subprocess.call(command) self._logger.debug("Rendering movie to %s" % output) + self._eventManager.FireEvent("MovieDone",output); def cleanCaptureDir(self): if not os.path.isdir(self._captureDir): @@ -134,8 +137,8 @@ class Timelapse(object): os.remove(os.path.join(self._captureDir, filename)) class ZTimelapse(Timelapse): - def __init__(self): - Timelapse.__init__(self) + def __init__(self,ev): + Timelapse.__init__(self,ev) self._logger.debug("ZTimelapse initialized") def onZChange(self, oldZ, newZ): @@ -143,8 +146,8 @@ class ZTimelapse(Timelapse): self.captureImage() class TimedTimelapse(Timelapse): - def __init__(self, interval=1): - Timelapse.__init__(self) + def __init__(self, ev,interval=1): + Timelapse.__init__(self,ev) self._interval = interval if self._interval < 1: diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 9ef8893..29ff801 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -178,11 +178,15 @@ class MachineCom(object): self._heatupWaitStartTime = 0 self._heatupWaitTimeLost = 0.0 self._printStartTime100 = None + self._eventManager = None self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() + def setEventManager(self,em): + self._eventManager = em + def _changeState(self, newState): if self._state == newState: return @@ -328,8 +332,8 @@ class MachineCom(object): 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. + # 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. @@ -468,6 +472,47 @@ class MachineCom(object): def _sendCommand(self, cmd): if self._serial is None: return + if self._eventManager: + t_cmd = cmd+' ' + t_cmd = cmd+' ' + # some useful event triggered from GCode commands + # pause for user input. M0 in Marlin and M1 in G-code standard RS274NGC + if re.search ("^\s*M226\D",t_cmd,re.I) or re.search ("^\s*M[01]\D",t_cmd,re.I): + self._eventManager.FireEvent ('Waiting') + # part cooler started + if re.search ("^\s*M245\D",t_cmd,re.I): + self._eventManager.FireEvent ('Cooling') + # part conveyor started + if re.search ("^\s*M240\D",t_cmd,re.I): + self._eventManager.FireEvent ('Conveyor') + # part ejector + if re.search ("^\s*M40\D",t_cmd,re.I): + self._eventManager.FireEvent ('Eject') + # user alert issued by sending beep command to printer... + if re.search ("^\s*M300\D",t_cmd,re.I): + self._eventManager.FireEvent ('Alert') + # Print head has moved to home + if re.search ("^\s*G28\D",t_cmd,re.I): + self._eventManager.FireEvent ('Home') + if re.search ("^\s*M112\D",t_cmd,re.I): + self._eventManager.FireEvent ('EStop') + if re.search ("^\s*M80\D",t_cmd,re.I): + self._eventManager.FireEvent ('PowerOn') + if re.search ("^\s*M81\D",t_cmd,re.I): + self._eventManager.FireEvent ('PowerOff') + if re.search ("^\s*M25\D",t_cmd,re.I): # SD Card pause + self._eventManager.FireEvent ('Paused') + + +# these comparisons assume that the searched-for string is not in a comment or a parameter, for example +# GCode lines like this: +# G0 X100 ; let's not do an M109 here!!! +# M420 R000 E000 B000 ; set LED color on makerbot to RGB (note the G is replaced with an E) +# M1090 ; some command > 999 +# could potentially trip us up here.... +# this can be avoided by checking only the START of the string for the command code +# and checking for whitespace after the command (after trimming any leading whitespace, as necessary) + if 'M109' in cmd or 'M190' in cmd: self._heatupWaitStartTime = time.time() if 'M104' in cmd or 'M109' in cmd: @@ -507,9 +552,13 @@ class MachineCom(object): self._printSection = line[1] line = line[0] try: - if line == 'M0' or line == 'M1': + if line == 'M0' or line == 'M1' or line=='M112': # M112 is also an LCD pause self.setPause(True) - line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. + line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. + + # LCD / user response pause can be used for things like mid-print filament changes, so + # always removing them may not be so good. Something to consider as a user preference? + if self._printSection in self._feedRateModifier: line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line) if ('G0' in line or 'G1' in line) and 'Z' in line: @@ -519,8 +568,8 @@ class MachineCom(object): self._callback.mcZChange(z) except: self._log("Unexpected error: %s" % (getExceptionString())) - checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line))) - self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum)) + checksum = reduce(lambda x,y:x^y, map(ord, "N%d %s " % (self._gcodePos, line))) + self._sendCommand("N%d %s *%d" % (self._gcodePos, line, checksum)) # added spaces between line # and command and checksum, because some firmware needs it and it's more readable in the terminal window self._gcodePos += 1 self._callback.mcProgress(self._gcodePos) From f6108b54223cd02008ec9dab579fb121bc595649 Mon Sep 17 00:00:00 2001 From: Lars Norpchen Date: Mon, 20 May 2013 21:14:00 -0600 Subject: [PATCH 7/7] Update README.md Back out previous changes --- README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/README.md b/README.md index 5876963..7dd80ab 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,3 @@ -Added support for system commands at print start, print end, print cancelled and z-height change. Add the commands as strings as a new section to the config.yaml file: - - appearance {} - feature: {} - serial: - baudrate: 115200 - port: /dev/ttyACM0 - system: {} - temperature {} - system_commands: - cancelled: echo cancelled _FILE_ at _PROGRESS_ percent done. - print_done: growlnotify "done with _FILE_" - print_started: echo starting _FILE_ - z_change: echo _LINE_ _PROGRESS_ _ZHEIGHT_ - -These commands take the tokens take _FILE_, _PERCENT_, _LINES_ and _ZHEIGHT_ which will be passed to external commands. - - - - OctoPrint =========