From 0471f1155e4c3c2798196b42ea137881800a1f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 8 Sep 2013 15:41:26 +0200 Subject: [PATCH] Timelapse configuration may now be saved Persisted configuration will automatically be loaded upon startup. May be overridden by custom settings. Current timelapse configuration is visible in State box. Closes #116 --- octoprint/server.py | 73 ++++++++++++++----- octoprint/settings.py | 6 +- octoprint/static/js/app/dataupdater.js | 6 +- octoprint/static/js/app/main.js | 4 +- .../static/js/app/viewmodels/printerstate.js | 21 ++++++ .../static/js/app/viewmodels/timelapse.js | 37 +++++++--- octoprint/templates/index.jinja2 | 11 ++- octoprint/timelapse.py | 43 +++++++++++ 8 files changed, 168 insertions(+), 33 deletions(-) diff --git a/octoprint/server.py b/octoprint/server.py index 047d1f3..87f9ab1 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -75,14 +75,19 @@ class PrinterStateConnection(SockJSConnection): self._logger.info("New connection from client: %s" % self._getRemoteAddress(info)) self._printer.registerCallback(self) self._gcodeManager.registerCallback(self) + octoprint.timelapse.registerCallback(self) self._eventManager.fire("ClientOpened") self._eventManager.subscribe("MovieDone", self._onMovieDone) + global timelapse + octoprint.timelapse.notifyCallbacks(timelapse) + def on_close(self): self._logger.info("Closed client connection") self._printer.unregisterCallback(self) self._gcodeManager.unregisterCallback(self) + octoprint.timelapse.unregisterCallback(self) self._eventManager.fire("ClientClosed") self._eventManager.unsubscribe("MovieDone", self._onMovieDone) @@ -120,6 +125,9 @@ class PrinterStateConnection(SockJSConnection): def sendFeedbackCommandOutput(self, name, output): self._emit("feedbackCommandOutput", {"name": name, "output": output}) + def sendTimelapseConfig(self, timelapseConfig): + self._emit("timelapse", timelapseConfig) + def addLog(self, data): with self._logBacklogMutex: self._logBacklog.append(data) @@ -529,28 +537,56 @@ def deleteTimelapse(filename): @app.route(BASEURL + "timelapse", methods=["POST"]) @restricted_access def setTimelapseConfig(): - global timelapse - if request.values.has_key("type"): - type = request.values["type"] - if type in ["zchange", "timed"]: - # valid timelapse type, check if there is an old one we need to stop first - if timelapse is not None: - timelapse.unload() - timelapse = None - if "zchange" == type: - timelapse = octoprint.timelapse.ZTimelapse() - elif "timed" == type: + config = { + "type": request.values["type"], + "options": {} + } + + if request.values.has_key("interval"): interval = 10 - if request.values.has_key("interval"): - try: - interval = int(request.values["interval"]) - except ValueError: - pass - timelapse = octoprint.timelapse.TimedTimelapse(interval) + try: + interval = int(request.values["interval"]) + except ValueError: + pass + + config["options"] = { + "interval": interval + } + + if admin_permission.can() and request.values.has_key("save") and request.values["save"] in valid_boolean_trues: + _configureTimelapse(config, True) + else: + _configureTimelapse(config) return getTimelapseData() +def _configureTimelapse(config=None, persist=False): + global timelapse + + if config is None: + config = settings().get(["webcam", "timelapse"]) + + if timelapse is not None: + timelapse.unload() + + type = config["type"] + if type is None or "off" == type: + timelapse = None + elif "zchange" == type: + timelapse = octoprint.timelapse.ZTimelapse() + elif "timed" == type: + interval = 10 + if "options" in config and "interval" in config["options"]: + interval = config["options"]["interval"] + timelapse = octoprint.timelapse.TimedTimelapse(interval) + + octoprint.timelapse.notifyCallbacks(timelapse) + + if persist: + settings().set(["webcam", "timelapse"], config) + settings().save() + #~~ settings @app.route(BASEURL + "settings", methods=["GET"]) @@ -1056,6 +1092,9 @@ class Server(): gcodeManager = gcodefiles.GcodeManager() printer = Printer(gcodeManager) + # configure timelapse + _configureTimelapse() + # setup system and gcode command triggers events.SystemCommandTrigger(printer) events.GcodeCommandTrigger(printer) diff --git a/octoprint/settings.py b/octoprint/settings.py index f95ed10..cad4c8c 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -46,7 +46,11 @@ default_settings = { "bitrate": "5000k", "watermark": True, "flipH": False, - "flipV": False + "flipV": False, + "timelapse": { + "type": "off", + "options": {} + } }, "feature": { "gCodeVisualizer": True, diff --git a/octoprint/static/js/app/dataupdater.js b/octoprint/static/js/app/dataupdater.js index b7bfdc3..919ea76 100644 --- a/octoprint/static/js/app/dataupdater.js +++ b/octoprint/static/js/app/dataupdater.js @@ -109,7 +109,11 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM } case "feedbackCommandOutput": { self.controlViewModel.fromFeedbackCommandData(payload); - break + break; + } + case "timelapse": { + self.printerStateViewModel.fromTimelapseData(payload); + break; } } } diff --git a/octoprint/static/js/app/main.js b/octoprint/static/js/app/main.js index 2bdba57..ee11483 100644 --- a/octoprint/static/js/app/main.js +++ b/octoprint/static/js/app/main.js @@ -5,13 +5,13 @@ $(function() { var usersViewModel = new UsersViewModel(loginStateViewModel); var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel); var connectionViewModel = new ConnectionViewModel(loginStateViewModel, settingsViewModel); - var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel); + var timelapseViewModel = new TimelapseViewModel(loginStateViewModel); + var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel, timelapseViewModel); var appearanceViewModel = new AppearanceViewModel(settingsViewModel); var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel); var controlViewModel = new ControlViewModel(loginStateViewModel, settingsViewModel); var terminalViewModel = new TerminalViewModel(loginStateViewModel, settingsViewModel); var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel); - var timelapseViewModel = new TimelapseViewModel(loginStateViewModel); var gcodeViewModel = new GcodeViewModel(loginStateViewModel); var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel); diff --git a/octoprint/static/js/app/viewmodels/printerstate.js b/octoprint/static/js/app/viewmodels/printerstate.js index 91bc505..6779d15 100644 --- a/octoprint/static/js/app/viewmodels/printerstate.js +++ b/octoprint/static/js/app/viewmodels/printerstate.js @@ -20,6 +20,7 @@ function PrinterStateViewModel(loginStateViewModel) { self.printTime = ko.observable(undefined); self.printTimeLeft = ko.observable(undefined); self.sd = ko.observable(undefined); + self.timelapse = ko.observable(undefined); self.filament = ko.observable(undefined); self.estimatedPrintTime = ko.observable(undefined); @@ -49,6 +50,22 @@ function PrinterStateViewModel(loginStateViewModel) { return "Pause"; }); + self.timelapseString = ko.computed(function() { + var timelapse = self.timelapse(); + + if (!timelapse || !timelapse.hasOwnProperty("type")) + return "-"; + + var type = timelapse["type"]; + if (type == "zchange") { + return "On Z Change"; + } else if (type == "timed") { + return "Timed (" + timelapse["options"]["interval"] + "s)"; + } else { + return "-"; + } + }); + self.fromCurrentData = function(data) { self._fromData(data); } @@ -57,6 +74,10 @@ function PrinterStateViewModel(loginStateViewModel) { self._fromData(data); } + self.fromTimelapseData = function(data) { + self.timelapse(data); + } + self._fromData = function(data) { self._processStateData(data.state) self._processJobData(data.job); diff --git a/octoprint/static/js/app/viewmodels/timelapse.js b/octoprint/static/js/app/viewmodels/timelapse.js index 6278fe2..a882df9 100644 --- a/octoprint/static/js/app/viewmodels/timelapse.js +++ b/octoprint/static/js/app/viewmodels/timelapse.js @@ -6,6 +6,9 @@ function TimelapseViewModel(loginStateViewModel) { self.timelapseType = ko.observable(undefined); self.timelapseTimedInterval = ko.observable(undefined); + self.persist = ko.observable(false); + self.isDirty = ko.observable(false); + self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); @@ -16,11 +19,21 @@ function TimelapseViewModel(loginStateViewModel) { self.intervalInputEnabled = ko.computed(function() { return ("timed" == self.timelapseType()); - }) + }); + self.saveButtonEnabled = ko.computed(function() { + return self.isDirty() && self.isOperational() && !self.isPrinting() && self.loginState.isUser(); + }); self.isOperational.subscribe(function(newValue) { self.requestData(); - }) + }); + + self.timelapseType.subscribe(function(newValue) { + self.isDirty(true); + }); + self.timelapseTimedInterval.subscribe(function(newValue) { + self.isDirty(true); + }); // initialize list helper self.listHelper = new ItemListHelper( @@ -51,7 +64,7 @@ function TimelapseViewModel(loginStateViewModel) { [], [], CONFIG_TIMELAPSEFILESPERPAGE - ) + ); self.requestData = function() { $.ajax({ @@ -60,17 +73,20 @@ function TimelapseViewModel(loginStateViewModel) { dataType: "json", success: self.fromResponse }); - } + }; self.fromResponse = function(response) { self.timelapseType(response.type); self.listHelper.updateItems(response.files); if (response.type == "timed" && response.config && response.config.interval) { - self.timelapseTimedInterval(response.config.interval) + self.timelapseTimedInterval(response.config.interval); } else { - self.timelapseTimedInterval(undefined) + self.timelapseTimedInterval(undefined); } + + self.persist(false); + self.isDirty(false); } self.fromCurrentData = function(data) { @@ -97,12 +113,13 @@ function TimelapseViewModel(loginStateViewModel) { type: "DELETE", dataType: "json", success: self.requestData - }) + }); } - self.save = function() { + self.save = function(data, event) { var data = { - "type": self.timelapseType() + "type": self.timelapseType(), + "save": self.persist() } if (self.timelapseType() == "timed") { @@ -115,6 +132,6 @@ function TimelapseViewModel(loginStateViewModel) { dataType: "json", data: data, success: self.fromResponse - }) + }); } } diff --git a/octoprint/templates/index.jinja2 b/octoprint/templates/index.jinja2 index 8f98b86..abbf86a 100644 --- a/octoprint/templates/index.jinja2 +++ b/octoprint/templates/index.jinja2 @@ -116,6 +116,7 @@ File:  (SD)
Filament:
Estimated Print Time:
+ Timelapse:
Height:
Print Time:
Print Time Left:
@@ -542,13 +543,19 @@
- + sec
+
+ +
+
- +
diff --git a/octoprint/timelapse.py b/octoprint/timelapse.py index 2e42ca1..a7fb95b 100644 --- a/octoprint/timelapse.py +++ b/octoprint/timelapse.py @@ -33,6 +33,26 @@ def getFinishedTimelapses(): }) return files +validTimelapseTypes = ["off", "timed", "zchange"] + +updateCallbacks = [] +def registerCallback(callback): + if not callback in updateCallbacks: + updateCallbacks.append(callback) + +def unregisterCallback(callback): + if callback in updateCallbacks: + updateCallbacks.remove(callback) + +def notifyCallbacks(timelapse): + for callback in updateCallbacks: + if timelapse is None: + config = None + else: + config = timelapse.configData() + try: callback.sendTimelapseConfig(config) + except: pass + class Timelapse(object): def __init__(self): self._logger = logging.getLogger(__name__) @@ -98,6 +118,16 @@ class Timelapse(object): """ return [] + def configData(self): + """ + Override this method to return the current timelapse configuration data. The data should have the following + form: + + type: "", + options: { } + """ + return None + def startTimelapse(self, gcodeFile): self._logger.debug("Starting timelapse for %s" % gcodeFile) self.cleanCaptureDir() @@ -212,6 +242,11 @@ class ZTimelapse(Timelapse): ("ZChange", self._onZChange) ] + def configData(self): + return { + "type": "zchange" + } + def _onZChange(self, event, payload): self.captureImage() @@ -227,6 +262,14 @@ class TimedTimelapse(Timelapse): def interval(self): return self._interval + def configData(self): + return { + "type": "timed", + "options": { + "interval": self._interval + } + } + def onPrintStarted(self, event, payload): Timelapse.onPrintStarted(self, event, payload) if self._timerThread is not None: