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

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' __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):
""" """

View File

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

View File

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

View File

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

View File

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

View File

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