Refactored timelapse core
Capturing is now queue based, rendering will not start until all images have been captured, and timed postroll does not depend on system time anymore. Also refactored some of the names to be python naming compliant while at it.
This commit is contained in:
parent
3c5a9766c6
commit
4f5dc70828
4 changed files with 225 additions and 139 deletions
|
|
@ -74,6 +74,9 @@ class Events(object):
|
|||
# Timelapse
|
||||
CAPTURE_START = "CaptureStart"
|
||||
CAPTURE_DONE = "CaptureDone"
|
||||
CAPTURE_FAILED = "CaptureFailed"
|
||||
POSTROLL_START = "PostRollStart"
|
||||
POSTROLL_END = "PostRollEnd"
|
||||
MOVIE_RENDERING = "MovieRendering"
|
||||
MOVIE_DONE = "MovieDone"
|
||||
MOVIE_FAILED = "MovieFailed"
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ def getTimelapseData():
|
|||
config = {"type": "off"}
|
||||
if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse):
|
||||
config["type"] = "zchange"
|
||||
config["postRoll"] = timelapse.postRoll()
|
||||
config["fps"] = timelapse.fps()
|
||||
config["postRoll"] = timelapse.post_roll
|
||||
config["fps"] = timelapse.fps
|
||||
elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse):
|
||||
config["type"] = "timed"
|
||||
config["postRoll"] = timelapse.postRoll()
|
||||
config["fps"] = timelapse.fps()
|
||||
config["postRoll"] = timelapse.post_roll
|
||||
config["fps"] = timelapse.fps
|
||||
config.update({
|
||||
"interval": timelapse.interval()
|
||||
"interval": timelapse.interval
|
||||
})
|
||||
|
||||
files = octoprint.timelapse.getFinishedTimelapses()
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ function DataUpdater(allViewModels) {
|
|||
var type = data["type"];
|
||||
var payload = data["payload"];
|
||||
var html = "";
|
||||
var format = {};
|
||||
|
||||
log.debug("Got event " + type + " with payload: " + JSON.stringify(payload));
|
||||
|
||||
|
|
@ -186,7 +187,23 @@ function DataUpdater(allViewModels) {
|
|||
} else if (type == "MovieFailed") {
|
||||
html = "<p>" + _.sprintf(gettext("Rendering of timelapse %(movie_basename)s failed with return code %(returncode)s"), payload) + "</p>";
|
||||
html += pnotifyAdditionalInfo('<pre style="overflow: auto">' + payload.error + '</pre>');
|
||||
new PNotify({title: gettext("Rendering failed"), text: html, type: "error", hide: false});
|
||||
new PNotify({
|
||||
title: gettext("Rendering failed"),
|
||||
text: html,
|
||||
type: "error",
|
||||
hide: false
|
||||
});
|
||||
} else if (type == "PostRollStart") {
|
||||
if (payload.postroll_duration > 60) {
|
||||
format = {duration: _.sprintf(gettext("%(minutes)d min"), {minutes: payload.postroll_duration / 60})};
|
||||
} else {
|
||||
format = {duration: _.sprintf(gettext("%(seconds)d sec"), {seconds: payload.postroll_duration})};
|
||||
}
|
||||
|
||||
new PNotify({
|
||||
title: gettext("Capturing timelapse postroll"),
|
||||
text: _.sprintf(gettext("Now capturing timelapse post roll, this will take approximately %(duration)s..."), format)
|
||||
});
|
||||
} else if (type == "SlicingStarted") {
|
||||
gcodeUploadProgress.addClass("progress-striped").addClass("active");
|
||||
gcodeUploadProgressBar.css("width", "100%");
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp
|
|||
import logging
|
||||
import os
|
||||
import threading
|
||||
import urllib
|
||||
import time
|
||||
import subprocess
|
||||
import fnmatch
|
||||
import datetime
|
||||
import sys
|
||||
import shutil
|
||||
import Queue
|
||||
import requests
|
||||
|
||||
import octoprint.util as util
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ def notifyCallbacks(timelapse):
|
|||
if timelapse is None:
|
||||
config = None
|
||||
else:
|
||||
config = timelapse.configData()
|
||||
config = timelapse.config_data()
|
||||
for callback in updateCallbacks:
|
||||
try: callback.sendTimelapseConfig(config)
|
||||
except: logging.getLogger(__name__).exception("Exception while pushing timelapse configuration")
|
||||
|
|
@ -86,12 +86,12 @@ def configureTimelapse(config=None, persist=False):
|
|||
if type is None or "off" == type:
|
||||
current = None
|
||||
elif "zchange" == type:
|
||||
current = ZTimelapse(postRoll=postRoll, fps=fps)
|
||||
current = ZTimelapse(post_roll=postRoll, fps=fps)
|
||||
elif "timed" == type:
|
||||
interval = 10
|
||||
if "options" in config and "interval" in config["options"] and config["options"]["interval"] > 0:
|
||||
interval = config["options"]["interval"]
|
||||
current = TimedTimelapse(postRoll=postRoll, interval=interval, fps=fps)
|
||||
current = TimedTimelapse(post_roll=postRoll, interval=interval, fps=fps)
|
||||
|
||||
notifyCallbacks(current)
|
||||
|
||||
|
|
@ -101,72 +101,83 @@ def configureTimelapse(config=None, persist=False):
|
|||
|
||||
|
||||
class Timelapse(object):
|
||||
def __init__(self, postRoll=0, fps=25):
|
||||
QUEUE_ENTRY_TYPE_CAPTURE = "capture"
|
||||
QUEUE_ENTRY_TYPE_CALLBACK = "callback"
|
||||
|
||||
def __init__(self, post_roll=0, fps=25):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._imageNumber = None
|
||||
self._inTimelapse = False
|
||||
self._gcodeFile = None
|
||||
self._image_number = None
|
||||
self._in_timelapse = False
|
||||
self._gcode_file = None
|
||||
|
||||
self._postRoll = postRoll
|
||||
self._postRollStart = None
|
||||
self._onPostRollDone = None
|
||||
self._post_roll = post_roll
|
||||
self._on_post_roll_done = None
|
||||
|
||||
self._captureDir = settings().getBaseFolder("timelapse_tmp")
|
||||
self._movieDir = settings().getBaseFolder("timelapse")
|
||||
self._snapshotUrl = settings().get(["webcam", "snapshot"])
|
||||
self._ffmpegThreads = settings().get(["webcam", "ffmpegThreads"])
|
||||
self._capture_dir = settings().getBaseFolder("timelapse_tmp")
|
||||
self._movie_dir = settings().getBaseFolder("timelapse")
|
||||
self._snapshot_url = settings().get(["webcam", "snapshot"])
|
||||
self._ffmpeg_threads = settings().get(["webcam", "ffmpegThreads"])
|
||||
|
||||
self._fps = fps
|
||||
|
||||
self._renderThread = None
|
||||
self._captureMutex = threading.Lock()
|
||||
self._render_thread = None
|
||||
|
||||
self._capture_mutex = threading.Lock()
|
||||
self._capture_queue = Queue.Queue()
|
||||
self._capture_queue_active = True
|
||||
|
||||
self._capture_queue_thread = threading.Thread(target=self._capture_queue_worker)
|
||||
self._capture_queue_thread.daemon = True
|
||||
self._capture_queue_thread.start()
|
||||
|
||||
# subscribe events
|
||||
eventManager().subscribe(Events.PRINT_STARTED, self.onPrintStarted)
|
||||
eventManager().subscribe(Events.PRINT_FAILED, self.onPrintDone)
|
||||
eventManager().subscribe(Events.PRINT_DONE, self.onPrintDone)
|
||||
eventManager().subscribe(Events.PRINT_RESUMED, self.onPrintResumed)
|
||||
for (event, callback) in self.eventSubscriptions():
|
||||
eventManager().subscribe(Events.PRINT_STARTED, self.on_print_started)
|
||||
eventManager().subscribe(Events.PRINT_FAILED, self.on_print_done)
|
||||
eventManager().subscribe(Events.PRINT_DONE, self.on_print_done)
|
||||
eventManager().subscribe(Events.PRINT_RESUMED, self.on_print_resumed)
|
||||
for (event, callback) in self.event_subscriptions():
|
||||
eventManager().subscribe(event, callback)
|
||||
|
||||
def postRoll(self):
|
||||
return self._postRoll
|
||||
@property
|
||||
def post_roll(self):
|
||||
return self._post_roll
|
||||
|
||||
@property
|
||||
def fps(self):
|
||||
return self._fps
|
||||
|
||||
def unload(self):
|
||||
if self._inTimelapse:
|
||||
self.stopTimelapse(doCreateMovie=False)
|
||||
if self._in_timelapse:
|
||||
self.stop_timelapse(doCreateMovie=False)
|
||||
|
||||
# unsubscribe events
|
||||
eventManager().unsubscribe(Events.PRINT_STARTED, self.onPrintStarted)
|
||||
eventManager().unsubscribe(Events.PRINT_FAILED, self.onPrintDone)
|
||||
eventManager().unsubscribe(Events.PRINT_DONE, self.onPrintDone)
|
||||
eventManager().unsubscribe(Events.PRINT_RESUMED, self.onPrintResumed)
|
||||
for (event, callback) in self.eventSubscriptions():
|
||||
eventManager().unsubscribe(Events.PRINT_STARTED, self.on_print_started)
|
||||
eventManager().unsubscribe(Events.PRINT_FAILED, self.on_print_done)
|
||||
eventManager().unsubscribe(Events.PRINT_DONE, self.on_print_done)
|
||||
eventManager().unsubscribe(Events.PRINT_RESUMED, self.on_print_resumed)
|
||||
for (event, callback) in self.event_subscriptions():
|
||||
eventManager().unsubscribe(event, callback)
|
||||
|
||||
def onPrintStarted(self, event, payload):
|
||||
def on_print_started(self, event, payload):
|
||||
"""
|
||||
Override this to perform additional actions upon start of a print job.
|
||||
"""
|
||||
self.startTimelapse(payload["file"])
|
||||
self.start_timelapse(payload["file"])
|
||||
|
||||
def onPrintDone(self, event, payload):
|
||||
def on_print_done(self, event, payload):
|
||||
"""
|
||||
Override this to perform additional actions upon the stop of a print job.
|
||||
"""
|
||||
self.stopTimelapse(success=(event==Events.PRINT_DONE))
|
||||
self.stop_timelapse(success=(event==Events.PRINT_DONE))
|
||||
|
||||
def onPrintResumed(self, event, payload):
|
||||
def on_print_resumed(self, event, payload):
|
||||
"""
|
||||
Override this to perform additional actions upon the pausing of a print job.
|
||||
"""
|
||||
if not self._inTimelapse:
|
||||
self.startTimelapse(payload["file"])
|
||||
if not self._in_timelapse:
|
||||
self.start_timelapse(payload["file"])
|
||||
|
||||
def eventSubscriptions(self):
|
||||
def event_subscriptions(self):
|
||||
"""
|
||||
Override this method to subscribe to additional events by returning an array of (event, callback) tuples.
|
||||
|
||||
|
|
@ -178,7 +189,7 @@ class Timelapse(object):
|
|||
"""
|
||||
return []
|
||||
|
||||
def configData(self):
|
||||
def config_data(self):
|
||||
"""
|
||||
Override this method to return the current timelapse configuration data. The data should have the following
|
||||
form:
|
||||
|
|
@ -188,94 +199,139 @@ class Timelapse(object):
|
|||
"""
|
||||
return None
|
||||
|
||||
def startTimelapse(self, gcodeFile):
|
||||
def start_timelapse(self, gcodeFile):
|
||||
self._logger.debug("Starting timelapse for %s" % gcodeFile)
|
||||
self.cleanCaptureDir()
|
||||
self.clean_capture_dir()
|
||||
|
||||
self._imageNumber = 0
|
||||
self._inTimelapse = True
|
||||
self._gcodeFile = os.path.basename(gcodeFile)
|
||||
self._image_number = 0
|
||||
self._in_timelapse = True
|
||||
self._gcode_file = os.path.basename(gcodeFile)
|
||||
|
||||
def stopTimelapse(self, doCreateMovie=True, success=True):
|
||||
def stop_timelapse(self, doCreateMovie=True, success=True):
|
||||
self._logger.debug("Stopping timelapse")
|
||||
|
||||
self._inTimelapse = False
|
||||
self._in_timelapse = False
|
||||
|
||||
def resetImageNumber():
|
||||
self._imageNumber = None
|
||||
self._image_number = None
|
||||
|
||||
def createMovie():
|
||||
self._renderThread = threading.Thread(target=self._createMovie, kwargs={"success": success})
|
||||
self._renderThread.daemon = True
|
||||
self._renderThread.start()
|
||||
self._render_thread = threading.Thread(target=self._create_movie, kwargs={"success": success})
|
||||
self._render_thread.daemon = True
|
||||
self._render_thread.start()
|
||||
|
||||
def resetAndCreate():
|
||||
resetImageNumber()
|
||||
createMovie()
|
||||
|
||||
if self._postRoll > 0:
|
||||
self._postRollStart = time.time()
|
||||
def waitForCaptures(callback):
|
||||
self._capture_queue.put(dict(type=self.__class__.QUEUE_ENTRY_TYPE_CALLBACK, callback=callback))
|
||||
|
||||
def getWaitForCaptures(callback):
|
||||
def f():
|
||||
waitForCaptures(callback)
|
||||
return f
|
||||
|
||||
if self._post_roll > 0:
|
||||
eventManager().fire(Events.POSTROLL_START, dict(postroll_duration=self.post_roll * self.fps, postroll_length=self.post_roll, postroll_fps=self.fps))
|
||||
self._post_roll_start = time.time()
|
||||
if doCreateMovie:
|
||||
self._onPostRollDone = resetAndCreate
|
||||
self._on_post_roll_done = getWaitForCaptures(resetAndCreate)
|
||||
else:
|
||||
self._onPostRollDone = resetImageNumber
|
||||
self.processPostRoll()
|
||||
self._on_post_roll_done = resetImageNumber
|
||||
self.process_post_roll()
|
||||
else:
|
||||
self._postRollStart = None
|
||||
self._post_roll_start = None
|
||||
if doCreateMovie:
|
||||
resetAndCreate()
|
||||
waitForCaptures(resetAndCreate)
|
||||
else:
|
||||
resetImageNumber()
|
||||
|
||||
def processPostRoll(self):
|
||||
pass
|
||||
def process_post_roll(self):
|
||||
self.post_roll_finished()
|
||||
|
||||
def post_roll_finished(self):
|
||||
if self.post_roll:
|
||||
eventManager().fire(Events.POSTROLL_END)
|
||||
if self._on_post_roll_done is not None:
|
||||
self._on_post_roll_done()
|
||||
|
||||
def captureImage(self):
|
||||
if self._captureDir is None:
|
||||
if self._capture_dir is None:
|
||||
self._logger.warn("Cannot capture image, capture directory is unset")
|
||||
return
|
||||
|
||||
if self._imageNumber is None:
|
||||
self._logger.warn("Cannot capture image, image number is unset")
|
||||
return
|
||||
with self._capture_mutex:
|
||||
if self._image_number is None:
|
||||
self._logger.warn("Cannot capture image, image number is unset")
|
||||
return
|
||||
|
||||
filename = os.path.join(self._capture_dir, "tmp_%05d.jpg" % self._image_number)
|
||||
self._image_number += 1
|
||||
|
||||
with self._captureMutex:
|
||||
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % self._imageNumber)
|
||||
self._imageNumber += 1
|
||||
self._logger.debug("Capturing image to %s" % filename)
|
||||
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})
|
||||
captureThread.daemon = True
|
||||
captureThread.start()
|
||||
entry = dict(type=self.__class__.QUEUE_ENTRY_TYPE_CAPTURE,
|
||||
filename=filename,
|
||||
onerror=self._on_capture_error)
|
||||
self._capture_queue.put(entry)
|
||||
return filename
|
||||
|
||||
def _captureWorker(self, filename):
|
||||
def _on_capture_error(self):
|
||||
with self._capture_mutex:
|
||||
if self._image_number is not None and self._image_number > 0:
|
||||
self._image_number -= 1
|
||||
|
||||
def _capture_queue_worker(self):
|
||||
while self._capture_queue_active:
|
||||
entry = self._capture_queue.get(block=True)
|
||||
|
||||
if entry["type"] == self.__class__.QUEUE_ENTRY_TYPE_CAPTURE and "filename" in entry:
|
||||
filename = entry["filename"]
|
||||
onerror = entry.pop("onerror", None)
|
||||
self._perform_capture(filename, onerror=onerror)
|
||||
|
||||
elif entry["type"] == self.__class__.QUEUE_ENTRY_TYPE_CALLBACK and "callback" in entry:
|
||||
args = entry.pop("args", [])
|
||||
kwargs = entry.pop("kwargs", dict())
|
||||
entry["callback"](*args, **kwargs)
|
||||
|
||||
def _perform_capture(self, filename, onerror=None):
|
||||
eventManager().fire(Events.CAPTURE_START, {"file": filename})
|
||||
try:
|
||||
urllib.urlretrieve(self._snapshotUrl, filename)
|
||||
self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl))
|
||||
self._logger.debug("Going to capture %s from %s" % (filename, self._snapshot_url))
|
||||
r = requests.get(self._snapshot_url, stream=True)
|
||||
with open (filename, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
f.flush()
|
||||
self._logger.debug("Image %s captured from %s" % (filename, self._snapshot_url))
|
||||
except:
|
||||
self._logger.exception("Could not capture image %s from %s, decreasing image counter again" % (filename, self._snapshotUrl))
|
||||
with self._captureMutex:
|
||||
if self._imageNumber is not None and self._imageNumber > 0:
|
||||
self._imageNumber -= 1
|
||||
eventManager().fire(Events.CAPTURE_DONE, {"file": filename})
|
||||
self._logger.exception("Could not capture image %s from %s" % (filename, self._snapshot_url))
|
||||
if callable(onerror):
|
||||
onerror()
|
||||
eventManager().fire(Events.CAPTURE_FAILED, {"file": filename})
|
||||
return False
|
||||
else:
|
||||
eventManager().fire(Events.CAPTURE_DONE, {"file": filename})
|
||||
return True
|
||||
|
||||
def _createMovie(self, success=True):
|
||||
def _create_movie(self, success=True):
|
||||
ffmpeg = settings().get(["webcam", "ffmpeg"])
|
||||
bitrate = settings().get(["webcam", "bitrate"])
|
||||
if ffmpeg is None or bitrate is None:
|
||||
self._logger.warn("Cannot create movie, path to ffmpeg or desired bitrate is unset")
|
||||
return
|
||||
|
||||
input = os.path.join(self._captureDir, "tmp_%05d.jpg")
|
||||
input = os.path.join(self._capture_dir, "tmp_%05d.jpg")
|
||||
if success:
|
||||
output = os.path.join(self._movieDir, "%s_%s.mpg" % (os.path.splitext(self._gcodeFile)[0], time.strftime("%Y%m%d%H%M%S")))
|
||||
output = os.path.join(self._movie_dir, "%s_%s.mpg" % (os.path.splitext(self._gcode_file)[0], time.strftime("%Y%m%d%H%M%S")))
|
||||
else:
|
||||
output = os.path.join(self._movieDir, "%s_%s-failed.mpg" % (os.path.splitext(self._gcodeFile)[0], time.strftime("%Y%m%d%H%M%S")))
|
||||
output = os.path.join(self._movie_dir, "%s_%s-failed.mpg" % (os.path.splitext(self._gcode_file)[0], time.strftime("%Y%m%d%H%M%S")))
|
||||
|
||||
# prepare ffmpeg command
|
||||
command = [
|
||||
ffmpeg, '-framerate', str(self._fps), '-loglevel', 'error', '-i', input, '-vcodec', 'mpeg2video', '-threads', str(self._ffmpegThreads), '-pix_fmt', 'yuv420p', '-r', str(self._fps), '-y', '-b', bitrate,
|
||||
ffmpeg, '-framerate', str(self._fps), '-loglevel', 'error', '-i', input, '-vcodec', 'mpeg2video', '-threads', str(self._ffmpeg_threads), '-pix_fmt', 'yuv420p', '-r', str(self._fps), '-y', '-b', bitrate,
|
||||
'-f', 'vob']
|
||||
|
||||
filters = []
|
||||
|
|
@ -315,7 +371,7 @@ class Timelapse(object):
|
|||
# finalize command with output file
|
||||
self._logger.debug("Rendering movie to %s" % output)
|
||||
command.append("\"" + output + "\"")
|
||||
eventManager().fire(Events.MOVIE_RENDERING, {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output)})
|
||||
eventManager().fire(Events.MOVIE_RENDERING, {"gcode": self._gcode_file, "movie": output, "movie_basename": os.path.basename(output)})
|
||||
|
||||
command_str = " ".join(command)
|
||||
self._logger.debug("Executing command: %s" % command_str)
|
||||
|
|
@ -323,75 +379,74 @@ class Timelapse(object):
|
|||
try:
|
||||
p = sarge.run(command_str, stderr=sarge.Capture())
|
||||
if p.returncode == 0:
|
||||
eventManager().fire(Events.MOVIE_DONE, {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output)})
|
||||
eventManager().fire(Events.MOVIE_DONE, {"gcode": self._gcode_file, "movie": output, "movie_basename": os.path.basename(output)})
|
||||
else:
|
||||
returncode = p.returncode
|
||||
stderr_text = p.stderr.text
|
||||
self._logger.warn("Could not render movie, got return code %r: %s" % (returncode, stderr_text))
|
||||
eventManager().fire(Events.MOVIE_FAILED, {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output), "returncode": returncode, "error": stderr_text})
|
||||
eventManager().fire(Events.MOVIE_FAILED, {"gcode": self._gcode_file, "movie": output, "movie_basename": os.path.basename(output), "returncode": returncode, "error": stderr_text})
|
||||
except:
|
||||
self._logger.exception("Could not render movie due to unknown error")
|
||||
eventManager().fire(Events.MOVIE_FAILED, {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output), "returncode": 255, "error": "Unknown error"})
|
||||
eventManager().fire(Events.MOVIE_FAILED, {"gcode": self._gcode_file, "movie": output, "movie_basename": os.path.basename(output), "returncode": 255, "error": "Unknown error"})
|
||||
|
||||
def cleanCaptureDir(self):
|
||||
if not os.path.isdir(self._captureDir):
|
||||
def clean_capture_dir(self):
|
||||
if not os.path.isdir(self._capture_dir):
|
||||
self._logger.warn("Cannot clean capture directory, it is unset")
|
||||
return
|
||||
|
||||
for filename in os.listdir(self._captureDir):
|
||||
for filename in os.listdir(self._capture_dir):
|
||||
if not fnmatch.fnmatch(filename, "*.jpg"):
|
||||
continue
|
||||
os.remove(os.path.join(self._captureDir, filename))
|
||||
os.remove(os.path.join(self._capture_dir, filename))
|
||||
|
||||
|
||||
class ZTimelapse(Timelapse):
|
||||
def __init__(self, postRoll=0, fps=25):
|
||||
Timelapse.__init__(self, postRoll=postRoll, fps=fps)
|
||||
def __init__(self, post_roll=0, fps=25):
|
||||
Timelapse.__init__(self, post_roll=post_roll, fps=fps)
|
||||
self._logger.debug("ZTimelapse initialized")
|
||||
|
||||
def eventSubscriptions(self):
|
||||
def event_subscriptions(self):
|
||||
return [
|
||||
(Events.Z_CHANGE, self._onZChange)
|
||||
(Events.Z_CHANGE, self._on_z_change)
|
||||
]
|
||||
|
||||
def configData(self):
|
||||
def config_data(self):
|
||||
return {
|
||||
"type": "zchange"
|
||||
}
|
||||
|
||||
def processPostRoll(self):
|
||||
Timelapse.processPostRoll(self)
|
||||
def process_post_roll(self):
|
||||
with self._capture_mutex:
|
||||
filename = os.path.join(self._capture_dir, "tmp_%05d.jpg" % self._image_number)
|
||||
self._image_number += 1
|
||||
|
||||
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % self._imageNumber)
|
||||
self._imageNumber += 1
|
||||
with self._captureMutex:
|
||||
self._captureWorker(filename)
|
||||
if self._perform_capture(filename):
|
||||
for _ in range(self._post_roll * self._fps):
|
||||
newFile = os.path.join(self._capture_dir, "tmp_%05d.jpg" % self._image_number)
|
||||
self._image_number += 1
|
||||
shutil.copyfile(filename, newFile)
|
||||
|
||||
for i in range(self._postRoll * self._fps):
|
||||
newFile = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber))
|
||||
self._imageNumber += 1
|
||||
shutil.copyfile(filename, newFile)
|
||||
Timelapse.process_post_roll(self)
|
||||
|
||||
if self._onPostRollDone is not None:
|
||||
self._onPostRollDone()
|
||||
|
||||
def _onZChange(self, event, payload):
|
||||
def _on_z_change(self, event, payload):
|
||||
self.captureImage()
|
||||
|
||||
|
||||
class TimedTimelapse(Timelapse):
|
||||
def __init__(self, postRoll=0, interval=1, fps=25):
|
||||
Timelapse.__init__(self, postRoll=postRoll, fps=fps)
|
||||
def __init__(self, post_roll=0, interval=1, fps=25):
|
||||
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._timerThread = None
|
||||
self._postroll_captures = 0
|
||||
self._timer = None
|
||||
self._logger.debug("TimedTimelapse initialized")
|
||||
|
||||
@property
|
||||
def interval(self):
|
||||
return self._interval
|
||||
|
||||
def configData(self):
|
||||
def config_data(self):
|
||||
return {
|
||||
"type": "timed",
|
||||
"options": {
|
||||
|
|
@ -399,25 +454,36 @@ class TimedTimelapse(Timelapse):
|
|||
}
|
||||
}
|
||||
|
||||
def onPrintStarted(self, event, payload):
|
||||
Timelapse.onPrintStarted(self, event, payload)
|
||||
if self._timerThread is not None:
|
||||
def on_print_started(self, event, payload):
|
||||
Timelapse.on_print_started(self, event, payload)
|
||||
if self._timer is not None:
|
||||
return
|
||||
|
||||
self._timerThread = threading.Thread(target=self._timerWorker)
|
||||
self._timerThread.daemon = True
|
||||
self._timerThread.start()
|
||||
|
||||
def onPrintDone(self, event, payload):
|
||||
Timelapse.onPrintDone(self, event, payload)
|
||||
self._timerThread = None
|
||||
|
||||
def _timerWorker(self):
|
||||
self._logger.debug("Starting timer for interval based timelapse")
|
||||
while self._inTimelapse or (self._postRollStart and time.time() - self._postRollStart <= self._postRoll * self._fps):
|
||||
self.captureImage()
|
||||
time.sleep(self._interval)
|
||||
from octoprint.util import RepeatedTimer
|
||||
self._timer = RepeatedTimer(self._interval, self._timer_task,
|
||||
run_first=True, condition=self._timer_active,
|
||||
on_finish=self._on_timer_finished)
|
||||
self._timer.start()
|
||||
|
||||
if self._postRollStart is not None and self._onPostRollDone is not None:
|
||||
self._onPostRollDone()
|
||||
self._postRollStart = None
|
||||
def on_print_done(self, event, payload):
|
||||
self._postroll_captures = self.post_roll * self.fps
|
||||
Timelapse.on_print_done(self, event, payload)
|
||||
|
||||
def process_post_roll(self):
|
||||
pass
|
||||
|
||||
def post_roll_finished(self):
|
||||
Timelapse.post_roll_finished(self)
|
||||
self._timer = None
|
||||
|
||||
def _timer_active(self):
|
||||
return self._in_timelapse or self._postroll_captures > 0
|
||||
|
||||
def _timer_task(self):
|
||||
self.captureImage()
|
||||
if self._postroll_captures > 0:
|
||||
self._postroll_captures -= 1
|
||||
|
||||
def _on_timer_finished(self):
|
||||
self.post_roll_finished()
|
||||
|
|
|
|||
Loading…
Reference in a new issue