Cleanup duration estimation

Estimation duration now uses the already parsed GCode instead of reparsing it.
It also computes a per layer duration estimation which can probably be used to
produce better ETAs.

The only difference is that it does not compute duration for Z layers changes,                                                                                                                        but it was probably super wrong already given how it is done (it only changes
the estimation by than 2s over 2 hours of print and 54 layers, and the feedrate
stuff mixes all the axes together).

I also detected a few potential issues in the code, which are marked by FIXMEs.
master
Guillaume Seguin 2013-05-15 21:49:12 +02:00
parent 9816ea7c8a
commit ea604380db
1 changed files with 70 additions and 84 deletions

134
gcoder.py
View File

@ -19,15 +19,6 @@ import re
import math import math
import datetime import datetime
def get_coordinate_value(axis, parts):
for i in parts:
if (axis in i):
return float(i[1:])
return None
def hypot3d(X1, Y1, Z1, X2 = 0.0, Y2 = 0.0, Z2 = 0.0):
return math.hypot(X2-X1, math.hypot(Y2-Y1, Z2-Z1))
gcode_parsed_args = ["x", "y", "e", "f", "z", "p"] gcode_parsed_args = ["x", "y", "e", "f", "z", "p"]
class Line(object): class Line(object):
@ -47,6 +38,8 @@ class Line(object):
command = None command = None
is_move = False is_move = False
duration = None
def __init__(self, l): def __init__(self, l):
self.raw = l.lower() self.raw = l.lower()
if ";" in self.raw: if ";" in self.raw:
@ -67,14 +60,17 @@ class Line(object):
if code in gcode_parsed_args and len(bit) > 1: if code in gcode_parsed_args and len(bit) > 1:
setattr(self, code, float(bit[1:])) setattr(self, code, float(bit[1:]))
def __str__(self): def __repr__(self):
return self.raw return self.raw.upper()
class Layer(object): class Layer(object):
lines = None
duration = None
def __init__(self, lines): def __init__(self, lines):
self.lines = lines self.lines = lines
def measure(self): def measure(self):
xmin = float("inf") xmin = float("inf")
ymin = float("inf") ymin = float("inf")
@ -122,8 +118,11 @@ class Layer(object):
return (xmin, xmax), (ymin, ymax), (zmin, zmax) return (xmin, xmax), (ymin, ymax), (zmin, zmax)
class GCode(object): class GCode(object):
lines = None
layers = None
def __init__(self,data): def __init__(self,data):
self.lines = [Line(l2) for l2 in self.lines = [Line(l2) for l2 in
(l.strip() for l in data) (l.strip() for l in data)
@ -132,7 +131,7 @@ class GCode(object):
self._create_layers() self._create_layers()
def _preprocess(self): def _preprocess(self):
#checks for G20, G21, G90 and G91, sets imperial and relative flags """Checks for G20, G21, G90 and G91, sets imperial and relative flags"""
imperial = False imperial = False
relative = False relative = False
relative_e = False relative_e = False
@ -156,15 +155,13 @@ class GCode(object):
line.relative_e = relative_e line.relative_e = relative_e
line.parse_coordinates(imperial) line.parse_coordinates(imperial)
# FIXME : looks like this needs to be tested with list Z on move
def _create_layers(self): def _create_layers(self):
self.layers = [] layers = {}
prev_z = None prev_z = None
cur_z = 0 cur_z = 0
cur_lines = [] cur_lines = []
layer_index = []
temp_layers = {}
for line in self.lines: for line in self.lines:
if line.command == "G92" and line.z != None: if line.command == "G92" and line.z != None:
cur_z = line.z cur_z = line.z
@ -176,38 +173,33 @@ class GCode(object):
cur_z = line.z cur_z = line.z
if cur_z != prev_z: if cur_z != prev_z:
old_lines = temp_layers.pop(prev_z,[]) old_lines = layers.get(prev_z, [])
old_lines += cur_lines old_lines += cur_lines
temp_layers[prev_z] = old_lines layers[prev_z] = old_lines
if not prev_z in layer_index:
layer_index.append(prev_z)
cur_lines = [] cur_lines = []
cur_lines.append(line) cur_lines.append(line)
prev_z = cur_z prev_z = cur_z
old_lines = layers.pop(prev_z, [])
old_lines = temp_layers.pop(prev_z,[])
old_lines += cur_lines old_lines += cur_lines
temp_layers[prev_z] = old_lines layers[prev_z] = old_lines
if not prev_z in layer_index: for idx in layers.keys():
layer_index.append(prev_z) cur_lines = layers[idx]
layer_index.sort()
for idx in layer_index:
cur_lines = temp_layers[idx]
has_movement = False has_movement = False
for l in cur_lines: for l in layers[idx]:
if l.is_move and l.e != None: if l.is_move and l.e != None:
has_movement = True has_movement = True
break break
if idx > 15:
print idx, has_movement, cur_lines
if has_movement: if has_movement:
self.layers.append(Layer(cur_lines)) layers[idx] = Layer(layers[idx])
else:
del layers[idx]
self.layers = layers
def num_layers(self): def num_layers(self):
return len(self.layers) return len(self.layers)
@ -220,7 +212,7 @@ class GCode(object):
ymax = float("-inf") ymax = float("-inf")
zmax = float("-inf") zmax = float("-inf")
for l in self.layers: for l in self.layers.values():
(xm, xM), (ym, yM), (zm, zM) = l.measure() (xm, xM), (ym, yM), (zm, zM) = l.measure()
xmin = min(xm, xmin) xmin = min(xm, xmin)
xmax = max(xM, xmax) xmax = max(xM, xmax)
@ -257,7 +249,7 @@ class GCode(object):
return total_e return total_e
def estimate_duration(self, g): def estimate_duration(self):
lastx = lasty = lastz = laste = lastf = 0.0 lastx = lasty = lastz = laste = lastf = 0.0
x = y = z = e = f = 0.0 x = y = z = e = f = 0.0
currenttravel = 0.0 currenttravel = 0.0
@ -272,58 +264,53 @@ class GCode(object):
# get device caps from firmware: max speed, acceleration/axis (including extruder) # get device caps from firmware: max speed, acceleration/axis (including extruder)
# calculate the maximum move duration accounting for above ;) # calculate the maximum move duration accounting for above ;)
# self.log(".... estimating ....") # self.log(".... estimating ....")
for i in g: zs = self.layers.keys()
i = i.split(";")[0] zs.sort()
if "G4" in i or "G1" in i: for z in zs:
if "G4" in i: layer = self.layers[z]
parts = i.split(" ") for line in layer.lines:
moveduration = get_coordinate_value("P", parts[1:]) if line.command not in ["G4", "G1"]:
if moveduration is None: continue
if line.command == "G4":
moveduration = line.p
if not moveduration:
continue continue
else: else:
moveduration /= 1000.0 moveduration /= 1000.0
if "G1" in i: elif line.command == "G1":
parts = i.split(" ") x = line.x if line.x != None else lastx
x = get_coordinate_value("X", parts[1:]) y = line.y if line.y != None else lasty
if x is None: x = lastx e = line.e if line.e != None else laste
y = get_coordinate_value("Y", parts[1:]) f = line.f / 60.0 if line.f != None else lastf # mm/s vs mm/m
if y is None: y = lasty
z = get_coordinate_value("Z", parts[1:])
if (z is None) or (z<lastz): z = lastz # Do not increment z if it's below the previous (Lift z on move fix)
e = get_coordinate_value("E", parts[1:])
if e is None: e = laste
f = get_coordinate_value("F", parts[1:])
if f is None: f = lastf
else: f /= 60.0 # mm/s vs mm/m
# given last feedrate and current feedrate calculate the distance needed to achieve current feedrate. # given last feedrate and current feedrate calculate the distance needed to achieve current feedrate.
# if travel is longer than req'd distance, then subtract distance to achieve full speed, and add the time it took to get there. # if travel is longer than req'd distance, then subtract distance to achieve full speed, and add the time it took to get there.
# then calculate the time taken to complete the remaining distance # then calculate the time taken to complete the remaining distance
currenttravel = hypot3d(x, y, z, lastx, lasty, lastz) currenttravel = math.hypot(x - lastx, y - lasty)
distance = abs(2* ((lastf+f) * (f-lastf) * 0.5 ) / acceleration) #2x because we have to accelerate and decelerate # FIXME: review this better
if distance <= currenttravel and ( lastf + f )!=0 and f!=0: # this looks wrong : there's little chance that the feedrate we'll decelerate to is the previous feedrate
# shouldn't we instead look at three consecutive moves ?
distance = 2 * abs(((lastf + f) * (f - lastf) * 0.5) / acceleration) # multiply by 2 because we have to accelerate and decelerate
if distance <= currenttravel and lastf + f != 0 and f != 0:
# Unsure about this formula -- iXce reviewing this code
moveduration = 2 * distance / (lastf + f) moveduration = 2 * distance / (lastf + f)
currenttravel -= distance currenttravel -= distance
moveduration += currenttravel/f moveduration += currenttravel/f
else: else:
moveduration = math.sqrt( 2 * distance / acceleration ) moveduration = math.sqrt(2 * distance / acceleration) # probably buggy : not taking actual travel into account
totalduration += moveduration totalduration += moveduration
if z > lastz:
layercount +=1
#self.log("layer z: ", lastz, " will take: ", time.strftime('%H:%M:%S', time.gmtime(totalduration-layerbeginduration)))
layerbeginduration = totalduration
lastx = x lastx = x
lasty = y lasty = y
lastz = z
laste = e laste = e
lastf = f lastf = f
#self.log("Total Duration: " #, time.strftime('%H:%M:%S', time.gmtime(totalduration))) layer.duration = totalduration - layerbeginduration
return "{0:d} layers, ".format(int(layercount)) + str(datetime.timedelta(seconds = int(totalduration))) layerbeginduration = totalduration
return "%d layers, %s" % (len(self.layers), str(datetime.timedelta(seconds = int(totalduration))))
def main(): def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
@ -332,8 +319,7 @@ def main():
# d = [i.replace("\n","") for i in open(sys.argv[1])] # d = [i.replace("\n","") for i in open(sys.argv[1])]
# gcode = GCode(d) # gcode = GCode(d)
d = list(open(sys.argv[1])) gcode = GCode(open(sys.argv[1]))
gcode = GCode(d)
gcode.measure() gcode.measure()
@ -343,7 +329,7 @@ def main():
print "\tZ: %0.02f - %0.02f (%0.02f)" % (gcode.zmin,gcode.zmax,gcode.height) print "\tZ: %0.02f - %0.02f (%0.02f)" % (gcode.zmin,gcode.zmax,gcode.height)
print "Filament used: %0.02fmm" % gcode.filament_length() print "Filament used: %0.02fmm" % gcode.filament_length()
print "Number of layers: %d" % gcode.num_layers() print "Number of layers: %d" % gcode.num_layers()
print "Estimated duration (pessimistic): ", gcode.estimate_duration(d) print "Estimated duration (pessimistic): %s" % gcode.estimate_duration()
if __name__ == '__main__': if __name__ == '__main__':