Event Manager

Added event manager to trigger system and gcode commands.
master
Lars Norpchen 2013-05-20 20:04:21 -07:00
parent 7b214d3f16
commit 525d73a20b
7 changed files with 343 additions and 84 deletions

150
octoprint/events.py Normal file
View File

@ -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")

63
octoprint/events.txt Normal file
View File

@ -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)

View File

@ -7,9 +7,8 @@ import datetime
import threading import threading
import copy import copy
import os import os
import subprocess
import re #import logging, logging.config
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
@ -28,8 +27,9 @@ def getConnectionOptions():
} }
class Printer(): class Printer():
def __init__(self, gcodeManager): def __init__(self, gcodeManager,eventManager):
self._gcodeManager = gcodeManager self._gcodeManager = gcodeManager
self._eventManager = eventManager
# state # state
self._temp = None self._temp = None
@ -94,9 +94,6 @@ 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):
@ -137,6 +134,7 @@ class Printer():
if self._comm is not None: if self._comm is not None:
self._comm.close() self._comm.close()
self._comm = comm.MachineCom(port, baudrate, callbackObject=self) self._comm = comm.MachineCom(port, baudrate, callbackObject=self)
self._comm.setEventManager(self._eventManager)
def disconnect(self): def disconnect(self):
""" """
@ -198,7 +196,7 @@ class Printer():
self._setCurrentZ(-1) self._setCurrentZ(-1)
self.executeSystemCommand(self.sys_command['print_started']) self._eventManager.FireEvent ('PrintStarted',filename)
self._comm.printGCode(self._gcodeList) self._comm.printGCode(self._gcodeList)
@ -225,9 +223,9 @@ class Printer():
self._setProgressData(None, None, None) self._setProgressData(None, None, None)
# mark print as failure # mark print as failure
self._gcodeManager.printFailed(self._filename) if self._filename:
self.executeSystemCommand(self.sys_command['cancelled']) self._gcodeManager.printFailed(self._filename)
#~~ state monitoring #~~ state monitoring
def setTimelapse(self, timelapse): def setTimelapse(self, timelapse):
@ -370,6 +368,13 @@ class Printer():
elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED: elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED:
self._timelapse.onPrintjobStarted(self._filename) 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 # forward relevant state changes to gcode manager
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:
@ -377,10 +382,10 @@ class Printer():
#hrm....we seem to hit this state and THEN the next failed state on a cancel request? #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 # oh well, add a check to see if we're really done before sending the success event external command
if self._printTimeLeft < 1: 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: 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._eventManager.FireEvent ('PrintFailed',filename)
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
@ -419,7 +424,7 @@ class Printer():
self.peakZ = newZ self.peakZ = newZ
if self._timelapse is not None: if self._timelapse is not None:
self._timelapse.onZChange(oldZ, newZ) self._timelapse.onZChange(oldZ, newZ)
self.executeSystemCommand(self.sys_command['z_change']) self._eventManager.FireEvent ('ZChange',newZ)
self._setCurrentZ(newZ) self._setCurrentZ(newZ)
@ -437,7 +442,7 @@ class Printer():
self._setCurrentZ(None) self._setCurrentZ(None)
self._setProgressData(None, None, None) self._setProgressData(None, None, None)
self._gcodeLoader = None self._gcodeLoader = None
self.eventManager.FireEvent("LoadDone",filename)
self._stateMonitor.setGcodeData({"filename": None, "progress": None}) self._stateMonitor.setGcodeData({"filename": None, "progress": None})
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
@ -489,31 +494,7 @@ 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):

View File

@ -17,6 +17,9 @@ import octoprint.timelapse as timelapse
import octoprint.gcodefiles as gcodefiles import octoprint.gcodefiles as gcodefiles
import octoprint.util as util import octoprint.util as util
import octoprint.events as events
SUCCESS = {} SUCCESS = {}
BASEURL = "/ajax/" BASEURL = "/ajax/"
app = Flask("octoprint") app = Flask("octoprint")
@ -24,6 +27,7 @@ app = Flask("octoprint")
# In order that threads don't start too early when running as a Daemon # In order that threads don't start too early when running as a Daemon
printer = None printer = None
gcodeManager = None gcodeManager = None
eventManager =None
#~~ Printer state #~~ Printer state
@ -41,12 +45,16 @@ class PrinterStateConnection(tornadio2.SocketConnection):
self._messageBacklogMutex = threading.Lock() self._messageBacklogMutex = threading.Lock()
def on_open(self, info): def on_open(self, info):
global eventManager
eventManager.FireEvent("ClientOpen")
self._logger.info("New connection from client") self._logger.info("New connection from client")
# Use of global here is smelly # Use of global here is smelly
printer.registerCallback(self) printer.registerCallback(self)
gcodeManager.registerCallback(self) gcodeManager.registerCallback(self)
def on_close(self): def on_close(self):
global eventManager
eventManager.FireEvent("ClientClosed")
self._logger.info("Closed client connection") self._logger.info("Closed client connection")
# Use of global here is smelly # Use of global here is smelly
printer.unregisterCallback(self) printer.unregisterCallback(self)
@ -103,7 +111,7 @@ def index():
webcamStream=settings().get(["webcam", "stream"]), webcamStream=settings().get(["webcam", "stream"]),
enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None),
enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]), 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 #~~ Printer control
@ -129,7 +137,9 @@ def connect():
@app.route(BASEURL + "control/disconnect", methods=["POST"]) @app.route(BASEURL + "control/disconnect", methods=["POST"])
def disconnect(): def disconnect():
global eventManager
printer.disconnect() printer.disconnect()
eventManager.FireEvent("Disconnected")
return jsonify(state="Offline") return jsonify(state="Offline")
@app.route(BASEURL + "control/command", methods=["POST"]) @app.route(BASEURL + "control/command", methods=["POST"])
@ -162,11 +172,15 @@ def printGcode():
@app.route(BASEURL + "control/pause", methods=["POST"]) @app.route(BASEURL + "control/pause", methods=["POST"])
def pausePrint(): def pausePrint():
global eventManager
eventManager.FireEvent("Paused")
printer.togglePausePrint() printer.togglePausePrint()
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "control/cancel", methods=["POST"]) @app.route(BASEURL + "control/cancel", methods=["POST"])
def cancelPrint(): def cancelPrint():
global eventManager
eventManager.FireEvent("Cancelled")
printer.cancelPrint() printer.cancelPrint()
return jsonify(SUCCESS) return jsonify(SUCCESS)
@ -256,6 +270,8 @@ def uploadGcodeFile():
if "gcode_file" in request.files.keys(): if "gcode_file" in request.files.keys():
file = request.files["gcode_file"] file = request.files["gcode_file"]
filename = gcodeManager.addFile(file) filename = gcodeManager.addFile(file)
global eventManager
eventManager.FireEvent("Upload",filename)
return jsonify(files=gcodeManager.getAllFileData(), filename=filename) return jsonify(files=gcodeManager.getAllFileData(), filename=filename)
@app.route(BASEURL + "gcodefiles/load", methods=["POST"]) @app.route(BASEURL + "gcodefiles/load", methods=["POST"])
@ -267,6 +283,8 @@ def loadGcodeFile():
filename = gcodeManager.getAbsolutePath(request.values["filename"]) filename = gcodeManager.getAbsolutePath(request.values["filename"])
if filename is not None: if filename is not None:
printer.loadGcode(filename, printAfterLoading) printer.loadGcode(filename, printAfterLoading)
global eventManager
eventManager.FireEvent("LoadStart",filename)
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
@ -317,11 +335,12 @@ def deleteTimelapse(filename):
@app.route(BASEURL + "timelapse/config", methods=["POST"]) @app.route(BASEURL + "timelapse/config", methods=["POST"])
def setTimelapseConfig(): def setTimelapseConfig():
global eventManager
if request.values.has_key("type"): if request.values.has_key("type"):
type = request.values["type"] type = request.values["type"]
lapse = None lapse = None
if "zchange" == type: if "zchange" == type:
lapse = timelapse.ZTimelapse() lapse = timelapse.ZTimelapse(eventManager)
elif "timed" == type: elif "timed" == type:
interval = 10 interval = 10
if request.values.has_key("interval"): if request.values.has_key("interval"):
@ -329,7 +348,7 @@ def setTimelapseConfig():
interval = int(request.values["interval"]) interval = int(request.values["interval"])
except ValueError: except ValueError:
pass pass
lapse = timelapse.TimedTimelapse(interval) lapse = timelapse.TimedTimelapse( eventManager,interval)
printer.setTimelapse(lapse) printer.setTimelapse(lapse)
return getTimelapseData() return getTimelapseData()
@ -374,16 +393,9 @@ def getSettings():
"profiles": s.get(["temperature", "profiles"]) "profiles": s.get(["temperature", "profiles"])
}, },
"system": { "system": {
"actions": s.get(["system", "actions"]) "actions": s.get(["system", "actions"]),
}, "events": s.get(["system", "events"])
"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"])
@ -424,13 +436,7 @@ 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 "events" in data["system"].keys(): s.set(["system", "events"], data["system"]["events"])
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()
@ -465,11 +471,13 @@ class Server():
self._port = port self._port = port
self._debug = debug self._debug = debug
def run(self): def run(self):
# Global as I can't work out a way to get it into PrinterStateConnection # Global as I can't work out a way to get it into PrinterStateConnection
global printer global printer
global gcodeManager global gcodeManager
global eventManager
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
@ -481,14 +489,21 @@ class Server():
# then initialize logging # then initialize logging
self._initLogging(self._debug) self._initLogging(self._debug)
eventManager = events.EventManager()
gcodeManager = gcodefiles.GcodeManager() 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: if self._host is None:
self._host = settings().get(["server", "host"]) self._host = settings().get(["server", "host"])
if self._port is None: if self._port is None:
self._port = settings().getInt(["server", "port"]) self._port = settings().getInt(["server", "port"])
logging.getLogger(__name__).info("Listening on http://%s:%d" % (self._host, self._port)) logging.getLogger(__name__).info("Listening on http://%s:%d" % (self._host, self._port))
app.debug = self._debug app.debug = self._debug
@ -499,6 +514,8 @@ class Server():
]) ])
self._server = HTTPServer(self._tornado_app) self._server = HTTPServer(self._tornado_app)
self._server.listen(self._port, address=self._host) self._server.listen(self._port, address=self._host)
eventManager.FireEvent("Startup")
IOLoop.instance().start() IOLoop.instance().start()
def _initSettings(self, configfile, basedir): def _initSettings(self, configfile, basedir):

View File

@ -68,14 +68,9 @@ default_settings = {
}, },
"controls": [], "controls": [],
"system": { "system": {
"actions": [] "actions": [],
"events": []
}, },
"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"]
@ -116,7 +111,8 @@ class Settings(object):
if os.path.exists(self._configfile) and os.path.isfile(self._configfile): if os.path.exists(self._configfile) and os.path.isfile(self._configfile):
with open(self._configfile, "r") as f: with open(self._configfile, "r") as f:
self._config = yaml.safe_load(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 = {} self._config = {}
def save(self, force=False): def save(self, force=False):

View File

@ -14,6 +14,7 @@ import time
import subprocess import subprocess
import fnmatch import fnmatch
import datetime import datetime
import octoprint.events as events
import sys import sys
@ -33,9 +34,9 @@ def getFinishedTimelapses():
return files return files
class Timelapse(object): class Timelapse(object):
def __init__(self): def __init__(self,ev):
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
self._eventManager = ev
self._imageNumber = None self._imageNumber = None
self._inTimelapse = False self._inTimelapse = False
self._gcodeFile = None self._gcodeFile = None
@ -85,7 +86,7 @@ class Timelapse(object):
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber)) filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber))
self._imageNumber += 1; self._imageNumber += 1;
self._logger.debug("Capturing image to %s" % filename) self._logger.debug("Capturing image to %s" % filename)
self._eventManager.FireEvent("CaptureStart",filename);
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename}) captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})
captureThread.daemon = True captureThread.daemon = True
captureThread.start() captureThread.start()
@ -93,6 +94,7 @@ class Timelapse(object):
def _captureWorker(self, filename): def _captureWorker(self, filename):
urllib.urlretrieve(self._snapshotUrl, filename) urllib.urlretrieve(self._snapshotUrl, filename)
self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl)) self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl))
self._eventManager.FireEvent("CaptureDone",filename);
def _createMovie(self): def _createMovie(self):
ffmpeg = settings().get(["webcam", "ffmpeg"]) ffmpeg = settings().get(["webcam", "ffmpeg"])
@ -122,6 +124,7 @@ class Timelapse(object):
command.append(output) command.append(output)
subprocess.call(command) subprocess.call(command)
self._logger.debug("Rendering movie to %s" % output) self._logger.debug("Rendering movie to %s" % output)
self._eventManager.FireEvent("MovieDone",output);
def cleanCaptureDir(self): def cleanCaptureDir(self):
if not os.path.isdir(self._captureDir): if not os.path.isdir(self._captureDir):
@ -134,8 +137,8 @@ class Timelapse(object):
os.remove(os.path.join(self._captureDir, filename)) os.remove(os.path.join(self._captureDir, filename))
class ZTimelapse(Timelapse): class ZTimelapse(Timelapse):
def __init__(self): def __init__(self,ev):
Timelapse.__init__(self) Timelapse.__init__(self,ev)
self._logger.debug("ZTimelapse initialized") self._logger.debug("ZTimelapse initialized")
def onZChange(self, oldZ, newZ): def onZChange(self, oldZ, newZ):
@ -143,8 +146,8 @@ class ZTimelapse(Timelapse):
self.captureImage() self.captureImage()
class TimedTimelapse(Timelapse): class TimedTimelapse(Timelapse):
def __init__(self, interval=1): def __init__(self, ev,interval=1):
Timelapse.__init__(self) Timelapse.__init__(self,ev)
self._interval = interval self._interval = interval
if self._interval < 1: if self._interval < 1:

View File

@ -178,11 +178,15 @@ class MachineCom(object):
self._heatupWaitStartTime = 0 self._heatupWaitStartTime = 0
self._heatupWaitTimeLost = 0.0 self._heatupWaitTimeLost = 0.0
self._printStartTime100 = None self._printStartTime100 = None
self._eventManager = None
self.thread = threading.Thread(target=self._monitor) self.thread = threading.Thread(target=self._monitor)
self.thread.daemon = True self.thread.daemon = True
self.thread.start() self.thread.start()
def setEventManager(self,em):
self._eventManager = em
def _changeState(self, newState): def _changeState(self, newState):
if self._state == newState: if self._state == newState:
return return
@ -328,8 +332,8 @@ class MachineCom(object):
if line.startswith('Error:'): if line.startswith('Error:'):
#Oh YEAH, consistency. #Oh YEAH, consistency.
# Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" # 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 !!" # 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. # So we can have an extra newline in the most common case. Awesome work people.
if re.match('Error:[0-9]\n', line): if re.match('Error:[0-9]\n', line):
line = line.rstrip() + self._readline() line = line.rstrip() + self._readline()
#Skip the communication errors, as those get corrected. #Skip the communication errors, as those get corrected.
@ -468,6 +472,47 @@ class MachineCom(object):
def _sendCommand(self, cmd): def _sendCommand(self, cmd):
if self._serial is None: if self._serial is None:
return 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: if 'M109' in cmd or 'M190' in cmd:
self._heatupWaitStartTime = time.time() self._heatupWaitStartTime = time.time()
if 'M104' in cmd or 'M109' in cmd: if 'M104' in cmd or 'M109' in cmd:
@ -507,9 +552,13 @@ class MachineCom(object):
self._printSection = line[1] self._printSection = line[1]
line = line[0] line = line[0]
try: 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) 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: 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) 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: if ('G0' in line or 'G1' in line) and 'Z' in line:
@ -519,8 +568,8 @@ class MachineCom(object):
self._callback.mcZChange(z) self._callback.mcZChange(z)
except: except:
self._log("Unexpected error: %s" % (getExceptionString())) self._log("Unexpected error: %s" % (getExceptionString()))
checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line))) 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)) 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._gcodePos += 1
self._callback.mcProgress(self._gcodePos) self._callback.mcProgress(self._gcodePos)