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._printer.registerCallback(self)
|
||||
self._gcodeManager.registerCallback(self)
|
||||
octoprint.timelapse.registerCallback(self)
|
||||
|
||||
self._eventManager.fire("ClientOpened")
|
||||
self._eventManager.subscribe("MovieDone", self._onMovieDone)
|
||||
|
||||
global timelapse
|
||||
octoprint.timelapse.notifyCallbacks(timelapse)
|
||||
|
||||
def on_close(self):
|
||||
self._logger.info("Closed client connection")
|
||||
self._printer.unregisterCallback(self)
|
||||
self._gcodeManager.unregisterCallback(self)
|
||||
octoprint.timelapse.unregisterCallback(self)
|
||||
|
||||
self._eventManager.fire("ClientClosed")
|
||||
self._eventManager.unsubscribe("MovieDone", self._onMovieDone)
|
||||
|
@ -120,6 +125,9 @@ class PrinterStateConnection(SockJSConnection):
|
|||
def sendFeedbackCommandOutput(self, name, output):
|
||||
self._emit("feedbackCommandOutput", {"name": name, "output": output})
|
||||
|
||||
def sendTimelapseConfig(self, timelapseConfig):
|
||||
self._emit("timelapse", timelapseConfig)
|
||||
|
||||
def addLog(self, data):
|
||||
with self._logBacklogMutex:
|
||||
self._logBacklog.append(data)
|
||||
|
@ -529,28 +537,56 @@ def deleteTimelapse(filename):
|
|||
@app.route(BASEURL + "timelapse", methods=["POST"])
|
||||
@restricted_access
|
||||
def setTimelapseConfig():
|
||||
global timelapse
|
||||
|
||||
if request.values.has_key("type"):
|
||||
type = request.values["type"]
|
||||
if type in ["zchange", "timed"]:
|
||||
# valid timelapse type, check if there is an old one we need to stop first
|
||||
if timelapse is not None:
|
||||
timelapse.unload()
|
||||
timelapse = None
|
||||
if "zchange" == type:
|
||||
timelapse = octoprint.timelapse.ZTimelapse()
|
||||
elif "timed" == type:
|
||||
config = {
|
||||
"type": request.values["type"],
|
||||
"options": {}
|
||||
}
|
||||
|
||||
if request.values.has_key("interval"):
|
||||
interval = 10
|
||||
if request.values.has_key("interval"):
|
||||
try:
|
||||
interval = int(request.values["interval"])
|
||||
except ValueError:
|
||||
pass
|
||||
timelapse = octoprint.timelapse.TimedTimelapse(interval)
|
||||
try:
|
||||
interval = int(request.values["interval"])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
@app.route(BASEURL + "settings", methods=["GET"])
|
||||
|
@ -1056,6 +1092,9 @@ class Server():
|
|||
gcodeManager = gcodefiles.GcodeManager()
|
||||
printer = Printer(gcodeManager)
|
||||
|
||||
# configure timelapse
|
||||
_configureTimelapse()
|
||||
|
||||
# setup system and gcode command triggers
|
||||
events.SystemCommandTrigger(printer)
|
||||
events.GcodeCommandTrigger(printer)
|
||||
|
|
|
@ -46,7 +46,11 @@ default_settings = {
|
|||
"bitrate": "5000k",
|
||||
"watermark": True,
|
||||
"flipH": False,
|
||||
"flipV": False
|
||||
"flipV": False,
|
||||
"timelapse": {
|
||||
"type": "off",
|
||||
"options": {}
|
||||
}
|
||||
},
|
||||
"feature": {
|
||||
"gCodeVisualizer": True,
|
||||
|
|
|
@ -109,7 +109,11 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
|
|||
}
|
||||
case "feedbackCommandOutput": {
|
||||
self.controlViewModel.fromFeedbackCommandData(payload);
|
||||
break
|
||||
break;
|
||||
}
|
||||
case "timelapse": {
|
||||
self.printerStateViewModel.fromTimelapseData(payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@ $(function() {
|
|||
var usersViewModel = new UsersViewModel(loginStateViewModel);
|
||||
var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel);
|
||||
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 temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
|
||||
var controlViewModel = new ControlViewModel(loginStateViewModel, settingsViewModel);
|
||||
var terminalViewModel = new TerminalViewModel(loginStateViewModel, settingsViewModel);
|
||||
var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel);
|
||||
var timelapseViewModel = new TimelapseViewModel(loginStateViewModel);
|
||||
var gcodeViewModel = new GcodeViewModel(loginStateViewModel);
|
||||
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ function PrinterStateViewModel(loginStateViewModel) {
|
|||
self.printTime = ko.observable(undefined);
|
||||
self.printTimeLeft = ko.observable(undefined);
|
||||
self.sd = ko.observable(undefined);
|
||||
self.timelapse = ko.observable(undefined);
|
||||
|
||||
self.filament = ko.observable(undefined);
|
||||
self.estimatedPrintTime = ko.observable(undefined);
|
||||
|
@ -49,6 +50,22 @@ function PrinterStateViewModel(loginStateViewModel) {
|
|||
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._fromData(data);
|
||||
}
|
||||
|
@ -57,6 +74,10 @@ function PrinterStateViewModel(loginStateViewModel) {
|
|||
self._fromData(data);
|
||||
}
|
||||
|
||||
self.fromTimelapseData = function(data) {
|
||||
self.timelapse(data);
|
||||
}
|
||||
|
||||
self._fromData = function(data) {
|
||||
self._processStateData(data.state)
|
||||
self._processJobData(data.job);
|
||||
|
|
|
@ -6,6 +6,9 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
self.timelapseType = ko.observable(undefined);
|
||||
self.timelapseTimedInterval = ko.observable(undefined);
|
||||
|
||||
self.persist = ko.observable(false);
|
||||
self.isDirty = ko.observable(false);
|
||||
|
||||
self.isErrorOrClosed = ko.observable(undefined);
|
||||
self.isOperational = ko.observable(undefined);
|
||||
self.isPrinting = ko.observable(undefined);
|
||||
|
@ -16,11 +19,21 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
|
||||
self.intervalInputEnabled = ko.computed(function() {
|
||||
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.requestData();
|
||||
})
|
||||
});
|
||||
|
||||
self.timelapseType.subscribe(function(newValue) {
|
||||
self.isDirty(true);
|
||||
});
|
||||
self.timelapseTimedInterval.subscribe(function(newValue) {
|
||||
self.isDirty(true);
|
||||
});
|
||||
|
||||
// initialize list helper
|
||||
self.listHelper = new ItemListHelper(
|
||||
|
@ -51,7 +64,7 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
[],
|
||||
[],
|
||||
CONFIG_TIMELAPSEFILESPERPAGE
|
||||
)
|
||||
);
|
||||
|
||||
self.requestData = function() {
|
||||
$.ajax({
|
||||
|
@ -60,17 +73,20 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
dataType: "json",
|
||||
success: self.fromResponse
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
self.timelapseType(response.type);
|
||||
self.listHelper.updateItems(response.files);
|
||||
|
||||
if (response.type == "timed" && response.config && response.config.interval) {
|
||||
self.timelapseTimedInterval(response.config.interval)
|
||||
self.timelapseTimedInterval(response.config.interval);
|
||||
} else {
|
||||
self.timelapseTimedInterval(undefined)
|
||||
self.timelapseTimedInterval(undefined);
|
||||
}
|
||||
|
||||
self.persist(false);
|
||||
self.isDirty(false);
|
||||
}
|
||||
|
||||
self.fromCurrentData = function(data) {
|
||||
|
@ -97,12 +113,13 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
type: "DELETE",
|
||||
dataType: "json",
|
||||
success: self.requestData
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
self.save = function() {
|
||||
self.save = function(data, event) {
|
||||
var data = {
|
||||
"type": self.timelapseType()
|
||||
"type": self.timelapseType(),
|
||||
"save": self.persist()
|
||||
}
|
||||
|
||||
if (self.timelapseType() == "timed") {
|
||||
|
@ -115,6 +132,6 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
dataType: "json",
|
||||
data: data,
|
||||
success: self.fromResponse
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,6 +116,7 @@
|
|||
File: <strong data-bind="text: filename"></strong> <strong data-bind="visible: sd">(SD)</strong><br>
|
||||
Filament: <strong data-bind="text: filament"></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>
|
||||
Print Time: <strong data-bind="text: printTime"></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()">
|
||||
<label for="webcam_timelapse_interval">Interval</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: loginState.isAdmin">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: persist"> Save as default
|
||||
</label>
|
||||
</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>
|
||||
|
||||
|
|
|
@ -33,6 +33,26 @@ def getFinishedTimelapses():
|
|||
})
|
||||
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):
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
@ -98,6 +118,16 @@ class Timelapse(object):
|
|||
"""
|
||||
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):
|
||||
self._logger.debug("Starting timelapse for %s" % gcodeFile)
|
||||
self.cleanCaptureDir()
|
||||
|
@ -212,6 +242,11 @@ class ZTimelapse(Timelapse):
|
|||
("ZChange", self._onZChange)
|
||||
]
|
||||
|
||||
def configData(self):
|
||||
return {
|
||||
"type": "zchange"
|
||||
}
|
||||
|
||||
def _onZChange(self, event, payload):
|
||||
self.captureImage()
|
||||
|
||||
|
@ -227,6 +262,14 @@ class TimedTimelapse(Timelapse):
|
|||
def interval(self):
|
||||
return self._interval
|
||||
|
||||
def configData(self):
|
||||
return {
|
||||
"type": "timed",
|
||||
"options": {
|
||||
"interval": self._interval
|
||||
}
|
||||
}
|
||||
|
||||
def onPrintStarted(self, event, payload):
|
||||
Timelapse.onPrintStarted(self, event, payload)
|
||||
if self._timerThread is not None:
|
||||
|
|
Loading…
Reference in New Issue