From 7e2382fed4199ee50db14e40cc8b03c694894a04 Mon Sep 17 00:00:00 2001 From: Dattas Moonchaser Date: Thu, 20 Oct 2016 19:06:26 -0600 Subject: [PATCH] Add option to repeat last captured frame instead of capturing new frames for the end of a timelapse. Defaults to current behavior (capturing new frames for the time after). Implements foosel/OctoPrint#1422 --- src/octoprint/server/api/timelapse.py | 14 ++++++- src/octoprint/settings.py | 3 +- .../static/js/app/viewmodels/timelapse.js | 12 ++++++ src/octoprint/templates/tabs/timelapse.jinja2 | 6 +++ src/octoprint/timelapse.py | 41 +++++++++++++++---- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/octoprint/server/api/timelapse.py b/src/octoprint/server/api/timelapse.py index 33b3bddd..872b204f 100644 --- a/src/octoprint/server/api/timelapse.py +++ b/src/octoprint/server/api/timelapse.py @@ -66,7 +66,8 @@ def getTimelapseData(): config = dict(type="timed", postRoll=timelapse.post_roll, fps=timelapse.fps, - interval=timelapse.interval) + interval=timelapse.interval, + capturePostRoll=timelapse.capture_post_roll) else: config = dict(type="off") @@ -204,6 +205,16 @@ def setTimelapseConfig(): else: return make_response("Invalid value for interval: %d" % interval) + if "capturePostRoll" in data: + config["options"]["capturePostRoll"] = True + try: + capturePostRoll = bool(data["capturePostRoll"]) + except ValueError: + return make_response("Invalid value for capturePostRoll: %r" % data["capturePostRoll"]) + else: + config["options"]["capturePostRoll"] = capturePostRoll + + if "retractionZHop" in request.values: config["options"] = { "retractionZHop": 0 @@ -219,6 +230,7 @@ def setTimelapseConfig(): else: return make_response("Invalid value for retraction Z-Hop: %d" % retractionZHop) + if admin_permission.can() and "save" in data and data["save"] in valid_boolean_trues: octoprint.timelapse.configure_timelapse(config, True) else: diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index bf0ac119..ddf5aabd 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -166,7 +166,8 @@ default_settings = { "type": "off", "options": {}, "postRoll": 0, - "fps": 25 + "fps": 25, + "capturePostRoll": True }, "cleanTmpAfterDays": 7 }, diff --git a/src/octoprint/static/js/app/viewmodels/timelapse.js b/src/octoprint/static/js/app/viewmodels/timelapse.js index a7230d89..f50b6da1 100644 --- a/src/octoprint/static/js/app/viewmodels/timelapse.js +++ b/src/octoprint/static/js/app/viewmodels/timelapse.js @@ -10,12 +10,14 @@ $(function() { self.defaultPostRoll = 0; self.defaultInterval = 10; self.defaultRetractionZHop = 0; + self.defaultCapturePostroll = true; self.timelapseType = ko.observable(undefined); self.timelapseTimedInterval = ko.observable(self.defaultInterval); self.timelapsePostRoll = ko.observable(self.defaultPostRoll); self.timelapseFps = ko.observable(self.defaultFps); self.timelapseRetractionZHop = ko.observable(self.defaultRetractionZHop); + self.timelapseCapturePostRoll = ko.observable(self.defaultCapturePostRoll); self.persist = ko.observable(false); self.isDirty = ko.observable(false); @@ -61,6 +63,9 @@ $(function() { self.timelapseRetractionZHop.subscribe(function(newValue) { self.isDirty(true); }); + self.timelapseCapturePostRoll.subscribe(function() { + self.isDirty(true); + }); // initialize list helper self.listHelper = new ItemListHelper( @@ -167,6 +172,12 @@ $(function() { self.timelapseFps(self.defaultFps); } + if (config.capturePostRoll != undefined){ + self.timelapseCapturePostRoll(config.capturePostRoll); + } else { + self.timelapseCapturePostRoll(self.defaultCapturePostRoll); + } + self.persist(false); self.isDirty(false); }; @@ -214,6 +225,7 @@ $(function() { if (self.timelapseType() == "timed") { payload["interval"] = self.timelapseTimedInterval(); + payload["capturePostRoll"] = self.timelapseCapturePostRoll(); } if (self.timelapseType() == "zchange") { diff --git a/src/octoprint/templates/tabs/timelapse.jinja2 b/src/octoprint/templates/tabs/timelapse.jinja2 index 041c3bc3..e01f4ebe 100644 --- a/src/octoprint/templates/tabs/timelapse.jinja2 +++ b/src/octoprint/templates/tabs/timelapse.jinja2 @@ -24,6 +24,12 @@ {{ _('sec') }} +
+ +
+
diff --git a/src/octoprint/timelapse.py b/src/octoprint/timelapse.py index 21442843..899e4e59 100644 --- a/src/octoprint/timelapse.py +++ b/src/octoprint/timelapse.py @@ -303,7 +303,10 @@ def configure_timelapse(config=None, persist=False): interval = 10 if "options" in config and "interval" in config["options"] and config["options"]["interval"] > 0: interval = config["options"]["interval"] - current = TimedTimelapse(post_roll=postRoll, interval=interval, fps=fps) + capture_post_roll = True + if "options" in config and "capturePostRoll" in config["options"] and isinstance(config["options"]["capturePostRoll"], bool): + capture_post_roll = config["options"]["capturePostRoll"] + current = TimedTimelapse(post_roll=postRoll, interval=interval, fps=fps, capture_post_roll=capture_post_roll) notify_callbacks(current) @@ -628,11 +631,12 @@ class ZTimelapse(Timelapse): class TimedTimelapse(Timelapse): - def __init__(self, post_roll=0, interval=1, fps=25): + def __init__(self, post_roll=0, interval=1, fps=25, capture_post_roll=True): Timelapse.__init__(self, post_roll=post_roll, fps=fps) self._interval = interval if self._interval < 1: self._interval = 1 # force minimum interval of 1s + self._capture_post_roll = capture_post_roll self._postroll_captures = 0 self._timer = None self._logger.debug("TimedTimelapse initialized") @@ -641,11 +645,16 @@ class TimedTimelapse(Timelapse): def interval(self): return self._interval + @property + def capture_post_roll(self): + return self._capture_post_roll + def config_data(self): return { "type": "timed", "options": { - "interval": self._interval + "interval": self._interval, + "capture_post_roll": self._capture_post_roll } } @@ -662,14 +671,31 @@ class TimedTimelapse(Timelapse): self._timer.start() def on_print_done(self, event, payload): - self._postroll_captures = self._post_roll * self._fps + if self._capture_post_roll: + self._postroll_captures = self._post_roll * self._fps Timelapse.on_print_done(self, event, payload) def calculate_post_roll(self): - return self._post_roll * self._fps * self._interval + if self._capture_post_roll: + return self._post_roll * self._fps * self._interval + else: + return Timelapse.calculate_post_roll(self) def process_post_roll(self): - pass + if self._capture_post_roll: + return + + with self._capture_mutex: + filename = os.path.join(self._capture_dir, _capture_format.format(prefix=self._file_prefix) % self._image_number) + self._image_number += 1 + + if self._perform_capture(filename): + for _ in range(self._post_roll * self._fps): + newFile = os.path.join(self._capture_dir, _capture_format.format(prefix=self._file_prefix) % self._image_number) + self._image_number += 1 + shutil.copyfile(filename, newFile) + + self.post_roll_finished() def post_roll_finished(self): Timelapse.post_roll_finished(self) @@ -684,7 +710,8 @@ class TimedTimelapse(Timelapse): self._postroll_captures -= 1 def _on_timer_finished(self): - self.post_roll_finished() + if self._capture_post_roll: + self.post_roll_finished() class TimelapseRenderJob(object):