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:
parent
2d925a2665
commit
8bf3f4ff50
7 changed files with 91 additions and 27 deletions
|
|
@ -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
|
||||
|
|
|
|||
3
setup.py
3
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":
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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') }}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue