diff --git a/docs/configuration/custom_controls.rst b/docs/configuration/custom_controls.rst
new file mode 100644
index 00000000..eb9af385
--- /dev/null
+++ b/docs/configuration/custom_controls.rst
@@ -0,0 +1,135 @@
+.. _sec-configuration-custom_controls:
+
+Custom Controls
+===============
+
+OctoPrint allows you to add custom controls to the "Control" tab of its interface. Control types reach from simple
+buttons which trigger sending of one or more lines of GCODE to the printer over more complex controls allowing
+parameterization of these commands with values entered by the user to full blown GCODE script templates backed by
+`Jinja2 `_.
+
+.. note::
+
+ At the current time, the configuration of custom controls is only possible through manually editing OctoPrint's
+ ``config.yaml`` file.
+
+ To make sure that your ``config.yaml`` stays valid when adding custom controls, don't hesitate to take a look at the
+ :ref:`YAML Primer `.
+
+.. _sec-configuration-custom_controls-configyaml:
+
+config.yaml
+-----------
+
+Custom controls are configured within ``config.yaml`` in a ``controls`` section which basically represents a hierarchical
+structure of all configured custom controls of various types.
+
+The following example defines a control for enabling the cooling fan with a variable speed defined by the user
+(default 255 and selectable through a slider UI element) and a control for disabling the fan, all within a section named
+"Fan", two example controls with multiple commands in a section "Example for multiple commands" and a command with printer
+feedback evaluation for the result of the M114 "Get Position" gcode inside a section named "Reporting".
+
+.. sourcecode:: yaml
+
+ controls:
+ - name: Fan
+ type: section
+ children:
+ - name: Enable Fan
+ type: parametric_command
+ command: M106 S%(speed)s
+ input:
+ - name: Speed (0-255)
+ parameter: speed
+ default: 255
+ slider:
+ min: 0
+ max: 255
+ - name: Disable Fan
+ type: command
+ command: M107
+ - name: Example for multiple commands
+ type: section
+ children:
+ - name: Move X (static)
+ type: commands
+ commands:
+ - G91
+ - G1 X10 F3000
+ - G90
+ - name: Move X (parametric)
+ type: parametric_commands
+ commands:
+ - G91
+ - G1 X%(distance)s F%(speed)s
+ - G90
+ input:
+ - default: 10
+ name: Distance
+ parameter: distance
+ - default: 3000
+ name: Speed
+ parameter: speed
+ - name: Reporting
+ type: section
+ children:
+ - command: M114
+ name: Get Position
+ type: feedback_command
+ regex: "X:([0-9.]+) Y:([0-9.]+) Z:([0-9.]+) E:([0-9.]+)"
+ template: "Position: X={0}, Y={1}, Z={2}, E={3}"
+
+Adding this to ``config.yaml``, restarting the OctoPrint server and switching to the "Control" tab within its
+interface yields the following visual representation:
+
+.. _fig-configuration-custom_controls-example:
+.. figure:: ../images/configuration-custom_controls-example.png
+ :align: center
+ :alt: The rendered output created through the example configuration
+
+As you can see there are quite a number of different custom controls already in this small example, each with their own
+attributes: :ref:`sections `, :ref:`commands `,
+:ref:`parametric commands ` and
+:ref:`feedback commands `. Two attributes are common for all
+of the types: ``name`` and ``type``.
+
+.. _sec-configuration-custom_controls-types:
+
+Types
+-----
+
+.. _sec-configuration-custom_controls-types:
+
+Sections
+........
+
+.. _sec-configuration-custom_controls-types:
+
+Rows
+....
+
+.. _sec-configuration-custom_controls-types:
+
+Section rows
+............
+
+.. _sec-configuration-custom_controls-types:
+
+Commands
+........
+
+.. _sec-configuration-custom_controls-types-parametric_command:
+
+Parametric commands
+...................
+
+.. _sec-configuration-custom_controls-types-script:
+
+Scripts
+.......
+
+.. _sec-configuration-custom_controls-types-feedback_command:
+
+Feedback commands
+.................
+
diff --git a/docs/configuration/index.rst b/docs/configuration/index.rst
new file mode 100644
index 00000000..7a078cef
--- /dev/null
+++ b/docs/configuration/index.rst
@@ -0,0 +1,11 @@
+.. _sec-configuration:
+
+#############
+Configuration
+#############
+
+.. toctree::
+ :maxdepth: 2
+
+ custom_controls.rst
+ yaml.rst
\ No newline at end of file
diff --git a/docs/configuration/yaml.rst b/docs/configuration/yaml.rst
new file mode 100644
index 00000000..4fc37ef7
--- /dev/null
+++ b/docs/configuration/yaml.rst
@@ -0,0 +1,175 @@
+.. _sec-configuration-yaml:
+
+A YAML Primer
+=============
+
+Most of OctoPrint's configuration is done under the hood through `YAML `_ files,
+which is why it makes sense to shed some light on the basics of this data serialization format.
+
+YAML is a text based format which excels at representing the most common of data structures in an easy and very human
+readable way, which is why it was chosen for OctoPrint's configuration files. A text editor is all you need in order
+to write YAML configuration files.
+
+.. _sec-configuration-yaml-basic:
+
+Basic Rules
+-----------
+
+First of all some basic things to know about working with YAML files:
+
+ * Never use tabs outside of quoted strings, especially not for indentation. The tab characters is illegal within
+ YAML files.
+ * Whitespace and indentation matters and plays an important part in structuring the data, so take special care
+ to stay consistent here.
+ * YAML's comments start with a `#` and go until the end of the line.
+
+.. _sec-configuration-yaml-types:
+
+Interesting data types
+----------------------
+
+You will probably only come across the three most basic types of data within OctoPrint's YAML files: scalars
+(such as strings, integers, ...), lists and associated arrays (aka key-value-paris, aka maps, aka dictionaries).
+
+.. _sec-configuration-yaml-types-scalar:
+
+Scalars
+.......
+
+Scalars are the most basic of all data types and are simple string, integer, float or boolean values.
+
+For most scalars you don't need any quotes at all, but if you need to define some piece of data which contains characters
+that could be mistaken with YAML syntax you need to quote it in either double ``"`` or single ``'`` quotes for the
+YAML file to stay valid. As simple rule of thumb, if your data contains any of these characters ``:-{}[]!#|>&%@`` better
+quote it. Also quote it if you want a string but it could be mistaken for a valid number (integer or float) or if
+it consists only of "Yes", "No", "yes", "no", "true" or "false", which would be converted to a boolean without quotes.
+
+In double quoted strings if you need to include a literal double quote in your string you can escape it by prefixing
+it with a backslash ``\`` (which you can in turn escape by itself). In single quoted strings the single quote character
+can be escaped by prefixing it with another single quote, basically doubling it. Backslashes in single quoted strings
+do not need to be escaped.
+
+Quoted strings can also span across multiple lines, just indent the following lines. Note that you'll need to add a
+completely empty line in order for force a line break, the data will not be actually wrapped across multiple lines
+just because you spread its representation across multiple lines.
+
+.. list-table::
+
+ - * **Data type**
+ * **Examples**
+ - * int
+ * .. sourcecode:: yaml
+
+ 23
+
+ 42
+
+ - * float
+ * .. sourcecode:: yaml
+
+ 23.5
+
+ 100.0
+
+ - * boolean
+ * .. sourcecode:: yaml
+
+ true
+
+ false
+
+ Yes
+
+ No
+
+ yes
+
+ no
+
+ - * string
+ * .. sourcecode:: yaml
+
+ a string
+
+ "some quoted string with a : colon and a { bracket and a quote \" and a backslash \\ - phew"
+
+ 'some single quoted string with a single quote '' and a backslash \ - yay'
+
+ "and a multiline string - just because we can we'll make it span
+ across not two but four YAML lines!
+
+ Including this paragraph. But in fact it will only be two lines :)"
+
+ "23"
+
+ "42.3"
+
+ "Yes"
+
+ "No"
+
+ "true"
+
+ "false"
+
+ yes and no
+
+ true or false
+
+.. _sec-configuration-yaml-types-lists:
+
+Lists
+.....
+
+Lists allow to "collect" a number of similar things into one data structure. They are created by prefixing one or more
+consecutive lines with a ``-``:
+
+.. sourcecode:: yaml
+
+ - item 1
+ - 23.42
+ - 57
+ - true
+
+Take special care to have all of your list items at the same indentation level!
+
+.. _sec-configuration-yaml-types-dicts:
+
+Dictionaries
+............
+
+Dictionaries (aka associative arrays aka maps) allow organizing the data in key value pairs, with the key and the value
+being separated through a colon ``:``:
+
+.. sourcecode:: yaml
+
+ key: value
+ anotherkey: another value
+
+.. _sec-configuration-yaml-examples:
+
+Examples
+--------
+
+Based on the three types explained above, quite complex data structures are possible:
+
+.. sourcecode:: yaml
+
+ general:
+ some_setting: some_value
+ a_list:
+ - item 1
+ - 23.42
+ - 57
+ - true
+ some_flag: true
+ quoted_string: "This string is quoted because {we have this here} and also > this and : that"
+ specific:
+ setting1: value1
+ setting2: value2
+ subsetting21: value11
+ subsetting22:
+ - subsubsetting221
+ - subsubsetting222
+ - subsubsetting223
+ the_end: yes
diff --git a/docs/images/configuration-custom_controls-example.png b/docs/images/configuration-custom_controls-example.png
new file mode 100644
index 00000000..eab2a58b
Binary files /dev/null and b/docs/images/configuration-custom_controls-example.png differ
diff --git a/docs/index.rst b/docs/index.rst
index 7acbf1f5..2803415b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -16,6 +16,7 @@ Contents
.. toctree::
:maxdepth: 2
+ configuration/index.rst
api/index.rst
events/index.rst
plugins/index.rst
diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py
index 816813a5..786ce971 100644
--- a/src/octoprint/settings.py
+++ b/src/octoprint/settings.py
@@ -466,7 +466,15 @@ class Settings(object):
def _migrate_config(self):
dirty = False
- for migrate in (self._migrate_event_config, self._migrate_reverse_proxy_config, self._migrate_printer_parameters, self._migrate_gcode_scripts):
+
+ migrators = (
+ self._migrate_event_config,
+ self._migrate_reverse_proxy_config,
+ self._migrate_printer_parameters,
+ self._migrate_gcode_scripts
+ )
+
+ for migrate in migrators:
dirty = migrate() or dirty
if dirty:
self.save(force=True)
diff --git a/src/octoprint/static/js/app/viewmodels/control.js b/src/octoprint/static/js/app/viewmodels/control.js
index 45a779fa..4c045b2a 100644
--- a/src/octoprint/static/js/app/viewmodels/control.js
+++ b/src/octoprint/static/js/app/viewmodels/control.js
@@ -122,18 +122,20 @@ $(function() {
};
self._processControl = function (control) {
- if (_.startsWith(control.type, "parametric_")) {
+ if (control.type == "feedback_command" || control.type == "feedback") {
+ control.output = ko.observable("");
+ self.feedbackControlLookup[control.name] = control.output;
+ } else if (control.type == "section" || control.type == "row" || control.type == "section_row") {
+ control.children = self._processControls(control.children);
+ }
+
+ if (control.hasOwnProperty("input")) {
for (var i = 0; i < control.input.length; i++) {
control.input[i].value = ko.observable(control.input[i].default);
if (!control.input[i].hasOwnProperty("slider")) {
control.input[i].slider = false;
}
}
- } else if (control.type == "feedback_command" || control.type == "feedback") {
- control.output = ko.observable("");
- self.feedbackControlLookup[control.name] = control.output;
- } else if (control.type == "section" || control.type == "row" || control.type == "section_row") {
- control.children = self._processControls(control.children);
}
var js;
@@ -275,25 +277,31 @@ $(function() {
})
};
var data = undefined;
- if (command.type == "command" || command.type == "parametric_command" || command.type == "feedback_command") {
+ if (command.hasOwnProperty("command")) {
// single command
data = {"command": command.command};
- } else if (command.type == "commands" || command.type == "parametric_commands") {
+ } else if (command.hasOwnProperty("commands")) {
// multi command
data = {"commands": command.commands};
- } else if (command.type == "script" || command.type == "parametric_script") {
+ } else if (command.hasOwnProperty("script")) {
data = {"script": command.script};
if (command.hasOwnProperty("context")) {
data["context"] = command.context;
}
+ } else {
+ return;
}
- if (command.type == "parametric_command" || command.type == "parametric_commands" || command.type == "parametric_script") {
+ if (command.hasOwnProperty("input")) {
// parametric command(s)
data["parameters"] = {};
- for (var i = 0; i < command.input.length; i++) {
- data["parameters"][command.input[i].parameter] = command.input[i].value();
- }
+ _.each(command.input, function(input) {
+ if (!input.hasOwnProperty("parameter") || !input.hasOwnProperty("value")) {
+ return;
+ }
+
+ data["parameters"][input.parameter] = input.value();
+ });
}
if (command.confirm) {