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.

master
Gina Häußge 2013-01-03 15:25:20 +01:00
parent 81fb1ae305
commit 35e6b19332
7 changed files with 196 additions and 16 deletions

View File

@ -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

View File

@ -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):
"""

View File

@ -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"])

View File

@ -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

View File

@ -97,4 +97,5 @@ table th.gcode_files_action, table td.gcode_files_action {
#webcam_container {
height: 440px;
background-color: #000000;
margin-bottom: 20px;
}

View File

@ -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();
}
);

View File

@ -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>