Gcode filemanagement now lives in its own module. Upload triggers gcode analysis, result is stored into metadata file. Print jobs triggered and their results are saved as well. Adjusted UI to display gcode analysis result and last print date and (color coded) result if available. Also adjusted gcode file list to color code entries according to last print result.

master
Gina Häußge 2013-01-30 20:56:17 +01:00
parent 84c9403e65
commit 49cd1ffbd6
9 changed files with 432 additions and 69 deletions

View File

@ -0,0 +1 @@

281
octoprint/gcodefiles.py Normal file
View File

@ -0,0 +1,281 @@
# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import os
import Queue
import threading
import datetime
import yaml
import time
import octoprint.util as util
import octoprint.util.gcodeInterpreter as gcodeInterpreter
from octoprint.settings import settings
from werkzeug.utils import secure_filename
class GcodeManager:
def __init__(self):
self._uploadFolder = settings().getBaseFolder("uploads")
self._metadata = {}
self._metadataDirty = False
self._metadataFile = os.path.join(self._uploadFolder, "metadata.yaml")
self._metadataFileAccessMutex = threading.Lock()
self._metadataAnalyzer = MetadataAnalyzer(getPathCallback=self.getAbsolutePath, loadedCallback=self._onMetadataAnalysisFinished)
self._loadMetadata()
def _onMetadataAnalysisFinished(self, filename, gcode):
print("Got gcode analysis for file %s: %r" % (filename, gcode))
if filename is None or gcode is None:
return
basename = os.path.basename(filename)
absolutePath = self.getAbsolutePath(basename)
if absolutePath is None:
return
analysisResult = {}
dirty = False
if gcode.totalMoveTimeMinute:
analysisResult["estimatedPrintTime"] = util.getFormattedTimeDelta(datetime.timedelta(minutes=gcode.totalMoveTimeMinute))
dirty = True
if gcode.extrusionAmount:
analysisResult["filament"] = "%.2fm" % (gcode.extrusionAmount / 1000)
dirty = True
if dirty:
metadata = self.getFileMetadata(basename)
metadata["gcodeAnalysis"] = analysisResult
self._metadata[basename] = metadata
self._metadataDirty = True
self._saveMetadata()
def _loadMetadata(self):
if os.path.exists(self._metadataFile) and os.path.isfile(self._metadataFile):
with self._metadataFileAccessMutex:
with open(self._metadataFile, "r") as f:
self._metadata = yaml.safe_load(f)
def _saveMetadata(self, force=False):
if not self._metadataDirty and not force:
return
with self._metadataFileAccessMutex:
with open(self._metadataFile, "wb") as f:
yaml.safe_dump(self._metadata, f, default_flow_style=False, indent=" ", allow_unicode=True)
self._metadataDirty = False
self._loadMetadata()
def _getBasicFilename(self, filename):
if filename.startswith(self._uploadFolder):
return filename[len(self._uploadFolder + os.path.sep):]
else:
return filename
def addFile(self, file):
if file:
absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
if absolutePath is not None:
file.save(absolutePath)
self._metadataAnalyzer.addFileToQueue(os.path.basename(absolutePath))
return absolutePath
return None
def removeFile(self, filename):
filename = self._getBasicFilename(filename)
absolutePath = self.getAbsolutePath(filename)
if absolutePath is not None:
os.remove(absolutePath)
def getAbsolutePath(self, filename, mustExist=True):
"""
Returns the absolute path of the given filename in the gcode upload folder.
Ensures that
<ul>
<li>The file has the extension ".gcode"</li>
<li>exists and is a file (not a directory) if "mustExist" is set to True</li>
</ul>
@param filename the name of the file for which to determine the absolute path
@param mustExist if set to true, the method also checks if the file exists and is a file
@return the absolute path of the file or None if the file is not valid
"""
filename = self._getBasicFilename(filename)
if not util.isAllowedFile(filename, set(["gcode"])):
return None
secure = os.path.join(self._uploadFolder, secure_filename(self._getBasicFilename(filename)))
if mustExist and (not os.path.exists(secure) or not os.path.isfile(secure)):
return None
return secure
def getAllFileData(self):
files = []
for osFile in os.listdir(self._uploadFolder):
fileData = self.getFileData(osFile)
if fileData is not None:
files.append(fileData)
return files
def getFileData(self, filename):
filename = self._getBasicFilename(filename)
absolutePath = self.getAbsolutePath(filename)
if absolutePath is None:
return None
statResult = os.stat(absolutePath)
fileData = {
"name": filename,
"size": util.getFormattedSize(statResult.st_size),
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime))
}
# enrich with additional metadata from analysis if available
if filename in self._metadata.keys():
for key in self._metadata[filename].keys():
if key == "prints":
val = self._metadata[filename][key]
formattedLast = None
if val["last"] is not None:
formattedLast = {
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(val["last"]["date"])),
"success": val["last"]["success"]
}
formattedPrints = {
"success": val["success"],
"failure": val["failure"],
"last": formattedLast
}
fileData["prints"] = formattedPrints
else:
fileData[key] = self._metadata[filename][key]
return fileData
def getFileMetadata(self, filename):
filename = self._getBasicFilename(filename)
if filename in self._metadata.keys():
return self._metadata[filename]
else:
return {
"prints": {
"success": 0,
"failure": 0,
"last": None
}
}
def setFileMetadata(self, filename, metadata):
filename = self._getBasicFilename(filename)
self._metadata[filename] = metadata
self._metadataDirty = True
def printSucceeded(self, filename):
filename = self._getBasicFilename(filename)
absolutePath = self.getAbsolutePath(filename)
if absolutePath is None:
return
metadata = self.getFileMetadata(filename)
metadata["prints"]["success"] += 1
metadata["prints"]["last"] = {
"date": time.time(),
"success": True
}
self.setFileMetadata(filename, metadata)
self._saveMetadata()
def printFailed(self, filename):
filename = self._getBasicFilename(filename)
absolutePath = self.getAbsolutePath(filename)
if absolutePath is None:
return
metadata = self.getFileMetadata(filename)
metadata["prints"]["failure"] += 1
metadata["prints"]["last"] = {
"date": time.time(),
"success": False
}
self.setFileMetadata(filename, metadata)
self._saveMetadata()
class MetadataAnalyzer:
def __init__(self, getPathCallback, loadedCallback):
self._getPathCallback = getPathCallback
self._loadedCallback = loadedCallback
self._active = threading.Event()
self._active.set()
self._currentFile = None
self._currentProgress = None
self._queue = Queue.Queue()
self._gcode = None
self._worker = threading.Thread(target=self._work)
self._worker.daemon = True
self._worker.start()
def addFileToQueue(self, filename):
self._queue.put(filename)
def working(self):
return self.isActive() and not (self._queue.empty() and self._currentFile is None)
def isActive(self):
return self._active.is_set()
def pause(self):
self._active.clear()
if self._gcode is not None:
self._gcode.abort()
def resume(self):
self._active.set()
def _work(self):
aborted = None
while True:
self._active.wait()
if aborted is not None:
filename = aborted
aborted = None
else:
filename = self._queue.get()
try:
self._analyzeGcode(filename)
self._queue.task_done()
except gcodeInterpreter.AnalysisAborted:
aborted = filename
def _analyzeGcode(self, filename):
path = self._getPathCallback(filename)
if path is None:
return
self._currentFile = filename
self._currentProgress = 0
try:
self._gcode = gcodeInterpreter.gcode()
self._gcode.progressCallback = self._onParsingProgress
self._gcode.load(path)
self._loadedCallback(self._currentFile, self._gcode)
finally:
self._gcode = None
self._currentProgress = None
self._currentFile = None
def _onParsingProgress(self, progress):
self._currentProgress = progress

View File

@ -9,6 +9,7 @@ import copy
import os import os
import octoprint.util.comm as comm import octoprint.util.comm as comm
import octoprint.util as util
from octoprint.util import gcodeInterpreter from octoprint.util import gcodeInterpreter
from octoprint.settings import settings from octoprint.settings import settings
@ -25,7 +26,9 @@ def getConnectionOptions():
} }
class Printer(): class Printer():
def __init__(self): def __init__(self, gcodeManager):
self._gcodeManager = gcodeManager
# state # state
self._temp = None self._temp = None
self._bedTemp = None self._bedTemp = None
@ -209,6 +212,9 @@ class Printer():
self._setCurrentZ(None) self._setCurrentZ(None)
self._setProgressData(None, None, None) self._setProgressData(None, None, None)
# mark print as failure
self._gcodeManager.printFailed(self._filename)
#~~ state monitoring #~~ state monitoring
def setTimelapse(self, timelapse): def setTimelapse(self, timelapse):
@ -249,11 +255,11 @@ class Printer():
formattedPrintTime = None formattedPrintTime = None
if (self._printTime): if (self._printTime):
formattedPrintTime = _getFormattedTimeDelta(datetime.timedelta(seconds=self._printTime)) formattedPrintTime = util.getFormattedTimeDelta(datetime.timedelta(seconds=self._printTime))
formattedPrintTimeLeft = None formattedPrintTimeLeft = None
if (self._printTimeLeft): if (self._printTimeLeft):
formattedPrintTimeLeft = _getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft)) formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft))
self._stateMonitor.setProgress({"progress": self._progress, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft}) self._stateMonitor.setProgress({"progress": self._progress, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft})
@ -292,7 +298,7 @@ class Printer():
formattedFilament = None formattedFilament = None
if self._gcode: if self._gcode:
if self._gcode.totalMoveTimeMinute: if self._gcode.totalMoveTimeMinute:
formattedPrintTimeEstimation = _getFormattedTimeDelta(datetime.timedelta(minutes=self._gcode.totalMoveTimeMinute)) formattedPrintTimeEstimation = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._gcode.totalMoveTimeMinute))
if self._gcode.extrusionAmount: if self._gcode.extrusionAmount:
formattedFilament = "%.2fm" % (self._gcode.extrusionAmount / 1000) formattedFilament = "%.2fm" % (self._gcode.extrusionAmount / 1000)
elif not settings().getBoolean("feature", "analyzeGcode"): elif not settings().getBoolean("feature", "analyzeGcode"):
@ -347,12 +353,19 @@ class Printer():
""" """
oldState = self._state oldState = self._state
#
if self._timelapse is not None: if self._timelapse is not None:
if oldState == self._comm.STATE_PRINTING: if oldState == self._comm.STATE_PRINTING and state != self._comm.STATE_PAUSED:
self._timelapse.onPrintjobStopped() self._timelapse.onPrintjobStopped()
elif state == self._comm.STATE_PRINTING: elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED:
self._timelapse.onPrintjobStarted(self._filename) self._timelapse.onPrintjobStarted(self._filename)
if oldState == self._comm.STATE_PRINTING:
if state == self._comm.STATE_OPERATIONAL:
self._gcodeManager.printSucceeded(self._filename)
elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR:
self._gcodeManager.printFailed(self._filename)
self._setState(state) self._setState(state)
@ -604,10 +617,3 @@ class StateMonitor(object):
"progress": self._progress "progress": self._progress
} }
def _getFormattedTimeDelta(d):
if d is None:
return None
hours = d.seconds // 3600
minutes = (d.seconds % 3600) // 60
seconds = d.seconds % 60
return "%02d:%02d:%02d" % (hours, minutes, seconds)

View File

@ -3,16 +3,17 @@ __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_from_directory, abort, url_for from flask import Flask, request, render_template, jsonify, send_from_directory, abort, url_for
from werkzeug import secure_filename from werkzeug.utils import secure_filename
import tornadio2 import tornadio2
import os import os
import fnmatch
import threading import threading
from octoprint.printer import Printer, getConnectionOptions, PrinterCallback from octoprint.printer import Printer, getConnectionOptions, PrinterCallback
from octoprint.settings import settings from octoprint.settings import settings
import octoprint.timelapse as timelapse import octoprint.timelapse as timelapse
import octoprint.gcodefiles as gcodefiles
import octoprint.util as util
BASEURL = "/ajax/" BASEURL = "/ajax/"
SUCCESS = {} SUCCESS = {}
@ -20,7 +21,8 @@ SUCCESS = {}
UPLOAD_FOLDER = settings().getBaseFolder("uploads") UPLOAD_FOLDER = settings().getBaseFolder("uploads")
app = Flask("octoprint") app = Flask("octoprint")
printer = Printer() gcodeManager = gcodefiles.GcodeManager()
printer = Printer(gcodeManager)
@app.route("/") @app.route("/")
def index(): def index():
@ -158,7 +160,7 @@ def setTargetTemperature():
if request.values.has_key("temp"): if request.values.has_key("temp"):
# set target temperature # set target temperature
temp = request.values["temp"]; temp = request.values["temp"]
printer.command("M104 S" + temp) printer.command("M104 S" + temp)
if request.values.has_key("bedTemp"): if request.values.has_key("bedTemp"):
@ -174,22 +176,22 @@ def jog():
# do not jog when a print job is running or we don"t have a connection # do not jog when a print job is running or we don"t have a connection
return jsonify(SUCCESS) return jsonify(SUCCESS)
if request.values.has_key("x"): if "x" in request.values.keys():
# jog x # jog x
x = request.values["x"] x = request.values["x"]
printer.commands(["G91", "G1 X" + x + " F6000", "G90"]) printer.commands(["G91", "G1 X" + x + " F6000", "G90"])
if request.values.has_key("y"): if "y" in request.values.keys():
# jog y # jog y
y = request.values["y"] y = request.values["y"]
printer.commands(["G91", "G1 Y" + y + " F6000", "G90"]) printer.commands(["G91", "G1 Y" + y + " F6000", "G90"])
if request.values.has_key("z"): if "z" in request.values.keys():
# jog z # jog z
z = request.values["z"] z = request.values["z"]
printer.commands(["G91", "G1 Z" + z + " F200", "G90"]) printer.commands(["G91", "G1 Z" + z + " F200", "G90"])
if request.values.has_key("homeXY"): if "homeXY" in request.values.keys():
# home x/y # home x/y
printer.command("G28 X0 Y0") printer.command("G28 X0 Y0")
if request.values.has_key("homeZ"): if "homeZ" in request.values.keys():
# home z # home z
printer.command("G28 Z0") printer.command("G28 Z0")
@ -197,7 +199,7 @@ def jog():
@app.route(BASEURL + "control/speed", methods=["GET"]) @app.route(BASEURL + "control/speed", methods=["GET"])
def getSpeedValues(): def getSpeedValues():
return jsonify(feedrate = printer.feedrateState()) return jsonify(feedrate=printer.feedrateState())
@app.route(BASEURL + "control/speed", methods=["POST"]) @app.route(BASEURL + "control/speed", methods=["POST"])
def speed(): def speed():
@ -205,7 +207,7 @@ def speed():
return jsonify(SUCCESS) return jsonify(SUCCESS)
for key in ["outerWall", "innerWall", "fill", "support"]: for key in ["outerWall", "innerWall", "fill", "support"]:
if request.values.has_key(key): if key in request.values.keys():
value = int(request.values[key]) value = int(request.values[key])
printer.setFeedrateModifier(key, value) printer.setFeedrateModifier(key, value)
@ -214,47 +216,34 @@ def speed():
@app.route(BASEURL + "control/custom", methods=["GET"]) @app.route(BASEURL + "control/custom", methods=["GET"])
def getCustomControls(): def getCustomControls():
customControls = settings().getObject("controls") customControls = settings().getObject("controls")
return jsonify(controls = customControls) return jsonify(controls=customControls)
#~~ GCODE file handling #~~ GCODE file handling
@app.route(BASEURL + "gcodefiles", methods=["GET"]) @app.route(BASEURL + "gcodefiles", methods=["GET"])
def readGcodeFiles(): def readGcodeFiles():
files = [] return jsonify(files=gcodeManager.getAllFileData())
for osFile in os.listdir(UPLOAD_FOLDER):
if not fnmatch.fnmatch(osFile, "*.gcode"):
continue
files.append({
"name": osFile,
"size": sizeof_fmt(os.stat(os.path.join(UPLOAD_FOLDER, osFile)).st_size)
})
return jsonify(files=files)
@app.route(BASEURL + "gcodefiles/upload", methods=["POST"]) @app.route(BASEURL + "gcodefiles/upload", methods=["POST"])
def uploadGcodeFile(): def uploadGcodeFile():
if request.files.has_key("gcode_file"): if "gcode_file" in request.files.keys():
file = request.files["gcode_file"] file = request.files["gcode_file"]
if file and allowed_file(file.filename, set(["gcode"])): gcodeManager.addFile(file)
secure = secure_filename(file.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():
if request.values.has_key("filename"): if "filename" in request.values.keys():
filename = request.values["filename"] filename = gcodeManager.getAbsolutePath(request.values["filename"])
printer.loadGcode(os.path.join(UPLOAD_FOLDER, filename)) if filename is not None:
printer.loadGcode(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 "filename" in request.values.keys():
filename = request.values["filename"] filename = request.values["filename"]
if allowed_file(filename, set(["gcode"])): gcodeManager.removeFile(filename)
secure = os.path.join(UPLOAD_FOLDER, secure_filename(filename))
if os.path.exists(secure):
os.remove(secure)
return readGcodeFiles() return readGcodeFiles()
#~~ timelapse handling #~~ timelapse handling
@ -275,7 +264,7 @@ def getTimelapseData():
files = timelapse.getFinishedTimelapses() files = timelapse.getFinishedTimelapses()
for file in files: for file in files:
file["size"] = sizeof_fmt(file["size"]) file["size"] = util.getFormattedSize(file["size"])
file["url"] = url_for("downloadTimelapse", filename=file["name"]) file["url"] = url_for("downloadTimelapse", filename=file["name"])
return jsonify({ return jsonify({
@ -286,12 +275,12 @@ def getTimelapseData():
@app.route(BASEURL + "timelapse/<filename>", methods=["GET"]) @app.route(BASEURL + "timelapse/<filename>", methods=["GET"])
def downloadTimelapse(filename): def downloadTimelapse(filename):
if allowed_file(filename, set(["mpg"])): if util.isAllowedFile(filename, set(["mpg"])):
return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True) return send_from_directory(settings().getBaseFolder("timelapse"), filename, as_attachment=True)
@app.route(BASEURL + "timelapse/<filename>", methods=["DELETE"]) @app.route(BASEURL + "timelapse/<filename>", methods=["DELETE"])
def deleteTimelapse(filename): def deleteTimelapse(filename):
if allowed_file(filename, set(["mpg"])): if util.isAllowedFile(filename, set(["mpg"])):
secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename)) secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename))
if os.path.exists(secure): if os.path.exists(secure):
os.remove(secure) os.remove(secure)
@ -337,21 +326,6 @@ def setSettings():
s.save() s.save()
return getSettings() return getSettings()
#~~ helper functions
def sizeof_fmt(num):
"""
Taken from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
"""
for x in ["bytes","KB","MB","GB"]:
if num < 1024.0:
return "%3.1f%s" % (num, x)
num /= 1024.0
return "%3.1f%s" % (num, "TB")
def allowed_file(filename, extensions):
return "." in filename and filename.rsplit(".", 1)[1] in extensions
#~~ startup code #~~ startup code
def run(host = "0.0.0.0", port = 5000, debug = False): def run(host = "0.0.0.0", port = 5000, debug = False):

View File

@ -134,3 +134,15 @@ table th.timelapse_files_action, table td.timelapse_files_action {
#webcam_container { #webcam_container {
width: 100%; width: 100%;
} }
#files .popover {
font-size: 85%;
}
#files .popover p:last-child {
margin-bottom: 0;
}
.overflow_visible {
overflow: visible !important;
}

View File

@ -695,6 +695,29 @@ function GcodeFilesViewModel() {
} }
} }
self.getPopoverContent = function(data) {
var output = "<p><strong>Uploaded:</strong> " + data["date"] + "</p>";
if (data["gcodeAnalysis"]) {
output += "<p>";
output += "<strong>Filament:</strong> " + data["gcodeAnalysis"]["filament"] + "<br>";
output += "<strong>Estimated Print Time:</strong> " + data["gcodeAnalysis"]["estimatedPrintTime"];
output += "</p>";
}
if (data["prints"] && data["prints"]["last"]) {
output += "<p>";
output += "<strong>Last Print:</strong> <span class=\"" + (data["prints"]["last"]["success"] ? "text-success" : "text-error") + "\">" + data["prints"]["last"]["date"] + "</span>";
output += "</p>";
}
return output;
}
self.getSuccessClass = function(data) {
if (!data["prints"] || !data["prints"]["last"]) {
return "";
}
return data["prints"]["last"]["success"] ? "text-success" : "text-error";
}
} }
function WebcamViewModel() { function WebcamViewModel() {
@ -962,6 +985,23 @@ $(function() {
//~~ knockout.js bindings //~~ knockout.js bindings
ko.bindingHandlers.popover = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var val = ko.utils.unwrapObservable(valueAccessor());
var options = {
title: val.title,
animation: val.animation,
placement: val.placement,
trigger: val.trigger,
delay: val.delay,
content: val.content,
html: val.html
};
$(element).popover(options);
}
}
ko.applyBindings(connectionViewModel, document.getElementById("connection")); ko.applyBindings(connectionViewModel, document.getElementById("connection"));
ko.applyBindings(printerStateViewModel, document.getElementById("state")); ko.applyBindings(printerStateViewModel, document.getElementById("state"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files")); ko.applyBindings(gcodeFilesViewModel, document.getElementById("files"));
@ -982,6 +1022,18 @@ $(function() {
gcodeFilesViewModel.requestData(); gcodeFilesViewModel.requestData();
webcamViewModel.requestData(); webcamViewModel.requestData();
//~~ UI stuff
$(".accordion-toggle[href='#files']").click(function() {
if ($("#files").hasClass("in")) {
$("#files").removeClass("overflow_visible");
} else {
setTimeout(function() {
$("#files").addClass("overflow_visible");
}, 1000);
}
})
} }
); );

View File

@ -77,7 +77,7 @@
<div class="accordion-heading"> <div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> Files</a> <a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> Files</a>
</div> </div>
<div class="accordion-body collapse in" id="files"> <div class="accordion-body collapse in overflow_visible" id="files">
<div class="accordion-inner"> <div class="accordion-inner">
<table class="table table-striped table-hover table-condensed table-hover" id="gcode_files"> <table class="table table-striped table-hover table-condensed table-hover" id="gcode_files">
<thead> <thead>
@ -88,7 +88,7 @@
</tr> </tr>
</thead> </thead>
<tbody data-bind="foreach: paginatedFiles"> <tbody data-bind="foreach: paginatedFiles">
<tr data-bind="attr: {title: name}"> <tr data-bind="css: $root.getSuccessClass($data), popover: { title: name, animation: true, html: true, placement: 'right', trigger: 'hover', delay: 0, content: $root.getPopoverContent($data), html: true }">
<td class="gcode_files_name" data-bind="text: name"></td> <td class="gcode_files_name" data-bind="text: name"></td>
<td class="gcode_files_size" data-bind="text: size"></td> <td class="gcode_files_size" data-bind="text: size"></td>
<td class="gcode_files_action"><a href="#" class="icon-trash" data-bind="click: function() { $root.removeFile($data.name); }"></a>&nbsp;|&nbsp;<a href="#" class="icon-folder-open" data-bind="click: function() { $root.loadFile($data.name); }"></a></td> <td class="gcode_files_action"><a href="#" class="icon-trash" data-bind="click: function() { $root.removeFile($data.name); }"></a>&nbsp;|&nbsp;<a href="#" class="icon-folder-open" data-bind="click: function() { $root.loadFile($data.name); }"></a></td>

View File

@ -0,0 +1,30 @@
# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
def getFormattedSize(num):
"""
Taken from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
"""
for x in ["bytes","KB","MB","GB"]:
if num < 1024.0:
return "%3.1f%s" % (num, x)
num /= 1024.0
return "%3.1f%s" % (num, "TB")
def isAllowedFile(filename, extensions):
return "." in filename and filename.rsplit(".", 1)[1] in extensions
def getFormattedTimeDelta(d):
if d is None:
return None
hours = d.seconds // 3600
minutes = (d.seconds % 3600) // 60
seconds = d.seconds % 60
return "%02d:%02d:%02d" % (hours, minutes, seconds)
def getFormattedDateTime(d):
if d is None:
return None
return d.strftime("%Y-%m-%d %H:%M")

View File

@ -22,6 +22,9 @@ def getPreference(key, default=None):
else: else:
return default return default
class AnalysisAborted(Exception):
pass
class gcodePath(object): class gcodePath(object):
def __init__(self, newType, pathType, layerThickness, startPoint): def __init__(self, newType, pathType, layerThickness, startPoint):
self.type = newType self.type = newType
@ -36,6 +39,7 @@ class gcode(object):
self.extrusionAmount = 0 self.extrusionAmount = 0
self.totalMoveTimeMinute = 0 self.totalMoveTimeMinute = 0
self.progressCallback = None self.progressCallback = None
self._abort = False
def load(self, filename): def load(self, filename):
if os.path.isfile(filename): if os.path.isfile(filename):
@ -47,6 +51,9 @@ class gcode(object):
def loadList(self, l): def loadList(self, l):
self._load(l) self._load(l)
def abort(self):
self._abort = True
def _load(self, gcodeFile): def _load(self, gcodeFile):
filePos = 0 filePos = 0
pos = util3d.Vector3() pos = util3d.Vector3()
@ -69,6 +76,8 @@ class gcode(object):
currentPath.list[0].extrudeAmountMultiply = extrudeAmountMultiply currentPath.list[0].extrudeAmountMultiply = extrudeAmountMultiply
currentLayer.append(currentPath) currentLayer.append(currentPath)
for line in gcodeFile: for line in gcodeFile:
if self._abort:
raise StopIteration
if type(line) is tuple: if type(line) is tuple:
line = line[0] line = line[0]
if self.progressCallback != None: if self.progressCallback != None:
@ -253,8 +262,6 @@ class gcode(object):
self.layerList.append(currentLayer) self.layerList.append(currentLayer)
self.extrusionAmount = maxExtrusion self.extrusionAmount = maxExtrusion
self.totalMoveTimeMinute = totalMoveTimeMinute self.totalMoveTimeMinute = totalMoveTimeMinute
#print "Extruded a total of: %d mm of filament" % (self.extrusionAmount)
#print "Estimated print duration: %.2f minutes" % (self.totalMoveTimeMinute)
def getCodeInt(self, line, code): def getCodeInt(self, line, code):
if code not in self.regMatch: if code not in self.regMatch: