Added timelapse file management

master
Gina Häußge 2013-01-04 13:11:00 +01:00
parent 2db1cff224
commit ef0820a067
5 changed files with 168 additions and 82 deletions

View File

@ -2,12 +2,12 @@
__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 from flask import Flask, request, render_template, jsonify, send_from_directory, abort, url_for
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 timelapse
import os import os
import fnmatch import fnmatch
@ -16,7 +16,6 @@ BASEURL="/ajax/"
SUCCESS={} SUCCESS={}
UPLOAD_FOLDER = settings().getBaseFolder("uploads") UPLOAD_FOLDER = settings().getBaseFolder("uploads")
ALLOWED_EXTENSIONS = set(["gcode"])
app = Flask("printer_webui") app = Flask("printer_webui")
printer = Printer() printer = Printer()
@ -205,70 +204,89 @@ def readGcodeFiles():
@app.route(BASEURL + "gcodefiles/upload", methods=["POST"]) @app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
def uploadGcodeFile(): def uploadGcodeFile():
file = request.files["gcode_file"] if request.files.has_key("gcode_file"):
if file and allowed_file(file.filename): file = request.files["gcode_file"]
secure = secure_filename(file.filename) if file and allowed_file(file.filename, set(["gcode"])):
filename = os.path.join(UPLOAD_FOLDER, secure) secure = secure_filename(file.filename)
file.save(filename) filename = os.path.join(UPLOAD_FOLDER, secure)
file.save(filename)
return readGcodeFiles() return readGcodeFiles()
@app.route(BASEURL + "gcodefiles/load", methods=["POST"]) @app.route(BASEURL + "gcodefiles/load", methods=["POST"])
def loadGcodeFile(): def loadGcodeFile():
filename = request.values["filename"] if request.values.has_key("filename"):
printer.loadGcode(os.path.join(UPLOAD_FOLDER, filename)) filename = request.values["filename"]
printer.loadGcode(os.path.join(UPLOAD_FOLDER, filename))
return jsonify(SUCCESS) return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
def deleteGcodeFile(): def deleteGcodeFile():
if request.values.has_key("filename"): if request.values.has_key("filename"):
filename = request.values["filename"] filename = request.values["filename"]
if allowed_file(filename): if allowed_file(filename, set(["gcode"])):
secure = os.path.join(UPLOAD_FOLDER, secure_filename(filename)) secure = os.path.join(UPLOAD_FOLDER, secure_filename(filename))
if os.path.exists(secure): if os.path.exists(secure):
os.remove(secure) os.remove(secure)
return readGcodeFiles() return readGcodeFiles()
#~~ timelapse configuration #~~ timelapse handling
@app.route(BASEURL + "timelapse", methods=["GET"]) @app.route(BASEURL + "timelapse", methods=["GET"])
def getTimelapseConfig(): def getTimelapseData():
timelapse = printer.getTimelapse() lapse = printer.getTimelapse()
type = "off" type = "off"
additionalConfig = {} additionalConfig = {}
if timelapse is not None and isinstance(timelapse, ZTimelapse): if lapse is not None and isinstance(lapse, timelapse.ZTimelapse):
type = "zchange" type = "zchange"
elif timelapse is not None and isinstance(timelapse, TimedTimelapse): elif lapse is not None and isinstance(lapse, timelapse.TimedTimelapse):
type = "timed" type = "timed"
additionalConfig = { additionalConfig = {
"interval": timelapse.interval "interval": lapse.interval
} }
files = timelapse.getFinishedTimelapses()
for file in files:
file["size"] = sizeof_fmt(file["size"])
file["url"] = url_for("downloadTimelapse", filename=file["name"])
return jsonify({ return jsonify({
"type": type, "type": type,
"config": additionalConfig "config": additionalConfig,
"files": files
}) })
@app.route(BASEURL + "timelapse", methods=["POST"]) @app.route(BASEURL + "timelapse/<filename>", methods=["GET"])
def downloadTimelapse(filename):
if allowed_file(filename, set(["mpg"])):
return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True)
@app.route(BASEURL + "timelapse/<filename>", methods=["DELETE"])
def deleteTimelapse(filename):
if allowed_file(filename, set(["mpg"])):
secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename))
if os.path.exists(secure):
os.remove(secure)
return getTimelapseData()
@app.route(BASEURL + "timelapse/config", methods=["POST"])
def setTimelapseConfig(): def setTimelapseConfig():
if not request.values.has_key("type"): if request.values.has_key("type"):
return getTimelapseConfig() type = request.values["type"]
lapse = None
if "zchange" == type:
lapse = timelapse.ZTimelapse()
elif "timed" == type:
interval = 10
if request.values.has_key("interval"):
try:
interval = int(request.values["interval"])
except ValueError:
pass
lapse = timelapse.TimedTimelapse(interval)
printer.setTimelapse(lapse)
type = request.values["type"] return getTimelapseData()
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
@ -303,8 +321,8 @@ def sizeof_fmt(num):
num /= 1024.0 num /= 1024.0
return "%3.1f%s" % (num, "TB") return "%3.1f%s" % (num, "TB")
def allowed_file(filename): def allowed_file(filename, extensions):
return "." in filename and filename.rsplit(".", 1)[1] in ALLOWED_EXTENSIONS return "." in filename and filename.rsplit(".", 1)[1] in extensions
#~~ startup code #~~ startup code

View File

@ -20,6 +20,19 @@ body {
margin-bottom: 0px; margin-bottom: 0px;
} }
.tab-content h1 {
display: block;
width: 100%;
padding: 0;
margin-bottom: 20px;
font-size: 21px;
line-height: 40px;
color: #333;
border: 0;
border-bottom: 1px solid #E5E5E5;
font-weight: normal;
}
table { table {
table-layout: fixed; table-layout: fixed;
} }
@ -98,4 +111,20 @@ table th.gcode_files_action, table td.gcode_files_action {
height: 440px; height: 440px;
background-color: #000000; background-color: #000000;
margin-bottom: 20px; margin-bottom: 20px;
} }
table th.timelapse_files_name, table td.timelapse_files_name {
text-overflow: ellipsis;
text-align: left;
width: 55%;
}
table th.timelapse_files_size, table td.timelapse_files_size {
text-align: right;
width: 25%;
}
table th.timelapse_files_action, table td.timelapse_files_action {
text-align: center;
width: 20%;
}

View File

@ -389,6 +389,7 @@ function WebcamViewModel() {
self.timelapseType = ko.observable(undefined); self.timelapseType = ko.observable(undefined);
self.timelapseTimedInterval = ko.observable(undefined); self.timelapseTimedInterval = ko.observable(undefined);
self.files = ko.observableArray([]);
self.isErrorOrClosed = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined); self.isOperational = ko.observable(undefined);
@ -417,6 +418,7 @@ function WebcamViewModel() {
self.fromResponse = function(response) { self.fromResponse = function(response) {
self.timelapseType(response.type) self.timelapseType(response.type)
self.files(response.files)
if (response.type == "timed" && response.config && response.config.interval) { if (response.type == "timed" && response.config && response.config.interval) {
self.timelapseTimedInterval(response.config.interval) self.timelapseTimedInterval(response.config.interval)
@ -435,6 +437,16 @@ function WebcamViewModel() {
self.isLoading(response.loading); self.isLoading(response.loading);
} }
self.removeFile = function() {
var filename = this.name;
$.ajax({
url: AJAX_BASEURL + "timelapse/" + filename,
type: "DELETE",
dataType: "json",
success: self.requestData
})
}
self.save = function() { self.save = function() {
var data = { var data = {
"type": self.timelapseType() "type": self.timelapseType()
@ -445,7 +457,7 @@ function WebcamViewModel() {
} }
$.ajax({ $.ajax({
url: AJAX_BASEURL + "timelapse", url: AJAX_BASEURL + "timelapse/config",
type: "POST", type: "POST",
dataType: "json", dataType: "json",
data: data, data: data,

View File

@ -125,36 +125,32 @@
</div> </div>
<div> <div>
<div class="form-horizontal" style="width: 49%; float: left; margin-bottom: 20px;"> <div class="form-horizontal" style="width: 49%; float: left; margin-bottom: 20px;">
<fieldset> <h1>Temperature</h1>
<legend>Temperature</legend>
<label>Current: <strong data-bind="text: tempString"></strong></label> <label>Current: <strong data-bind="text: tempString"></strong></label>
<label>Target: <strong data-bind="text: targetTempString"></strong></label> <label>Target: <strong data-bind="text: targetTempString"></strong></label>
<label for="temp_newTemp">New Target</label> <label for="temp_newTemp">New Target</label>
<div class="input-append"> <div class="input-append">
<input class="span1" type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}"> <input class="span1" type="text" id="temp_newTemp" data-bind="attr: {placeholder: targetTemp}">
<span class="add-on">°C</span> <span class="add-on">°C</span>
</div> </div>
<button type="submit" class="btn" id="temp_newTemp_set">Set</button> <button type="submit" class="btn" id="temp_newTemp_set">Set</button>
</fieldset>
</div> </div>
<div class="form-horizontal" style="width: 49%; margin-left: 2%; float: left; margin-bottom: 20px;"> <div class="form-horizontal" style="width: 49%; margin-left: 2%; float: left; margin-bottom: 20px;">
<fieldset> <h1>Bed Temperature</h1>
<legend>Bed Temperature</legend>
<label>Current: <strong data-bind="text: bedTempString"></strong></label> <label>Current: <strong data-bind="text: bedTempString"></strong></label>
<label>Target: <strong data-bind="text: bedTargetTempString"></strong></label> <label>Target: <strong data-bind="text: bedTargetTempString"></strong></label>
<label for="temp_newBedTemp">New Target</label> <label for="temp_newBedTemp">New Target</label>
<div class="input-append"> <div class="input-append">
<input class="span1" type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}"> <input class="span1" type="text" id="temp_newBedTemp" data-bind="attr: {placeholder: bedTargetTemp}">
<span class="add-on">°C</span> <span class="add-on">°C</span>
</div> </div>
<button type="submit" class="btn" id="temp_newBedTemp_set">Set</button> <button type="submit" class="btn" id="temp_newBedTemp_set">Set</button>
</fieldset>
</div> </div>
</div> </div>
</div> </div>
@ -222,25 +218,42 @@
<img id="webcam_image" src="{{ webcamStream }}"> <img id="webcam_image" src="{{ webcamStream }}">
</div> </div>
<fieldset> <h1>Timelapse Configuration</h1>
<legend>Timelapse</legend>
<label for="webcam_timelapse_mode">Timelapse Mode</label> <label for="webcam_timelapse_mode">Timelapse Mode</label>
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting()"> <select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting()">
<option value="off">Off</option> <option value="off">Off</option>
<option value="zchange">On Z Change</option> <option value="zchange">On Z Change</option>
<option value="timed">Timed</option> <option value="timed">Timed</option>
</select> </select>
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()"> <div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
<label for="webcam_timelapse_interval">Interval</label> <label for="webcam_timelapse_interval">Interval</label>
<input type="text" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting()"> <input type="text" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, enable: isOperational() && !isPrinting()">
</div> </div>
<div> <div>
<button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting()">Save Settings</button> <button class="btn" data-bind="click: save, enable: isOperational() && !isPrinting()">Save Settings</button>
</div> </div>
</fieldset>
<h1>Finished Timelapses</h1>
<table class="table table-striped table-hover table-condensed table-hover" id="timelapse_files">
<thead>
<tr>
<th class="timelapse_files_name">Name</th>
<th class="timelapse_files_size">Size</th>
<th class="timelapse_files_action">Action</th>
</tr>
</thead>
<tbody data-bind="foreach: files">
<tr data-bind="attr: {title: name}">
<td class="timelapse_files_name" data-bind="text: name"></td>
<td class="timelapse_files_size" data-bind="text: size"></td>
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: $parent.removeFile"></a>&nbsp;|&nbsp;<a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
</tr>
</tbody>
</table>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -9,7 +9,19 @@ import threading
import urllib import urllib
import time import time
import subprocess import subprocess
import glob import fnmatch
def getFinishedTimelapses():
files = []
basedir = settings().getBaseFolder("timelapse")
for osFile in os.listdir(basedir):
if not fnmatch.fnmatch(osFile, "*.mpg"):
continue
files.append({
"name": osFile,
"size": os.stat(os.path.join(basedir, osFile)).st_size
})
return files
class Timelapse(object): class Timelapse(object):
def __init__(self): def __init__(self):
@ -75,8 +87,10 @@ class Timelapse(object):
if not os.path.isdir(self.captureDir): if not os.path.isdir(self.captureDir):
return return
for filename in glob.glob(os.path.join(self.captureDir, "*.jpg")): for filename in os.listdir(self.captureDir):
os.remove(filename) if not fnmatch.fnmatch(filename, "*.jpg"):
continue
os.remove(os.path.join(self.captureDir, filename))
class ZTimelapse(Timelapse): class ZTimelapse(Timelapse):
def __init__(self): def __init__(self):