diff --git a/octoprint/server.py b/octoprint/server.py index 737435d..920ba9c 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -328,20 +328,64 @@ def setTimelapseConfig(): @app.route(BASEURL + "settings", methods=["GET"]) def getSettings(): s = settings() + + [movementSpeedX, movementSpeedY, movementSpeedZ, movementSpeedE] = s.get(["printerParameters", "movementSpeed", ["x", "y", "z", "e"]]) + return jsonify({ - "serial_port": s.get("serial", "port"), - "serial_baudrate": s.get("serial", "baudrate") + "printer": { + "movementSpeedX": movementSpeedX, + "movementSpeedY": movementSpeedY, + "movementSpeedZ": movementSpeedZ, + "movementSpeedE": movementSpeedE, + }, + "webcam": { + "streamUrl": s.get(["webcam", "stream"]), + "snapshotUrl": s.get(["webcam", "snapshot"]), + "ffmpegPath": s.get(["webcam", "ffmpeg"]), + "bitrate": s.get(["webcam", "bitrate"]) + }, + "feature": { + "gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]), + "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]) + }, + "folder": { + "uploads": s.getBaseFolder("uploads"), + "timelapse": s.getBaseFolder("timelapse"), + "timelapseTmp": s.getBaseFolder("timelapse_tmp"), + "logs": s.getBaseFolder("logs") + } }) @app.route(BASEURL + "settings", methods=["POST"]) def setSettings(): - s = settings() - if request.values.has_key("serial_port"): - s.set("serial", "port", request.values["serial_port"]) - if request.values.has_key("serial_baudrate"): - s.set("serial", "baudrate", request.values["serial_baudrate"]) + if "application/json" in request.headers["Content-Type"]: + data = request.json + s = settings() + + if "printer" in data.keys(): + if "movementSpeedX" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "x"], data["printer"]["movementSpeedX"]) + if "movementSpeedY" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "y"], data["printer"]["movementSpeedY"]) + if "movementSpeedZ" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "z"], data["printer"]["movementSpeedZ"]) + if "movementSpeedE" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "e"], data["printer"]["movementSpeedE"]) + + if "webcam" in data.keys(): + if "streamUrl" in data["webcam"].keys(): s.set(["webcam", "stream"], data["webcam"]["streamUrl"]) + if "snapshot" in data["webcam"].keys(): s.set(["webcam", "snapshot"], data["webcam"]["snapshotUrl"]) + if "ffmpeg" in data["webcam"].keys(): s.set(["webcam", "ffmpeg"], data["webcam"]["ffmpeg"]) + if "bitrate" in data["webcam"].keys(): s.set(["webcam", "bitrate"], data["webcam"]["bitrate"]) + + if "feature" in data.keys(): + if "gcodeViewer" in data["feature"].keys(): s.setBoolean(["feature", "gCodeVisualizer"], data["feature"]["gcodeViewer"]) + if "waitForStart" in data["feature"].keys(): s.setBoolean(["feature", "waitForStartOnConnect"], data["feature"]["waitForStart"]) + + if "folder" in data.keys(): + if "uploads" in data["folder"].keys(): s.setBaseFolder("uploads", data["folder"]["uploads"]) + if "timelapse" in data["folder"].keys(): s.setBaseFolder("timelapse", data["folder"]["timelapse"]) + if "timelapseTmp" in data["folder"].keys(): s.setBaseFolder("timelapse_tmp", data["folder"]["timelapseTmp"]) + if "logs" in data["folder"].keys(): s.setBaseFolder("logs", data["folder"]["logs"]) + + s.save() - s.save() return getSettings() #~~ startup code diff --git a/octoprint/settings.py b/octoprint/settings.py index eae685d..1e78772 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -6,6 +6,7 @@ import ConfigParser import sys import os import yaml +import logging APPNAME="OctoPrint" OLD_APPNAME="PrinterWebUI" @@ -63,6 +64,8 @@ valid_boolean_trues = ["true", "yes", "y", "1"] class Settings(object): def __init__(self): + self._logger = logging.getLogger(__name__) + self.settings_dir = None self._config = None @@ -79,6 +82,12 @@ class Settings(object): if os.path.exists(old_settings_dir) and os.path.isdir(old_settings_dir) and not os.path.exists(self.settings_dir): os.rename(old_settings_dir, self.settings_dir) + def _getDefaultFolder(self, type): + folder = default_settings["folder"][type] + if folder is None: + folder = os.path.join(self.settings_dir, type.replace("_", os.path.sep)) + return folder + #~~ load and save def load(self): @@ -164,6 +173,7 @@ class Settings(object): try: return int(value) except ValueError: + self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path)) return None def getBoolean(self, path): @@ -175,12 +185,12 @@ class Settings(object): return value.lower() in valid_boolean_trues def getBaseFolder(self, type): - if type not in old_default_settings["folder"].keys(): + if type not in default_settings["folder"].keys(): return None folder = self.get(["folder", type]) if folder is None: - folder = os.path.join(self.settings_dir, type.replace("_", os.path.sep)) + folder = self._getDefaultFolder(type) if not os.path.isdir(folder): os.makedirs(folder) @@ -189,7 +199,7 @@ class Settings(object): #~~ setter - def set(self, path, value): + def set(self, path, value, force=False): if len(path) == 0: return @@ -198,38 +208,63 @@ class Settings(object): while len(path) > 1: key = path.pop(0) - if key in config.keys(): + if key in config.keys() and key in defaults.keys(): config = config[key] + defaults = defaults[key] elif key in defaults.keys(): config[key] = {} config = config[key] + defaults = defaults[key] else: return key = path.pop(0) - config[key] = value - self._dirty = True + if not force and key in defaults.keys() and key in config.keys() and defaults[key] == value: + del config[key] + self._dirty = True + elif force or (not key in config.keys() and defaults[key] != value) or (key in config.keys() and config[key] != value): + if value is None: + del config[key] + else: + config[key] = value + self._dirty = True - def setInt(self, path, value): + def setInt(self, path, value, force=False): if value is None: - return + self.set(path, None, force) try: intValue = int(value) except ValueError: + self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path)) return - self.set(path, intValue) + self.set(path, intValue, force) - def setBoolean(self, path, value): - if value is None: - return - elif isinstance(value, bool): - self.set(path, value) + def setBoolean(self, path, value, force=False): + if value is None or isinstance(value, bool): + self.set(path, value, force) elif value.lower() in valid_boolean_trues: - self.set(path, True) + self.set(path, True, force) else: - self.set(path, False) + self.set(path, False, force) + + def setBaseFolder(self, type, path, force=False): + if type not in default_settings["folder"].keys(): + return None + + currentPath = self.getBaseFolder(type) + defaultPath = self._getDefaultFolder(type) + if (path is None or path == defaultPath) and "folder" in self._config.keys() and type in self._config["folder"].keys(): + del self._config["folder"][type] + if not self._config["folder"]: + del self._config["folder"] + self._dirty = True + elif (path != currentPath and path != defaultPath) or force: + if not "folder" in self._config.keys(): + self._config["folder"] = {} + self._config["folder"][type] = path + self._dirty = True def _resolveSettingsDir(applicationName): # taken from http://stackoverflow.com/questions/1084697/how-do-i-store-desktop-application-data-in-a-cross-platform-way-for-python diff --git a/octoprint/static/css/ui.css b/octoprint/static/css/ui.css index fbf1c81..3925cb8 100644 --- a/octoprint/static/css/ui.css +++ b/octoprint/static/css/ui.css @@ -1,8 +1,12 @@ +/** Top bar */ + body { padding-top: 60px; } -.tab-content { +/** OctoPrint application tabs */ + +.octoprint-container .tab-content { padding: 9px 15px; border-left: 1px solid #DDD; border-right: 1px solid #DDD; @@ -16,13 +20,11 @@ body { border-bottom-left-radius: 4px; } -.accordion-heading a.accordion-toggle { display: inline-block; } - -.nav { +.octoprint-container .nav { margin-bottom: 0px; } -.tab-content h1 { +.octoprint-container .tab-content h1 { display: block; width: 100%; padding: 0; @@ -35,6 +37,17 @@ body { font-weight: normal; } +/** Accordions */ + +.octoprint-container .accordion-heading a.accordion-toggle { display: inline-block; } + +.octoprint-container .accordion-heading .settings-trigger { + float: right; + padding: 0px 15px; +} + +/** Tables */ + table { table-layout: fixed; } @@ -59,6 +72,24 @@ table th.gcode_files_action, table td.gcode_files_action { width: 20%; } +table th.timelapse_files_name, table td.timelapse_files_name { + text-overflow: ellipsis; + text-align: left; + width: 55%; +} + +table th.timelapse_files_size, table td.timelapse_files_size { + text-align: right; + width: 25%; +} + +table th.timelapse_files_action, table td.timelapse_files_action { + text-align: center; + width: 20%; +} + +/** Temperature tab */ + #temperature-graph { height: 350px; width: 100%; @@ -75,11 +106,14 @@ table th.gcode_files_action, table td.gcode_files_action { text-align: right; } +/** Connection settings */ #connection_ports, #connection_baudrates { width: 100%; } +/** Offline overlay */ + #offline_overlay { position: fixed; top: 0; @@ -116,26 +150,14 @@ table th.gcode_files_action, table td.gcode_files_action { margin: auto; } -table th.timelapse_files_name, table td.timelapse_files_name { - text-overflow: ellipsis; - text-align: left; - width: 55%; -} - -table th.timelapse_files_size, table td.timelapse_files_size { - text-align: right; - width: 25%; -} - -table th.timelapse_files_action, table td.timelapse_files_action { - text-align: center; - width: 20%; -} +/** Webcam */ #webcam_container { width: 100%; } +/** GCODE file manager */ + #files .popover { font-size: 85%; } @@ -144,9 +166,7 @@ table th.timelapse_files_action, table td.timelapse_files_action { margin-bottom: 0; } -.overflow_visible { - overflow: visible !important; -} +/** Controls */ #controls { overflow: hidden; @@ -193,11 +213,13 @@ table th.timelapse_files_action, table td.timelapse_files_action { height: 30px; } -.accordion-heading .settings-trigger { - float: right; - padding: 0px 15px; -} +/** General helper classes */ .text-right { text-align: right; } + +.overflow_visible { + overflow: visible !important; +} + diff --git a/octoprint/static/gcodeviewer/css/style.css b/octoprint/static/gcodeviewer/css/style.css index dd3244a..d41a4f4 100644 --- a/octoprint/static/gcodeviewer/css/style.css +++ b/octoprint/static/gcodeviewer/css/style.css @@ -85,7 +85,7 @@ margin-right: 5px; } -.mbut{ +#gcode .mbut{ height:60px; width: 250px; font-size: 1em; @@ -96,20 +96,20 @@ padding: 0px; } -.mtab{ +#gcode .mtab{ background: transparent; border: none; font-weight: normal; font-size: 0.5em; } -.mtab-defstate{ +#gcode .mtab-defstate{ background: transparent; border: none; } -.mtab-content{ +#gcode .mtab-content{ background: #ffffff; background-image: none; border: 0px none #dddddd; @@ -117,22 +117,22 @@ margin: 0px; } -.bar { +#gcode .bar { -webkit-transition: width 0s linear !important; -moz-transition: width 0s linear !important; -o-transition: width 0s linear !important; transition: width 0s linear !important; } -.nav { +#gcode .nav { margin-bottom: 0px !important; } -.tab-content { +#gcode .tab-content { overflow: visible; } -.aboutpage { +#gcode .aboutpage { margin: 10px; } @@ -150,12 +150,12 @@ margin-bottom: 10px; } -.colorBox { +#gcode .colorBox { width: 50px; height: 15px; border: 1px solid #000000; float:left; } -.activeline {background: #fff0b6 !important;} +#gcode .activeline {background: #fff0b6 !important;} diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index b1c30ad..4b5fb18 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -998,6 +998,97 @@ function GcodeViewModel() { } +function SettingsViewModel() { + var self = this; + + self.printer_movementSpeedX = ko.observable(undefined); + self.printer_movementSpeedY = ko.observable(undefined); + self.printer_movementSpeedZ = ko.observable(undefined); + self.printer_movementSpeedE = ko.observable(undefined); + + self.webcam_streamUrl = ko.observable(undefined); + self.webcam_snapshotUrl = ko.observable(undefined); + self.webcam_ffmpegPath = ko.observable(undefined); + self.webcam_bitrate = ko.observable(undefined); + + self.feature_gcodeViewer = ko.observable(undefined); + self.feature_waitForStart = ko.observable(undefined); + + self.folder_uploads = ko.observable(undefined); + self.folder_timelapse = ko.observable(undefined); + self.folder_timelapseTmp = ko.observable(undefined); + self.folder_logs = ko.observable(undefined); + + self.requestData = function() { + $.ajax({ + url: AJAX_BASEURL + "settings", + type: "GET", + dataType: "json", + success: self.fromResponse + }) + } + + self.fromResponse = function(response) { + self.printer_movementSpeedX(response.printer.movementSpeedX); + self.printer_movementSpeedY(response.printer.movementSpeedY); + self.printer_movementSpeedZ(response.printer.movementSpeedZ); + self.printer_movementSpeedE(response.printer.movementSpeedE); + + 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.feature_gcodeViewer(response.feature.gcodeViewer); + self.feature_waitForStart(response.feature.waitForStart); + + 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.saveData = function() { + var data = { + "printer": { + "movementSpeedX": self.printer_movementSpeedX(), + "movementSpeedY": self.printer_movementSpeedY(), + "movementSpeedZ": self.printer_movementSpeedZ(), + "movementSpeedE": self.printer_movementSpeedE() + }, + "webcam": { + "streamUrl": self.webcam_streamUrl(), + "snapshotUrl": self.webcam_snapshotUrl(), + "ffmpegPath": self.webcam_ffmpegPath(), + "bitrate": self.webcam_bitrate() + }, + "feature": { + "gcodeViewer": self.feature_gcodeViewer(), + "waitForStart": self.feature_waitForStart() + }, + "folder": { + "uploads": self.folder_uploads(), + "timelapse": self.folder_timelapse(), + "timelapseTmp": self.folder_timelapseTmp(), + "logs": self.folder_logs() + } + } + + $.ajax({ + url: AJAX_BASEURL + "settings", + type: "POST", + dataType: "json", + contentType: "application/json; charset=UTF-8", + data: JSON.stringify(data), + success: function(response) { + self.fromResponse(response); + $("#settings_dialog").modal("hide"); + } + }) + } + +} + function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, controlsViewModel, speedViewModel, terminalViewModel, gcodeFilesViewModel, webcamViewModel, gcodeViewModel) { var self = this; @@ -1074,6 +1165,7 @@ $(function() { var gcodeFilesViewModel = new GcodeFilesViewModel(); var webcamViewModel = new WebcamViewModel(); var gcodeViewModel = new GcodeViewModel(); + var settingsViewModel = new SettingsViewModel(); var dataUpdater = new DataUpdater( connectionViewModel, @@ -1237,6 +1329,7 @@ $(function() { ko.applyBindings(terminalViewModel, document.getElementById("term")); ko.applyBindings(speedViewModel, document.getElementById("speed")); ko.applyBindings(gcodeViewModel, document.getElementById("gcode")); + ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog")); var webcamElement = document.getElementById("webcam"); if (webcamElement) { @@ -1252,6 +1345,7 @@ $(function() { controlsViewModel.requestData(); gcodeFilesViewModel.requestData(); webcamViewModel.requestData(); + settingsViewModel.requestData(); //~~ UI stuff diff --git a/octoprint/templates/index.html b/octoprint/templates/index.html index b5eec1c..c932d2a 100644 --- a/octoprint/templates/index.html +++ b/octoprint/templates/index.html @@ -25,10 +25,15 @@ -
+
@@ -450,6 +455,7 @@
+
@@ -469,6 +475,132 @@
+ +