Timelapse configuration may now be saved
Persisted configuration will automatically be loaded upon startup. May be overridden by custom settings. Current timelapse configuration is visible in State box. Closes #116master
parent
715dea7eb3
commit
0471f1155e
|
@ -75,14 +75,19 @@ class PrinterStateConnection(SockJSConnection):
|
||||||
self._logger.info("New connection from client: %s" % self._getRemoteAddress(info))
|
self._logger.info("New connection from client: %s" % self._getRemoteAddress(info))
|
||||||
self._printer.registerCallback(self)
|
self._printer.registerCallback(self)
|
||||||
self._gcodeManager.registerCallback(self)
|
self._gcodeManager.registerCallback(self)
|
||||||
|
octoprint.timelapse.registerCallback(self)
|
||||||
|
|
||||||
self._eventManager.fire("ClientOpened")
|
self._eventManager.fire("ClientOpened")
|
||||||
self._eventManager.subscribe("MovieDone", self._onMovieDone)
|
self._eventManager.subscribe("MovieDone", self._onMovieDone)
|
||||||
|
|
||||||
|
global timelapse
|
||||||
|
octoprint.timelapse.notifyCallbacks(timelapse)
|
||||||
|
|
||||||
def on_close(self):
|
def on_close(self):
|
||||||
self._logger.info("Closed client connection")
|
self._logger.info("Closed client connection")
|
||||||
self._printer.unregisterCallback(self)
|
self._printer.unregisterCallback(self)
|
||||||
self._gcodeManager.unregisterCallback(self)
|
self._gcodeManager.unregisterCallback(self)
|
||||||
|
octoprint.timelapse.unregisterCallback(self)
|
||||||
|
|
||||||
self._eventManager.fire("ClientClosed")
|
self._eventManager.fire("ClientClosed")
|
||||||
self._eventManager.unsubscribe("MovieDone", self._onMovieDone)
|
self._eventManager.unsubscribe("MovieDone", self._onMovieDone)
|
||||||
|
@ -120,6 +125,9 @@ class PrinterStateConnection(SockJSConnection):
|
||||||
def sendFeedbackCommandOutput(self, name, output):
|
def sendFeedbackCommandOutput(self, name, output):
|
||||||
self._emit("feedbackCommandOutput", {"name": name, "output": output})
|
self._emit("feedbackCommandOutput", {"name": name, "output": output})
|
||||||
|
|
||||||
|
def sendTimelapseConfig(self, timelapseConfig):
|
||||||
|
self._emit("timelapse", timelapseConfig)
|
||||||
|
|
||||||
def addLog(self, data):
|
def addLog(self, data):
|
||||||
with self._logBacklogMutex:
|
with self._logBacklogMutex:
|
||||||
self._logBacklog.append(data)
|
self._logBacklog.append(data)
|
||||||
|
@ -529,28 +537,56 @@ def deleteTimelapse(filename):
|
||||||
@app.route(BASEURL + "timelapse", methods=["POST"])
|
@app.route(BASEURL + "timelapse", methods=["POST"])
|
||||||
@restricted_access
|
@restricted_access
|
||||||
def setTimelapseConfig():
|
def setTimelapseConfig():
|
||||||
global timelapse
|
|
||||||
|
|
||||||
if request.values.has_key("type"):
|
if request.values.has_key("type"):
|
||||||
type = request.values["type"]
|
config = {
|
||||||
if type in ["zchange", "timed"]:
|
"type": request.values["type"],
|
||||||
# valid timelapse type, check if there is an old one we need to stop first
|
"options": {}
|
||||||
if timelapse is not None:
|
}
|
||||||
timelapse.unload()
|
|
||||||
timelapse = None
|
if request.values.has_key("interval"):
|
||||||
if "zchange" == type:
|
|
||||||
timelapse = octoprint.timelapse.ZTimelapse()
|
|
||||||
elif "timed" == type:
|
|
||||||
interval = 10
|
interval = 10
|
||||||
if request.values.has_key("interval"):
|
try:
|
||||||
try:
|
interval = int(request.values["interval"])
|
||||||
interval = int(request.values["interval"])
|
except ValueError:
|
||||||
except ValueError:
|
pass
|
||||||
pass
|
|
||||||
timelapse = octoprint.timelapse.TimedTimelapse(interval)
|
config["options"] = {
|
||||||
|
"interval": interval
|
||||||
|
}
|
||||||
|
|
||||||
|
if admin_permission.can() and request.values.has_key("save") and request.values["save"] in valid_boolean_trues:
|
||||||
|
_configureTimelapse(config, True)
|
||||||
|
else:
|
||||||
|
_configureTimelapse(config)
|
||||||
|
|
||||||
return getTimelapseData()
|
return getTimelapseData()
|
||||||
|
|
||||||
|
def _configureTimelapse(config=None, persist=False):
|
||||||
|
global timelapse
|
||||||
|
|
||||||
|
if config is None:
|
||||||
|
config = settings().get(["webcam", "timelapse"])
|
||||||
|
|
||||||
|
if timelapse is not None:
|
||||||
|
timelapse.unload()
|
||||||
|
|
||||||
|
type = config["type"]
|
||||||
|
if type is None or "off" == type:
|
||||||
|
timelapse = None
|
||||||
|
elif "zchange" == type:
|
||||||
|
timelapse = octoprint.timelapse.ZTimelapse()
|
||||||
|
elif "timed" == type:
|
||||||
|
interval = 10
|
||||||
|
if "options" in config and "interval" in config["options"]:
|
||||||
|
interval = config["options"]["interval"]
|
||||||
|
timelapse = octoprint.timelapse.TimedTimelapse(interval)
|
||||||
|
|
||||||
|
octoprint.timelapse.notifyCallbacks(timelapse)
|
||||||
|
|
||||||
|
if persist:
|
||||||
|
settings().set(["webcam", "timelapse"], config)
|
||||||
|
settings().save()
|
||||||
|
|
||||||
#~~ settings
|
#~~ settings
|
||||||
|
|
||||||
@app.route(BASEURL + "settings", methods=["GET"])
|
@app.route(BASEURL + "settings", methods=["GET"])
|
||||||
|
@ -1056,6 +1092,9 @@ class Server():
|
||||||
gcodeManager = gcodefiles.GcodeManager()
|
gcodeManager = gcodefiles.GcodeManager()
|
||||||
printer = Printer(gcodeManager)
|
printer = Printer(gcodeManager)
|
||||||
|
|
||||||
|
# configure timelapse
|
||||||
|
_configureTimelapse()
|
||||||
|
|
||||||
# setup system and gcode command triggers
|
# setup system and gcode command triggers
|
||||||
events.SystemCommandTrigger(printer)
|
events.SystemCommandTrigger(printer)
|
||||||
events.GcodeCommandTrigger(printer)
|
events.GcodeCommandTrigger(printer)
|
||||||
|
|
|
@ -46,7 +46,11 @@ default_settings = {
|
||||||
"bitrate": "5000k",
|
"bitrate": "5000k",
|
||||||
"watermark": True,
|
"watermark": True,
|
||||||
"flipH": False,
|
"flipH": False,
|
||||||
"flipV": False
|
"flipV": False,
|
||||||
|
"timelapse": {
|
||||||
|
"type": "off",
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"feature": {
|
"feature": {
|
||||||
"gCodeVisualizer": True,
|
"gCodeVisualizer": True,
|
||||||
|
|
|
@ -109,7 +109,11 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
|
||||||
}
|
}
|
||||||
case "feedbackCommandOutput": {
|
case "feedbackCommandOutput": {
|
||||||
self.controlViewModel.fromFeedbackCommandData(payload);
|
self.controlViewModel.fromFeedbackCommandData(payload);
|
||||||
break
|
break;
|
||||||
|
}
|
||||||
|
case "timelapse": {
|
||||||
|
self.printerStateViewModel.fromTimelapseData(payload);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,13 @@ $(function() {
|
||||||
var usersViewModel = new UsersViewModel(loginStateViewModel);
|
var usersViewModel = new UsersViewModel(loginStateViewModel);
|
||||||
var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel);
|
var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel);
|
||||||
var connectionViewModel = new ConnectionViewModel(loginStateViewModel, settingsViewModel);
|
var connectionViewModel = new ConnectionViewModel(loginStateViewModel, settingsViewModel);
|
||||||
var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel);
|
var timelapseViewModel = new TimelapseViewModel(loginStateViewModel);
|
||||||
|
var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel, timelapseViewModel);
|
||||||
var appearanceViewModel = new AppearanceViewModel(settingsViewModel);
|
var appearanceViewModel = new AppearanceViewModel(settingsViewModel);
|
||||||
var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
|
var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
|
||||||
var controlViewModel = new ControlViewModel(loginStateViewModel, settingsViewModel);
|
var controlViewModel = new ControlViewModel(loginStateViewModel, settingsViewModel);
|
||||||
var terminalViewModel = new TerminalViewModel(loginStateViewModel, settingsViewModel);
|
var terminalViewModel = new TerminalViewModel(loginStateViewModel, settingsViewModel);
|
||||||
var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel);
|
var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel);
|
||||||
var timelapseViewModel = new TimelapseViewModel(loginStateViewModel);
|
|
||||||
var gcodeViewModel = new GcodeViewModel(loginStateViewModel);
|
var gcodeViewModel = new GcodeViewModel(loginStateViewModel);
|
||||||
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
|
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ function PrinterStateViewModel(loginStateViewModel) {
|
||||||
self.printTime = ko.observable(undefined);
|
self.printTime = ko.observable(undefined);
|
||||||
self.printTimeLeft = ko.observable(undefined);
|
self.printTimeLeft = ko.observable(undefined);
|
||||||
self.sd = ko.observable(undefined);
|
self.sd = ko.observable(undefined);
|
||||||
|
self.timelapse = ko.observable(undefined);
|
||||||
|
|
||||||
self.filament = ko.observable(undefined);
|
self.filament = ko.observable(undefined);
|
||||||
self.estimatedPrintTime = ko.observable(undefined);
|
self.estimatedPrintTime = ko.observable(undefined);
|
||||||
|
@ -49,6 +50,22 @@ function PrinterStateViewModel(loginStateViewModel) {
|
||||||
return "Pause";
|
return "Pause";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.timelapseString = ko.computed(function() {
|
||||||
|
var timelapse = self.timelapse();
|
||||||
|
|
||||||
|
if (!timelapse || !timelapse.hasOwnProperty("type"))
|
||||||
|
return "-";
|
||||||
|
|
||||||
|
var type = timelapse["type"];
|
||||||
|
if (type == "zchange") {
|
||||||
|
return "On Z Change";
|
||||||
|
} else if (type == "timed") {
|
||||||
|
return "Timed (" + timelapse["options"]["interval"] + "s)";
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.fromCurrentData = function(data) {
|
self.fromCurrentData = function(data) {
|
||||||
self._fromData(data);
|
self._fromData(data);
|
||||||
}
|
}
|
||||||
|
@ -57,6 +74,10 @@ function PrinterStateViewModel(loginStateViewModel) {
|
||||||
self._fromData(data);
|
self._fromData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.fromTimelapseData = function(data) {
|
||||||
|
self.timelapse(data);
|
||||||
|
}
|
||||||
|
|
||||||
self._fromData = function(data) {
|
self._fromData = function(data) {
|
||||||
self._processStateData(data.state)
|
self._processStateData(data.state)
|
||||||
self._processJobData(data.job);
|
self._processJobData(data.job);
|
||||||
|
|
|
@ -6,6 +6,9 @@ function TimelapseViewModel(loginStateViewModel) {
|
||||||
self.timelapseType = ko.observable(undefined);
|
self.timelapseType = ko.observable(undefined);
|
||||||
self.timelapseTimedInterval = ko.observable(undefined);
|
self.timelapseTimedInterval = ko.observable(undefined);
|
||||||
|
|
||||||
|
self.persist = ko.observable(false);
|
||||||
|
self.isDirty = ko.observable(false);
|
||||||
|
|
||||||
self.isErrorOrClosed = ko.observable(undefined);
|
self.isErrorOrClosed = ko.observable(undefined);
|
||||||
self.isOperational = ko.observable(undefined);
|
self.isOperational = ko.observable(undefined);
|
||||||
self.isPrinting = ko.observable(undefined);
|
self.isPrinting = ko.observable(undefined);
|
||||||
|
@ -16,11 +19,21 @@ function TimelapseViewModel(loginStateViewModel) {
|
||||||
|
|
||||||
self.intervalInputEnabled = ko.computed(function() {
|
self.intervalInputEnabled = ko.computed(function() {
|
||||||
return ("timed" == self.timelapseType());
|
return ("timed" == self.timelapseType());
|
||||||
})
|
});
|
||||||
|
self.saveButtonEnabled = ko.computed(function() {
|
||||||
|
return self.isDirty() && self.isOperational() && !self.isPrinting() && self.loginState.isUser();
|
||||||
|
});
|
||||||
|
|
||||||
self.isOperational.subscribe(function(newValue) {
|
self.isOperational.subscribe(function(newValue) {
|
||||||
self.requestData();
|
self.requestData();
|
||||||
})
|
});
|
||||||
|
|
||||||
|
self.timelapseType.subscribe(function(newValue) {
|
||||||
|
self.isDirty(true);
|
||||||
|
});
|
||||||
|
self.timelapseTimedInterval.subscribe(function(newValue) {
|
||||||
|
self.isDirty(true);
|
||||||
|
});
|
||||||
|
|
||||||
// initialize list helper
|
// initialize list helper
|
||||||
self.listHelper = new ItemListHelper(
|
self.listHelper = new ItemListHelper(
|
||||||
|
@ -51,7 +64,7 @@ function TimelapseViewModel(loginStateViewModel) {
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
CONFIG_TIMELAPSEFILESPERPAGE
|
CONFIG_TIMELAPSEFILESPERPAGE
|
||||||
)
|
);
|
||||||
|
|
||||||
self.requestData = function() {
|
self.requestData = function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
@ -60,17 +73,20 @@ function TimelapseViewModel(loginStateViewModel) {
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: self.fromResponse
|
success: self.fromResponse
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
self.fromResponse = function(response) {
|
self.fromResponse = function(response) {
|
||||||
self.timelapseType(response.type);
|
self.timelapseType(response.type);
|
||||||
self.listHelper.updateItems(response.files);
|
self.listHelper.updateItems(response.files);
|
||||||
|
|
||||||
if (response.type == "timed" && response.config && response.config.interval) {
|
if (response.type == "timed" && response.config && response.config.interval) {
|
||||||
self.timelapseTimedInterval(response.config.interval)
|
self.timelapseTimedInterval(response.config.interval);
|
||||||
} else {
|
} else {
|
||||||
self.timelapseTimedInterval(undefined)
|
self.timelapseTimedInterval(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.persist(false);
|
||||||
|
self.isDirty(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.fromCurrentData = function(data) {
|
self.fromCurrentData = function(data) {
|
||||||
|
@ -97,12 +113,13 @@ function TimelapseViewModel(loginStateViewModel) {
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: self.requestData
|
success: self.requestData
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save = function() {
|
self.save = function(data, event) {
|
||||||
var data = {
|
var data = {
|
||||||
"type": self.timelapseType()
|
"type": self.timelapseType(),
|
||||||
|
"save": self.persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.timelapseType() == "timed") {
|
if (self.timelapseType() == "timed") {
|
||||||
|
@ -115,6 +132,6 @@ function TimelapseViewModel(loginStateViewModel) {
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
data: data,
|
data: data,
|
||||||
success: self.fromResponse
|
success: self.fromResponse
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,7 @@
|
||||||
File: <strong data-bind="text: filename"></strong> <strong data-bind="visible: sd">(SD)</strong><br>
|
File: <strong data-bind="text: filename"></strong> <strong data-bind="visible: sd">(SD)</strong><br>
|
||||||
Filament: <strong data-bind="text: filament"></strong><br>
|
Filament: <strong data-bind="text: filament"></strong><br>
|
||||||
Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br>
|
Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br>
|
||||||
|
Timelapse: <strong data-bind="text: timelapseString"></strong><br>
|
||||||
Height: <strong data-bind="text: heightString"></strong><br>
|
Height: <strong data-bind="text: heightString"></strong><br>
|
||||||
Print Time: <strong data-bind="text: printTime"></strong><br>
|
Print Time: <strong data-bind="text: printTime"></strong><br>
|
||||||
Print Time Left: <strong data-bind="text: printTimeLeft"></strong><br>
|
Print Time Left: <strong data-bind="text: printTimeLeft"></strong><br>
|
||||||
|
@ -542,13 +543,19 @@
|
||||||
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
|
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
|
||||||
<label for="webcam_timelapse_interval">Interval</label>
|
<label for="webcam_timelapse_interval">Interval</label>
|
||||||
<div class="input-append">
|
<div class="input-append">
|
||||||
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting() && loginState.isUser()">
|
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()">
|
||||||
<span class="add-on">sec</span>
|
<span class="add-on">sec</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div data-bind="visible: loginState.isAdmin">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" data-bind="checked: persist"> Save as default
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting() && loginState.isUser()">Save Settings</button>
|
<button class="btn" data-bind="click: save, enable: saveButtonEnabled">Save config</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,26 @@ def getFinishedTimelapses():
|
||||||
})
|
})
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
validTimelapseTypes = ["off", "timed", "zchange"]
|
||||||
|
|
||||||
|
updateCallbacks = []
|
||||||
|
def registerCallback(callback):
|
||||||
|
if not callback in updateCallbacks:
|
||||||
|
updateCallbacks.append(callback)
|
||||||
|
|
||||||
|
def unregisterCallback(callback):
|
||||||
|
if callback in updateCallbacks:
|
||||||
|
updateCallbacks.remove(callback)
|
||||||
|
|
||||||
|
def notifyCallbacks(timelapse):
|
||||||
|
for callback in updateCallbacks:
|
||||||
|
if timelapse is None:
|
||||||
|
config = None
|
||||||
|
else:
|
||||||
|
config = timelapse.configData()
|
||||||
|
try: callback.sendTimelapseConfig(config)
|
||||||
|
except: pass
|
||||||
|
|
||||||
class Timelapse(object):
|
class Timelapse(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._logger = logging.getLogger(__name__)
|
self._logger = logging.getLogger(__name__)
|
||||||
|
@ -98,6 +118,16 @@ class Timelapse(object):
|
||||||
"""
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def configData(self):
|
||||||
|
"""
|
||||||
|
Override this method to return the current timelapse configuration data. The data should have the following
|
||||||
|
form:
|
||||||
|
|
||||||
|
type: "<type of timelapse>",
|
||||||
|
options: { <additional options> }
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
def startTimelapse(self, gcodeFile):
|
def startTimelapse(self, gcodeFile):
|
||||||
self._logger.debug("Starting timelapse for %s" % gcodeFile)
|
self._logger.debug("Starting timelapse for %s" % gcodeFile)
|
||||||
self.cleanCaptureDir()
|
self.cleanCaptureDir()
|
||||||
|
@ -212,6 +242,11 @@ class ZTimelapse(Timelapse):
|
||||||
("ZChange", self._onZChange)
|
("ZChange", self._onZChange)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def configData(self):
|
||||||
|
return {
|
||||||
|
"type": "zchange"
|
||||||
|
}
|
||||||
|
|
||||||
def _onZChange(self, event, payload):
|
def _onZChange(self, event, payload):
|
||||||
self.captureImage()
|
self.captureImage()
|
||||||
|
|
||||||
|
@ -227,6 +262,14 @@ class TimedTimelapse(Timelapse):
|
||||||
def interval(self):
|
def interval(self):
|
||||||
return self._interval
|
return self._interval
|
||||||
|
|
||||||
|
def configData(self):
|
||||||
|
return {
|
||||||
|
"type": "timed",
|
||||||
|
"options": {
|
||||||
|
"interval": self._interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def onPrintStarted(self, event, payload):
|
def onPrintStarted(self, event, payload):
|
||||||
Timelapse.onPrintStarted(self, event, payload)
|
Timelapse.onPrintStarted(self, event, payload)
|
||||||
if self._timerThread is not None:
|
if self._timerThread is not None:
|
||||||
|
|
Loading…
Reference in New Issue