Perform gcode analysis in subprocess
This commit is contained in:
parent
5e0b53b651
commit
639d8d4c5f
2 changed files with 191 additions and 45 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue