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 #116
master
Gina Häußge 2013-09-08 15:41:26 +02:00
parent 715dea7eb3
commit 0471f1155e
8 changed files with 168 additions and 33 deletions

View File

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

View File

@ -46,7 +46,11 @@ default_settings = {
"bitrate": "5000k",
"watermark": True,
"flipH": False,
"flipV": False
"flipV": False,
"timelapse": {
"type": "off",
"options": {}
}
},
"feature": {
"gCodeVisualizer": True,

View File

@ -109,7 +109,11 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
}
case "feedbackCommandOutput": {
self.controlViewModel.fromFeedbackCommandData(payload);
break
break;
}
case "timelapse": {
self.printerStateViewModel.fromTimelapseData(payload);
break;
}
}
}

View File

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

View File

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

View File

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

View File

@ -116,6 +116,7 @@
File: <strong data-bind="text: filename"></strong>&nbsp;<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>

View File

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