Merge branch 'devel' into dev/firstRunWizard

This commit is contained in:
Gina Häußge 2015-08-17 12:14:31 +02:00
commit 1238507806
8 changed files with 353 additions and 176 deletions

View file

@ -155,7 +155,11 @@ class FileManager(object):
self._slicing_progress_callbacks.append(callback)
def unregister_slicingprogress_callback(self, callback):
self._slicing_progress_callbacks.remove(callback)
try:
self._slicing_progress_callbacks.remove(callback)
except ValueError:
# callback was not registered
pass
def _determine_analysis_backlog(self, storage_type, storage_manager):
counter = 0

View file

@ -285,7 +285,11 @@ def setSettings():
logger.exception("Could not save settings for plugin {name} ({version})".format(version=plugin._plugin_version, name=plugin._plugin_name))
if s.save():
eventManager().fire(Events.SETTINGS_UPDATED)
payload = dict(
config_hash=s.config_hash,
effective_hash=s.effective_hash
)
eventManager().fire(Events.SETTINGS_UPDATED, payload=payload)
return getSettings()

View file

@ -13,6 +13,7 @@ import time
import octoprint.timelapse
import octoprint.server
from octoprint.events import Events
from octoprint.settings import settings
import octoprint.printer
@ -58,8 +59,16 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.
plugin_hash = hashlib.md5()
plugin_hash.update(",".join(ui_plugins))
config_hash = settings().config_hash
# connected => update the API key, might be necessary if the client was left open while the server restarted
self._emit("connected", {"apikey": octoprint.server.UI_API_KEY, "version": octoprint.server.VERSION, "display_version": octoprint.server.DISPLAY_VERSION, "plugin_hash": plugin_hash.hexdigest()})
self._emit("connected", dict(
apikey=octoprint.server.UI_API_KEY,
version=octoprint.server.VERSION,
display_version=octoprint.server.DISPLAY_VERSION,
plugin_hash=plugin_hash.hexdigest(),
config_hash=config_hash
))
self._printer.register_callback(self)
self._fileManager.register_slicingprogress_callback(self)

View file

@ -545,6 +545,20 @@ class Settings(object):
import yaml
return yaml.safe_dump(self.effective)
@property
def effective_hash(self):
import hashlib
hash = hashlib.md5()
hash.update(repr(self.effective))
return hash.hexdigest()
@property
def config_hash(self):
import hashlib
hash = hashlib.md5()
hash.update(repr(self._config))
return hash.hexdigest()
#~~ load and save
def load(self, migrate=False):

View file

@ -10,6 +10,7 @@ function DataUpdater(allViewModels) {
self._autoReconnectDialogIndex = 1;
self._pluginHash = undefined;
self._configHash = undefined;
self.reloadOverlay = $("#reloadui_overlay");
$("#reloadui_overlay_reload").click(function() { location.reload(true); });
@ -127,6 +128,9 @@ function DataUpdater(allViewModels) {
var oldPluginHash = self._pluginHash;
self._pluginHash = data["plugin_hash"];
var oldConfigHash = self._configHash;
self._configHash = data["config_hash"];
if ($("#offline_overlay").is(":visible")) {
hideOfflineOverlay();
_.each(self.allViewModels, function(viewModel) {
@ -140,7 +144,10 @@ function DataUpdater(allViewModels) {
}
}
if (oldVersion != VERSION || (oldPluginHash != undefined && oldPluginHash != self._pluginHash)) {
var versionChanged = oldVersion != VERSION;
var pluginsChanged = oldPluginHash != undefined && oldPluginHash != self._pluginHash;
var configChanged = oldConfigHash != undefined && oldConfigHash != self._configHash;
if (versionChanged || pluginsChanged || configChanged) {
self.reloadOverlay.show();
}
@ -180,7 +187,11 @@ function DataUpdater(allViewModels) {
log.debug("Got event " + type + " with payload: " + JSON.stringify(payload));
if (type == "MovieRendering") {
if (type == "SettingsUpdated") {
if (payload && payload.hasOwnProperty("config_hash")) {
self._configHash = payload.config_hash;
}
} else if (type == "MovieRendering") {
new PNotify({title: gettext("Rendering timelapse"), text: _.sprintf(gettext("Now rendering timelapse %(movie_basename)s"), payload)});
} else if (type == "MovieDone") {
new PNotify({title: gettext("Timelapse ready"), text: _.sprintf(gettext("New timelapse %(movie_basename)s is done rendering."), payload)});

View file

@ -488,3 +488,110 @@ function splitTextToArray(text, sep, stripEmpty, filter) {
function(item) { return (stripEmpty ? item : true) && (filter ? filter(item) : true); }
);
}
/**
* Returns true if comparing data and oldData yields changes, false otherwise.
*
* E.g.
*
* hasDataChanged(
* {foo: "bar", fnord: {one: "1", two: "2", three: "three", key: "value"}},
* {foo: "bar", fnord: {one: "1", two: "2", three: "3", four: "4"}}
* )
*
* will return
*
* true
*
* and
*
* hasDataChanged(
* {foo: "bar", fnord: {one: "1", two: "2", three: "3"}},
* {foo: "bar", fnord: {one: "1", two: "2", three: "3"}}
* )
*
* will return
*
* false
*
* Note that this will assume data and oldData to be structurally identical (same keys)
* and is optimized to check for value changes, not key updates.
*/
function hasDataChanged(data, oldData) {
if (data == undefined) {
return false;
}
if (oldData == undefined) {
return true;
}
if (_.isPlainObject(data)) {
return _.any(_.keys(data), function(key) {return hasDataChanged(data[key], oldData[key]);});
} else {
return !_.isEqual(data, oldData);
}
}
/**
* Compare provided data and oldData plain objects and only return those
* substructures of data that actually changed.
*
* E.g.
*
* getOnlyChangedData(
* {foo: "bar", fnord: {one: "1", two: "2", three: "three"}},
* {foo: "bar", fnord: {one: "1", two: "2", three: "3"}}
* )
*
* will return
*
* {fnord: {three: "three"}}
*
* and
*
* getOnlyChangedData(
* {foo: "bar", fnord: {one: "1", two: "2", three: "3"}},
* {foo: "bar", fnord: {one: "1", two: "2", three: "3"}}
* )
*
* will return
*
* {}
*
* Note that this will assume data and oldData to be structurally identical (same keys)
* and is optimized to check for value changes, not key updates.
*/
function getOnlyChangedData(data, oldData) {
if (data == undefined) {
return {};
}
if (oldData == undefined) {
return data;
}
var f = function(root, oldRoot) {
if (!_.isPlainObject(root)) {
return root;
}
var retval = {};
_.forOwn(root, function(value, key) {
var oldValue = oldRoot[key];
if (_.isPlainObject(value)) {
if (hasDataChanged(value, oldValue)) {
retval[key] = f(value, oldValue);
}
} else {
if (!_.isEqual(value, oldValue)) {
retval[key] = value;
}
}
});
return retval;
};
return f(data, oldData);
}

View file

@ -22,6 +22,7 @@ $(function() {
self.appearance_defaultLanguage = ko.observable();
self.settingsDialog = undefined;
self.settings_dialog_update_detected = undefined;
self.translationManagerDialog = undefined;
self.translationUploadElement = $("#settings_appearance_managelanguagesdialog_upload");
self.translationUploadButton = $("#settings_appearance_managelanguagesdialog_upload_start");
@ -164,6 +165,7 @@ $(function() {
self.server_commands_serverRestartCommand = ko.observable(undefined);
self.settings = undefined;
self.lastReceivedSettings = undefined;
self.addTemperatureProfile = function() {
self.temperature_profiles.push({name: "New", extruder:0, bed:0});
@ -182,11 +184,16 @@ $(function() {
};
self.onSettingsShown = function() {
self.requestData();
self.requestData();
};
self.isDialogActive = function() {
return self.settingsDialog.is(":visible");
};
self.onStartup = function() {
self.settingsDialog = $('#settings_dialog');
self.settingsUpdatedDialog = $('#settings_dialog_update_detected');
self.translationManagerDialog = $('#settings_appearance_managelanguagesdialog');
self.translationUploadElement = $("#settings_appearance_managelanguagesdialog_upload");
self.translationUploadButton = $("#settings_appearance_managelanguagesdialog_upload_start");
@ -247,6 +254,19 @@ $(function() {
}
});
});
$(".reload_all", self.settingsUpdatedDialog).click(function(e) {
e.preventDefault();
self.settingsUpdatedDialog.modal("hide");
self.requestData();
return false;
});
$(".reload_nonconflicts", self.settingsUpdatedDialog).click(function(e) {
e.preventDefault();
self.settingsUpdatedDialog.modal("hide");
self.requestData(undefined, true);
return false;
});
};
self.show = function() {
@ -270,7 +290,7 @@ $(function() {
return false;
};
self.requestData = function(callback) {
self.requestData = function(callback, local) {
if (self.receiving()) {
if (callback) {
self.callbacks.push(callback);
@ -289,7 +309,7 @@ $(function() {
}
try {
self.fromResponse(response);
self.fromResponse(response, local);
var cb;
while (self.callbacks.length) {
@ -380,92 +400,150 @@ $(function() {
})
};
self.fromResponse = function(response) {
/**
* Fetches the settings as currently stored in this client instance.
*/
self._getLocalData = function() {
var data = {};
if (self.settings != undefined) {
data = ko.mapping.toJS(self.settings);
}
// some special read functions for various observables
var specialMappings = {
feature: {
externalHeatupDetection: function() { return !self.feature_disableExternalHeatupDetection() }
},
serial: {
additionalPorts : function() { return commentableLinesToArray(self.serial_additionalPorts()) },
additionalBaudrates: function() { return _.map(splitTextToArray(self.serial_additionalBaudrates(), ",", true, function(item) { return !isNaN(parseInt(item)); }), function(item) { return parseInt(item); }) },
longRunningCommands: function() { return splitTextToArray(self.serial_longRunningCommands(), ",", true) },
checksumRequiringCommands: function() { return splitTextToArray(self.serial_checksumRequiringCommands(), ",", true) }
}
};
var mapFromObservables = function(data, mapping, keyPrefix) {
var flag = false;
var result = {};
// process all key-value-pairs here
_.forOwn(data, function(value, key) {
var observable = key;
if (keyPrefix != undefined) {
observable = keyPrefix + "_" + observable;
}
if (_.isPlainObject(value)) {
// value is another object, we'll dive deeper
var subresult = mapFromObservables(value, (mapping && mapping[key]) ? mapping[key] : undefined, observable);
if (subresult != undefined) {
// we only set something on our result if we got something back
result[key] = subresult;
flag = true;
}
} else {
// if we have a custom read function for this, we'll use it
if (mapping && mapping[key] && _.isFunction(mapping[key])) {
result[key] = mapping[key]();
flag = true;
} else if (self.hasOwnProperty(observable)) {
result[key] = self[observable]();
flag = true;
}
}
});
// if we set something on our result (flag is true), we return result, else we return undefined
return flag ? result : undefined;
};
// map local observables based on our existing data
var dataFromObservables = mapFromObservables(data, specialMappings);
data = _.extend(data, dataFromObservables);
return data;
};
self.fromResponse = function(response, local) {
// server side changes to set
var serverChangedData;
// client side changes to keep
var clientChangedData;
if (local) {
// local is true, so we'll keep all local changes and only update what's been updated server side
serverChangedData = getOnlyChangedData(response, self.lastReceivedSettings);
clientChangedData = getOnlyChangedData(self._getLocalData(), self.lastReceivedSettings);
} else {
// local is false or unset, so we'll forcefully update with the settings from the server
serverChangedData = response;
clientChangedData = undefined;
}
// last received settings reset to response
self.lastReceivedSettings = response;
if (self.settings === undefined) {
self.settings = ko.mapping.fromJS(response);
self.settings = ko.mapping.fromJS(serverChangedData);
} else {
ko.mapping.fromJS(response, self.settings);
ko.mapping.fromJS(serverChangedData, self.settings);
}
self.api_enabled(response.api.enabled);
self.api_key(response.api.key);
self.api_allowCrossOrigin(response.api.allowCrossOrigin);
// some special apply functions for various observables
var specialMappings = {
appearance: {
defaultLanguage: function(value) {
self.appearance_defaultLanguage("_default");
if (_.includes(self.locale_languages, value)) {
self.appearance_defaultLanguage(value);
}
}
},
feature: {
externalHeatupDetection: function(value) { self.feature_disableExternalHeatupDetection(!value) }
},
serial: {
additionalPorts : function(value) { self.serial_additionalPorts(value.join("\n"))},
additionalBaudrates: function(value) { self.serial_additionalBaudrates(value.join(", "))},
longRunningCommands: function(value) { self.serial_longRunningCommands(value.join(", "))},
checksumRequiringCommands: function(value) { self.serial_checksumRequiringCommands(value.join(", "))}
}
};
self.appearance_name(response.appearance.name);
self.appearance_color(response.appearance.color);
self.appearance_colorTransparent(response.appearance.colorTransparent);
self.appearance_defaultLanguage("_default");
if (_.includes(self.locale_languages, response.appearance.defaultLanguage)) {
self.appearance_defaultLanguage(response.appearance.defaultLanguage);
}
var mapToObservables = function(data, mapping, local, keyPrefix) {
if (!_.isPlainObject(data)) {
return;
}
self.printer_defaultExtrusionLength(response.printer.defaultExtrusionLength);
// process all key-value-pairs here
_.forOwn(data, function(value, key) {
var observable = key;
if (keyPrefix != undefined) {
observable = keyPrefix + "_" + observable;
}
self.webcam_streamUrl(response.webcam.streamUrl);
self.webcam_snapshotUrl(response.webcam.snapshotUrl);
self.webcam_ffmpegPath(response.webcam.ffmpegPath);
self.webcam_bitrate(response.webcam.bitrate);
self.webcam_ffmpegThreads(response.webcam.ffmpegThreads);
self.webcam_watermark(response.webcam.watermark);
self.webcam_flipH(response.webcam.flipH);
self.webcam_flipV(response.webcam.flipV);
self.webcam_rotate90(response.webcam.rotate90);
if (_.isPlainObject(value)) {
// value is another object, we'll dive deeper
mapToObservables(value, (mapping && mapping[key]) ? mapping[key] : undefined, (local && local[key]) ? local[key] : undefined, observable);
} else {
// if we have a local version of this field, we'll not override it
if (local && local.hasOwnProperty(key)) {
return;
}
self.feature_gcodeViewer(response.feature.gcodeViewer);
self.feature_temperatureGraph(response.feature.temperatureGraph);
self.feature_waitForStart(response.feature.waitForStart);
self.feature_alwaysSendChecksum(response.feature.alwaysSendChecksum);
self.feature_sdSupport(response.feature.sdSupport);
self.feature_sdAlwaysAvailable(response.feature.sdAlwaysAvailable);
self.feature_swallowOkAfterResend(response.feature.swallowOkAfterResend);
self.feature_repetierTargetTemp(response.feature.repetierTargetTemp);
self.feature_disableExternalHeatupDetection(!response.feature.externalHeatupDetection);
self.feature_keyboardControl(response.feature.keyboardControl);
self.feature_pollWatched(response.feature.pollWatched);
if (mapping && mapping[key] && _.isFunction(mapping[key])) {
// if we have a custom apply function for this, we'll use it
mapping[key](value);
} else if (self.hasOwnProperty(observable)) {
// else if we have a matching observable, we'll use that
self[observable](value);
}
}
});
};
self.serial_port(response.serial.port);
self.serial_baudrate(response.serial.baudrate);
self.serial_portOptions(response.serial.portOptions);
self.serial_baudrateOptions(response.serial.baudrateOptions);
self.serial_autoconnect(response.serial.autoconnect);
self.serial_timeoutConnection(response.serial.timeoutConnection);
self.serial_timeoutDetection(response.serial.timeoutDetection);
self.serial_timeoutCommunication(response.serial.timeoutCommunication);
self.serial_timeoutTemperature(response.serial.timeoutTemperature);
self.serial_timeoutSdStatus(response.serial.timeoutSdStatus);
self.serial_log(response.serial.log);
self.serial_additionalPorts(response.serial.additionalPorts.join("\n"));
self.serial_additionalBaudrates(response.serial.additionalBaudrates.join(", "));
self.serial_longRunningCommands(response.serial.longRunningCommands.join(", "));
self.serial_checksumRequiringCommands(response.serial.checksumRequiringCommands.join(", "));
self.serial_helloCommand(response.serial.helloCommand);
self.folder_uploads(response.folder.uploads);
self.folder_timelapse(response.folder.timelapse);
self.folder_timelapseTmp(response.folder.timelapseTmp);
self.folder_logs(response.folder.logs);
self.folder_watched(response.folder.watched);
self.temperature_profiles(response.temperature.profiles);
self.scripts_gcode_beforePrintStarted(response.scripts.gcode.beforePrintStarted);
self.scripts_gcode_afterPrintDone(response.scripts.gcode.afterPrintDone);
self.scripts_gcode_afterPrintCancelled(response.scripts.gcode.afterPrintCancelled);
self.scripts_gcode_afterPrintPaused(response.scripts.gcode.afterPrintPaused);
self.scripts_gcode_beforePrintResumed(response.scripts.gcode.beforePrintResumed);
self.scripts_gcode_afterPrinterConnected(response.scripts.gcode.afterPrinterConnected);
self.scripts_gcode_beforePrinterDisconnected(response.scripts.gcode.beforePrinterDisconnected);
self.temperature_profiles(response.temperature.profiles);
self.temperature_cutoff(response.temperature.cutoff);
self.system_actions(response.system.actions);
self.terminalFilters(response.terminalFilters);
self.server_commands_systemShutdownCommand(response.server.commands.systemShutdownCommand);
self.server_commands_systemRestartCommand(response.server.commands.systemRestartCommand);
self.server_commands_serverRestartCommand(response.server.commands.serverRestartCommand);
mapToObservables(serverChangedData, specialMappings, clientChangedData);
};
self.saveData = function (data, successCallback) {
@ -474,97 +552,9 @@ $(function() {
if (data == undefined) {
// we only set sending to true when we didn't include data
self.sending(true);
data = ko.mapping.toJS(self.settings);
data = _.extend(data, {
"api" : {
"enabled": self.api_enabled(),
"key": self.api_key(),
"allowCrossOrigin": self.api_allowCrossOrigin()
},
"appearance" : {
"name": self.appearance_name(),
"color": self.appearance_color(),
"colorTransparent": self.appearance_colorTransparent(),
"defaultLanguage": self.appearance_defaultLanguage()
},
"printer": {
"defaultExtrusionLength": self.printer_defaultExtrusionLength()
},
"webcam": {
"streamUrl": self.webcam_streamUrl(),
"snapshotUrl": self.webcam_snapshotUrl(),
"ffmpegPath": self.webcam_ffmpegPath(),
"bitrate": self.webcam_bitrate(),
"ffmpegThreads": self.webcam_ffmpegThreads(),
"watermark": self.webcam_watermark(),
"flipH": self.webcam_flipH(),
"flipV": self.webcam_flipV(),
"rotate90": self.webcam_rotate90()
},
"feature": {
"gcodeViewer": self.feature_gcodeViewer(),
"temperatureGraph": self.feature_temperatureGraph(),
"waitForStart": self.feature_waitForStart(),
"alwaysSendChecksum": self.feature_alwaysSendChecksum(),
"sdSupport": self.feature_sdSupport(),
"sdAlwaysAvailable": self.feature_sdAlwaysAvailable(),
"swallowOkAfterResend": self.feature_swallowOkAfterResend(),
"repetierTargetTemp": self.feature_repetierTargetTemp(),
"externalHeatupDetection": !self.feature_disableExternalHeatupDetection(),
"keyboardControl": self.feature_keyboardControl(),
"pollWatched": self.feature_pollWatched()
},
"serial": {
"port": self.serial_port(),
"baudrate": self.serial_baudrate(),
"autoconnect": self.serial_autoconnect(),
"timeoutConnection": self.serial_timeoutConnection(),
"timeoutDetection": self.serial_timeoutDetection(),
"timeoutCommunication": self.serial_timeoutCommunication(),
"timeoutTemperature": self.serial_timeoutTemperature(),
"timeoutSdStatus": self.serial_timeoutSdStatus(),
"log": self.serial_log(),
"additionalPorts": commentableLinesToArray(self.serial_additionalPorts()),
"additionalBaudrates": _.map(splitTextToArray(self.serial_additionalBaudrates(), ",", true, function(item) { return !isNaN(parseInt(item)); }), function(item) { return parseInt(item); }),
"longRunningCommands": splitTextToArray(self.serial_longRunningCommands(), ",", true),
"checksumRequiringCommands": splitTextToArray(self.serial_checksumRequiringCommands(), ",", true),
"helloCommand": self.serial_helloCommand()
},
"folder": {
"uploads": self.folder_uploads(),
"timelapse": self.folder_timelapse(),
"timelapseTmp": self.folder_timelapseTmp(),
"logs": self.folder_logs(),
"watched": self.folder_watched()
},
"temperature": {
"profiles": self.temperature_profiles(),
"cutoff": self.temperature_cutoff()
},
"system": {
"actions": self.system_actions()
},
"terminalFilters": self.terminalFilters(),
"scripts": {
"gcode": {
"beforePrintStarted": self.scripts_gcode_beforePrintStarted(),
"afterPrintDone": self.scripts_gcode_afterPrintDone(),
"afterPrintCancelled": self.scripts_gcode_afterPrintCancelled(),
"afterPrintPaused": self.scripts_gcode_afterPrintPaused(),
"beforePrintResumed": self.scripts_gcode_beforePrintResumed(),
"afterPrinterConnected": self.scripts_gcode_afterPrinterConnected(),
"beforePrinterDisconnected": self.scripts_gcode_beforePrinterDisconnected()
}
},
"server": {
"commands": {
"systemShutdownCommand": self.server_commands_systemShutdownCommand(),
"systemRestartCommand": self.server_commands_systemRestartCommand(),
"serverRestartCommand": self.server_commands_serverRestartCommand()
}
}
});
// we also only send data that actually changed when no data is specified
data = getOnlyChangedData(self._getLocalData(), self.lastReceivedSettings);
}
$.ajax({
@ -590,7 +580,23 @@ $(function() {
};
self.onEventSettingsUpdated = function() {
self.requestData();
if (self.isDialogActive()) {
// dialog is open and not currently busy...
if (self.sending() || self.receiving()) {
return;
}
if (!hasDataChanged(self._getLocalData(), self.lastReceivedSettings)) {
// we don't have local changes, so just fetch new data
self.requestData();
} else {
// we have local changes, show update dialog
self.settingsUpdatedDialog.modal("show");
}
} else {
// dialog is not open, just fetch new data
self.requestData();
}
};
self.enqueueForSaving = function(data) {

View file

@ -52,3 +52,25 @@
<button class="btn btn-primary" data-bind="click: function() { saveData(undefined, $root.hide) }, enable: !sending(), css: {disabled: sending()}"><i class="icon-spinner icon-spin" data-bind="visible: sending"></i> {{ _('Save') }}</button>
</div>
</div>
<div id="settings_dialog_update_detected" class="modal hide fade" data-keyboard="false">
<div class="modal-header">
<h3>{{ _('Settings update detected') }}</h3>
</div>
<div class="modal-body">
<p>{% trans %}
The settings have been updated on the server. You may reload all settings,
overwriting any changes you might have done locally, or alternatively
only reload those settings you haven't changed locally.
{% endtrans %}</p>
<p>{% trans %}
How do you want to proceed?
{% endtrans %}</p>
</div>
<div class="modal-footer">
<div class="row-fluid">
<a href="#" class="btn btn-danger span6 reload_all">{{ _('Reload all') }}</a>
<a href="#" class="btn btn-primary span6 reload_nonconflicts">{{ _('Reload only non-conflicting changes') }}</a>
</div>
</div>
</div>