diff --git a/THIRDPARTYLICENSES.md b/THIRDPARTYLICENSES.md index a8a4f733..6f0dd0d3 100644 --- a/THIRDPARTYLICENSES.md +++ b/THIRDPARTYLICENSES.md @@ -47,6 +47,7 @@ * [Flask-Principal](http://packages.python.org/Flask-Principal/): MIT * [future](https://python-future.org/): MIT * [futures](https://github.com/agronholm/pythonfutures): Python + * [monotonic](https://github.com/atdt/monotonic): Apache License 2.0 * [netaddr](https://github.com/drkjam/netaddr/): BSD * [netifaces](https://bitbucket.org/al45tair/netifaces): MIT * [pkginfo](http://pypi.python.org/pypi/pkginfo/): Python diff --git a/setup.py b/setup.py index 6cf46a1f..9e1c924f 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,8 @@ INSTALL_REQUIRES = [ "python-dateutil>=2.6,<2.7", "wrapt>=1.10.10,<1.11", "futures>=3.1.1,<3.2", - "emoji>=0.4.5,<0.5" + "emoji>=0.4.5,<0.5", + "monotonic>=1.3,<1.4" ] if sys.platform == "darwin": diff --git a/src/octoprint/server/api/timelapse.py b/src/octoprint/server/api/timelapse.py index 340e4d45..ae160c8a 100644 --- a/src/octoprint/server/api/timelapse.py +++ b/src/octoprint/server/api/timelapse.py @@ -22,6 +22,7 @@ from octoprint.server.api import api from octoprint.server import NO_CONTENT +_DATA_FORMAT_VERSION = "v2" #~~ timelapse handling @@ -36,7 +37,8 @@ def _config_for_timelapse(timelapse): return dict(type="zchange", postRoll=timelapse.post_roll, fps=timelapse.fps, - retractionZHop=timelapse.retraction_zhop) + retractionZHop=timelapse.retraction_zhop, + minDelay=timelapse.min_delay) elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse): return dict(type="timed", postRoll=timelapse.post_roll, @@ -67,6 +69,7 @@ def _etag(unrendered, lm=None): hash = hashlib.sha1() hash.update(str(lm)) hash.update(repr(config)) + hash.update(repr(_DATA_FORMAT_VERSION)) return hash.hexdigest() @@ -219,10 +222,7 @@ def setTimelapseConfig(): except ValueError: return make_response("Invalid value for capturePostRoll: %r" % data["capturePostRoll"], 400) else: - if capturePostRoll >= 0: - config["options"]["capturePostRoll"] = capturePostRoll - else: - return make_response("Invalid value for capturePostRoll: %d" % capturePostRoll, 400) + config["options"]["capturePostRoll"] = capturePostRoll if "retractionZHop" in data: try: @@ -233,8 +233,18 @@ def setTimelapseConfig(): if retractionZHop >= 0: config["options"]["retractionZHop"] = retractionZHop else: - return make_response("Invalid value for retraction Z-Hop: %d" % retractionZHop, 400) + return make_response("Invalid value for retraction Z-Hop: %f" % retractionZHop, 400) + if "minDelay" in data: + try: + minDelay = float(data["minDelay"]) + except ValueError: + return make_response("Invalid value for minimum delay: %r" % data["minDelay"], 400) + else: + if minDelay >= 0: + config["options"]["minDelay"] = minDelay + else: + return make_response("Invalid value for minimum delay: %f" % minDelay, 400) if admin_permission.can() and "save" in data and data["save"] in valid_boolean_trues: octoprint.timelapse.configure_timelapse(config, True) diff --git a/src/octoprint/static/js/app/viewmodels/timelapse.js b/src/octoprint/static/js/app/viewmodels/timelapse.js index e0b6417c..2d60f33f 100644 --- a/src/octoprint/static/js/app/viewmodels/timelapse.js +++ b/src/octoprint/static/js/app/viewmodels/timelapse.js @@ -10,6 +10,7 @@ $(function() { self.defaultPostRoll = 0; self.defaultInterval = 10; self.defaultRetractionZHop = 0; + self.defaultMinDelay = 5.0; self.defaultCapturePostRoll = true; self.timelapseType = ko.observable(undefined); @@ -17,6 +18,7 @@ $(function() { self.timelapsePostRoll = ko.observable(self.defaultPostRoll); self.timelapseFps = ko.observable(self.defaultFps); self.timelapseRetractionZHop = ko.observable(self.defaultRetractionZHop); + self.timelapseMinDelay = ko.observable(self.defaultMinDelay); self.timelapseCapturePostRoll = ko.observable(self.defaultCapturePostRoll); self.persist = ko.observable(false); @@ -42,10 +44,10 @@ $(function() { }); self.timelapseTypeSelected = ko.pureComputed(function() { - return ("off" != self.timelapseType()); + return ("off" !== self.timelapseType()); }); self.intervalInputEnabled = ko.pureComputed(function() { - return ("timed" == self.timelapseType()); + return ("timed" === self.timelapseType()); }); self.saveButtonEnabled = ko.pureComputed(function() { return self.isDirty() && !self.isPrinting() && self.loginState.isUser(); @@ -66,6 +68,9 @@ $(function() { self.timelapseRetractionZHop.subscribe(function(newValue) { self.isDirty(true); }); + self.timelapseMinDelay.subscribe(function() { + self.isDirty(true); + }); self.timelapseCapturePostRoll.subscribe(function() { self.isDirty(true); }); @@ -155,31 +160,37 @@ $(function() { // timelapse config self.timelapseType(config.type); - if (config.type == "timed" && config.interval != undefined && config.interval > 0) { + if (config.type === "timed" && config.interval !== undefined && config.interval > 0) { self.timelapseTimedInterval(config.interval); } else { self.timelapseTimedInterval(self.defaultInterval); } - if (config.type == "timed" && config.capturePostRoll != undefined){ + if (config.type === "timed" && config.capturePostRoll !== undefined){ self.timelapseCapturePostRoll(config.capturePostRoll); } else { self.timelapseCapturePostRoll(self.defaultCapturePostRoll); } - if (config.type == "zchange" && config.retractionZHop != undefined && config.retractionZHop > 0) { + if (config.type === "zchange" && config.retractionZHop !== undefined && config.retractionZHop > 0) { self.timelapseRetractionZHop(config.retractionZHop); } else { self.timelapseRetractionZHop(self.defaultRetractionZHop); } - if (config.postRoll != undefined && config.postRoll >= 0) { + if (config.type === "zchange" && config.minDelay !== undefined && config.minDelay >= 0) { + self.timelapseMinDelay(config.minDelay); + } else { + self.timelapseMinDelay(self.defaultMinDelay); + } + + if (config.postRoll !== undefined && config.postRoll >= 0) { self.timelapsePostRoll(config.postRoll); } else { self.timelapsePostRoll(self.defaultPostRoll); } - if (config.fps != undefined && config.fps > 0) { + if (config.fps !== undefined && config.fps > 0) { self.timelapseFps(config.fps); } else { self.timelapseFps(self.defaultFps); @@ -363,13 +374,14 @@ $(function() { "save": self.persist() }; - if (self.timelapseType() == "timed") { + if (self.timelapseType() === "timed") { payload["interval"] = self.timelapseTimedInterval(); payload["capturePostRoll"] = self.timelapseCapturePostRoll(); } - if (self.timelapseType() == "zchange") { + if (self.timelapseType() === "zchange") { payload["retractionZHop"] = self.timelapseRetractionZHop(); + payload["minDelay"] = self.timelapseMinDelay(); } OctoPrint.timelapse.saveConfig(payload) @@ -384,7 +396,7 @@ $(function() { _.extend(options, { callbacks: { before_close: function(notice) { - if (self.timelapsePopup == notice) { + if (self.timelapsePopup === notice) { self.timelapsePopup = undefined; } } @@ -465,7 +477,7 @@ $(function() { self.onEventMovieFailed = function(payload) { var title, html; - if (payload.reason == "no_frames") { + if (payload.reason === "no_frames") { title = gettext("Cannot render timelapse"); html = "

" + _.sprintf(gettext("Rendering of timelapse %(movie_prefix)s is not possible since no frames were captured. Is the snapshot URL configured correctly?"), payload) + "

"; } else if (payload.reason = "returncode") { @@ -492,7 +504,7 @@ $(function() { type: "success", callbacks: { before_close: function(notice) { - if (self.timelapsePopup == notice) { + if (self.timelapsePopup === notice) { self.timelapsePopup = undefined; } } diff --git a/src/octoprint/templates/tabs/timelapse.jinja2 b/src/octoprint/templates/tabs/timelapse.jinja2 index 1ea865d7..df81a93e 100644 --- a/src/octoprint/templates/tabs/timelapse.jinja2 +++ b/src/octoprint/templates/tabs/timelapse.jinja2 @@ -16,20 +16,20 @@
- + {{ _('sec') }}
- + {{ _('fps') }}
- + {{ _('sec') }}
@@ -42,12 +42,21 @@
- + {{ _('mm') }}
{{ _('Enter the retraction z-hop used in the firmware or the gcode file to trigger snapshots for the timelapse only if a real layer change happens. For this to work properly your retraction z-hop has to be different from your layerheight!') }}
+
+ +
+ + {{ _('sec') }} +
+ {{ _('OctoPrint will not allow a snapshot to be taken based on a z-change unless the last one was more than this delay ago. This is to prevent issues if you accidentally print a vase mode type file while this timelapse mode is active.') }} +
+