From 236e26979f8dd722e4b819993afeee9a9865e36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 16 Jun 2013 21:50:50 +0200 Subject: [PATCH] Switched Timelapses to using Eventbus instead of direct connection --- octoprint/events.md | 5 +-- octoprint/events.py | 21 +++++++++--- octoprint/printer.py | 13 ++------ octoprint/server.py | 13 ++++++-- octoprint/static/js/ui.js | 31 +++++++++--------- octoprint/templates/index.jinja2 | 2 +- octoprint/timelapse.py | 55 ++++++++++++++++++++++---------- octoprint/util/comm.py | 27 ++++++++++------ 8 files changed, 105 insertions(+), 62 deletions(-) diff --git a/octoprint/events.md b/octoprint/events.md index aa5b28c..62932bd 100644 --- a/octoprint/events.md +++ b/octoprint/events.md @@ -40,8 +40,9 @@ Available Events: * ''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) + * ''FileSelected'': a gcode file has been selected for printing (data is filename) + * ''TransferStart'': a gcode file transfer to SD has started (data is filename) + * ''TransferDone'': a gcode file transfer to SD has finished (data is filename) * ''PrintStarted'': a print has started * ''PrintFailed'': a print failed * ''PrintDone'': a print completed successfully diff --git a/octoprint/events.py b/octoprint/events.py index 3c003eb..9769355 100644 --- a/octoprint/events.py +++ b/octoprint/events.py @@ -41,7 +41,7 @@ class EventManager(object): if not event in self._registeredListeners.keys(): return - self._logger.debug("Firing event: %s (%r)" % (event, payload)) + self._logger.debug("Firing event: %s (Payload: %r)" % (event, payload)) eventListeners = self._registeredListeners[event] for listener in eventListeners: @@ -107,6 +107,20 @@ class GenericEventListener(object): """ pass +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)) + class CommandTrigger(GenericEventListener): def __init__(self, triggerType, printer): GenericEventListener.__init__(self) @@ -194,9 +208,8 @@ class CommandTrigger(GenericEventListener): 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 \ - and "progress" in currentData["progress"].keys() and currentData["progress"]["progress"] is not None \ - and currentData["job"]["lines"] is not None: - params["progress"] = str(round(currentData["progress"]["progress"] * 100 / currentData["job"]["lines"])) + and "progress" in currentData["progress"].keys() and currentData["progress"]["progress"] is not None: + params["progress"] = str(round(currentData["progress"]["progress"] * 100)) return command % params diff --git a/octoprint/printer.py b/octoprint/printer.py index 7e7eaea..a82d45c 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -217,7 +217,7 @@ class Printer(): def setTimelapse(self, timelapse): if self._timelapse is not None and self.isPrinting(): - self._timelapse.onPrintjobStopped() + self._timelapse.stopTimelapse() del self._timelapse self._timelapse = timelapse @@ -368,13 +368,6 @@ class Printer(): """ oldState = self._state - # forward relevant state changes to timelapse - if self._timelapse is not None: - if oldState == self._comm.STATE_PRINTING and state != self._comm.STATE_PAUSED: - self._timelapse.onPrintjobStopped() - elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED: - self._timelapse.onPrintjobStarted(self._selectedFile["filename"]) - # forward relevant state changes to gcode manager if self._comm is not None and oldState == self._comm.STATE_PRINTING: if self._selectedFile is not None: @@ -409,11 +402,9 @@ class Printer(): """ oldZ = self._currentZ # only do this if we hit a new Z peak level. Some slicers do a Z-lift when retracting / moving without printing - # and some do ananti-backlash up-then-down movement when advancing layers + # and some do anti-backlash up-then-down movement when advancing layers if newZ > self.peakZ: self.peakZ = newZ - if self._timelapse is not None: - self._timelapse.onZChange(oldZ, newZ) eventManager().fire("ZChange", newZ) self._setCurrentZ(newZ) diff --git a/octoprint/server.py b/octoprint/server.py index 6444af3..c575d3e 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -64,6 +64,7 @@ class PrinterStateConnection(tornadio2.SocketConnection): gcodeManager.registerCallback(self) self._eventManager.fire("ClientOpened") + self._eventManager.subscribe("MovieDone", self._onMovieDone) def on_close(self): self._logger.info("Closed client connection") @@ -72,6 +73,7 @@ class PrinterStateConnection(tornadio2.SocketConnection): gcodeManager.unregisterCallback(self) self._eventManager.fire("ClientClosed") + self._eventManager.unsubscribe("MovieDone", self._onMovieDone) def on_message(self, message): pass @@ -115,6 +117,9 @@ class PrinterStateConnection(tornadio2.SocketConnection): with self._temperatureBacklogMutex: self._temperatureBacklog.append(data) + def _onMovieDone(self, event, payload): + self.sendUpdateTrigger("timelapseFiles") + # Did attempt to make webserver an encapsulated class but ended up with __call__ failures @app.route("/") @@ -364,9 +369,9 @@ def getTimelapseData(): file["url"] = url_for("downloadTimelapse", filename=file["name"]) return jsonify({ - "type": type, - "config": additionalConfig, - "files": files + "type": type, + "config": additionalConfig, + "files": files }) @app.route(BASEURL + "timelapse/", methods=["GET"]) @@ -719,6 +724,8 @@ class Server(): # setup system and gcode command triggers events.SystemCommandTrigger(printer) events.GcodeCommandTrigger(printer) + if self._debug: + events.DebugEventListener() if settings().getBoolean(["accessControl", "enabled"]): userManagerName = settings().get(["accessControl", "userManager"]) diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index e585a75..126736f 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -999,8 +999,7 @@ function TimelapseViewModel(loginStateViewModel) { self.isLoading(data.flags.loading); } - self.removeFile = function() { - var filename = this.name; + self.removeFile = function(filename) { $.ajax({ url: AJAX_BASEURL + "timelapse/" + filename, type: "DELETE", @@ -1526,6 +1525,8 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM self._socket.on("updateTrigger", function(type) { if (type == "gcodeFiles") { gcodeFilesViewModel.requestData(); + } else if (type == "timelapseFiles") { + timelapseViewModel.requestData(); } }) @@ -1737,28 +1738,28 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor self._saveCurrentSortingToLocalStorage = function() { if ( self._initializeLocalStorage() ) { - var currentSorting = self.currentSorting(); - if (currentSorting !== undefined) - localStorage[self.listType + "." + "currentSorting"] = currentSorting; - else - localStorage[self.listType + "." + "currentSorting"] = undefined; + var currentSorting = self.currentSorting(); + if (currentSorting !== undefined) + localStorage[self.listType + "." + "currentSorting"] = currentSorting; + else + localStorage[self.listType + "." + "currentSorting"] = undefined; } } self._loadCurrentSortingFromLocalStorage = function() { if ( self._initializeLocalStorage() ) { - if (_.contains(_.keys(supportedSorting), localStorage[self.listType + "." + "currentSorting"])) - self.currentSorting(localStorage[self.listType + "." + "currentSorting"]); - else - self.currentSorting(defaultSorting); - } + if (_.contains(_.keys(supportedSorting), localStorage[self.listType + "." + "currentSorting"])) + self.currentSorting(localStorage[self.listType + "." + "currentSorting"]); + else + self.currentSorting(defaultSorting); + } } self._saveCurrentFiltersToLocalStorage = function() { if ( self._initializeLocalStorage() ) { - var filters = _.intersection(_.keys(self.supportedFilters), self.currentFilters()); - localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(filters); - } + var filters = _.intersection(_.keys(self.supportedFilters), self.currentFilters()); + localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(filters); + } } self._loadCurrentFiltersFromLocalStorage = function() { diff --git a/octoprint/templates/index.jinja2 b/octoprint/templates/index.jinja2 index 0684fbc..b4ad9b7 100644 --- a/octoprint/templates/index.jinja2 +++ b/octoprint/templates/index.jinja2 @@ -542,7 +542,7 @@ -  |  +  |  diff --git a/octoprint/timelapse.py b/octoprint/timelapse.py index 35db867..0730546 100644 --- a/octoprint/timelapse.py +++ b/octoprint/timelapse.py @@ -47,13 +47,30 @@ class Timelapse(object): self._renderThread = None self._captureMutex = threading.Lock() - def onPrintjobStarted(self, gcodeFile): - self.startTimelapse(gcodeFile) + eventManager().subscribe("PrintStarted", self.onPrintStarted) + eventManager().subscribe("PrintFailed", self.onPrintDone) + eventManager().subscribe("PrintDone", self.onPrintDone) + self.subscribeToEvents() - def onPrintjobStopped(self): + def onPrintStarted(self, event, payload): + """ + Override this to perform additional actions upon start of a print job. + """ + self.startTimelapse(payload) + + def onPrintDone(self, event, payload): + """ + Override this to perform additional actions upon the stop of a print job. + """ self.stopTimelapse() - def onZChange(self, oldZ, newZ): + def subscribeToEvents(self): + """ + Override this method to subscribe to additional events. Events that are already subscribed: + * PrintStarted - self.onPrintStarted + * PrintFailed - self.onPrintDone + * PrintDone - self.onPrintDone + """ pass def startTimelapse(self, gcodeFile): @@ -96,7 +113,7 @@ class Timelapse(object): ffmpeg = settings().get(["webcam", "ffmpeg"]) bitrate = settings().get(["webcam", "bitrate"]) if ffmpeg is None or bitrate is None: - self._logger.warn("Cannot create movie, path to ffmpeg is unset") + self._logger.warn("Cannot create movie, path to ffmpeg or desired bitrate is unset") return input = os.path.join(self._captureDir, "tmp_%05d.jpg") @@ -119,8 +136,11 @@ class Timelapse(object): # finalize command with output file self._logger.debug("Rendering movie to %s" % output) command.append(output) - subprocess.call(command) - eventManager().fire("MovieDone", output); + try: + subprocess.check_call(command) + eventManager().fire("MovieDone", output); + except subprocess.CalledProcessError as (e): + self._logger.warn("Could not render movie, got return code %r" % e.returncode) def cleanCaptureDir(self): if not os.path.isdir(self._captureDir): @@ -137,35 +157,38 @@ class ZTimelapse(Timelapse): Timelapse.__init__(self) self._logger.debug("ZTimelapse initialized") - def onZChange(self, oldZ, newZ): - self._logger.debug("Z change detected, capturing image") + def subscribeToEvents(self): + eventManager().subscribe("ZChange", self._onZChange) + + def _onZChange(self, event, payload): self.captureImage() class TimedTimelapse(Timelapse): def __init__(self, interval=1): Timelapse.__init__(self) - self._interval = interval if self._interval < 1: self._interval = 1 # force minimum interval of 1s - self._timerThread = None - self._logger.debug("TimedTimelapse initialized") def interval(self): return self._interval - def onPrintjobStarted(self, filename): - Timelapse.onPrintjobStarted(self, filename) + def onPrintStarted(self, event, payload): + Timelapse.onPrintStarted(self, event, payload) if self._timerThread is not None: return - self._timerThread = threading.Thread(target=self.timerWorker) + self._timerThread = threading.Thread(target=self._timerWorker) self._timerThread.daemon = True self._timerThread.start() - def timerWorker(self): + def onPrintDone(self, event, payload): + Timelapse.onPrintDone(self, event, payload) + self._timerThread = None + + def _timerWorker(self): self._logger.debug("Starting timer for interval based timelapse") while self._inTimelapse: self.captureImage() diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index b9f5e50..c720b56 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -345,7 +345,7 @@ class MachineCom(object): STATE_CLOSED = 8 STATE_ERROR = 9 STATE_CLOSED_WITH_ERROR = 10 - STATE_RECEIVING_FILE = 11 + STATE_TRANSFERING_FILE = 11 def __init__(self, port = None, baudrate = None, callbackObject = None): self._logger = logging.getLogger(__name__) @@ -445,8 +445,8 @@ class MachineCom(object): return "Error: %s" % (self.getShortErrorString()) if self._state == self.STATE_CLOSED_WITH_ERROR: return "Error: %s" % (self.getShortErrorString()) - if self._state == self.STATE_RECEIVING_FILE: - return "Sending file to SD" + if self._state == self.STATE_TRANSFERING_FILE: + return "Transfering file to SD" return "?%d?" % (self._state) def getShortErrorString(self): @@ -464,7 +464,7 @@ class MachineCom(object): return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR def isOperational(self): - return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED or self._state == self.STATE_RECEIVING_FILE + return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED or self._state == self.STATE_TRANSFERING_FILE def isPrinting(self): return self._state == self.STATE_PRINTING @@ -611,7 +611,7 @@ class MachineCom(object): eventManager().fire("Error", self.getErrorString()) - ##~~ SD file list + ##~~ SD file list # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing if self._sdFileList and not 'End file list' in line: self._sdFiles.append(line.strip().lower()) @@ -669,6 +669,7 @@ class MachineCom(object): elif 'File selected' in line: # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" self._callback.mcFileSelected(self._currentFile.getFilename(), self._currentFile.getFilesize(), True) + eventManager().fire("FileSelected", self._currentFile.getFilename()) elif 'Writing to file' in line: # anwer to M28, at least on Marlin, Repetier and Sprinter: "Writing to file: %s" self._printSection = "CUSTOM" @@ -676,8 +677,9 @@ class MachineCom(object): elif 'Done printing file' in line: # printer is reporting file finished printing self._sdFilePos = 0 - self._changeState(self.STATE_OPERATIONAL) self._callback.mcPrintjobDone() + self._changeState(self.STATE_OPERATIONAL) + eventManager().fire("PrintDone") ##~~ Message handling elif line.strip() != '' and line.strip() != 'ok' and not line.startswith("wait") and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational(): @@ -971,10 +973,12 @@ class MachineCom(object): self._sendCommand("M29") self._currentFile = None self._callback.mcFileTransferDone() + self._changeState(self.STATE_OPERATIONAL) + eventManager().fire("TransferDone", self._currentFile.getFilename()) else: self._callback.mcPrintjobDone() - self._changeState(self.STATE_OPERATIONAL) - eventManager().fire('PrintDone') + self._changeState(self.STATE_OPERATIONAL) + eventManager().fire("PrintDone", self._currentFile.getFilename()) return if type(line) is tuple: @@ -1012,7 +1016,7 @@ class MachineCom(object): self._printSection = "CUSTOM" self._changeState(self.STATE_PRINTING) - eventManager().fire("PrintStarted") + eventManager().fire("PrintStarted", self._currentFile.getFilename()) try: self._currentFile.start() @@ -1035,6 +1039,7 @@ class MachineCom(object): self._currentFile.start() self.sendCommand("M28 %s" % remoteFilename) + eventManager().fire("TransferStart", remoteFilename) self._callback.mcFileTransferStarted(remoteFilename, self._currentFile.getFilesize()) def selectFile(self, filename, sd): @@ -1048,6 +1053,7 @@ class MachineCom(object): self.sendCommand("M23 %s" % filename) else: self._currentFile = PrintingGcodeFileInformation(filename) + eventManager().fire("FileSelected", filename) self._callback.mcFileSelected(filename, self._currentFile.getFilesize(), False) def cancelPrint(self): @@ -1087,7 +1093,7 @@ class MachineCom(object): if not self.isOperational() or self.isBusy(): return - self._changeState(self.STATE_RECEIVING_FILE) + self._changeState(self.STATE_TRANSFERING_FILE) self.sendCommand("M28 %s" % filename.lower()) def endSdFileTransfer(self, filename): @@ -1251,6 +1257,7 @@ class PrintingGcodeFileInformation(PrintingFileInformation): except Exception as (e): if self._filehandle is not None: self._filehandle.close() + self._filehandle = None raise e def getLineCount(self):