Added custom controls via configuration file.

Closes #13
master
Gina Häußge 2013-01-27 18:28:11 +01:00
parent 8c038a6829
commit ee7a1f9615
5 changed files with 207 additions and 145 deletions

119
README.md
View File

@ -12,7 +12,6 @@ allows
* while printing, gaining information regarding the current progress of the print job (height, percentage etc)
* reading the communication log and send arbitrary codes to be executed by the printer
* moving the X, Y and Z axis (jog controls, although very ugly ones right now)
* changing the speed modifiers for inner & outer wall, fill and support
* optional: visual monitoring of the printer via webcam stream integrated into the UI (using e.g. MJPG-Streamer)
* optional: creation of timelapse recordings of the printjob via webcam stream (using e.g. MJPG-Streamer) -- currently two timelaspe methods are implemented, triggering a shot on z-layer change or every "n" seconds
@ -66,50 +65,81 @@ on Linux, at `%APPDATA%/OctoPrint` on Windows and at `~/Library/Application Supp
The following example config should explain the available options:
[serial]
# Use the following option to define the default serial port, defaults to unset (= AUTO)
port = /dev/ttyACM0
# Use the following settings to configure the serial connection to the printer
serial:
# Use the following option to define the default serial port, defaults to unset (= AUTO)
port: /dev/ttyACM0
# Use the following option to define the default baudrate, defaults to unset (= AUTO)
baudrate = 115200
# Use the following option to define the default baudrate, defaults to unset (= AUTO)
baudrate: 115200
[server]
# Use this option to define the host to which to bind the server, defaults to "0.0.0.0" (= all interfaces)
host = 0.0.0.0
# Use the following settings to configure the web server
server:
# Use this option to define the host to which to bind the server, defaults to "0.0.0.0" (= all interfaces)
host: 0.0.0.0
# Use this option to define the port to which to bind the server, defaults to 5000
port = 5000
# Use this option to define the port to which to bind the server, defaults to 5000
port: 5000
[webcam]
# Use this option to enable display of a webcam stream in the UI, e.g. via MJPG-Streamer.
# Webcam support will be disabled if not set
stream = http://<stream host>:<stream port>/?action=stream
# Use the following settings to configure webcam support
webcam:
# Use this option to enable display of a webcam stream in the UI, e.g. via MJPG-Streamer.
# Webcam support will be disabled if not set
stream: http://<stream host>:<stream port>/?action=stream
# Use this option to enable timelapse support via snapshot, e.g. via MJPG-Streamer.
# Timelapse support will be disabled if not set
snapshot = http://<stream host>:<stream port>/?action=snapshot
# Use this option to enable timelapse support via snapshot, e.g. via MJPG-Streamer.
# Timelapse support will be disabled if not set
snapshot: http://<stream host>:<stream port>/?action=snapshot
# Path to ffmpeg binary to use for creating timelapse recordings.
# Timelapse support will be disabled if not set
ffmpeg = /path/to/ffmpeg
# Path to ffmpeg binary to use for creating timelapse recordings.
# Timelapse support will be disabled if not set
ffmpeg: /path/to/ffmpeg
[feature]
# Whether to enable gcode analysis for displaying needed filament and estimated print time. Disabling this (set
# to False) will speed up the loading of gcode files before printing significantly, but the mentioned statistical
# data will not be available
analyzeGcode = True
# Use the following settings to enable or disable OctoPrint features
feature:
# Whether to enable gcode analysis for displaying needed filament and estimated print time. Disabling this (set
# to false) will speed up the loading of gcode files before printing significantly, but the mentioned statistical
# data will not be available
analyzeGcode: true
[folder]
# Absolute path where to store gcode uploads. Defaults to the uploads folder in the OctoPrint settings folder
uploads = /path/to/upload/folder
# Use the following settings to set custom paths for folders used by OctoPrint
folder:
# Absolute path where to store gcode uploads. Defaults to the uploads folder in the OctoPrint settings folder
uploads: /path/to/upload/folder
# Absolute path where to store finished timelapse recordings. Defaults to the timelapse folder in the OctoPrint
# settings dir
timelapse = /path/to/timelapse/folder
# Absolute path where to store finished timelapse recordings. Defaults to the timelapse folder in the OctoPrint
# settings dir
timelapse: /path/to/timelapse/folder
# Absolute path where to store temporary timelapse files. Defaults to the timelapse/tmp folder in the OctoPrint
# settings dir
timelapse_tmp: /path/timelapse/tmp/folder
# Use the following settings to add custom controls to the "Controls" tab within OctoPrint
#
# Controls consist at least of a name, a type and type-specific further attributes. Currently recognized types are
# - section: Creates a visual section in the UI, you can use this to separate functional blocks
# - command: Creates a button that sends a defined GCODE command to the printer when clicked
# - parametrized_command: Creates a button that sends a parametrized GCODE command to the printer, parameters
# needed for the command are added to the UI as input fields and are named
#
# The following example defines a control for enabling the cooling fan with a variable speed defined by the user
# (default 255) and a control for disabling the fan, all within a section named "Fan".
controls:
- name: Fan
type: section
children:
- name: Enable Fan
type: parametrized_command
command: M106 S%(speed)s
input:
- name: Speed (0-255)
parameter: speed
default: 255
- name: Disable Fan
type: command
command: M107
# Absolute path where to store temporary timelapse files. Defaults to the timelapse/tmp folder in the OctoPrint
# settings dir
timelapse_tmp = /path/timelapse/tmp/folder
Setup on a Raspberry Pi running Raspbian
----------------------------------------
@ -158,10 +188,10 @@ This should hopefully run through without any compilation errors. You should the
If you now point your browser to `http://<your Raspi's IP>:8080/?action=stream`, you should see a moving picture at 5fps.
Open `~/.octoprint/config.ini` and add the following lines to it:
[webcam]
stream = http://<your Raspi's IP>:8080/?action=stream
snapshot = http://127.0.0.1:8080/?action=snapshot
ffmpeg = /usr/bin/avconv
webcam:
stream: http://<your Raspi's IP>:8080/?action=stream
snapshot: http://127.0.0.1:8080/?action=snapshot
ffmpeg: /usr/bin/avconv
Restart the OctoPrint server and reload its frontend. You should now see a Webcam tab with content.
@ -179,6 +209,7 @@ It also uses the following libraries and frameworks for backend and frontend:
* Flask: http://flask.pocoo.org/
* Tornado: http://www.tornadoweb.org/
* Tornadio2: https://github.com/MrJoes/tornadio2
* PyYAML: http://pyyaml.org/
* Socket.io: http://socket.io/
* jQuery: http://jquery.com/
* Bootstrap: http://twitter.github.com/bootstrap/
@ -201,14 +232,14 @@ What do I have to do after the rename from Printer WebUI to OctoPrint?
----------------------------------------------------------------------
If you did checkout OctoPrint from its previous location at https://github.com/foosel/PrinterWebUI.git, you'll have to
update your so-called remote references in git in order to make 'git pull' use the new repository location as origin.
update your so-called remote references in git in order to make `git pull` use the new repository location as origin.
To do so you'll only need to execute the following command in your OctoPrint/PrinterWebUI folder:
git remote set-url origin https://github.com/foosel/OctoPrint.git
After that you might also want to rename your base directory (which probably still is called 'PrinterWebUI') to 'OctoPrint'
and delete the folder 'printer_webui' in your base folder (which stays there thanks to Python's compiled bytecode files
even after a rename of the Python package to 'octoprint').
After that you might also want to rename your base directory (which probably still is called `PrinterWebUI`) to `OctoPrint`
and delete the folder `printer_webui` in your base folder (which stays there thanks to Python's compiled bytecode files
even after a rename of the Python package to `octoprint`).
After that you are set, the configuration files are migrated automatically :)
After that you are set, the configuration files are migrated automatically.

View File

@ -120,6 +120,19 @@ def disconnect():
@app.route(BASEURL + "control/command", methods=["POST"])
def printerCommand():
command = request.form["command"]
# if parameters for the command are given, retrieve them from the request and format the command string with them
parameters = {}
for requestParameter in request.values.keys():
if not requestParameter.startswith("parameter_"):
continue
parameterName = requestParameter[len("parameter_"):]
parameterValue = request.values[requestParameter]
parameters[parameterName] = parameterValue
if len(parameters) > 0:
command = command % parameters
printer.command(command)
return jsonify(SUCCESS)
@ -182,6 +195,10 @@ def jog():
return jsonify(SUCCESS)
@app.route(BASEURL + "control/speed", methods=["GET"])
def getSpeedValues():
return jsonify(feedrate = printer.feedrateState())
@app.route(BASEURL + "control/speed", methods=["POST"])
def speed():
if not printer.isOperational():
@ -192,12 +209,11 @@ def speed():
value = int(request.values[key])
printer.setFeedrateModifier(key, value)
return jsonify(feedrate = printer.feedrateState())
return getSpeedValues()
@app.route(BASEURL + "control/custom", methods=["GET"])
def getCustomControls():
customControls = settings().getObject("controls")
print("custom controls: %r" % customControls)
return jsonify(controls = customControls)
#~~ GCODE file handling

View File

@ -45,46 +45,7 @@ old_default_settings = {
default_settings = old_default_settings.copy()
default_settings.update({
"controls": [
{
"name": "Motors",
"type": "section",
"children": [
{
"name": "Enable Motors",
"type": "command",
"command": "M17"
},
{
"name": "Disable Motors",
"type": "command",
"command": "M18"
}
]
},
{
"name": "Fan",
"type": "section",
"children": [
{
"name": "Enable Fan",
"type": "parameterized_command",
"command": "M106 S%(speed)",
"input": [{
"name": "Speed (0-255)",
"parameter": "speed",
"type": "integer",
"range": [0, 255]
}]
},
{
"name": "Disable Fan",
"type": "command",
"command": "M107"
}
]
}
]
"controls": []
})
class Settings(object):
@ -128,7 +89,7 @@ class Settings(object):
self._config[section][option] = config.get(section, option)
self._dirty = True
self.save(force=True)
#os.rename(oldFilename, oldFilename + ".bck")
os.rename(oldFilename, oldFilename + ".bck")
else:
self._config = {}
@ -195,7 +156,7 @@ class Settings(object):
def set(self, section, key, value):
if section not in default_settings.keys():
return None
return
if self._config.has_key(section):
sectionConfig = self._config[section]
@ -204,6 +165,14 @@ class Settings(object):
sectionConfig[key] = value
self._config[section] = sectionConfig
self._dirty = True
def setObject(self, key, value):
if key not in default_settings.keys():
return
self._config[key] = value
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

View File

@ -387,16 +387,66 @@ function ControlsViewModel() {
}
self._fromResponse = function(response) {
self.controls(response.controls);
self.controls(self._enhanceControls(response.controls));
}
self._enhanceControls = function(controls) {
for (var i = 0; i < controls.length; i++) {
controls[i] = self._enhanceControl(controls[i]);
}
return controls;
}
self._enhanceControl = function(control) {
if (control.type == "parametrized_command") {
for (var i = 0; i < control.input.length; i++) {
control.input[i].value = control.input[i].default;
}
} else if (control.type == "section") {
control.children = self._enhanceControls(control.children);
}
return control;
}
self.sendJogCommand = function(axis, distance) {
$.ajax({
url: AJAX_BASEURL + "control/jog",
type: "POST",
dataType: "json",
data: axis + "=" + distance
})
}
self.sendHomeCommand = function(axis) {
$.ajax({
url: AJAX_BASEURL + "control/jog",
type: "POST",
dataType: "json",
data: "home" + axis
})
}
self.sendCustomCommand = function(command) {
if (command) {
if (!command)
return;
if (command.type == "command") {
$.ajax({
url: AJAX_BASEURL + "control/command",
type: "POST",
dataType: "json",
data: "command=" + command
data: "command=" + command.command
})
} else if (command.type="parametrized_command") {
var data = {"command": command.command};
for (var i = 0; i < command.input.length; i++) {
data["parameter_" + command.input[i].parameter] = command.input[i].value;
}
$.ajax({
url: AJAX_BASEURL + "control/command",
type: "POST",
dataType: "json",
data: data
})
}
}
@ -406,8 +456,9 @@ function ControlsViewModel() {
case "section":
return "customControls_sectionTemplate";
case "command":
case "parameterized_command":
return "customControls_commandTemplate";
case "parametrized_command":
return "customControls_parametrizedCommandTemplate";
default:
return "customControls_emptyTemplate";
}
@ -449,19 +500,28 @@ function SpeedViewModel() {
self.isLoading(data.flags.loading);
}
/*
if (response.feedrate) {
self.outerWall(response.feedrate.outerWall);
self.innerWall(response.feedrate.innerWall);
self.fill(response.feedrate.fill);
self.support(response.feedrate.support);
} else {
self.outerWall(undefined);
self.innerWall(undefined);
self.fill(undefined);
self.support(undefined);
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "control/speed",
type: "GET",
dataType: "json",
success: self._fromResponse
});
}
self._fromResponse = function(response) {
if (response.feedrate) {
self.outerWall(response.feedrate.outerWall);
self.innerWall(response.feedrate.innerWall);
self.fill(response.feedrate.fill);
self.support(response.feedrate.support);
} else {
self.outerWall(undefined);
self.innerWall(undefined);
self.fill(undefined);
self.support(undefined);
}
}
*/
}
function TerminalViewModel() {
@ -667,6 +727,7 @@ function WebcamViewModel() {
dataType: "json",
success: self.fromResponse
});
$("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime());
}
self.fromResponse = function(response) {
@ -847,33 +908,6 @@ $(function() {
temperatureViewModel.updatePlot();
});
//~~ Jog controls
function jogCommand(axis, distance) {
$.ajax({
url: AJAX_BASEURL + "control/jog",
type: "POST",
dataType: "json",
data: axis + "=" + distance
})
}
function homeCommand(axis) {
$.ajax({
url: AJAX_BASEURL + "control/jog",
type: "POST",
dataType: "json",
data: "home" + axis
})
}
$("#jog_x_inc").click(function() {jogCommand("x", "10")});
$("#jog_x_dec").click(function() {jogCommand("x", "-10")});
$("#jog_y_inc").click(function() {jogCommand("y", "10")});
$("#jog_y_dec").click(function() {jogCommand("y", "-10")});
$("#jog_z_inc").click(function() {jogCommand("z", "10")});
$("#jog_z_dec").click(function() {jogCommand("z", "-10")});
$("#jog_xy_home").click(function() {homeCommand("XY")});
$("#jog_z_home").click(function() {homeCommand("Z")});
//~~ Speed controls
function speedCommand(structure) {
@ -932,7 +966,7 @@ $(function() {
ko.applyBindings(printerStateViewModel, document.getElementById("state"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files"));
ko.applyBindings(temperatureViewModel, document.getElementById("temp"));
ko.applyBindings(controlsViewModel, document.getElementById("jog"));
ko.applyBindings(controlsViewModel, document.getElementById("controls"));
ko.applyBindings(terminalViewModel, document.getElementById("term"));
ko.applyBindings(speedViewModel, document.getElementById("speed"));

View File

@ -10,8 +10,9 @@
<link href="{{ url_for('static', filename='css/ui.css') }}" rel="stylesheet" media="screen">
<script lang="javascript">
var AJAX_BASEURL = '/ajax/';
var AJAX_BASEURL = "/ajax/";
var CONFIG_FILESPERPAGE = 5;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/WebSocketMain.swf') }}";
var WEB_SOCKET_DEBUG = true;
@ -124,7 +125,7 @@
<div class="tabbable span8">
<ul class="nav nav-tabs" id="tabs">
<li class="active"><a href="#temp" data-toggle="tab">Temperature</a></li>
<li><a href="#jog" data-toggle="tab">Controls</a></li>
<li><a href="#controls" data-toggle="tab">Controls</a></li>
<!--<li><a href="#speed" data-toggle="tab">Speed</a></li>-->
<li><a href="#term" data-toggle="tab">Terminal</a></li>
{% if webcamStream %}<li><a href="#webcam" data-toggle="tab">Webcam</a></li>{% endif %}
@ -166,24 +167,24 @@
</div>
</div>
</div>
<div class="tab-pane" id="jog">
<div class="tab-pane" id="controls">
<div style="width: 350px; height: 70px">
<div style="width: 70px; float: left;">&nbsp;</div>
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_y_inc" data-bind="enable: isOperational() && !isPrinting()">Y+</button></div>
<div style="width: 70px; float: left;"><button class="btn btn-block" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('y', 10) }">Y+</button></div>
<div style="width: 70px; float: left;">&nbsp;</div>
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" id="jog_z_inc" data-bind="enable: isOperational() && !isPrinting()">Z+</button></div>
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('z', 10) }">Z+</button></div>
</div>
<div style="width: 350px; height: 70px">
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_x_dec" data-bind="enable: isOperational() && !isPrinting()">X-</button></div>
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_xy_home" data-bind="enable: isOperational() && !isPrinting()">Home</button></div>
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_x_inc" data-bind="enable: isOperational() && !isPrinting()">X+</button></div>
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" id="jog_z_home" data-bind="enable: isOperational() && !isPrinting()">Home</button></div>
<div style="width: 70px; float: left;"><button class="btn btn-block" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('x', -10) }">X-</button></div>
<div style="width: 70px; float: left;"><button class="btn btn-block" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendHomeCommand('XY') }">Home</button></div>
<div style="width: 70px; float: left;"><button class="btn btn-block" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('x', 10) }">X+</button></div>
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" id="jog_z_home" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendHomeCommand('Z') }">Home</button></div>
</div>
<div style="width: 350px; height: 70px">
<div style="width: 70px; float: left;">&nbsp;</div>
<div style="width: 70px; float: left;"><button class="btn btn-block" id="jog_y_dec" data-bind="enable: isOperational() && !isPrinting()">Y-</button></div>
<div style="width: 70px; float: left;"><button class="btn btn-block" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('y', -10) }">Y-</button></div>
<div style="width: 70px; float: left;">&nbsp;</div>
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" id="jog_z_dec" data-bind="enable: isOperational() && !isPrinting()">Z-</button></div>
<div style="width: 70px; float: left; margin-left: 20px"><button class="btn btn-block" data-bind="enable: isOperational() && !isPrinting(), click: function() { $root.sendJogCommand('z', -10) }">Z-</button></div>
</div>
<div data-bind="template: { name: $root.displayMode, foreach: controls }"></div>
@ -195,9 +196,20 @@
<div data-bind="template: { name: $root.displayMode, foreach: children }"></div>
</script>
<script type="text/html" id="customControls_commandTemplate">
<button class="btn" data-bind="text: name, enable: $root.isOperational(), click: function() { $root.sendCustomCommand($data.command) }"></button>
<form class="form-inline">
<button class="btn" data-bind="text: name, enable: $root.isOperational(), click: function() { $root.sendCustomCommand($data) }"></button>
</form>
</script>
<script type="text/html" id="customControls_emptyTemplate"></script>
<script type="text/html" id="customControls_parametrizedCommandTemplate">
<form class="form-inline">
<!-- ko foreach: input -->
<label data-bind="text: name"></label>
<input type="text" class="input-small" data-bind="attr: {placeholder: name}, value: value">
<!-- /ko -->
<button class="btn" data-bind="text: name, enable: $root.isOperational(), click: function() { $root.sendCustomCommand($data) }"></button>
</form>
</script>
<script type="text/html" id="customControls_emptyTemplate"><div></div></script>
<!-- End of templates for custom controls -->
</div>
<div class="tab-pane" id="speed">