2013-05-26 22:56:57 +00:00
|
|
|
# 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'
|
|
|
|
|
2013-05-21 03:04:21 +00:00
|
|
|
import datetime
|
2013-05-21 20:59:13 +00:00
|
|
|
import logging
|
2013-05-21 03:04:21 +00:00
|
|
|
import subprocess
|
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
from octoprint.settings import settings
|
|
|
|
|
|
|
|
# singleton
|
|
|
|
_instance = None
|
2013-05-21 03:04:21 +00:00
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
def eventManager():
|
|
|
|
global _instance
|
|
|
|
if _instance is None:
|
|
|
|
_instance = EventManager()
|
|
|
|
return _instance
|
2013-05-21 20:59:13 +00:00
|
|
|
|
2013-05-21 03:04:21 +00:00
|
|
|
class EventManager(object):
|
2013-05-21 20:59:13 +00:00
|
|
|
"""
|
2013-05-26 22:56:57 +00:00
|
|
|
Handles receiving events and dispatching them to subscribers
|
2013-05-21 20:59:13 +00:00
|
|
|
"""
|
|
|
|
|
2013-05-21 03:04:21 +00:00
|
|
|
def __init__(self):
|
2013-05-26 22:56:57 +00:00
|
|
|
self._registeredListeners = {}
|
|
|
|
self._logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
def fire(self, event, payload=None):
|
2013-05-21 20:59:13 +00:00
|
|
|
"""
|
2013-05-26 22:56:57 +00:00
|
|
|
Fire an event to anyone subscribed to it
|
2013-05-21 20:59:13 +00:00
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
Any object can generate an event and any object can subscribe to the event's name as a string (arbitrary, but
|
2013-05-21 20:59:13 +00:00
|
|
|
case sensitive) and any extra payload data that may pertain to the event.
|
2013-05-26 22:56:57 +00:00
|
|
|
|
|
|
|
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.
|
2013-05-21 20:59:13 +00:00
|
|
|
"""
|
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
if not event in self._registeredListeners.keys():
|
|
|
|
return
|
2013-06-16 19:50:50 +00:00
|
|
|
self._logger.debug("Firing event: %s (Payload: %r)" % (event, payload))
|
2013-05-26 22:56:57 +00:00
|
|
|
|
|
|
|
eventListeners = self._registeredListeners[event]
|
|
|
|
for listener in eventListeners:
|
|
|
|
self._logger.debug("Sending action to %r" % listener)
|
2013-08-27 20:13:39 +00:00
|
|
|
try:
|
|
|
|
listener(event, payload)
|
|
|
|
except:
|
|
|
|
self._logger.exception("Got an exception while sending event %s (Payload: %r) to %s" % (event, payload, listener))
|
2013-05-21 03:04:21 +00:00
|
|
|
|
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
def subscribe(self, event, callback):
|
2013-05-21 20:59:13 +00:00
|
|
|
"""
|
2013-05-26 22:56:57 +00:00
|
|
|
Subscribe a listener to an event -- pass in the event name (as a string) and the callback object
|
2013-05-21 20:59:13 +00:00
|
|
|
"""
|
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
if not event in self._registeredListeners.keys():
|
|
|
|
self._registeredListeners[event] = []
|
2013-05-21 20:59:13 +00:00
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
if callback in self._registeredListeners[event]:
|
|
|
|
# callback is already subscribed to the event
|
|
|
|
return
|
2013-05-21 03:04:21 +00:00
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
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):
|
2013-05-21 20:59:13 +00:00
|
|
|
"""
|
2013-05-26 22:56:57 +00:00
|
|
|
The GenericEventListener can be subclassed to easily create custom event listeners.
|
2013-05-21 20:59:13 +00:00
|
|
|
"""
|
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
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
|
|
|
|
|
2013-06-16 19:50:50 +00:00
|
|
|
class DebugEventListener(GenericEventListener):
|
|
|
|
def __init__(self):
|
|
|
|
GenericEventListener.__init__(self)
|
|
|
|
|
|
|
|
events = ["Startup", "Connected", "Disconnected", "ClientOpen", "ClientClosed", "PowerOn", "PowerOff", "Upload",
|
|
|
|
"FileSelected", "TransferStarted", "TransferDone", "PrintStarted", "PrintDone", "PrintFailed",
|
|
|
|
"Cancelled", "Home", "ZChange", "Paused", "Waiting", "Cooling", "Alert", "Conveyor", "Eject",
|
|
|
|
"CaptureStart", "CaptureDone", "MovieDone", "EStop", "Error"]
|
|
|
|
self.subscribe(events)
|
|
|
|
|
|
|
|
def eventCallback(self, event, payload):
|
|
|
|
GenericEventListener.eventCallback(self, event, payload)
|
|
|
|
self._logger.debug("Received event: %s (Payload: %r)" % (event, payload))
|
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
class CommandTrigger(GenericEventListener):
|
|
|
|
def __init__(self, triggerType, printer):
|
|
|
|
GenericEventListener.__init__(self)
|
2013-05-21 03:04:21 +00:00
|
|
|
self._printer = printer
|
2013-05-26 22:56:57 +00:00
|
|
|
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]):
|
2013-05-21 03:04:21 +00:00
|
|
|
return
|
2013-05-21 20:59:13 +00:00
|
|
|
|
2013-05-26 22:56:57 +00:00
|
|
|
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:
|
|
|
|
|
2013-06-29 12:33:05 +00:00
|
|
|
- %(currentZ)s : current Z position of the print head, or -1 if not available
|
2013-05-26 22:56:57 +00:00
|
|
|
- %(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"])
|
|
|
|
|
2013-05-26 23:20:45 +00:00
|
|
|
if "job" in currentData.keys() and currentData["job"] is not None:
|
|
|
|
params["filename"] = currentData["job"]["filename"]
|
|
|
|
if "progress" in currentData.keys() and currentData["progress"] is not None \
|
2013-06-16 19:50:50 +00:00
|
|
|
and "progress" in currentData["progress"].keys() and currentData["progress"]["progress"] is not None:
|
|
|
|
params["progress"] = str(round(currentData["progress"]["progress"] * 100))
|
2013-05-26 22:56:57 +00:00
|
|
|
|
|
|
|
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):
|
2013-05-21 03:04:21 +00:00
|
|
|
try:
|
2013-05-26 22:56:57 +00:00
|
|
|
self._logger.info("Executing system command: %s" % command)
|
|
|
|
subprocess.Popen(command, shell=True)
|
2013-05-21 03:04:21 +00:00
|
|
|
except subprocess.CalledProcessError, e:
|
2013-05-26 22:56:57 +00:00
|
|
|
self._logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message))
|
2013-05-21 03:04:21 +00:00
|
|
|
except Exception, ex:
|
2013-05-26 22:56:57 +00:00
|
|
|
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(","))
|