Skip to content

Commit

Permalink
Added settings dialog for configuring OctoPrint
Browse files Browse the repository at this point in the history
Warning: Many settings will need a restart of OctoPrint to take effect, adding corresponding notes is still a TODO. There's also no proper validation and error handling yet, so use at your own risk.
  • Loading branch information
foosel committed Feb 17, 2013
1 parent cbae792 commit 1c4203b
Show file tree
Hide file tree
Showing 6 changed files with 389 additions and 62 deletions.
60 changes: 52 additions & 8 deletions octoprint/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 51 additions & 16 deletions octoprint/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import sys
import os
import yaml
import logging

APPNAME="OctoPrint"
OLD_APPNAME="PrinterWebUI"
Expand Down Expand Up @@ -63,6 +64,8 @@ def settings():
class Settings(object):

def __init__(self):
self._logger = logging.getLogger(__name__)

self.settings_dir = None

self._config = None
Expand All @@ -79,6 +82,12 @@ def _init_settings_dir(self):
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):
Expand Down Expand Up @@ -164,6 +173,7 @@ def getInt(self, path):
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):
Expand All @@ -175,12 +185,12 @@ def getBoolean(self, path):
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)
Expand All @@ -189,7 +199,7 @@ def getBaseFolder(self, type):

#~~ setter

def set(self, path, value):
def set(self, path, value, force=False):
if len(path) == 0:
return

Expand All @@ -198,38 +208,63 @@ def set(self, path, value):

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
Expand Down
76 changes: 49 additions & 27 deletions octoprint/static/css/ui.css
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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%;
Expand All @@ -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;
Expand Down Expand Up @@ -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%;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

0 comments on commit 1c4203b

Please sign in to comment.