Timelapse support. At the moment the feature only creates a series of (grabbed) images in the timelapse folder in the settings dir. Next step will be to create a movie from that.
parent
81fb1ae305
commit
35e6b19332
|
@ -12,7 +12,8 @@ allows
|
||||||
* reading the communication log and send arbitrary codes to be executed by the printer
|
* 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)
|
* 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
|
* 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 MJPG-Streamer)
|
* 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
|
||||||
|
|
||||||
The intended usecase is to run the Printer WebUI on a single-board computer like the Raspberry Pi and a WiFi module,
|
The intended usecase is to run the Printer WebUI on a single-board computer like the Raspberry Pi and a WiFi module,
|
||||||
connect the printer to the server and therefore create a WiFi-enabled 3D printer.
|
connect the printer to the server and therefore create a WiFi-enabled 3D printer.
|
||||||
|
@ -65,6 +66,8 @@ The following example config should explain the available options:
|
||||||
[webcam]
|
[webcam]
|
||||||
# use this option to enable display of a webcam stream in the UI, e.g. via MJPG-Streamer
|
# use this option to enable display of a webcam stream in the UI, e.g. via MJPG-Streamer
|
||||||
stream = http://10.0.0.2:8080/?action=stream
|
stream = http://10.0.0.2:8080/?action=stream
|
||||||
|
# use this option to enable timelapse support via snapshot, e.g. via MJPG-Streamer
|
||||||
|
snapshot = http://10.0.0.1:8080/?action=snapshot
|
||||||
|
|
||||||
Setup on a Raspberry Pi running Raspbian
|
Setup on a Raspberry Pi running Raspbian
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
@ -132,6 +135,6 @@ It also uses the following libraries and frameworks for backend and frontend:
|
||||||
* Flot: http://www.flotcharts.org/
|
* Flot: http://www.flotcharts.org/
|
||||||
* jQuery File Upload: http://blueimp.github.com/jQuery-File-Upload/
|
* jQuery File Upload: http://blueimp.github.com/jQuery-File-Upload/
|
||||||
|
|
||||||
And this for Webcam support:
|
The following software is recommended for Webcam support on the Raspberry Pi:
|
||||||
|
|
||||||
* MJPG-Streamer: http://sourceforge.net/apps/mediawiki/mjpg-streamer/index.php?title=Main_Page
|
* MJPG-Streamer: http://sourceforge.net/apps/mediawiki/mjpg-streamer/index.php?title=Main_Page
|
||||||
|
|
|
@ -3,7 +3,6 @@ __author__ = "Gina Häußge <osd@foosel.net>"
|
||||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import os
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
@ -58,6 +57,8 @@ class Printer():
|
||||||
|
|
||||||
self.feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"}
|
self.feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"}
|
||||||
|
|
||||||
|
self.timelapse = None
|
||||||
|
|
||||||
# comm
|
# comm
|
||||||
self.comm = None
|
self.comm = None
|
||||||
|
|
||||||
|
@ -97,6 +98,15 @@ class Printer():
|
||||||
|
|
||||||
self.comm.setFeedrateModifier(self.feedrateModifierMapping[structure], percentage / 100.0)
|
self.comm.setFeedrateModifier(self.feedrateModifierMapping[structure], percentage / 100.0)
|
||||||
|
|
||||||
|
def setTimelapse(self, timelapse):
|
||||||
|
if self.timelapse is not None and self.isPrinting():
|
||||||
|
self.timelapse.onPrintjobStopped()
|
||||||
|
del self.timelapse
|
||||||
|
self.timelapse = timelapse
|
||||||
|
|
||||||
|
def getTimelapse(self):
|
||||||
|
return self.timelapse
|
||||||
|
|
||||||
def mcLog(self, message):
|
def mcLog(self, message):
|
||||||
"""
|
"""
|
||||||
Callback method for the comm object, called upon log output.
|
Callback method for the comm object, called upon log output.
|
||||||
|
@ -135,8 +145,15 @@ class Printer():
|
||||||
Callback method for the comm object, called if the connection state changes.
|
Callback method for the comm object, called if the connection state changes.
|
||||||
New state is stored for retrieval by the frontend.
|
New state is stored for retrieval by the frontend.
|
||||||
"""
|
"""
|
||||||
|
oldState = self.state
|
||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
|
if self.timelapse is not None:
|
||||||
|
if oldState == self.comm.STATE_PRINTING:
|
||||||
|
self.timelapse.onPrintjobStopped()
|
||||||
|
elif state == self.comm.STATE_PRINTING:
|
||||||
|
self.timelapse.onPrintjobStarted()
|
||||||
|
|
||||||
def mcMessage(self, message):
|
def mcMessage(self, message):
|
||||||
"""
|
"""
|
||||||
Callback method for the comm object, called upon message exchanges via serial.
|
Callback method for the comm object, called upon message exchanges via serial.
|
||||||
|
@ -152,13 +169,19 @@ class Printer():
|
||||||
"""
|
"""
|
||||||
self.printTime = self.comm.getPrintTime()
|
self.printTime = self.comm.getPrintTime()
|
||||||
self.printTimeLeft = self.comm.getPrintTimeRemainingEstimate()
|
self.printTimeLeft = self.comm.getPrintTimeRemainingEstimate()
|
||||||
|
oldProgress = self.progress;
|
||||||
self.progress = self.comm.getPrintPos()
|
self.progress = self.comm.getPrintPos()
|
||||||
|
if self.timelapse is not None:
|
||||||
|
self.timelapse.onPrintjobProgress(oldProgress, self.progress, int(round(self.progress * 100 / len(self.gcodeList))))
|
||||||
|
|
||||||
def mcZChange(self, newZ):
|
def mcZChange(self, newZ):
|
||||||
"""
|
"""
|
||||||
Callback method for the comm object, called upon change of the z-layer.
|
Callback method for the comm object, called upon change of the z-layer.
|
||||||
"""
|
"""
|
||||||
|
oldZ = self.currentZ
|
||||||
self.currentZ = newZ
|
self.currentZ = newZ
|
||||||
|
if self.timelapse is not None:
|
||||||
|
self.timelapse.onZChange(oldZ, self.currentZ)
|
||||||
|
|
||||||
def onGcodeLoaded(self, gcodeLoader):
|
def onGcodeLoaded(self, gcodeLoader):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2,29 +2,22 @@
|
||||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||||
|
|
||||||
from flask import Flask, request, render_template, jsonify, send_file, abort
|
from flask import Flask, request, render_template, jsonify
|
||||||
from werkzeug import secure_filename
|
from werkzeug import secure_filename
|
||||||
|
|
||||||
from printer_webui.printer import Printer, getConnectionOptions
|
from printer_webui.printer import Printer, getConnectionOptions
|
||||||
from printer_webui.settings import settings
|
from printer_webui.settings import settings
|
||||||
|
from printer_webui.timelapse import ZTimelapse, TimedTimelapse
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import StringIO
|
|
||||||
|
|
||||||
BASEURL="/ajax/"
|
BASEURL="/ajax/"
|
||||||
SUCCESS={}
|
SUCCESS={}
|
||||||
|
|
||||||
UPLOAD_FOLDER = os.path.join(settings().settings_dir, "uploads")
|
UPLOAD_FOLDER = settings().getBaseFolder("uploads")
|
||||||
if not os.path.isdir(UPLOAD_FOLDER):
|
|
||||||
os.makedirs(UPLOAD_FOLDER)
|
|
||||||
ALLOWED_EXTENSIONS = set(["gcode"])
|
ALLOWED_EXTENSIONS = set(["gcode"])
|
||||||
|
|
||||||
WEBCAM_FOLDER = os.path.join(settings().settings_dir, "webcam")
|
|
||||||
if not os.path.isdir(WEBCAM_FOLDER):
|
|
||||||
os.makedirs(WEBCAM_FOLDER)
|
|
||||||
|
|
||||||
app = Flask("printer_webui")
|
app = Flask("printer_webui")
|
||||||
printer = Printer()
|
printer = Printer()
|
||||||
|
|
||||||
|
@ -235,6 +228,48 @@ def deleteGcodeFile():
|
||||||
os.remove(secure)
|
os.remove(secure)
|
||||||
return readGcodeFiles()
|
return readGcodeFiles()
|
||||||
|
|
||||||
|
#~~ timelapse configuration
|
||||||
|
|
||||||
|
@app.route(BASEURL + "timelapse", methods=["GET"])
|
||||||
|
def getTimelapseConfig():
|
||||||
|
timelapse = printer.getTimelapse()
|
||||||
|
|
||||||
|
type = "off"
|
||||||
|
additionalConfig = {}
|
||||||
|
if timelapse is not None and isinstance(timelapse, ZTimelapse):
|
||||||
|
type = "zchange"
|
||||||
|
elif timelapse is not None and isinstance(timelapse, TimedTimelapse):
|
||||||
|
type = "timed"
|
||||||
|
additionalConfig = {
|
||||||
|
"interval": timelapse.interval
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"type": type,
|
||||||
|
"config": additionalConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route(BASEURL + "timelapse", methods=["POST"])
|
||||||
|
def setTimelapseConfig():
|
||||||
|
if not request.values.has_key("type"):
|
||||||
|
return getTimelapseConfig()
|
||||||
|
|
||||||
|
type = request.values["type"]
|
||||||
|
timelapse = None
|
||||||
|
if "zchange" == type:
|
||||||
|
timelapse = ZTimelapse()
|
||||||
|
elif "timed" == type:
|
||||||
|
interval = 10
|
||||||
|
if request.values.has_key("interval"):
|
||||||
|
try:
|
||||||
|
interval = int(request.values["interval"])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
timelapse = TimedTimelapse(interval)
|
||||||
|
|
||||||
|
printer.setTimelapse(timelapse)
|
||||||
|
return getTimelapseConfig()
|
||||||
|
|
||||||
#~~ settings
|
#~~ settings
|
||||||
|
|
||||||
@app.route(BASEURL + "settings", methods=["GET"])
|
@app.route(BASEURL + "settings", methods=["GET"])
|
||||||
|
|
|
@ -26,7 +26,12 @@ default_settings = {
|
||||||
"port": 5000
|
"port": 5000
|
||||||
},
|
},
|
||||||
"webcam": {
|
"webcam": {
|
||||||
"stream": None
|
"stream": None,
|
||||||
|
"snapshot": None
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"uploads": None,
|
||||||
|
"timelapse": None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,6 +79,7 @@ class Settings(object):
|
||||||
with open(os.path.join(self.settings_dir, "config.ini"), "wb") as configFile:
|
with open(os.path.join(self.settings_dir, "config.ini"), "wb") as configFile:
|
||||||
self._config.write(configFile)
|
self._config.write(configFile)
|
||||||
self._changes = None
|
self._changes = None
|
||||||
|
self.load()
|
||||||
|
|
||||||
def get(self, section, key):
|
def get(self, section, key):
|
||||||
if section not in default_settings.keys():
|
if section not in default_settings.keys():
|
||||||
|
@ -100,6 +106,19 @@ class Settings(object):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def getBaseFolder(self, type):
|
||||||
|
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)
|
||||||
|
|
||||||
|
if not os.path.isdir(folder):
|
||||||
|
os.makedirs(folder)
|
||||||
|
|
||||||
|
return folder
|
||||||
|
|
||||||
def set(self, section, key, value):
|
def set(self, section, key, value):
|
||||||
if section not in default_settings.keys():
|
if section not in default_settings.keys():
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -97,4 +97,5 @@ table th.gcode_files_action, table td.gcode_files_action {
|
||||||
#webcam_container {
|
#webcam_container {
|
||||||
height: 440px;
|
height: 440px;
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
|
@ -384,7 +384,78 @@ function GcodeFilesViewModel() {
|
||||||
}
|
}
|
||||||
var gcodeFilesViewModel = new GcodeFilesViewModel();
|
var gcodeFilesViewModel = new GcodeFilesViewModel();
|
||||||
|
|
||||||
function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, speedViewModel, terminalViewModel) {
|
function WebcamViewModel() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.timelapseType = ko.observable(undefined);
|
||||||
|
self.timelapseTimedInterval = ko.observable(undefined);
|
||||||
|
|
||||||
|
self.isErrorOrClosed = ko.observable(undefined);
|
||||||
|
self.isOperational = ko.observable(undefined);
|
||||||
|
self.isPrinting = ko.observable(undefined);
|
||||||
|
self.isPaused = ko.observable(undefined);
|
||||||
|
self.isError = ko.observable(undefined);
|
||||||
|
self.isReady = ko.observable(undefined);
|
||||||
|
self.isLoading = ko.observable(undefined);
|
||||||
|
|
||||||
|
self.intervalInputEnabled = ko.computed(function() {
|
||||||
|
return ("timed" == self.timelapseType());
|
||||||
|
})
|
||||||
|
|
||||||
|
self.isOperational.subscribe(function(newValue) {
|
||||||
|
self.requestData();
|
||||||
|
})
|
||||||
|
|
||||||
|
self.requestData = function() {
|
||||||
|
$.ajax({
|
||||||
|
url: AJAX_BASEURL + "timelapse",
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: self.fromResponse
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fromResponse = function(response) {
|
||||||
|
self.timelapseType(response.type)
|
||||||
|
|
||||||
|
if (response.type == "timed" && response.config && response.config.interval) {
|
||||||
|
self.timelapseTimedInterval(response.config.interval)
|
||||||
|
} else {
|
||||||
|
self.timelapseTimedInterval(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fromStateResponse = function(response) {
|
||||||
|
self.isErrorOrClosed(response.closedOrError);
|
||||||
|
self.isOperational(response.operational);
|
||||||
|
self.isPaused(response.paused);
|
||||||
|
self.isPrinting(response.printing);
|
||||||
|
self.isError(response.error);
|
||||||
|
self.isReady(response.ready);
|
||||||
|
self.isLoading(response.loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save = function() {
|
||||||
|
var data = {
|
||||||
|
"type": self.timelapseType()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.timelapseType() == "timed") {
|
||||||
|
data["interval"] = self.timelapseTimedInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: AJAX_BASEURL + "timelapse",
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
data: data,
|
||||||
|
success: self.fromResponse
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var webcamViewModel = new WebcamViewModel();
|
||||||
|
|
||||||
|
function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, speedViewModel, terminalViewModel, webcamViewModel) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.updateInterval = 500;
|
self.updateInterval = 500;
|
||||||
|
@ -397,6 +468,7 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
|
||||||
self.temperatureViewModel = temperatureViewModel;
|
self.temperatureViewModel = temperatureViewModel;
|
||||||
self.terminalViewModel = terminalViewModel;
|
self.terminalViewModel = terminalViewModel;
|
||||||
self.speedViewModel = speedViewModel;
|
self.speedViewModel = speedViewModel;
|
||||||
|
self.webcamViewModel = webcamViewModel;
|
||||||
|
|
||||||
self.requestData = function() {
|
self.requestData = function() {
|
||||||
var parameters = {};
|
var parameters = {};
|
||||||
|
@ -418,6 +490,7 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
|
||||||
self.printerStateViewModel.fromResponse(response);
|
self.printerStateViewModel.fromResponse(response);
|
||||||
self.connectionViewModel.fromStateResponse(response);
|
self.connectionViewModel.fromStateResponse(response);
|
||||||
self.speedViewModel.fromResponse(response);
|
self.speedViewModel.fromResponse(response);
|
||||||
|
self.webcamViewModel.fromStateResponse(response);
|
||||||
|
|
||||||
if (response.temperatures)
|
if (response.temperatures)
|
||||||
self.temperatureViewModel.fromResponse(response);
|
self.temperatureViewModel.fromResponse(response);
|
||||||
|
@ -436,7 +509,7 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var dataUpdater = new DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, speedViewModel, terminalViewModel);
|
var dataUpdater = new DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, speedViewModel, terminalViewModel, webcamViewModel);
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
|
|
||||||
|
@ -578,11 +651,17 @@ $(function() {
|
||||||
ko.applyBindings(terminalViewModel, document.getElementById("term"));
|
ko.applyBindings(terminalViewModel, document.getElementById("term"));
|
||||||
ko.applyBindings(speedViewModel, document.getElementById("speed"));
|
ko.applyBindings(speedViewModel, document.getElementById("speed"));
|
||||||
|
|
||||||
|
var webcamElement = document.getElementById("webcam");
|
||||||
|
if (webcamElement) {
|
||||||
|
ko.applyBindings(webcamViewModel, document.getElementById("webcam"));
|
||||||
|
}
|
||||||
|
|
||||||
//~~ startup commands
|
//~~ startup commands
|
||||||
|
|
||||||
dataUpdater.requestData();
|
dataUpdater.requestData();
|
||||||
connectionViewModel.requestData();
|
connectionViewModel.requestData();
|
||||||
gcodeFilesViewModel.requestData();
|
gcodeFilesViewModel.requestData();
|
||||||
|
webcamViewModel.requestData();
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -221,6 +221,26 @@
|
||||||
<div id="webcam_container">
|
<div id="webcam_container">
|
||||||
<img id="webcam_image" src="{{ webcamStream }}">
|
<img id="webcam_image" src="{{ webcamStream }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Timelapse</legend>
|
||||||
|
|
||||||
|
<label for="webcam_timelapse_mode">Timelapse Mode</label>
|
||||||
|
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting()">
|
||||||
|
<option value="off">Off</option>
|
||||||
|
<option value="zchange">On Z Change</option>
|
||||||
|
<option value="timed">Timed</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
|
||||||
|
<label for="webcam_timelapse_interval">Interval</label>
|
||||||
|
<input type="text" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting()">Save Settings</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue