From c6363ea0461e2390ab16ffab22b1a69247662840 Mon Sep 17 00:00:00 2001 From: Lars Norpchen Date: Thu, 9 May 2013 14:31:54 -0700 Subject: [PATCH] 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"]