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.master
parent
b609123d8a
commit
c6363ea046
13
README.md
13
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
|
||||
=========
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
Loading…
Reference in New Issue