MrDraw/src/octoprint/gcodefiles.py
Gina Häußge 592f3dce9a Bugfix: Do not run gcode analyzer when a print is ongoing
Evaluate active flag on gcode analyzer AFTER fetching an item from the work queue, otherwise it will always start working once it finds something if the active flag was true once but then switched to false while the queue was still empty.

Thanks to @Salandora for spotting this.

Fixes #357
2014-02-15 10:26:39 +01:00

602 lines
17 KiB
Python

# coding=utf-8
import re
__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 yaml
import time
import logging
import octoprint.util as util
import octoprint.util.gcodeInterpreter as gcodeInterpreter
from octoprint.settings import settings
from octoprint.events import eventManager, Events
from octoprint.filemanager.destinations import FileDestinations
from werkzeug.utils import secure_filename
GCODE_EXTENSIONS = ["gcode", "gco", "g"]
STL_EXTENSIONS = ["stl"]
SUPPORTED_EXTENSIONS = GCODE_EXTENSIONS + STL_EXTENSIONS
def isGcodeFileName(filename):
"""Simple helper to determine if a filename has the .gcode extension.
:param filename: :class: `str`
:returns boolean:
"""
return "." in filename and filename.rsplit(".", 1)[1].lower() in GCODE_EXTENSIONS
def isSTLFileName(filename):
"""Simple helper to determine if a filename has the .stl extension.
:param filename: :class: `str`
:returns boolean:
"""
return "." in filename and filename.rsplit(".", 1)[1].lower() in STL_EXTENSIONS
def genGcodeFileName(filename):
if not filename:
return None
name, ext = filename.rsplit(".", 1)
return name + ".gcode"
def genStlFileName(filename):
if not filename:
return None
name, ext = filename.rsplit(".", 1)
return name + ".stl"
class GcodeManager:
def __init__(self):
self._logger = logging.getLogger(__name__)
self._settings = settings()
self._uploadFolder = self._settings.getBaseFolder("uploads")
self._callbacks = []
self._metadata = {}
self._metadataDirty = False
self._metadataFile = os.path.join(self._uploadFolder, "metadata.yaml")
self._metadataTempFile = os.path.join(self._uploadFolder, "metadata.yaml.tmp")
self._metadataFileAccessMutex = threading.Lock()
self._metadataAnalyzer = MetadataAnalyzer(getPathCallback=self.getAbsolutePath, loadedCallback=self._onMetadataAnalysisFinished)
self._loadMetadata(migrate=True)
self._processAnalysisBacklog()
def _processAnalysisBacklog(self):
for osFile in os.listdir(self._uploadFolder):
filename = self._getBasicFilename(osFile)
if not isGcodeFileName(filename):
continue
absolutePath = self.getAbsolutePath(filename)
if absolutePath is None:
continue
fileData = self.getFileData(filename)
if fileData is not None and "gcodeAnalysis" in fileData.keys():
continue
self._metadataAnalyzer.addFileToBacklog(filename)
def _onMetadataAnalysisFinished(self, 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"] = gcode.totalMoveTimeMinute * 60
dirty = True
if gcode.extrusionAmount:
analysisResult["filament"] = {}
for i in range(len(gcode.extrusionAmount)):
analysisResult["filament"]["tool%d" % i] = {
"length": gcode.extrusionAmount[i],
"volume": gcode.extrusionVolume[i]
}
dirty = True
if dirty:
metadata = self.getFileMetadata(basename)
metadata["gcodeAnalysis"] = analysisResult
self._metadata[basename] = metadata
self._metadataDirty = True
self._saveMetadata()
eventManager().fire(Events.METADATA_ANALYSIS_FINISHED, {"file": basename, "result": analysisResult})
def _loadMetadata(self, migrate=False):
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)
if self._metadata is None:
self._metadata = {}
# TODO: Remove in a couple of versions (2013-12-21)
if migrate:
self._migrateMetadata()
def _migrateMetadata(self):
self._logger.info("Migrating metadata if necessary...")
printTimeRe = re.compile("(\d+):(\d{2}):(\d{2})")
filamentRe = re.compile("(\d*\.\d+)m(\s/\s(\d*\.\d+)cm.)?")
hoursToSeconds = 60 * 60
minutesToSeconds = 60
updateCount = 0
for metadata in self._metadata.values():
if not "gcodeAnalysis" in metadata:
continue
updated = False
if "estimatedPrintTime" in metadata["gcodeAnalysis"]:
estimatedPrintTime = metadata["gcodeAnalysis"]["estimatedPrintTime"]
if isinstance(estimatedPrintTime, (str, unicode)):
match = re.match(printTimeRe, estimatedPrintTime)
if match:
metadata["gcodeAnalysis"]["estimatedPrintTime"] = int(match.group(1)) * hoursToSeconds + int(match.group(2)) * minutesToSeconds + int(match.group(3))
self._metadataDirty = True
updated = True
if "filament" in metadata["gcodeAnalysis"]:
filament = metadata["gcodeAnalysis"]["filament"]
if isinstance(filament, (str, unicode)):
match = re.match(filamentRe, filament)
if match:
metadata["gcodeAnalysis"]["filament"] = {
"tool0": {
"length": int(float(match.group(1)) * 1000)
}
}
if match.group(3) is not None:
metadata["gcodeAnalysis"]["filament"]["tool0"].update({
"volume": float(match.group(3))
})
self._metadataDirty = True
updated = True
elif isinstance(filament, dict) and ("length" in filament.keys() or "volume" in filament.keys()):
metadata["gcodeAnalysis"]["filament"] = {
"tool0": {}
}
if "length" in filament.keys():
metadata["gcodeAnalysis"]["filament"]["tool0"].update({
"length": filament["length"]
})
if "volume" in filament.keys():
metadata["gcodeAnalysis"]["filament"]["tool0"].update({
"volume": filament["volume"]
})
self._metadataDirty = True
updated = True
if updated:
updateCount += 1
self._saveMetadata()
self._logger.info("Updated %d sets of metadata to new format" % updateCount)
def _saveMetadata(self, force=False):
if not self._metadataDirty and not force:
return
with self._metadataFileAccessMutex:
with open(self._metadataTempFile, "wb") as f:
yaml.safe_dump(self._metadata, f, default_flow_style=False, indent=" ", allow_unicode=True)
self._metadataDirty = False
util.safeRename(self._metadataTempFile, self._metadataFile)
self._loadMetadata()
def _getBasicFilename(self, filename):
if filename.startswith(self._uploadFolder):
return filename[len(self._uploadFolder + os.path.sep):]
else:
return filename
#~~ callback handling
def registerCallback(self, callback):
self._callbacks.append(callback)
def unregisterCallback(self, callback):
if callback in self._callbacks:
self._callbacks.remove(callback)
def _sendUpdateTrigger(self, type):
for callback in self._callbacks:
try: callback.sendEvent(type)
except: pass
#~~ file handling
def addFile(self, file, destination, uploadCallback=None):
"""
Adds the given file for the given destination to the systems. Takes care of slicing if enabled and
necessary.
If the file's processing won't be finished directly with the return from this method but happen
asynchronously in the background (e.g. due to slicing), returns a tuple containing the just added file's
filename and False. Otherwise returns a tuple (filename, True).
"""
if not file or not destination:
return None, True
curaEnabled = self._settings.getBoolean(["cura", "enabled"])
filename = file.filename
absolutePath = self.getAbsolutePath(filename, mustExist=False)
gcode = isGcodeFileName(filename)
if absolutePath is None or (not curaEnabled and not gcode):
return None, True
file.save(absolutePath)
if gcode:
return self.processGcode(absolutePath, destination, uploadCallback), True
else:
if curaEnabled and isSTLFileName(filename):
return self.processStl(absolutePath, destination, uploadCallback), False
else:
return filename, False
def getFutureFileName(self, file):
if not file:
return None
absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
if absolutePath is None:
return None
return self._getBasicFilename(absolutePath)
def processStl(self, absolutePath, destination, uploadCallback=None):
from octoprint.slicers.cura import CuraFactory
cura = CuraFactory.create_slicer()
gcodePath = genGcodeFileName(absolutePath)
config = self._settings.get(["cura", "config"])
slicingStart = time.time()
def stlProcessed(stlPath, gcodePath, error=None):
if error:
eventManager().fire(Events.SLICING_FAILED, {"stl": self._getBasicFilename(stlPath), "gcode": self._getBasicFilename(gcodePath), "reason": error})
if os.path.exists(stlPath):
os.remove(stlPath)
else:
slicingStop = time.time()
eventManager().fire(Events.SLICING_DONE, {"stl": self._getBasicFilename(stlPath), "gcode": self._getBasicFilename(gcodePath), "time": slicingStop - slicingStart})
self.processGcode(gcodePath, destination, uploadCallback)
eventManager().fire(Events.SLICING_STARTED, {"stl": self._getBasicFilename(absolutePath), "gcode": self._getBasicFilename(gcodePath)})
cura.process_file(config, gcodePath, absolutePath, stlProcessed, [absolutePath, gcodePath])
return self._getBasicFilename(gcodePath)
def processGcode(self, absolutePath, destination, uploadCallback=None):
if absolutePath is None:
return None
filename = self._getBasicFilename(absolutePath)
if filename in self._metadata.keys():
# delete existing metadata entry, since the file is going to get overwritten
del self._metadata[filename]
self._metadataDirty = True
self._saveMetadata()
self._metadataAnalyzer.addFileToQueue(os.path.basename(absolutePath))
if uploadCallback is not None:
return uploadCallback(filename, absolutePath, destination)
else:
return filename
def getFutureFilename(self, file):
if not file:
return None
absolutePath = self.getAbsolutePath(file.filename, mustExist=False)
if absolutePath is None:
return None
return self._getBasicFilename(absolutePath)
def removeFile(self, filename):
filename = self._getBasicFilename(filename)
absolutePath = self.getAbsolutePath(filename)
stlPath = genStlFileName(absolutePath)
if absolutePath is None:
return
os.remove(absolutePath)
if os.path.exists(stlPath):
os.remove(stlPath)
if filename in self._metadata.keys():
del self._metadata[filename]
self._metadataDirty = True
self._saveMetadata()
def getAbsolutePath(self, filename, mustExist=True):
"""
Returns the absolute path of the given filename in the correct upload folder.
Ensures that the file
<ul>
<li>has any of the extensions listed in SUPPORTED_EXTENSIONS</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.lower(), set(SUPPORTED_EXTENSIONS)):
return None
# TODO: detect which type of file and add in the extra folder portion
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 getAllFilenames(self):
return map(lambda x: x["name"], self.getAllFileData())
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):
if not filename:
return
filename = self._getBasicFilename(filename)
# TODO: Make this more robust when STLs will be viewable from the client
if isSTLFileName(filename):
return
absolutePath = self.getAbsolutePath(filename)
if absolutePath is None:
return None
statResult = os.stat(absolutePath)
fileData = {
"name": filename,
"size": statResult.st_size,
"origin": FileDestinations.LOCAL,
"date": int(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]
last = None
if "last" in val and val["last"] is not None:
last = {
"date": val["last"]["date"],
"success": val["last"]["success"]
}
prints = {
"success": val["success"],
"failure": val["failure"],
"last": last
}
fileData["prints"] = prints
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
#~~ print job data
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()
def changeLastPrintSuccess(self, filename, succeeded):
filename = self._getBasicFilename(filename)
absolutePath = self.getAbsolutePath(filename)
if absolutePath is None:
return
metadata = self.getFileMetadata(filename)
if metadata is None:
return
if "prints" in metadata.keys():
if "last" in metadata.keys() and metadata["prints"]["last"] is not None:
currentSucceeded = metadata["prints"]["last"]["success"]
if currentSucceeded != succeeded:
metadata["prints"]["last"]["success"] = succeeded
if currentSucceeded:
# last print job was counted as success but actually failed
metadata["prints"]["success"] -= 1
metadata["prints"]["failure"] += 1
else:
# last print job was counted as a failure but actually succeeded
metadata["prints"]["success"] += 1
metadata["prints"]["failure"] -= 1
self.setFileMetadata(filename, metadata)
self._saveMetadata()
#~~ analysis control
def pauseAnalysis(self):
self._metadataAnalyzer.pause()
def resumeAnalysis(self):
self._metadataAnalyzer.resume()
class MetadataAnalyzer:
def __init__(self, getPathCallback, loadedCallback):
self._logger = logging.getLogger(__name__)
self._getPathCallback = getPathCallback
self._loadedCallback = loadedCallback
self._active = threading.Event()
self._active.set()
self._currentFile = None
self._currentProgress = None
self._queue = Queue.PriorityQueue()
self._gcode = None
self._worker = threading.Thread(target=self._work)
self._worker.daemon = True
self._worker.start()
def addFileToQueue(self, filename):
self._logger.debug("Adding file %s to analysis queue (high priority)" % filename)
self._queue.put((0, filename))
def addFileToBacklog(self, filename):
self._logger.debug("Adding file %s to analysis backlog (low priority)" % filename)
self._queue.put((100, 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._logger.debug("Pausing Gcode analyzer")
self._active.clear()
if self._gcode is not None:
self._logger.debug("Aborting running analysis, will restart when Gcode analyzer is resumed")
self._gcode.abort()
def resume(self):
self._logger.debug("Resuming Gcode analyzer")
self._active.set()
def _work(self):
aborted = None
while True:
if aborted is not None:
filename = aborted
aborted = None
self._logger.debug("Got an aborted analysis job for file %s, processing this instead of first item in queue" % filename)
else:
(priority, filename) = self._queue.get()
self._logger.debug("Processing file %s from queue (priority %d)" % (filename, priority))
self._active.wait()
try:
self._analyzeGcode(filename)
self._queue.task_done()
except gcodeInterpreter.AnalysisAborted:
aborted = filename
self._logger.debug("Running analysis of file %s aborted" % filename)
def _analyzeGcode(self, filename):
path = self._getPathCallback(filename)
if path is None or not os.path.exists(path):
return
self._currentFile = filename
self._currentProgress = 0
try:
self._logger.debug("Starting analysis of file %s" % filename)
eventManager().fire(Events.METADATA_ANALYSIS_STARTED, {"file": filename})
self._gcode = gcodeInterpreter.gcode()
self._gcode.progressCallback = self._onParsingProgress
self._gcode.load(path)
self._logger.debug("Analysis of file %s finished, notifying callback" % filename)
self._loadedCallback(self._currentFile, self._gcode)
finally:
self._gcode = None
self._currentProgress = None
self._currentFile = None
def _onParsingProgress(self, progress):
self._currentProgress = progress