Rate limit snapshots on for z-timelapses

This is to prevent against an endless stream of snapshots when
accidentally printing a vase mode file despite the warning.

As mentioned in #2067
This commit is contained in:
Gina Häußge 2017-10-26 16:03:54 +02:00
parent 2d925a2665
commit 8bf3f4ff50
7 changed files with 91 additions and 27 deletions

View file

@ -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

View file

@ -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":

View file

@ -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)

View file

@ -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 = "<p>" + _.sprintf(gettext("Rendering of timelapse %(movie_prefix)s is not possible since no frames were captured. Is the snapshot URL configured correctly?"), payload) + "</p>";
} 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;
}
}

View file

@ -16,20 +16,20 @@
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled">
<label for="webcam_timelapse_interval">{{ _('Interval between snapshots (in seconds)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser()">
<input type="number" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser()">
<span class="add-on">{{ _('sec') }}</span>
</div>
</div>
<label for="webcam_timelapse_fps">{{ _('Timelapse frame rate (in frames per second)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_fps" data-bind="value: timelapseFps, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<input type="number" class="input-mini" id="webcam_timelapse_fps" data-bind="value: timelapseFps, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<span class="add-on">{{ _('fps') }}</span>
</div>
<label for="webcam_timelapse_postRoll">{{ _('Timelapse post roll (in rendered seconds)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<input type="number" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<span class="add-on">{{ _('sec') }}</span>
</div>
<div id="webcam_timelapse_capturePostRoll" data-bind="visible: timelapseType() == 'timed'">
@ -42,12 +42,21 @@
<div id="webcam_timelapse_retractionsettings" data-bind="visible: timelapseType() == 'zchange'">
<label for="webcam_timelapse_retractionZHop">{{ _('Retraction Z-Hop (in mm)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_retractionZHop" data-bind="value: timelapseRetractionZHop, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser()">
<input type="number" class="input-mini" id="webcam_timelapse_retractionZHop" data-bind="value: timelapseRetractionZHop, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser()">
<span class="add-on">{{ _('mm') }}</span>
</div>
<span class="help-block">{{ _('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!') }}</span>
</div>
<div id="webcam_timelapse_mindelay" data-bind="visible: timelapseType() == 'zchange'">
<label for="webcam_timelapse_minDelay">{{ _('Minimum delay between snapshots (in sec)') }}</label>
<div class="input-append">
<input type="number" class="input-mini" id="webcam_timelapse_minDelay" data-bind="value: timelapseMinDelay, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser()">
<span class="add-on">{{ _('sec') }}</span>
</div>
<span class="help-block">{{ _('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.') }}</span>
</div>
<div data-bind="visible: loginState.isAdmin">
<label class="checkbox">
<input type="checkbox" data-bind="checked: persist, enable: isOperational() && !isPrinting() && loginState.isUser()"> {{ _('Save as default') }}

View file

@ -21,6 +21,7 @@ import octoprint.util as util
from octoprint.settings import settings
from octoprint.events import eventManager, Events
from octoprint.util import monotonic_time
import sarge
import collections
@ -303,7 +304,11 @@ def configure_timelapse(config=None, persist=False):
if "options" in config and "retractionZHop" in config["options"] and config["options"]["retractionZHop"] > 0:
retractionZHop = config["options"]["retractionZHop"]
current = ZTimelapse(post_roll=postRoll, retraction_zhop=retractionZHop, fps=fps)
minDelay = 5
if "options" in config and "minDelay" in config["options"] and config["options"]["minDelay"] >= 0:
minDelay = config["options"]["minDelay"]
current = ZTimelapse(post_roll=postRoll, retraction_zhop=retractionZHop, min_delay=minDelay, fps=fps)
elif "timed" == type:
interval = 10
@ -605,14 +610,24 @@ class Timelapse(object):
class ZTimelapse(Timelapse):
def __init__(self, post_roll=0, retraction_zhop=0, fps=25):
def __init__(self, retraction_zhop=0, min_delay=5.0, post_roll=0, fps=25):
Timelapse.__init__(self, post_roll=post_roll, fps=fps)
if min_delay < 0:
min_delay = 0
self._retraction_zhop = retraction_zhop
self._min_delay = min_delay
self._last_snapshot = None
self._logger.debug("ZTimelapse initialized")
@property
def retraction_zhop(self):
return self._retraction_zhop
@property
def min_delay(self):
return self._min_delay
def event_subscriptions(self):
return [
@ -634,18 +649,25 @@ class ZTimelapse(Timelapse):
Timelapse.process_post_roll(self)
def _on_z_change(self, event, payload):
# check if height difference equals z-hop, if so don't take a picture
if self._retraction_zhop != 0 and payload["old"] is not None and payload["new"] is not None:
# check if height difference equals z-hop, if so don't take a picture
diff = round(abs(payload["new"] - payload["old"]), 3)
zhop = round(self._retraction_zhop, 3)
if diff == zhop:
return
# check if last picture has been less than min_delay ago, if so don't take a picture (anti vase mode...)
now = monotonic_time()
if self._min_delay and self._last_snapshot and self._last_snapshot + self._min_delay > now:
self._logger.debug("Rate limited z-change, not taking a snapshot")
return
self.capture_image()
self._last_snapshot = now
class TimedTimelapse(Timelapse):
def __init__(self, post_roll=0, interval=1, fps=25, capture_post_roll=True):
def __init__(self, interval=1, capture_post_roll=True, post_roll=0, fps=25):
Timelapse.__init__(self, post_roll=post_roll, fps=fps)
self._interval = interval
if self._interval < 1:

View file

@ -891,6 +891,15 @@ except ImportError:
return drive + pathname
try:
import monotonic
monotonic_time = monotonic.monotonic
except RuntimeError:
# no source of monotonic time available, nothing left but using time.time *cringe*
import time
monotonic_time = time.time
class RepeatedTimer(threading.Thread):
"""
This class represents an action that should be run repeatedly in an interval. It is similar to python's