Refactoring of event management
parent
c836c79179
commit
be99930021
|
@ -1,149 +1,228 @@
|
|||
# coding=utf-8
|
||||
|
||||
__author__ = "Lars Norpchen"
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import logging
|
||||
import subprocess
|
||||
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
|
||||
from octoprint.settings import settings
|
||||
|
||||
class event_record(object):
|
||||
def __init__(self, what, who, action):
|
||||
self.what = what
|
||||
self.who = who
|
||||
self.action = action
|
||||
# singleton
|
||||
_instance = None
|
||||
|
||||
def eventManager():
|
||||
global _instance
|
||||
if _instance is None:
|
||||
_instance = EventManager()
|
||||
return _instance
|
||||
|
||||
class EventManager(object):
|
||||
"""
|
||||
Handles receiving events and dispatching them to listeners
|
||||
Handles receiving events and dispatching them to subscribers
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.registered_events = []
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def fire(self, name, payload=None):
|
||||
"""
|
||||
Fire an event to anyone listening.
|
||||
self._registeredListeners = {}
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
Any object can generate an event and any object can listen pass in the event_name as a string (arbitrary, but
|
||||
def fire(self, event, payload=None):
|
||||
"""
|
||||
Fire an event to anyone subscribed to it
|
||||
|
||||
Any object can generate an event and any object can subscribe to the event's name as a string (arbitrary, but
|
||||
case sensitive) and any extra payload data that may pertain to the event.
|
||||
|
||||
Callbacks must implement the signature "callback(event, payload)", with "event" being the event's name and
|
||||
payload being a payload object specific to the event.
|
||||
"""
|
||||
|
||||
self.logger.debug("Firing event: %s (%r)" % (name, payload))
|
||||
for event in self.registered_events:
|
||||
(who, what, action) = event
|
||||
if name == what:
|
||||
self.logger.debug("Sending action to %r" % who)
|
||||
if action is not None:
|
||||
action(name, payload)
|
||||
if not event in self._registeredListeners.keys():
|
||||
return
|
||||
self._logger.debug("Firing event: %s (%r)" % (event, payload))
|
||||
|
||||
eventListeners = self._registeredListeners[event]
|
||||
for listener in eventListeners:
|
||||
self._logger.debug("Sending action to %r" % listener)
|
||||
listener(event, payload)
|
||||
|
||||
|
||||
def subscribe(self, name, target, action):
|
||||
def subscribe(self, event, callback):
|
||||
"""
|
||||
Subscribe a listener to an event -- pass in the event name (as a string), the target object
|
||||
and the callback object
|
||||
Subscribe a listener to an event -- pass in the event name (as a string) and the callback object
|
||||
"""
|
||||
|
||||
newEvent = (name, target, action)
|
||||
self.registered_events = self.registered_events.append(newEvent)
|
||||
self.logger.debug("Registered event \"%s\" to invoke \"%r\" on %r" % (name, action, target))
|
||||
if not event in self._registeredListeners.keys():
|
||||
self._registeredListeners[event] = []
|
||||
|
||||
def unsubscribe (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
|
||||
|
||||
class EventResponse(object):
|
||||
"""
|
||||
Hooks the event manager to system events, gcode, etc. Creates listeners to any events defined in the settings.
|
||||
"""
|
||||
|
||||
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 event in availableEvents:
|
||||
name = event["event"].strip()
|
||||
action = event["type"].strip()
|
||||
data = event["command"]
|
||||
|
||||
self._eventManager.subscribe(event.event_string, self, self.eventRec)
|
||||
|
||||
self.registered_responses = self.registered_responses.append(event)
|
||||
self.logger.debug("Registered %s event \"%s\" to execute \"%s\"" % (event.type, event.event_string, event.command_data))
|
||||
self.logger.debug("Registered %d events" % len(self.registered_responses))
|
||||
|
||||
def eventRec (self,event_name, event_data):
|
||||
self.logger.debug("Received event: %s (%r)" % (event_name, 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)
|
||||
|
||||
def doStringProcessing (self, command_string):
|
||||
"""
|
||||
Handles 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._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.debug("GCode command: " + command_string)
|
||||
self._printer.commands(command_string.split(','))
|
||||
|
||||
def executeSystemCommand(self, command_string):
|
||||
if command_string is None:
|
||||
if callback in self._registeredListeners[event]:
|
||||
# callback is already subscribed to the event
|
||||
return
|
||||
|
||||
self._registeredListeners[event].append(callback)
|
||||
self._logger.debug("Subscribed listener %r for event %s" % (callback, event))
|
||||
|
||||
def unsubscribe (self, event, callback):
|
||||
if not event in self._registeredListeners:
|
||||
# no callback registered for callback, just return
|
||||
return
|
||||
|
||||
if not callback in self._registeredListeners[event]:
|
||||
# callback not subscribed to event, just return
|
||||
return
|
||||
|
||||
self._registeredListeners[event].remove(callback)
|
||||
self._logger.debug("Unsubscribed listener %r for event %s" % (callback, event))
|
||||
|
||||
class GenericEventListener(object):
|
||||
"""
|
||||
The GenericEventListener can be subclassed to easily create custom event listeners.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
def subscribe(self, events):
|
||||
"""
|
||||
Subscribes the eventCallback method for all events in the given list.
|
||||
"""
|
||||
|
||||
for event in events:
|
||||
eventManager().subscribe(event, self.eventCallback)
|
||||
|
||||
def unsubscribe(self, events):
|
||||
"""
|
||||
Unsubscribes the eventCallback method for all events in the given list
|
||||
"""
|
||||
|
||||
for event in events:
|
||||
eventManager().unsubscribe(event, self.eventCallback)
|
||||
|
||||
def eventCallback(self, event, payload):
|
||||
"""
|
||||
Actual event callback called with name of event and optional payload. Not implemented here, override in
|
||||
child classes.
|
||||
"""
|
||||
pass
|
||||
|
||||
class CommandTrigger(GenericEventListener):
|
||||
def __init__(self, triggerType, printer):
|
||||
GenericEventListener.__init__(self)
|
||||
self._printer = printer
|
||||
self._subscriptions = {}
|
||||
|
||||
self._initSubscriptions(triggerType)
|
||||
|
||||
def _initSubscriptions(self, triggerType):
|
||||
"""
|
||||
Subscribes all events as defined in "events > $triggerType > subscriptions" in the settings with their
|
||||
respective commands.
|
||||
"""
|
||||
if not settings().get(["events", triggerType]):
|
||||
return
|
||||
|
||||
if not settings().getBoolean(["events", triggerType, "enabled"]):
|
||||
return
|
||||
|
||||
eventsToSubscribe = []
|
||||
for subscription in settings().get(["events", triggerType, "subscriptions"]):
|
||||
if not "event" in subscription.keys() or not "command" in subscription.keys():
|
||||
self._logger.info("Invalid %s, missing either event or command: %r" % (triggerType, subscription))
|
||||
continue
|
||||
|
||||
event = subscription["event"]
|
||||
command = subscription["command"]
|
||||
|
||||
if not event in self._subscriptions.keys():
|
||||
self._subscriptions[event] = []
|
||||
self._subscriptions[event].append(command)
|
||||
|
||||
if not event in eventsToSubscribe:
|
||||
eventsToSubscribe.append(event)
|
||||
|
||||
self.subscribe(eventsToSubscribe)
|
||||
|
||||
def eventCallback(self, event, payload):
|
||||
"""
|
||||
Event callback, iterates over all subscribed commands for the given event, processes the command
|
||||
string and then executes the command via the abstract executeCommand method.
|
||||
"""
|
||||
|
||||
GenericEventListener.eventCallback(self, event, payload)
|
||||
|
||||
if not event in self._subscriptions:
|
||||
return
|
||||
|
||||
for command in self._subscriptions[event]:
|
||||
processedCommand = self._processCommand(command, payload)
|
||||
self.executeCommand(processedCommand)
|
||||
|
||||
def executeCommand(self, command):
|
||||
"""
|
||||
Not implemented, override in child classes
|
||||
"""
|
||||
pass
|
||||
|
||||
def _processCommand(self, command, payload):
|
||||
"""
|
||||
Performs string substitutions in the command string based on a couple of current parameters.
|
||||
|
||||
The following substitutions are currently supported:
|
||||
|
||||
- %(currentZ)s : current Z position of the print head
|
||||
- %(filename)s : current selected filename, or "NO FILE" if no file is selected
|
||||
- %(progress)s : current print progress in percent, 0 if no print is in progress
|
||||
- %(data)s : the string representation of the event's payload
|
||||
- %(now)s : ISO 8601 representation of the current date and time
|
||||
"""
|
||||
|
||||
params = {
|
||||
"currentZ": "-1",
|
||||
"filename": "NO FILE",
|
||||
"progress": "0",
|
||||
"data": str(payload),
|
||||
"now": datetime.datetime.now().isoformat()
|
||||
}
|
||||
|
||||
currentData = self._printer.getCurrentData()
|
||||
|
||||
if "currentZ" in currentData.keys() and currentData["currentZ"] is not None:
|
||||
params["currentZ"] = str(currentData["currentZ"])
|
||||
|
||||
if "jobData" in currentData.keys() and currentData["jobData"] is not None:
|
||||
params["filename"] = currentData["jobData"]["filename"]
|
||||
if "progress" in currentData.keys() and currentData["progress"] is not None and currentData["jobData"]["lines"] is not None:
|
||||
params["progress"] = str(round(currentData["progress"] * 100 / currentData["jobData"]["lines"]))
|
||||
|
||||
return command % params
|
||||
|
||||
class SystemCommandTrigger(CommandTrigger):
|
||||
"""
|
||||
Performs configured system commands for configured events.
|
||||
"""
|
||||
|
||||
def __init__(self, printer):
|
||||
CommandTrigger.__init__(self, "systemCommandTrigger", printer)
|
||||
|
||||
def executeCommand(self, command):
|
||||
try:
|
||||
command_string = self.doStringProcessing(command_string)
|
||||
self.logger.info("Executing system command: %s" % 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)
|
||||
self._logger.info("Executing system command: %s" % command)
|
||||
subprocess.Popen(command, shell=True)
|
||||
except subprocess.CalledProcessError, e:
|
||||
self.logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message))
|
||||
self._logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message))
|
||||
except Exception, ex:
|
||||
self.logger.exception("Command failed")
|
||||
self._logger.exception("Command failed")
|
||||
|
||||
class GcodeCommandTrigger(CommandTrigger):
|
||||
"""
|
||||
Sends configured GCODE commands to the printer for configured events.
|
||||
"""
|
||||
|
||||
def __init__(self, printer):
|
||||
CommandTrigger.__init__(self, "gcodeCommandTrigger", printer)
|
||||
|
||||
def executeCommand(self, command):
|
||||
self._logger.debug("Executing GCode command: %s" % command)
|
||||
self._printer.commands(command.split(","))
|
||||
|
|
|
@ -14,6 +14,7 @@ import octoprint.util.comm as comm
|
|||
import octoprint.util as util
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
|
||||
def getConnectionOptions():
|
||||
"""
|
||||
|
@ -27,9 +28,8 @@ def getConnectionOptions():
|
|||
}
|
||||
|
||||
class Printer():
|
||||
def __init__(self, gcodeManager,eventManager):
|
||||
def __init__(self, gcodeManager):
|
||||
self._gcodeManager = gcodeManager
|
||||
self._eventManager = eventManager
|
||||
|
||||
# state
|
||||
self._temp = None
|
||||
|
@ -124,7 +124,7 @@ class Printer():
|
|||
try: callback.sendCurrentData(copy.deepcopy(data))
|
||||
except: pass
|
||||
|
||||
#~~ printer commands
|
||||
#~~ printer commands
|
||||
|
||||
def connect(self, port=None, baudrate=None):
|
||||
"""
|
||||
|
@ -134,7 +134,6 @@ 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):
|
||||
"""
|
||||
|
@ -143,6 +142,7 @@ class Printer():
|
|||
if self._comm is not None:
|
||||
self._comm.close()
|
||||
self._comm = None
|
||||
eventManager().fire("Disconnected")
|
||||
|
||||
def command(self, command):
|
||||
"""
|
||||
|
@ -196,8 +196,6 @@ class Printer():
|
|||
|
||||
self._setCurrentZ(-1)
|
||||
|
||||
#self._eventManager.fire('PrintStarted', self.filename)
|
||||
|
||||
self._comm.printGCode(self._gcodeList)
|
||||
|
||||
def togglePausePrint(self):
|
||||
|
@ -225,6 +223,7 @@ class Printer():
|
|||
# mark print as failure
|
||||
if self._filename:
|
||||
self._gcodeManager.printFailed(self._filename)
|
||||
eventManager().fire("PrintFailed", self._filename)
|
||||
|
||||
#~~ state monitoring
|
||||
|
||||
|
@ -344,6 +343,9 @@ class Printer():
|
|||
"ready": self.isReady()
|
||||
}
|
||||
|
||||
def getCurrentData(self):
|
||||
return self._stateMonitor.getCurrentData()
|
||||
|
||||
#~~ callbacks triggered from self._comm
|
||||
|
||||
def mcLog(self, message):
|
||||
|
@ -368,24 +370,12 @@ 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.fire('PrintStarted', self._filename)
|
||||
if state == self._comm.STATE_OPERATIONAL and (oldState <= self._comm.STATE_CONNECTING or oldState >=self._comm.STATE_CLOSED):
|
||||
self._eventManager.fire('Connected',self._comm._port+" at " +self._comm._baudrate)
|
||||
if state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR:
|
||||
self._eventManager.fire('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:
|
||||
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._eventManager.fire('PrintDone', 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._eventManager.fire('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
|
||||
|
@ -424,7 +414,7 @@ class Printer():
|
|||
self.peakZ = newZ
|
||||
if self._timelapse is not None:
|
||||
self._timelapse.onZChange(oldZ, newZ)
|
||||
self._eventManager.FireEvent ('ZChange',newZ)
|
||||
eventManager().fire("ZChange", newZ)
|
||||
|
||||
self._setCurrentZ(newZ)
|
||||
|
||||
|
@ -442,10 +432,11 @@ class Printer():
|
|||
self._setCurrentZ(None)
|
||||
self._setProgressData(None, None, None)
|
||||
self._gcodeLoader = None
|
||||
self._eventManager.fire("LoadDone", filename)
|
||||
self._stateMonitor.setGcodeData({"filename": None, "progress": None})
|
||||
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
|
||||
|
||||
eventManager().fire("LoadDone", filename)
|
||||
|
||||
def _onGcodeLoadedToPrint(self, filename, gcodeList):
|
||||
self._onGcodeLoaded(filename, gcodeList)
|
||||
self.startPrint()
|
||||
|
|
|
@ -20,7 +20,6 @@ import octoprint.gcodefiles as gcodefiles
|
|||
import octoprint.util as util
|
||||
import octoprint.users as users
|
||||
|
||||
|
||||
import octoprint.events as events
|
||||
|
||||
SUCCESS = {}
|
||||
|
@ -32,7 +31,7 @@ app = Flask("octoprint")
|
|||
printer = None
|
||||
gcodeManager = None
|
||||
userManager = None
|
||||
eventManager =None
|
||||
eventManager = None
|
||||
|
||||
principals = Principal(app)
|
||||
admin_permission = Permission(RoleNeed("admin"))
|
||||
|
@ -41,7 +40,7 @@ user_permission = Permission(RoleNeed("user"))
|
|||
#~~ Printer state
|
||||
|
||||
class PrinterStateConnection(tornadio2.SocketConnection):
|
||||
def __init__(self, printer, gcodeManager, userManager, session, endpoint=None):
|
||||
def __init__(self, printer, gcodeManager, userManager, eventManager, session, endpoint=None):
|
||||
tornadio2.SocketConnection.__init__(self, session, endpoint)
|
||||
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
@ -56,23 +55,24 @@ class PrinterStateConnection(tornadio2.SocketConnection):
|
|||
self._printer = printer
|
||||
self._gcodeManager = gcodeManager
|
||||
self._userManager = userManager
|
||||
self._eventManager = eventManager
|
||||
|
||||
def on_open(self, info):
|
||||
global eventManager
|
||||
eventManager.fire("ClientOpen")
|
||||
self._logger.info("New connection from client")
|
||||
# Use of global here is smelly
|
||||
printer.registerCallback(self)
|
||||
gcodeManager.registerCallback(self)
|
||||
|
||||
self._eventManager.fire("ClientOpened")
|
||||
|
||||
def on_close(self):
|
||||
global eventManager
|
||||
eventManager.fire("ClientClosed")
|
||||
self._logger.info("Closed client connection")
|
||||
# Use of global here is smelly
|
||||
printer.unregisterCallback(self)
|
||||
gcodeManager.unregisterCallback(self)
|
||||
|
||||
self._eventManager.fire("ClientClosed")
|
||||
|
||||
def on_message(self, message):
|
||||
pass
|
||||
|
||||
|
@ -152,7 +152,6 @@ def connect():
|
|||
printer.connect(port=port, baudrate=baudrate)
|
||||
elif "command" in request.values.keys() and request.values["command"] == "disconnect":
|
||||
printer.disconnect()
|
||||
eventManager.fire("Disconnected")
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
@ -188,10 +187,8 @@ def printJobControl():
|
|||
printer.startPrint()
|
||||
elif request.values["command"] == "pause":
|
||||
printer.togglePausePrint()
|
||||
eventManager.fire("Paused")
|
||||
elif request.values["command"] == "cancel":
|
||||
printer.cancelPrint()
|
||||
eventManager.fire("Cancelled")
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + "control/temperature", methods=["POST"])
|
||||
|
@ -281,8 +278,9 @@ def uploadGcodeFile():
|
|||
if "gcode_file" in request.files.keys():
|
||||
file = request.files["gcode_file"]
|
||||
filename = gcodeManager.addFile(file)
|
||||
|
||||
global eventManager
|
||||
eventManager.fire("Upload",filename)
|
||||
eventManager.fire("Upload", filename)
|
||||
return jsonify(files=gcodeManager.getAllFileData(), filename=filename)
|
||||
|
||||
@app.route(BASEURL + "gcodefiles/load", methods=["POST"])
|
||||
|
@ -295,8 +293,9 @@ def loadGcodeFile():
|
|||
filename = gcodeManager.getAbsolutePath(request.values["filename"])
|
||||
if filename is not None:
|
||||
printer.loadGcode(filename, printAfterLoading)
|
||||
|
||||
global eventManager
|
||||
eventManager.fire("LoadStart",filename)
|
||||
eventManager.fire("LoadStart", filename)
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
|
||||
|
@ -350,12 +349,11 @@ def deleteTimelapse(filename):
|
|||
@app.route(BASEURL + "timelapse", methods=["POST"])
|
||||
@login_required
|
||||
def setTimelapseConfig():
|
||||
global eventManager
|
||||
if request.values.has_key("type"):
|
||||
type = request.values["type"]
|
||||
lapse = None
|
||||
if "zchange" == type:
|
||||
lapse = timelapse.ZTimelapse(eventManager)
|
||||
lapse = timelapse.ZTimelapse()
|
||||
elif "timed" == type:
|
||||
interval = 10
|
||||
if request.values.has_key("interval"):
|
||||
|
@ -363,7 +361,7 @@ def setTimelapseConfig():
|
|||
interval = int(request.values["interval"])
|
||||
except ValueError:
|
||||
pass
|
||||
lapse = timelapse.TimedTimelapse( eventManager,interval)
|
||||
lapse = timelapse.TimedTimelapse(interval)
|
||||
printer.setTimelapse(lapse)
|
||||
|
||||
return getTimelapseData()
|
||||
|
@ -671,16 +669,13 @@ class Server():
|
|||
self._initLogging(self._debug)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
eventManager = events.EventManager()
|
||||
eventManager = events.eventManager()
|
||||
gcodeManager = gcodefiles.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")
|
||||
|
||||
printer = Printer(gcodeManager)
|
||||
|
||||
# setup system and gcode command triggers
|
||||
events.SystemCommandTrigger(printer)
|
||||
events.GcodeCommandTrigger(printer)
|
||||
|
||||
if settings().getBoolean(["accessControl", "enabled"]):
|
||||
userManagerName = settings().get(["accessControl", "userManager"])
|
||||
|
@ -719,8 +714,8 @@ class Server():
|
|||
IOLoop.instance().start()
|
||||
|
||||
def _createSocketConnection(self, session, endpoint=None):
|
||||
global printer, gcodeManager, userManager
|
||||
return PrinterStateConnection(printer, gcodeManager, userManager, session, endpoint)
|
||||
global printer, gcodeManager, userManager, eventManager
|
||||
return PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session, endpoint)
|
||||
|
||||
def _initSettings(self, configfile, basedir):
|
||||
s = settings(init=True, basedir=basedir, configfile=configfile)
|
||||
|
|
|
@ -68,13 +68,20 @@ default_settings = {
|
|||
},
|
||||
"controls": [],
|
||||
"system": {
|
||||
"actions": [],
|
||||
"events": []
|
||||
"actions": []
|
||||
},
|
||||
"accessControl": {
|
||||
"enabled": False,
|
||||
"userManager": "octoprint.users.FilebasedUserManager",
|
||||
"userfile": None
|
||||
},
|
||||
"events": {
|
||||
"systemCommandTrigger": {
|
||||
"enabled": False
|
||||
},
|
||||
"gcodeCommandTrigger": {
|
||||
"enabled": False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
# coding=utf-8
|
||||
import logging
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
from octoprint.settings import settings
|
||||
import octoprint.util as util
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import urllib
|
||||
|
@ -14,10 +11,13 @@ import time
|
|||
import subprocess
|
||||
import fnmatch
|
||||
import datetime
|
||||
import octoprint.events as events
|
||||
|
||||
import sys
|
||||
|
||||
import octoprint.util as util
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
|
||||
def getFinishedTimelapses():
|
||||
files = []
|
||||
basedir = settings().getBaseFolder("timelapse")
|
||||
|
@ -34,9 +34,8 @@ def getFinishedTimelapses():
|
|||
return files
|
||||
|
||||
class Timelapse(object):
|
||||
def __init__(self,ev):
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._eventManager = ev
|
||||
self._imageNumber = None
|
||||
self._inTimelapse = False
|
||||
self._gcodeFile = None
|
||||
|
@ -86,15 +85,15 @@ 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.fire("CaptureStart",filename);
|
||||
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})
|
||||
captureThread.daemon = True
|
||||
captureThread.start()
|
||||
|
||||
def _captureWorker(self, filename):
|
||||
eventManager().fire("CaptureStart", filename);
|
||||
urllib.urlretrieve(self._snapshotUrl, filename)
|
||||
self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl))
|
||||
self._eventManager.fire("CaptureDone",filename);
|
||||
eventManager().fire("CaptureDone", filename);
|
||||
|
||||
def _createMovie(self):
|
||||
ffmpeg = settings().get(["webcam", "ffmpeg"])
|
||||
|
@ -121,10 +120,10 @@ class Timelapse(object):
|
|||
command.extend(['-vf', 'movie=%s [wm]; [in][wm] overlay=10:main_h-overlay_h-10 [out]' % watermark])
|
||||
|
||||
# finalize command with output file
|
||||
self._logger.debug("Rendering movie to %s" % output)
|
||||
command.append(output)
|
||||
subprocess.call(command)
|
||||
self._logger.debug("Rendering movie to %s" % output)
|
||||
self._eventManager.fire("MovieDone",output);
|
||||
eventManager().fire("MovieDone", output);
|
||||
|
||||
def cleanCaptureDir(self):
|
||||
if not os.path.isdir(self._captureDir):
|
||||
|
@ -137,8 +136,8 @@ class Timelapse(object):
|
|||
os.remove(os.path.join(self._captureDir, filename))
|
||||
|
||||
class ZTimelapse(Timelapse):
|
||||
def __init__(self,ev):
|
||||
Timelapse.__init__(self,ev)
|
||||
def __init__(self):
|
||||
Timelapse.__init__(self)
|
||||
self._logger.debug("ZTimelapse initialized")
|
||||
|
||||
def onZChange(self, oldZ, newZ):
|
||||
|
@ -146,8 +145,8 @@ class ZTimelapse(Timelapse):
|
|||
self.captureImage()
|
||||
|
||||
class TimedTimelapse(Timelapse):
|
||||
def __init__(self, ev,interval=1):
|
||||
Timelapse.__init__(self,ev)
|
||||
def __init__(self, interval=1):
|
||||
Timelapse.__init__(self)
|
||||
|
||||
self._interval = interval
|
||||
if self._interval < 1:
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import re
|
||||
|
||||
def getFormattedSize(num):
|
||||
"""
|
||||
Taken from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
|
||||
|
@ -38,4 +40,7 @@ def getClass(name):
|
|||
m = __import__(module)
|
||||
for comp in parts[1:]:
|
||||
m = getattr(m, comp)
|
||||
return m
|
||||
return m
|
||||
|
||||
def matchesGcode(line, gcode):
|
||||
return re.search("^\s*%s\D" % gcode, line, re.I)
|
|
@ -17,6 +17,7 @@ from octoprint.util.avr_isp import stk500v2
|
|||
from octoprint.util.avr_isp import ispBase
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
|
||||
try:
|
||||
import _winreg
|
||||
|
@ -55,6 +56,21 @@ def baudrateList():
|
|||
ret.insert(0, prev)
|
||||
return ret
|
||||
|
||||
gcodeToEvent = {
|
||||
"M226": "Waiting", # pause for user input
|
||||
"M0": "Waiting",
|
||||
"M1": "Waiting",
|
||||
"M245": "Cooling", # part cooler
|
||||
"M240": "Conveyor", # part conveyor
|
||||
"M40": "Eject", # part ejector
|
||||
"M300": "Alert", # user alert
|
||||
"G28": "Home", # home print head
|
||||
"M112": "EStop",
|
||||
"M80": "PowerOn",
|
||||
"M81": "PowerOff",
|
||||
"M25": "Paused" # SD Card pause
|
||||
}
|
||||
|
||||
class VirtualPrinter():
|
||||
def __init__(self):
|
||||
self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
|
||||
|
@ -178,15 +194,11 @@ 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
|
||||
|
@ -312,6 +324,7 @@ class MachineCom(object):
|
|||
self._log("Failed to open serial port (%s)" % (self._port))
|
||||
self._errorValue = 'Failed to autodetect serial port.'
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire("Error", self.getErrorString())
|
||||
return
|
||||
self._log("Connected to: %s, starting monitor" % (self._serial))
|
||||
if self._baudrate == 0:
|
||||
|
@ -340,8 +353,12 @@ class MachineCom(object):
|
|||
if 'checksum mismatch' in line or 'Line Number is not Last Line Number' in line or 'No Line Number with checksum' in line or 'No Checksum with line number' in line:
|
||||
pass
|
||||
elif not self.isError():
|
||||
if self.isPrinting():
|
||||
eventManager().fire("PrintFailed")
|
||||
|
||||
self._errorValue = line[6:]
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire("Error", self.getErrorString())
|
||||
if ' T:' in line or line.startswith('T:'):
|
||||
self._temp = float(re.search("-?[0-9\.]*", line.split('T:')[1]).group(0))
|
||||
if ' B:' in line:
|
||||
|
@ -361,6 +378,7 @@ class MachineCom(object):
|
|||
self.close()
|
||||
self._errorValue = "No more baudrates to test, and no suitable baudrate found."
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire("Error", self.getErrorString())
|
||||
elif self._baudrateDetectRetry > 0:
|
||||
self._baudrateDetectRetry -= 1
|
||||
self._serial.write('\n')
|
||||
|
@ -390,6 +408,7 @@ class MachineCom(object):
|
|||
self._sendCommand("M999")
|
||||
self._serial.timeout = 2
|
||||
self._changeState(self.STATE_OPERATIONAL)
|
||||
eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate))
|
||||
else:
|
||||
self._testingBaudrate = False
|
||||
elif self._state == self.STATE_CONNECTING:
|
||||
|
@ -399,6 +418,7 @@ class MachineCom(object):
|
|||
startSeen = True
|
||||
elif 'ok' in line and startSeen:
|
||||
self._changeState(self.STATE_OPERATIONAL)
|
||||
eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate))
|
||||
elif time.time() > timeout:
|
||||
self.close()
|
||||
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
|
||||
|
@ -458,6 +478,7 @@ class MachineCom(object):
|
|||
return ret
|
||||
|
||||
def close(self, isError = False):
|
||||
printing = self.isPrinting() or self.isPaused()
|
||||
if self._serial != None:
|
||||
self._serial.close()
|
||||
if isError:
|
||||
|
@ -465,53 +486,21 @@ class MachineCom(object):
|
|||
else:
|
||||
self._changeState(self.STATE_CLOSED)
|
||||
self._serial = None
|
||||
|
||||
|
||||
if printing:
|
||||
eventManager().fire("PrintFailed")
|
||||
eventManager().fire("Disconnected")
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
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.fire ('Waiting')
|
||||
# part cooler started
|
||||
if re.search ("^\s*M245\D",t_cmd,re.I):
|
||||
self._eventManager.fire ('Cooling')
|
||||
# part conveyor started
|
||||
if re.search ("^\s*M240\D",t_cmd,re.I):
|
||||
self._eventManager.fire ('Conveyor')
|
||||
# part ejector
|
||||
if re.search ("^\s*M40\D",t_cmd,re.I):
|
||||
self._eventManager.fire ('Eject')
|
||||
# user alert issued by sending beep command to printer...
|
||||
if re.search ("^\s*M300\D",t_cmd,re.I):
|
||||
self._eventManager.fire ('Alert')
|
||||
# Print head has moved to home
|
||||
if re.search ("^\s*G28\D",t_cmd,re.I):
|
||||
self._eventManager.fire ('Home')
|
||||
if re.search ("^\s*M112\D",t_cmd,re.I):
|
||||
self._eventManager.fire ('EStop')
|
||||
if re.search ("^\s*M80\D",t_cmd,re.I):
|
||||
self._eventManager.fire ('PowerOn')
|
||||
if re.search ("^\s*M81\D",t_cmd,re.I):
|
||||
self._eventManager.fire ('PowerOff')
|
||||
if re.search ("^\s*M25\D",t_cmd,re.I): # SD Card pause
|
||||
self._eventManager.fire ('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)
|
||||
for gcode in gcodeToEvent.keys():
|
||||
if gcode in cmd:
|
||||
eventManager().fire(gcodeToEvent[gcode])
|
||||
|
||||
if 'M109' in cmd or 'M190' in cmd:
|
||||
self._heatupWaitStartTime = time.time()
|
||||
|
@ -544,7 +533,9 @@ class MachineCom(object):
|
|||
def _sendNext(self):
|
||||
if self._gcodePos >= len(self._gcodeList):
|
||||
self._changeState(self.STATE_OPERATIONAL)
|
||||
eventManager().fire('PrintDone')
|
||||
return
|
||||
|
||||
if self._gcodePos == 100:
|
||||
self._printStartTime100 = time.time()
|
||||
line = self._gcodeList[self._gcodePos]
|
||||
|
@ -552,13 +543,9 @@ class MachineCom(object):
|
|||
self._printSection = line[1]
|
||||
line = line[0]
|
||||
try:
|
||||
if line == 'M0' or line == 'M1' or line=='M112': # M112 is also an LCD pause
|
||||
if line == 'M0' or line == 'M1':
|
||||
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.
|
||||
|
||||
# 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:
|
||||
|
@ -591,11 +578,13 @@ class MachineCom(object):
|
|||
self._printStartTime = time.time()
|
||||
for i in xrange(0, 6):
|
||||
self._sendNext()
|
||||
eventManager().fire("PrintStarted")
|
||||
|
||||
def cancelPrint(self):
|
||||
if self.isOperational():
|
||||
self._changeState(self.STATE_OPERATIONAL)
|
||||
|
||||
eventManager().fire("PrintCancelled")
|
||||
|
||||
def setPause(self, pause):
|
||||
if not pause and self.isPaused():
|
||||
self._changeState(self.STATE_PRINTING)
|
||||
|
@ -603,6 +592,7 @@ class MachineCom(object):
|
|||
self._sendNext()
|
||||
if pause and self.isPrinting():
|
||||
self._changeState(self.STATE_PAUSED)
|
||||
eventManager().fire("Paused")
|
||||
|
||||
def setFeedrateModifier(self, type, value):
|
||||
self._feedRateModifier[type] = value
|
||||
|
|
Loading…
Reference in New Issue