From be999300212c5dab2c6e6fc0ed5caa42ae056d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 27 May 2013 00:56:57 +0200 Subject: [PATCH] Refactoring of event management --- octoprint/events.py | 325 +++++++++++++++++++++++-------------- octoprint/printer.py | 31 ++-- octoprint/server.py | 47 +++--- octoprint/settings.py | 11 +- octoprint/timelapse.py | 31 ++-- octoprint/util/__init__.py | 7 +- octoprint/util/comm.py | 92 +++++------ 7 files changed, 305 insertions(+), 239 deletions(-) diff --git a/octoprint/events.py b/octoprint/events.py index 9e0bfb9e..ecbd6f72 100644 --- a/octoprint/events.py +++ b/octoprint/events.py @@ -1,149 +1,228 @@ +# coding=utf-8 + +__author__ = "Lars Norpchen" +__author__ = "Gina Häußge " +__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(",")) diff --git a/octoprint/printer.py b/octoprint/printer.py index af2bd02a..cc0e41e5 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -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() diff --git a/octoprint/server.py b/octoprint/server.py index 894f26f4..5358ad05 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -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) diff --git a/octoprint/settings.py b/octoprint/settings.py index 7d2d470c..bf89f667 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -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 + } } } diff --git a/octoprint/timelapse.py b/octoprint/timelapse.py index 171cb458..1caeb0c0 100644 --- a/octoprint/timelapse.py +++ b/octoprint/timelapse.py @@ -1,12 +1,9 @@ # coding=utf-8 -import logging __author__ = "Gina Häußge " __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: diff --git a/octoprint/util/__init__.py b/octoprint/util/__init__.py index f762414c..921ea9fb 100644 --- a/octoprint/util/__init__.py +++ b/octoprint/util/__init__.py @@ -2,6 +2,8 @@ __author__ = "Gina Häußge " __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 \ No newline at end of file + return m + +def matchesGcode(line, gcode): + return re.search("^\s*%s\D" % gcode, line, re.I) \ No newline at end of file diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 022283b3..575036d5 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -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