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.
parent
84c9403e65
commit
49cd1ffbd6
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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> | <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> | <a href="#" class="icon-folder-open" data-bind="click: function() { $root.loadFile($data.name); }"></a></td>
|
||||||
|
|
|
@ -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")
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue