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
|
||||
* 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 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,
|
||||
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]
|
||||
# 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
|
||||
# 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
|
||||
----------------------------------------
|
||||
|
@ -132,6 +135,6 @@ It also uses the following libraries and frameworks for backend and frontend:
|
|||
* Flot: http://www.flotcharts.org/
|
||||
* 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
|
||||
|
|
|
@ -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'
|
||||
|
||||
import time
|
||||
import os
|
||||
from threading import Thread
|
||||
import datetime
|
||||
|
||||
|
@ -58,6 +57,8 @@ class Printer():
|
|||
|
||||
self.feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"}
|
||||
|
||||
self.timelapse = None
|
||||
|
||||
# comm
|
||||
self.comm = None
|
||||
|
||||
|
@ -97,6 +98,15 @@ class Printer():
|
|||
|
||||
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):
|
||||
"""
|
||||
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.
|
||||
New state is stored for retrieval by the frontend.
|
||||
"""
|
||||
oldState = self.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):
|
||||
"""
|
||||
Callback method for the comm object, called upon message exchanges via serial.
|
||||
|
@ -152,13 +169,19 @@ class Printer():
|
|||
"""
|
||||
self.printTime = self.comm.getPrintTime()
|
||||
self.printTimeLeft = self.comm.getPrintTimeRemainingEstimate()
|
||||
oldProgress = self.progress;
|
||||
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):
|
||||
"""
|
||||
Callback method for the comm object, called upon change of the z-layer.
|
||||
"""
|
||||
oldZ = self.currentZ
|
||||
self.currentZ = newZ
|
||||
if self.timelapse is not None:
|
||||
self.timelapse.onZChange(oldZ, self.currentZ)
|
||||
|
||||
def onGcodeLoaded(self, gcodeLoader):
|
||||
"""
|
||||
|
|
|
@ -2,29 +2,22 @@
|
|||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__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 printer_webui.printer import Printer, getConnectionOptions
|
||||
from printer_webui.settings import settings
|
||||
from printer_webui.timelapse import ZTimelapse, TimedTimelapse
|
||||
|
||||
import sys
|
||||
import os
|
||||
import fnmatch
|
||||
import StringIO
|
||||
|
||||
BASEURL="/ajax/"
|
||||
SUCCESS={}
|
||||
|
||||
UPLOAD_FOLDER = os.path.join(settings().settings_dir, "uploads")
|
||||
if not os.path.isdir(UPLOAD_FOLDER):
|
||||
os.makedirs(UPLOAD_FOLDER)
|
||||
UPLOAD_FOLDER = settings().getBaseFolder("uploads")
|
||||
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")
|
||||
printer = Printer()
|
||||
|
||||
|
@ -235,6 +228,48 @@ def deleteGcodeFile():
|
|||
os.remove(secure)
|
||||
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
|
||||
|
||||
@app.route(BASEURL + "settings", methods=["GET"])
|
||||
|
|
|
@ -26,7 +26,12 @@ default_settings = {
|
|||
"port": 5000
|
||||
},
|
||||
"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:
|
||||
self._config.write(configFile)
|
||||
self._changes = None
|
||||
self.load()
|
||||
|
||||
def get(self, section, key):
|
||||
if section not in default_settings.keys():
|
||||
|
@ -100,6 +106,19 @@ class Settings(object):
|
|||
except ValueError:
|
||||
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):
|
||||
if section not in default_settings.keys():
|
||||
return None
|
||||
|
|
|
@ -97,4 +97,5 @@ table th.gcode_files_action, table td.gcode_files_action {
|
|||
#webcam_container {
|
||||
height: 440px;
|
||||
background-color: #000000;
|
||||
margin-bottom: 20px;
|
||||
}
|
|
@ -384,7 +384,78 @@ function 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;
|
||||
|
||||
self.updateInterval = 500;
|
||||
|
@ -397,6 +468,7 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
|
|||
self.temperatureViewModel = temperatureViewModel;
|
||||
self.terminalViewModel = terminalViewModel;
|
||||
self.speedViewModel = speedViewModel;
|
||||
self.webcamViewModel = webcamViewModel;
|
||||
|
||||
self.requestData = function() {
|
||||
var parameters = {};
|
||||
|
@ -418,6 +490,7 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
|
|||
self.printerStateViewModel.fromResponse(response);
|
||||
self.connectionViewModel.fromStateResponse(response);
|
||||
self.speedViewModel.fromResponse(response);
|
||||
self.webcamViewModel.fromStateResponse(response);
|
||||
|
||||
if (response.temperatures)
|
||||
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() {
|
||||
|
||||
|
@ -578,11 +651,17 @@ $(function() {
|
|||
ko.applyBindings(terminalViewModel, document.getElementById("term"));
|
||||
ko.applyBindings(speedViewModel, document.getElementById("speed"));
|
||||
|
||||
var webcamElement = document.getElementById("webcam");
|
||||
if (webcamElement) {
|
||||
ko.applyBindings(webcamViewModel, document.getElementById("webcam"));
|
||||
}
|
||||
|
||||
//~~ startup commands
|
||||
|
||||
dataUpdater.requestData();
|
||||
connectionViewModel.requestData();
|
||||
gcodeFilesViewModel.requestData();
|
||||
webcamViewModel.requestData();
|
||||
|
||||
}
|
||||
);
|
||||
|
|
|
@ -221,6 +221,26 @@
|
|||
<div id="webcam_container">
|
||||
<img id="webcam_image" src="{{ webcamStream }}">
|
||||
</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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue