Added timelapse file management
parent
2db1cff224
commit
ef0820a067
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -99,3 +112,19 @@ table th.gcode_files_action, table td.gcode_files_action {
|
||||||
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%;
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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> | <a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue