Perform gcode analysis in subprocess

This commit is contained in:
Gina Häußge 2017-03-08 13:36:37 +01:00
parent 5e0b53b651
commit 639d8d4c5f
2 changed files with 191 additions and 45 deletions

View file

@ -305,39 +305,95 @@ class GcodeAnalysisQueue(AbstractAnalysisQueue):
* The extruded volume in cm³
"""
def _do_analysis(self, high_priority=False):
try:
throttle = settings().getFloat(["gcodeAnalysis", "throttle_highprio"]) if high_priority else settings().getFloat(["gcodeAnalysis", "throttle_normalprio"])
throttle_lines = settings().getInt(["gcodeAnalysis", "throttle_lines"])
if throttle > 0:
def throttle_callback(filePos, readBytes):
if filePos % throttle_lines == 0:
# only apply throttle every 100 lines
time.sleep(throttle)
else:
throttle_callback = None
def __init__(self, finished_callback):
AbstractAnalysisQueue.__init__(self, finished_callback)
self._gcode = gcodeInterpreter.gcode()
self._gcode.load(self._current.absolute_path, self._current.printer_profile, throttle=throttle_callback)
self._aborted = False
self._reenqueue = False
def _do_analysis(self, high_priority=False):
import sarge
import sys
import yaml
try:
throttle = settings().getFloat(["gcodeAnalysis", "throttle_highprio"]) if high_priority \
else settings().getFloat(["gcodeAnalysis", "throttle_normalprio"])
throttle_lines = settings().getInt(["gcodeAnalysis", "throttle_lines"])
max_extruders = settings().getInt(["gcodeAnalysis", "maxExtruders"])
g90_extruder = settings().getBoolean(["feature", "g90InfluencesExtruder"])
speedx = self._current.printer_profile["axes"]["x"]["speed"]
speedy = self._current.printer_profile["axes"]["y"]["speed"]
offsets = self._current.printer_profile["extruder"]["offsets"]
interpreter = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "util", "gcodeInterpreter.py"))
command = [sys.executable, interpreter, "--speed-x={}".format(speedx), "--speed-y={}".format(speedy),
"--max-t={}".format(max_extruders), "--throttle={}".format(throttle),
"--throttle-lines={}".format(throttle_lines)]
for offset in offsets[1:]:
command += ["--offset", offset[0], offset[1]]
if g90_extruder:
command += ["--g90-extruder"]
command.append(self._current.absolute_path)
self._logger.info("Invoking analysis command: {}".format(" ".join(command)))
self._aborted = False
p = sarge.run(command, async=True, stdout=sarge.Capture())
while len(p.commands) == 0:
# somewhat ugly... we can't use wait_events because
# the events might not be all set if an exception
# by sarge is triggered within the async process
# thread
time.sleep(0.01)
# by now we should have a command, let's wait for its
# process to have been prepared
p.commands[0].process_ready.wait()
if not p.commands[0].process:
# the process might have been set to None in case of any exception
raise RuntimeError(u"Error while trying to run command {}".format(" ".join(command)))
try:
# let's wait for stuff to finish
while p.returncode is None:
if self._aborted:
# oh, we shall abort, let's do so!
p.commands[0].terminate()
raise AnalysisAborted(reenqueue=self._reenqueue)
# else continue
p.commands[0].poll()
finally:
p.close()
output = p.stdout.text
self._logger.debug("Got output: {!r}".format(output))
if not "RESULTS:" in output:
raise RuntimeError("No analysis result found")
_, output = output.split("RESULTS:")
analysis = yaml.safe_load(output)
result = dict()
result["printingArea"] = self._gcode.printing_area
result["dimensions"] = self._gcode.dimensions
if self._gcode.totalMoveTimeMinute:
result["estimatedPrintTime"] = self._gcode.totalMoveTimeMinute * 60
if self._gcode.extrusionAmount:
result["printingArea"] = analysis["printing_area"]
result["dimensions"] = analysis["dimensions"]
if analysis["total_time"]:
result["estimatedPrintTime"] = analysis["total_time"] * 60
if analysis["extrusion_length"]:
result["filament"] = dict()
for i in range(len(self._gcode.extrusionAmount)):
for i in range(len(analysis["extrusion_length"])):
result["filament"]["tool%d" % i] = {
"length": self._gcode.extrusionAmount[i],
"volume": self._gcode.extrusionVolume[i]
"length": analysis["extrusion_length"][i],
"volume": analysis["extrusion_volume"][i]
}
return result
except gcodeInterpreter.AnalysisAborted as ex:
raise AnalysisAborted(reenqueue=ex.reenqueue)
finally:
self._gcode = None
def _do_abort(self, reenqueue=True):
if self._gcode:
self._gcode.abort(reenqueue=reenqueue)
self._aborted = True
self._reenqueue = reenqueue

View file

@ -10,8 +10,7 @@ import os
import base64
import zlib
import logging
from octoprint.settings import settings
import codecs
class Vector3D(object):
@ -178,18 +177,18 @@ class AnalysisAborted(Exception):
class gcode(object):
def __init__(self):
def __init__(self, progress_callback=None):
self._logger = logging.getLogger(__name__)
self.layerList = None
self.extrusionAmount = [0]
self.extrusionVolume = [0]
self.totalMoveTimeMinute = 0
self.filename = None
self.progressCallback = None
self._abort = False
self._reenqueue = True
self._filamentDiameter = 0
self._minMax = MinMax3D()
self._progress_callback = progress_callback
@property
def dimensions(self):
@ -207,20 +206,19 @@ class gcode(object):
maxY=self._minMax.max.y,
maxZ=self._minMax.max.z)
def load(self, filename, printer_profile, throttle=None):
def load(self, filename, throttle=None, speedx=6000, speedy=6000, offsets=None, max_extruders=10, g90_extruder=False):
if os.path.isfile(filename):
self.filename = filename
self._fileSize = os.stat(filename).st_size
import codecs
with codecs.open(filename, encoding="utf-8", errors="replace") as f:
self._load(f, printer_profile, throttle=throttle)
self._load(f, throttle=throttle, speedx=speedx, speedy=speedy, offsets=offsets, max_extruders=max_extruders, g90_extruder=g90_extruder)
def abort(self, reenqueue=True):
self._abort = True
self._reenqueue = reenqueue
def _load(self, gcodeFile, printer_profile, throttle=None):
def _load(self, gcodeFile, throttle=None, speedx=6000, speedy=6000, offsets=None, max_extruders=10, g90_extruder=False):
lineNo = 0
readBytes = 0
pos = Vector3D(0.0, 0.0, 0.0)
@ -236,21 +234,23 @@ class gcode(object):
fwretractTime = 0
fwretractDist = 0
fwrecoverTime = 0
feedrate = min(printer_profile["axes"]["x"]["speed"], printer_profile["axes"]["y"]["speed"])
feedrate = min(speedx, speedy)
if feedrate == 0:
# some somewhat sane default if axes speeds are insane...
feedrate = 2000
offsets = printer_profile["extruder"]["offsets"]
g90InfluencesExtruder = settings().getBoolean(["feature", "g90InfluencesExtruder"])
if offsets is None or not isinstance(offsets, (list, tuple)):
offsets = []
if len(offsets) < max_extruders:
offsets += [(0, 0)] * (max_extruders - len(offsets))
for line in gcodeFile:
if self._abort:
raise AnalysisAborted(reenqueue=self._reenqueue)
lineNo += 1
readBytes += len(line)
readBytes += len(line.encode("utf-8"))
if isinstance(gcodeFile, (file)):
if isinstance(gcodeFile, (file, codecs.StreamReaderWriter)):
percentage = float(readBytes) / float(self._fileSize)
elif isinstance(gcodeFile, (list)):
percentage = float(lineNo) / float(len(gcodeFile))
@ -258,8 +258,8 @@ class gcode(object):
percentage = None
try:
if self.progressCallback is not None and (lineNo % 1000 == 0) and percentage is not None:
self.progressCallback(percentage)
if self._progress_callback is not None and (lineNo % 1000 == 0) and percentage is not None:
self._progress_callback(percentage)
except:
pass
@ -395,11 +395,11 @@ class gcode(object):
pos.z = center.z
elif G == 90: #Absolute position
relativeMode = False
if g90InfluencesExtruder:
if g90_extruder:
relativeE = False
elif G == 91: #Relative position
relativeMode = True
if g90InfluencesExtruder:
if g90_extruder:
relativeE = True
elif G == 92:
x = getCodeFloat(line, 'X')
@ -440,7 +440,7 @@ class gcode(object):
fwrecoverTime = (fwretractDist + s) / f
elif T is not None:
if T > settings().getInt(["gcodeAnalysis", "maxExtruders"]):
if T > max_extruders:
self._logger.warn("GCODE tried to select tool %d, that looks wrong, ignoring for GCODE analysis" % T)
else:
toolOffset.x -= offsets[currentExtruder][0] if currentExtruder < len(offsets) else 0
@ -463,8 +463,8 @@ class gcode(object):
if throttle is not None:
throttle(lineNo, readBytes)
if self.progressCallback is not None:
self.progressCallback(100.0)
if self._progress_callback is not None:
self._progress_callback(100.0)
self.extrusionAmount = maxExtrusion
self.extrusionVolume = [0] * len(maxExtrusion)
@ -476,6 +476,13 @@ class gcode(object):
def _parseCuraProfileString(self, comment, prefix):
return {key: value for (key, value) in map(lambda x: x.split("=", 1), zlib.decompress(base64.b64decode(comment[len(prefix):])).split("\b"))}
def get_result(self):
return dict(total_time=self.totalMoveTimeMinute,
extrusion_length=self.extrusionAmount,
extrusion_volume=self.extrusionVolume,
dimensions=self.dimensions,
printing_area=self.printing_area)
def getCodeInt(line, code):
n = line.find(code) + 1
if n < 1:
@ -503,3 +510,86 @@ def getCodeFloat(line, code):
return val if not (math.isnan(val) or math.isinf(val)) else None
except:
return None
if __name__ == "__main__":
import click
import time
import yaml
import sys
@click.command()
@click.option("--throttle", "throttle", type=float, default=None)
@click.option("--throttle-lines", "throttle_lines", type=int, default=None)
@click.option("--speed-x", "speedx", type=float, default=6000)
@click.option("--speed-y", "speedy", type=float, default=6000)
@click.option("--speed-z", "speedz", type=float, default=300)
@click.option("--offset", "offset", type=(float, float), multiple=True)
@click.option("--max-t", "maxt", type=int, default=10)
@click.option("--g90-extruder", "g90_extruder", is_flag=True)
@click.option("--progress", "progress", is_flag=True)
@click.argument("path", type=click.Path())
def main(path, speedx, speedy, speedz, offset, maxt, throttle, throttle_lines, g90_extruder, progress):
throttle_callback = None
if throttle:
def throttle_callback(filePos, readBytes):
if filePos % throttle_lines == 0:
# only apply throttle every $throttle_lines lines
time.sleep(throttle)
offsets = offset
if offsets is None:
offsets = []
elif isinstance(offset, tuple):
offsets = list(offsets)
offsets = [(0, 0)] + offsets
if len(offsets) < maxt:
offsets += [(0, 0)] * (maxt - len(offsets))
start_time = time.time()
progress_callback = None
if progress:
def progress_callback(percentage):
print("PROGRESS:{}".format(percentage))
interpreter = gcode(progress_callback=progress_callback)
interpreter.load(path,
speedx=speedx,
speedy=speedy,
offsets=offsets,
throttle=throttle_callback,
max_extruders=maxt,
g90_extruder=g90_extruder)
print("DONE:{}s".format(time.time() - start_time))
print("RESULTS:")
print(yaml.safe_dump(interpreter.get_result(), default_flow_style=False, indent=" ", allow_unicode=True))
# os args are gained differently on win32
try:
from click.utils import get_os_args
args = get_os_args()
except ImportError:
# for whatever reason we are running an older Click version?
args = sys.argv[1:]
if len(args) >= len(sys.argv):
# Now some ugly preprocessing of our arguments starts. We have a somewhat difficult situation on our hands
# here if we are running under Windows and want to be able to handle utf-8 command line parameters (think
# plugin parameters such as names or something, e.g. for the "dev plugin:new" command) while at the same
# time also supporting sys.argv rewriting for debuggers etc (e.g. PyCharm).
#
# So what we try to do here is solve this... Generally speaking, sys.argv and whatever Windows returns
# for its CommandLineToArgvW win32 function should have the same length. If it doesn't however and
# sys.argv is shorter than the win32 specific command line arguments, obviously stuff was cut off from
# sys.argv which also needs to be cut off of the win32 command line arguments.
#
# So this is what we do here.
# -1 because first entry is the script that was called
sys_args_length = len(sys.argv) - 1
# cut off stuff from the beginning
args = args[-1 * sys_args_length:] if sys_args_length else []
main(args=args, prog_name="octoprint-gcode-analysis")