From 4a4994822329bb5b00c52a515bbe8f0f72b79196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 14 Aug 2015 14:06:22 +0200 Subject: [PATCH 1/6] FileMgr: Only remove callback if it exists. --- src/octoprint/filemanager/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index dae5ed51..fad1fc41 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -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 From ff820837a590e4125d0af5d496119c30ad37f376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 14 Aug 2015 14:09:31 +0200 Subject: [PATCH 2/6] Include config hash in SettingsUpdated event and SockJS connected message --- src/octoprint/server/api/settings.py | 6 +++++- src/octoprint/server/util/sockjs.py | 11 ++++++++++- src/octoprint/settings.py | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 36efd324..0afb6913 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -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() diff --git a/src/octoprint/server/util/sockjs.py b/src/octoprint/server/util/sockjs.py index 8e7df023..44b682f8 100644 --- a/src/octoprint/server/util/sockjs.py +++ b/src/octoprint/server/util/sockjs.py @@ -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) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index d20b56b9..b09d8b82 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -544,6 +544,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): From 7518b44e6dbc9fcc771680219d8c13560716fb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 14 Aug 2015 14:11:50 +0200 Subject: [PATCH 3/6] Use configHash to check if frontend needs to be reloaded If after a reconnect the server side configuration has changed, we want to reload. Using the SettingsUpdated event the clients can track the current config hash during runtime so that no unnecessary reloads during runtime should be demanded. --- src/octoprint/static/js/app/dataupdater.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index 169786ea..3c2e6dbc 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -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)}); From 833a618dbe5ac41a48cd18693fa7708e17d1d736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 14 Aug 2015 14:34:41 +0200 Subject: [PATCH 4/6] More user control for server side settings update when dialog is open The user will now be offered a dialog with two choices if a server side update of the settings is detected while the user has the settings dialog opened and made local changes: Either reload all changes, effectively overwriting any local changes, or only set those settings changed on the server that are not also changed locally. --- src/octoprint/static/js/app/helpers.js | 50 +++ .../static/js/app/viewmodels/settings.js | 376 ++++++++++-------- .../templates/dialogs/settings.jinja2 | 22 + 3 files changed, 279 insertions(+), 169 deletions(-) diff --git a/src/octoprint/static/js/app/helpers.js b/src/octoprint/static/js/app/helpers.js index e64ee9a1..8f8bff9b 100644 --- a/src/octoprint/static/js/app/helpers.js +++ b/src/octoprint/static/js/app/helpers.js @@ -461,3 +461,53 @@ function splitTextToArray(text, sep, stripEmpty, filter) { function(item) { return (stripEmpty ? item : true) && (filter ? filter(item) : true); } ); } + +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); + } +}; + +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); +}; + diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 71df828d..49ed3cb3 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -20,6 +20,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"); @@ -162,6 +163,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}); @@ -180,11 +182,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"); @@ -245,6 +252,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() { @@ -268,7 +288,7 @@ $(function() { return false; }; - self.requestData = function(callback) { + self.requestData = function(callback, local) { if (self.receiving()) { if (callback) { self.callbacks.push(callback); @@ -287,7 +307,7 @@ $(function() { } try { - self.fromResponse(response); + self.fromResponse(response, local); var cb; while (self.callbacks.length) { @@ -378,92 +398,185 @@ $(function() { }) }; - self.fromResponse = function(response) { + self._getLocalData = function() { + var data = {}; + if (self.settings != undefined) { + 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() + } + } + }); + 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); + _.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); + // applying the value usually means we'll call the observable with it + var apply = function(v) { if (self.hasOwnProperty(observable)) { self[observable](v) } }; - 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); + // if we have a custom apply function for this, we'll use it instead + if (mapping && mapping[key] && _.isFunction(mapping[key])) { + apply = mapping[key]; + } - 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); + apply(value); + } + }); + }; - 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) { @@ -472,97 +585,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 sent data that actually changed when no data is specified + data = getOnlyChangedData(self._getLocalData(), self.lastReceivedSettings); } $.ajax({ @@ -588,7 +613,20 @@ $(function() { }; self.onEventSettingsUpdated = function() { - self.requestData(); + if (self.isDialogActive()) { + if (self.sending() || self.receiving()) { + return; + } + + if (!hasDataChanged(self._getLocalData(), self.lastReceivedSettings)) { + self.requestData(); + return; + } + + self.settingsUpdatedDialog.modal("show"); + } else { + self.requestData(); + } }; } diff --git a/src/octoprint/templates/dialogs/settings.jinja2 b/src/octoprint/templates/dialogs/settings.jinja2 index f3d3f708..b8e79a5c 100644 --- a/src/octoprint/templates/dialogs/settings.jinja2 +++ b/src/octoprint/templates/dialogs/settings.jinja2 @@ -52,3 +52,25 @@ + + From ea1661533786e5067a8b1534c05dbc884a2f21d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 14 Aug 2015 15:05:10 +0200 Subject: [PATCH 5/6] Map settings from observables automatically --- .../static/js/app/viewmodels/settings.js | 131 ++++++------------ 1 file changed, 44 insertions(+), 87 deletions(-) diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 49ed3cb3..1088ec01 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -404,95 +404,52 @@ $(function() { data = ko.mapping.toJS(self.settings); } - data = _.extend(data, { - "api" : { - "enabled": self.api_enabled(), - "key": self.api_key(), - "allowCrossOrigin": self.api_allowCrossOrigin() + // some special apply functions for various observables + var specialMappings = { + feature: { + externalHeatupDetection: function() { return !self.feature_disableExternalHeatupDetection() } }, - "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() - } + 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 = {}; + _.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) { + 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; + } + } + }); + return flag ? result : undefined; + }; + + var dataFromObservables = mapFromObservables(data, specialMappings); + + data = _.extend(data, dataFromObservables); return data; }; From 31db13e0c3b74b44e33214c00acd69188c380453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 17 Aug 2015 12:12:45 +0200 Subject: [PATCH 6/6] Some more comments for the new settings processing --- src/octoprint/static/js/app/helpers.js | 61 ++++++++++++++++++- .../static/js/app/viewmodels/settings.js | 35 +++++++---- 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/octoprint/static/js/app/helpers.js b/src/octoprint/static/js/app/helpers.js index 8f8bff9b..99c8e23d 100644 --- a/src/octoprint/static/js/app/helpers.js +++ b/src/octoprint/static/js/app/helpers.js @@ -462,6 +462,34 @@ function splitTextToArray(text, sep, stripEmpty, filter) { ); } +/** + * 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; @@ -476,8 +504,37 @@ function hasDataChanged(data, oldData) { } 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 {}; @@ -509,5 +566,5 @@ function getOnlyChangedData(data, oldData) { }; return f(data, oldData); -}; +} diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 1088ec01..02ea875b 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -398,13 +398,16 @@ $(function() { }) }; + /** + * 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 apply functions for various observables + // some special read functions for various observables var specialMappings = { feature: { externalHeatupDetection: function() { return !self.feature_disableExternalHeatupDetection() } @@ -420,6 +423,8 @@ $(function() { 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) { @@ -430,6 +435,7 @@ $(function() { // 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; } @@ -444,9 +450,12 @@ $(function() { } } }); + + // 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); @@ -505,6 +514,7 @@ $(function() { return; } + // process all key-value-pairs here _.forOwn(data, function(value, key) { var observable = key; if (keyPrefix != undefined) { @@ -520,15 +530,13 @@ $(function() { return; } - // applying the value usually means we'll call the observable with it - var apply = function(v) { if (self.hasOwnProperty(observable)) { self[observable](v) } }; - - // if we have a custom apply function for this, we'll use it instead if (mapping && mapping[key] && _.isFunction(mapping[key])) { - apply = 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); } - - apply(value); } }); }; @@ -543,7 +551,7 @@ $(function() { // we only set sending to true when we didn't include data self.sending(true); - // we also only sent data that actually changed when no data is specified + // we also only send data that actually changed when no data is specified data = getOnlyChangedData(self._getLocalData(), self.lastReceivedSettings); } @@ -571,17 +579,20 @@ $(function() { self.onEventSettingsUpdated = function() { 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(); - return; + } else { + // we have local changes, show update dialog + self.settingsUpdatedDialog.modal("show"); } - - self.settingsUpdatedDialog.modal("show"); } else { + // dialog is not open, just fetch new data self.requestData(); } };