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
|
OctoPrint
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,9 @@ import datetime
|
||||||
import threading
|
import threading
|
||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
import logging, logging.config
|
||||||
|
|
||||||
import octoprint.util.comm as comm
|
import octoprint.util.comm as comm
|
||||||
import octoprint.util as util
|
import octoprint.util as util
|
||||||
|
@ -53,6 +56,7 @@ class Printer():
|
||||||
|
|
||||||
self._currentZ = None
|
self._currentZ = None
|
||||||
|
|
||||||
|
self.peakZ = -1
|
||||||
self._progress = None
|
self._progress = None
|
||||||
self._printTime = None
|
self._printTime = None
|
||||||
self._printTimeLeft = None
|
self._printTimeLeft = None
|
||||||
|
@ -90,6 +94,9 @@ class Printer():
|
||||||
currentZ=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):
|
def registerCallback(self, callback):
|
||||||
|
@ -190,6 +197,9 @@ class Printer():
|
||||||
return
|
return
|
||||||
|
|
||||||
self._setCurrentZ(-1)
|
self._setCurrentZ(-1)
|
||||||
|
|
||||||
|
self.executeSystemCommand(self.sys_command['print_started'])
|
||||||
|
|
||||||
self._comm.printGCode(self._gcodeList)
|
self._comm.printGCode(self._gcodeList)
|
||||||
|
|
||||||
def togglePausePrint(self):
|
def togglePausePrint(self):
|
||||||
|
@ -216,7 +226,8 @@ class Printer():
|
||||||
|
|
||||||
# mark print as failure
|
# mark print as failure
|
||||||
self._gcodeManager.printFailed(self._filename)
|
self._gcodeManager.printFailed(self._filename)
|
||||||
|
self.executeSystemCommand(self.sys_command['cancelled'])
|
||||||
|
|
||||||
#~~ state monitoring
|
#~~ state monitoring
|
||||||
|
|
||||||
def setTimelapse(self, timelapse):
|
def setTimelapse(self, timelapse):
|
||||||
|
@ -363,8 +374,13 @@ class Printer():
|
||||||
if self._comm is not None and oldState == self._comm.STATE_PRINTING:
|
if self._comm is not None and oldState == self._comm.STATE_PRINTING:
|
||||||
if state == self._comm.STATE_OPERATIONAL:
|
if state == self._comm.STATE_OPERATIONAL:
|
||||||
self._gcodeManager.printSucceeded(self._filename)
|
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:
|
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.printFailed(self._filename)
|
||||||
|
self.executeSystemCommand(self.sys_command['cancelled'])
|
||||||
self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing
|
self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing
|
||||||
elif self._comm is not None and state == self._comm.STATE_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._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.
|
Callback method for the comm object, called upon change of the z-layer.
|
||||||
"""
|
"""
|
||||||
oldZ = self._currentZ
|
oldZ = self._currentZ
|
||||||
if self._timelapse is not None:
|
# only do this if we hit a new Z peak level. Some slicers do a Z-lift when retracting / moving without printing
|
||||||
self._timelapse.onZChange(oldZ, newZ)
|
# 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)
|
self._setCurrentZ(newZ)
|
||||||
|
|
||||||
#~~ callbacks triggered by gcodeLoader
|
#~~ callbacks triggered by gcodeLoader
|
||||||
|
@ -468,6 +489,32 @@ class Printer():
|
||||||
|
|
||||||
def isLoading(self):
|
def isLoading(self):
|
||||||
return self._gcodeLoader is not None
|
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):
|
class GcodeLoader(threading.Thread):
|
||||||
"""
|
"""
|
||||||
|
@ -526,6 +573,7 @@ class StateMonitor(object):
|
||||||
self._jobData = None
|
self._jobData = None
|
||||||
self._gcodeData = None
|
self._gcodeData = None
|
||||||
self._currentZ = None
|
self._currentZ = None
|
||||||
|
self._peakZ = -1
|
||||||
self._progress = None
|
self._progress = None
|
||||||
|
|
||||||
self._changeEvent = threading.Event()
|
self._changeEvent = threading.Event()
|
||||||
|
|
|
@ -375,7 +375,15 @@ def getSettings():
|
||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"actions": s.get(["system", "actions"])
|
"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"])
|
@app.route(BASEURL + "settings", methods=["POST"])
|
||||||
|
@ -417,6 +425,12 @@ def setSettings():
|
||||||
if "system" in data.keys():
|
if "system" in data.keys():
|
||||||
if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"])
|
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()
|
s.save()
|
||||||
|
|
||||||
return getSettings()
|
return getSettings()
|
||||||
|
|
|
@ -69,8 +69,14 @@ default_settings = {
|
||||||
"controls": [],
|
"controls": [],
|
||||||
"system": {
|
"system": {
|
||||||
"actions": []
|
"actions": []
|
||||||
}
|
},
|
||||||
}
|
"system_commands": {
|
||||||
|
"z_change":None,
|
||||||
|
"print_started":None,
|
||||||
|
"cancelled":None,
|
||||||
|
"print_done":None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
valid_boolean_trues = ["true", "yes", "y", "1"]
|
valid_boolean_trues = ["true", "yes", "y", "1"]
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue