diff --git a/README.md b/README.md index 45e909a..53c68bf 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,15 @@ This module automatically performs movements on the x, y, x diagonal, and y diagonal axes, and measures your steppers missed steps at various accelerations/velocities. With the default configuration, this may take *awhile* (up to an hour). Most of the testing time is waiting for your printer to home. -On my printer with default settings (except MAX_MISSED), it takes 1398.34s for acceleration, 1201.21s for velocity, and 331.38s for the validation test. +On my printer with default settings (except MAX_MISSED), it takes ~23 minutes for acceleration, ~20 minutes for velocity, and 5 minutes for the validation test. **Sensorless homing**: If you're using sensorless homing `MAX_MISSED=1.0` is probably too low. The endstop variance check will tell you how many steps you lose when homing. For instance, on my printer I lose around 0-4.2 steps each home. I run `AUTO_SPEED MAX_MISSED=10.0` to account for that variance, and occasional wildly different results. -**This module is under development**, and has only been validated on CoreXY printers: You may run into issues or bugs, feel free to message me on discord, or post an issue here. -This module has two dedicated discord channels - - [DOOMCUBE User Projects](https://discord.com/channels/825469421346226226/1162192150822404106) - - [Voron VOC Projects](https://discord.com/channels/460117602945990666/1161906191623016540) +**This module is under development**, and has only been validated on CoreXY printers: You may run into issues or bugs, feel free to use the discord channel, or post an issue here. + - [Discord - DOOMCUBE User Projects](https://discord.com/channels/825469421346226226/1162192150822404106) Your printer shouldn't have any crashes due to the movement patterns used, and re-homing before/after each test, so it's safe to walk away and let it do it's thing. @@ -33,6 +31,7 @@ Your printer shouldn't have any crashes due to the movement patterns used, and r - [AUTO_SPEED_ACCEL](https://github.com/Anonoei/klipper_auto_speed#auto_speed_accel) - [AUTO_SPEED_VELOCITY](https://github.com/Anonoei/klipper_auto_speed#auto_speed_velocity) - [AUTO_SPEED_VALIDATE](https://github.com/Anonoei/klipper_auto_speed#auto_speed_validate) + - [AUTO_SPEED_GRAPH](https://github.com/Anonoei/klipper_auto_speed#auto_speed_graph) - [Console Output](https://github.com/Anonoei/klipper_auto_speed#console-output) ## Overview @@ -42,9 +41,15 @@ Your printer shouldn't have any crashes due to the movement patterns used, and r - Default usage (find max accel/velocity) - `AUTO_SPEED` - Find maximum acceleration on y axis - - `AUTO_SPEED_ACCEL X=0 Y=1 DIAG_X=0 DIAG_Y=0` + - `AUTO_SPEED_ACCEL AXIS="y"` +- Find maximum acceleration on y, then x axis + - `AUTO_SPEED_VELOCITY AXIS="y,x"` - Validate your printer's current accel/velocity - `AUTO_SPEED_VALIDATE` +- Graph your printer's max accel between v00 and v1000 + - `AUTO_SPEED_GRAPH VELOCITY_MIN=100 VELOCITY_MAX=1000` +- Graph your printer's max accel between v100 and v1000, over 10 steps + - `AUTO_SPEED_GRAPH VELOCITY_MIN=100 VELOCITY_MAX=1000 VELOCITY_DIV=10` ## How does it work? 1. Home your printer @@ -98,7 +103,9 @@ cd klipper_auto_speed 3. `cd klipper_auto_speed` 2. Link auto_speed to klipper 1. `ln -sf ~/klipper_auto_speed/auto_speed.py ~/klipper/klippy/extras/auto_speed.py` -3. Restart klipper +3. Install matplotlib + 1. `~/klippy-env/bin/python -m pip install matplotlib` +4. Restart klipper 1. `sudo systemctl restart klipper` ### Configuration @@ -109,10 +116,7 @@ Place this in your printer.cfg The values listed below are the defaults Auto Speed uses. You can include them if you wish to change their values or run into issues. ``` [auto_speed] -x: False ; By default, check x axis -y: False ; By default, check y axis -diag_x: True ; By default, check x diagonal -diag_y: True ; By default, check y diagonal +axis: diag_x, diag_y ; One or multiple of `x`, `y`, `diag_x`, `diag_y` z: Unset ; Z position to run Auto Speed, defaults to 10% of z axis length margin: 20 ; How far away from your axis maximums to perform the test movement @@ -122,9 +126,6 @@ settling_home: True ; Perform settling home before starting Auto Speed max_missed: 1.0 ; Maximum full steps that can be missed endstop_samples: 3 ; How many endstop samples to take for endstop variance -test_attempts: 2 ; Re-test this many times if test fails -stress_iterations: 50 ; While finding final maximums, perform the test movement this many times - accel_min: 1000.0 ; Minimum acceleration test may try accel_max: 50000.0 ; Maximum acceleration test may try accel_dist: 10.0 ; Distance to move when testing, if 0, use total axis - margin @@ -138,19 +139,22 @@ velocity_ittr: 1 ; How many iterations of the test to perform velocity_accu: 50.0 ; Keep binary searching until the result is this small derate: 0.8 ; Derate discovered results by this amount + +validate_margin: Unset ; Margin for VALIDATE, Defaults to margin +validate_inner_margin: 20.0 ; Margin for VALIDATE inner pattern +validate_iterations: 50 ; Perform VALIDATE pattern this many times ``` ### Macro -Auto Speed is split into 4 separate macros. The default `AUTO_SPEED` automatically calls the other three (`AUTO_SPEED_ACCEL`, `AUTO_SPEED_VELOCITY`, `AUTO_SPEED_VALIDATE`). You can use any argument from those macros when you call `AUTO_SPEED`. +Auto Speed is split into 5 separate macros. The default `AUTO_SPEED` automatically calls the other three (`AUTO_SPEED_ACCEL`, `AUTO_SPEED_VELOCITY`, `AUTO_SPEED_VALIDATE`). You can use any argument from those macros when you call `AUTO_SPEED`. + +You can also use `AUTO_SPEED_GRAPH` to find your printers velocity-to-accel relationship. #### AUTO_SPEED `AUTO_SPEED` finds maximum acceleration, velocity, and validates results at the end. Argument | Default | Description ----------------- | ------- | ----------- -X | 0 | Perform test on X axis -Y | 0 | Perform test on Y axis -DIAG_X | 1 | Perform test on x diagonal (stepper B) -DIAG_X | 1 | Perform test on y diagonal (stepper A) +AXIS | Unset | Perform test on these axes, defaults to diag_x, diag_y Z | 50 | Z position to run Auto Speed MARGIN | 20 | How far away from your axis maximums to perform the test movement SETTLING_HOME | 1 | Perform settling home before starting Auto Speed @@ -174,10 +178,7 @@ VARIANCE | 1 | Check endstop variance `AUTO_SPEED_ACCEL` find maximum acceleration Argument | Default | Description ---------- | ------- | ----------- - X | 0 | Perform test on X axis - Y | 0 | Perform test on Y axis - DIAG_X | 1 | Perform test on x diagonal (stepper B) - DIAG_X | 1 | Perform test on y diagonal (stepper A) + AXIS | Unset | Perform test on these axes, defaults to diag_x, diag_y MARGIN | 20.0 | Used when DIST is 0.0, how far away from axis to perform movements DERATE | 0.8 | How much to derate maximum values for the recommended max MAX_MISSED | 1.0 | Maximum fulls steps that can be missed @@ -191,15 +192,12 @@ VARIANCE | 1 | Check endstop variance `AUTO_SPEED_VELOCITY` finds maximum velocity Argument | Default | Description ------------- | ------- | ----------- - X | 0 | Perform test on X axis - Y | 0 | Perform test on Y axis - DIAG_X | 1 | Perform test on x diagonal (stepper B) - DIAG_X | 1 | Perform test on y diagonal (stepper A) + AXIS | Unset | Perform test on these axes, defaults to diag_x, diag_y MARGIN | 20.0 | Used when DIST is 0.0, how far away from axis to perform movements DERATE | 0.8 | How much to derate maximum values for the recommended max MAX_MISSED | 1.0 | Maximum fulls steps that can be missed - VELOCITY_MIN | 1000.0 | Minimum velocity test may try - VELOCITY_MAX | 50000.0 | Maximum velocity test may try + VELOCITY_MIN | 100.0 | Minimum velocity test may try + VELOCITY_MAX | 5000.0 | Maximum velocity test may try VELOCITY_DIST | 0.0 | Distance to move when testing, if 0, use (total axis - margin) VELOCITY_ITTR | 1 | How many iterations of the test to perform VELOCITY_ACCU | 50.0 | Keep binary searching until the result is this small @@ -216,6 +214,29 @@ VARIANCE | 1 | Check endstop variance VELOCITY | Unset | Defaults to current max velocity +#### AUTO_SPEED_GRAPH + `AUTO_SPEED_GRAPH` graphs your printer's velocity-to-accel relationship on specified axes + You must specify `VELOCITY_MIN` and `VELOCITY_MAX`. + Results are saved to `~/printer_data/config` + Argument | Default | Description + ------------- | ------- | ----------- + AXIS | Unset | Perform test on these axes, defaults to diag_x, diag_y + MARGIN | 20.0 | Used when DIST is 0.0, how far away from axis to perform movements + DERATE | 0.8 | How much to derate maximum values for the recommended max + MAX_MISSED | 1.0 | Maximum fulls steps that can be missed + VELOCITY_MIN | Unset | Minimum velocity test may try + VELOCITY_MAX | Unset | Maximum velocity test may try + VELOCITY_DIV | 5 | How many velocities to test + VELOCITY_DIST | 0.0 | Distance to move when testing, if 0, use (total axis - margin) + VELOCITY_ITTR | 1 | How many iterations of the test to perform + VELOCITY_ACCU | 0.05 | Keep binary searching until the result within this percent + ACCEL_MIN_A | 0.23 | Accel min parabola equation 'a' + ACCEL_MIN_B | -300 | Accel min parabola equation 'b' + ACCEL_MIN_C | 85000 | Accel min parabola equation 'c' - y at velocity 0 + ACCEL_MAX_A | 0.19 | Accel max parabola equation 'a' + ACCEL_MAX_B | -300 | Accel max parabola equation 'b' + ACCEL_MAX_C | 200000 | Accel max parabola equation 'c' - y at velocity 0 + ## Console Output Console output is slightly different depending on whether testing acceleration/velocity, and which axis is being tested. diff --git a/auto_speed.py b/auto_speed.py index 2472e1d..e7e1544 100644 --- a/auto_speed.py +++ b/auto_speed.py @@ -6,6 +6,10 @@ import math from time import perf_counter +def constant_factor(v, k, d=1, a=0): + c = (k*v)**(1/d) + return c + a + class AttemptWrapper: def __init__(self): self.min: float = None @@ -20,12 +24,24 @@ class AttemptWrapper: self.accel: float = None self.veloc: float = None + def __str__(self): + fmt = f"AttemptWrapper {self.axis}\n" + fmt += f"| Min: {self.min}, Max: {self.max}\n" + fmt += f"| Accuracy: {self.accuracy}, Ittr: {self.iterations}, Max Missed: {self.max_missed}\n" + fmt += f"| Travel: {self.travel}, Dist: {self.dist}, Accel: {self.accel}, Veloc: {self.veloc}\n" + return fmt + class ResultsWrapper: def __init__(self): self.name: str = "" self.duration: float = None self.vals: dict = {} + def __str__(self): + fmt = f"ResultsWrapper {self.name}, duration: {self.duration}\n" + fmt += f"| Vals: {self.vals}" + return fmt + def derate(self, derate): vList = [] newVals = {} @@ -40,11 +56,16 @@ class AutoSpeed: def __init__(self, config): self.config = config self.printer = config.get_printer() + self.gcode = self.printer.lookup_object('gcode') - self.x = config.getboolean('x', default=False) - self.y = config.getboolean('y', default=False) - self.diag_x = config.getboolean('diag_x', default=True) - self.diag_y = config.getboolean('diag_y', default=True) + self.valid_axes = ["x", "y", "diag_x", "diag_y"] + self.axes = self._parse_axis(config.get('axis', 'diag_x, diag_y')) + + self.default_axes = '' + + for axis in self.axes: + self.default_axes += f"{axis}," + self.default_axes = self.default_axes[:-1] self.z = config.getfloat('z', default=None) self.margin = config.getfloat('margin', default=20.0, above=0.0) @@ -77,7 +98,6 @@ class AutoSpeed: self.printer.register_event_handler("klippy:connect", self.handle_connect) self.printer.register_event_handler("homing:home_rails_end", self.handle_home_rails_end) - self.gcode = self.printer.lookup_object('gcode') self.gcode.register_command('AUTO_SPEED', self.cmd_AUTO_SPEED, desc=self.cmd_AUTO_SPEED_help) @@ -87,15 +107,17 @@ class AutoSpeed: self.gcode.register_command('AUTO_SPEED_ACCEL', self.cmd_AUTO_SPEED_ACCEL, desc=self.cmd_AUTO_SPEED_ACCEL_help) - self.gcode.register_command('AUTO_SPEED_VALIDATE', self.cmd_AUTO_SPEED_VALIDATE, desc=self.cmd_AUTO_SPEED_VALIDATE_help) + self.gcode.register_command('AUTO_SPEED_GRAPH', + self.cmd_AUTO_SPEED_GRAPH, + desc=self.cmd_AUTO_SPEED_GRAPH_help) self.level = None self.steppers = {} - self.axes = {} + self.axis_limits = {} def handle_connect(self): self.toolhead = self.printer.lookup_object('toolhead') @@ -127,13 +149,13 @@ class AutoSpeed: self.steppers[name[-1]] = [pos_min, pos_max, microsteps] if self.steppers.get("x", None) is not None: - self.axes["x"] = { + self.axis_limits["x"] = { "min": self.steppers["x"][0], "max": self.steppers["x"][1], "center": (self.steppers["x"][0] + self.steppers["x"][1]) / 2 } if self.steppers.get("y", None) is not None: - self.axes["y"] = { + self.axis_limits["y"] = { "min": self.steppers["y"][0], "max": self.steppers["y"][1], "center": (self.steppers["y"][0] + self.steppers["y"][1]) / 2 @@ -147,7 +169,7 @@ class AutoSpeed: if not len(self.steppers.keys()) == 3: raise gcmd.error(f"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.") - validate = gcmd.get_int('VALIDATE', 1, minval=0, maxval=1) + validate = gcmd.get_int('VALIDATE', 0, minval=0, maxval=1) self._prepare(gcmd) start = perf_counter() @@ -155,7 +177,7 @@ class AutoSpeed: veloc_results = self.cmd_AUTO_SPEED_VELOCITY(gcmd) respond = f"AUTO SPEED found recommended acceleration and velocity after {perf_counter() - start:.2f}s\n" - for axis in ["x", "y", "diag_x", "diag_y"]: + for axis in self.valid_axes: aR = accel_results.vals.get(axis, None) vR = veloc_results.vals.get(axis, None) if aR is not None or vR is not None: @@ -179,10 +201,7 @@ class AutoSpeed: def cmd_AUTO_SPEED_ACCEL(self, gcmd): if not len(self.steppers.keys()) == 3: raise gcmd.error(f"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.") - x = gcmd.get_int("X", self.x, minval=0, maxval=1) - y = gcmd.get_int("Y", self.y, minval=0, maxval=1) - diag_x = gcmd.get_int("DIAG_X", self.diag_x, minval=0, maxval=1) - diag_y = gcmd.get_int("DIAG_Y", self.diag_y, minval=0, maxval=1) + axes = self._parse_axis(gcmd.get("AXIS", self._axis_to_str(self.axes))) margin = gcmd.get_float("MARGIN", self.margin, above=0.0) derate = gcmd.get_float('DERATE', self.derate, above=0.0, below=1.0) @@ -197,14 +216,8 @@ class AutoSpeed: veloc = gcmd.get_float('VELOCITY', 0.0, above=0.0) respond = "AUTO SPEED finding maximum acceleration on" - if x == 1: - respond += " X," - if y == 1: - respond += " Y," - if diag_x == 1: - respond += " DIAG X," - if diag_y == 1: - respond += " DIAG Y," + for axis in axes: + respond += f" {axis.upper().replace('_', ' ')}," self.gcode.respond_info(respond[:-1]) aw = AttemptWrapper() @@ -215,17 +228,17 @@ class AutoSpeed: aw.dist = accel_dist aw.accuracy = accel_accu aw.veloc = veloc - accel_results = self.find_max(aw, margin, self._attempt_accel, x, y, diag_x, diag_y) + accel_results = self.find_max(aw, margin, self._attempt_accel, axes) accel_results.name = "acceleration" respond = f"AUTO SPEED found maximum acceleration after {accel_results.duration:.2f}s\n" - for axis in ["x", "y", "diag_x", "diag_y"]: + for axis in self.valid_axes: if accel_results.vals.get(axis, None) is not None: respond += f"| {axis.replace('_', ' ').upper()} max: {accel_results.vals[axis]:.0f}\n" respond += f"\n" accel_results.derate(derate) respond += f"Recommended values:\n" - for axis in ["x", "y", "diag_x", "diag_y"]: + for axis in self.valid_axes: if accel_results.vals.get(axis, None) is not None: respond += f"| {axis.replace('_', ' ').upper()} max: {accel_results.vals[axis]:.0f}\n" respond += f"Reommended acceleration: {accel_results.vals['rec']:.0f}\n" @@ -237,10 +250,7 @@ class AutoSpeed: def cmd_AUTO_SPEED_VELOCITY(self, gcmd): if not len(self.steppers.keys()) == 3: raise gcmd.error(f"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.") - x = gcmd.get_int("X", self.x, minval=0, maxval=1) - y = gcmd.get_int("Y", self.y, minval=0, maxval=1) - diag_x = gcmd.get_int("DIAG_X", self.diag_x, minval=0, maxval=1) - diag_y = gcmd.get_int("DIAG_Y", self.diag_y, minval=0, maxval=1) + axes = self._parse_axis(gcmd.get("AXIS", self._axis_to_str(self.axes))) margin = gcmd.get_float("MARGIN", self.margin, above=0.0) derate = gcmd.get_float('DERATE', self.derate, above=0.0, below=1.0) @@ -255,14 +265,8 @@ class AutoSpeed: accel = gcmd.get_float('ACCEL', 0.0, above=0.0) respond = "AUTO SPEED finding maximum velocity on" - if x == 1: - respond += " X," - if y == 1: - respond += " Y," - if diag_x == 1: - respond += " DIAG X," - if diag_y == 1: - respond += " DIAG Y," + for axis in axes: + respond += f" {axis.upper().replace('_', ' ')}," self.gcode.respond_info(respond[:-1]) aw = AttemptWrapper() @@ -273,17 +277,17 @@ class AutoSpeed: aw.dist = veloc_dist aw.accuracy = veloc_accu aw.accel = accel - veloc_results = self.find_max(aw, margin, self._attempt_veloc, x, y, diag_x, diag_y) + veloc_results = self.find_max(aw, margin, self._attempt_veloc, axes) veloc_results.name = "velocity" respond = f"AUTO SPEED found maximum velocity after {veloc_results.duration:.2f}s\n" - for axis in ["x", "y", "diag_x", "diag_y"]: + for axis in self.valid_axes: if veloc_results.vals.get(axis, None) is not None: respond += f"| {axis.replace('_', ' ').upper()} max: {veloc_results.vals[axis]:.0f}\n" respond += "\n" veloc_results.derate(derate) respond += f"Recommended values\n" - for axis in ["x", "y", "diag_x", "diag_y"]: + for axis in self.valid_axes: if veloc_results.vals.get(axis, None) is not None: respond += f"| {axis.replace('_', ' ').upper()} max: {veloc_results.vals[axis]:.0f}\n" respond += f"Recommended velocity: {veloc_results.vals['rec']:.0f}\n" @@ -316,12 +320,133 @@ class AutoSpeed: respond += f"Missed X {missed_x:.2f}, Y {missed_y:.2f}" self.gcode.respond_info(respond) return valid + + cmd_AUTO_SPEED_GRAPH_help = ("Graph your printer's maximum acceleration at given velocities") + def cmd_AUTO_SPEED_GRAPH(self, gcmd): + import matplotlib.pyplot as plt + if not len(self.steppers.keys()) == 3: + raise gcmd.error(f"Printer must be homed first! Found {len(self.steppers.keys())} homed axes.") + axes = self._parse_axis(gcmd.get("AXIS", self._axis_to_str(self.axes))) + + margin = gcmd.get_float("MARGIN", self.margin, above=0.0) + derate = gcmd.get_float('DERATE', self.derate, above=0.0, below=1.0) + max_missed = gcmd.get_float('MAX_MISSED', self.max_missed, above=0.0) + + veloc_min = gcmd.get_float('VELOCITY_MIN', above=0.0) + veloc_max = gcmd.get_float('VELOCITY_MAX', above=veloc_min) + veloc_div = gcmd.get_int( 'VELOCITY_DIV', 5, minval=0) + veloc_dist = gcmd.get_float('VELOCITY_DIST', self.veloc_dist, above=0.0) + veloc_ittr = gcmd.get_int( 'VELOCITY_ITTR', self.veloc_ittr, minval=0) + + accel_accu = gcmd.get_float('ACCEL_ACCU', 0.05, above=0.0, below=1.0) + + accel_min_a = gcmd.get_float('ACCEL_MIN_A', 0.23, above=0) + accel_min_b = gcmd.get_float('ACCEL_MIN_B', -300, below=0) + accel_min_c = gcmd.get_float('ACCEL_MIN_C', 85_000, above=0) + + accel_max_a = gcmd.get_float('ACCEL_MAX_A', 0.19, above=0) + accel_max_b = gcmd.get_float('ACCEL_MAX_B', -300, below=0) + accel_max_c = gcmd.get_float('ACCEL_MAX_C', 200_000, above=0) + + veloc_step = (veloc_max - veloc_min)//(veloc_div - 1) + velocs = [round((v * veloc_step) + veloc_min) for v in range(0, veloc_div)] + respond = "AUTO SPEED graphing maximum accel from velocities on" + for axis in axes: + respond += f" {axis.upper().replace('_', ' ')}," + respond = respond[:-1] + "\n" + respond += f"V_MIN: {veloc_min}, V_MAX: {veloc_max}, V_STEP: {veloc_step}\n" + self.gcode.respond_info(respond) + + aw = AttemptWrapper() + aw.max_missed = max_missed + aw.iterations = veloc_ittr + aw.min = veloc_min + aw.max = veloc_max + aw.dist = veloc_dist + for axis in axes: + aw.axis = axis + if axis == "diag_x": + aw.func = self._check_diag_x + if aw.dist == 0.0: + aw.dist = min([self.axis_limits["y"]["center"] - margin, self.axis_limits["x"]["center"] - margin]) + aw.travel = self._calc_travel(aw.dist, aw.dist) + elif axis == "diag_y": + aw.func = self._check_diag_y + if aw.dist == 0.0: + aw.dist = min([self.axis_limits["y"]["center"] - margin, self.axis_limits["x"]["center"] - margin]) + aw.travel = self._calc_travel(aw.dist, aw.dist) + elif axis == "x": + aw.func = self._check_x + if aw.dist == 0.0: + aw.dist = (self.axis_limits["x"]["center"] - margin) + aw.travel = aw.dist + elif axis == "y": + aw.func = self._check_y + if aw.dist == 0.0: + aw.dist = (self.axis_limits["y"]["center"] - margin) + aw.travel = aw.dist + else: + raise gcmd.error(f"Unknown axis '{axis}'") + aw.travel = round(aw.travel * 2) + + accels = [] + accel_mins = [] + accel_maxs = [] + for veloc in velocs: + self.gcode.respond_info(f"AUTO SPEED graph {aw.axis} - v{veloc}") + a_min = round(self._calc_accel_parabola(veloc, accel_min_a, accel_min_b, accel_min_c)) + a_max = round(self._calc_accel_parabola(veloc, accel_max_a, accel_max_b, accel_max_c)) + if accel_mins and a_min > accel_mins[-1]: + a_min = accel_mins[-1] + if accel_maxs and a_max > accel_maxs[-1]: + a_max = accel_maxs[-1] + accel_mins.append(a_min) + accel_maxs.append(a_max) + accel = round(a_min + (a_max-a_min) // 3) + measuring = True + while measuring: + #self.gcode.respond_info(f"a_min: {a_min:.0f}, a_max: {a_max:.0f} - a{accel:.0f}") + valid, missed_x, missed_y, timeMove, timeAttempt = self._attempt(aw, accel, veloc) + respond = f"AUTO SPEED graph {aw.axis} ({timeAttempt:.2f}s)\n" + respond += f"Missed X {missed_x:.2f}, Y {missed_y:.2f} at a{accel:.0f}/v{veloc:.0f} over {timeMove:.2f}s" + self.gcode.respond_info(respond) + if accel * (1 + accel_accu) > a_max or accel * (1 - accel_accu) < a_min: + measuring = False + + if valid: + a_min = accel + else: + a_max = accel + accel = round((a_min + a_max)/2) + accels.append(accel) + plt.plot(velocs, accels, 'go-', label='measured') + plt.plot(velocs, accel_mins, 'bo-', label='min') + plt.plot(velocs, accel_maxs, 'ro-', label='max') + plt.plot(velocs, [a*derate for a in accels], 'go-', label='derated') + plt.legend(loc='upper right') + plt.title(f"Max accel at velocity over {aw.travel}mm on {aw.axis} to {int(accel_accu*100)}%") + plt.xlabel("Velocity") + plt.ylabel("Acceleration") + path = f"../printer_data/config/AUTO_SPEED_GRAPH_{aw.axis}.png" + self.gcode.respond_info(f"Velocs: {velocs}") + self.gcode.respond_info(f"Accels: {accels}") + self.gcode.respond_info(f"Saving graph to {path}") + plt.savefig(path, bbox_inches='tight') + plt.close() + # ------------------------------------------------------- # # Internal Methods # # ------------------------------------------------------- + def _calc_accel_parabola(self, x, a, b, c): + a = a * x**2 + b = b * x + result = a + b + c + if result < 0: + return 0 + return result def _prepare(self, gcmd): if not len(self.steppers.keys()) == 3: @@ -332,7 +457,7 @@ class AutoSpeed: start = perf_counter() # Level the printer if it's not leveled self._level(gcmd) - self._move([self.axes["x"]["center"], self.axes["y"]["center"], z], self.th_veloc) + self._move([self.axis_limits["x"]["center"], self.axis_limits["y"]["center"], z], self.th_veloc) self._variance(gcmd) @@ -394,40 +519,50 @@ class AutoSpeed: raise gcmd.error(f"Please increase MAX_MISSED (currently {max_missed}), or tune your steppers/homing macro.") - def find_max(self, aw: AttemptWrapper, margin, func: callable, x=True, y=True, diag_x=True, diag_y = True): + def find_max(self, aw: AttemptWrapper, margin, func: callable, axes): rw = ResultsWrapper() start = perf_counter() - if diag_x: - if aw.dist == 0.0: - aw.dist = min([self.axes["y"]["center"] - margin, self.axes["x"]["center"] - margin]) - aw.axis = "diag_x" - aw.travel = self._calc_travel(aw.dist, aw.dist) - aw.func = self._check_diag_x - rw.vals[aw.axis] = func(aw) - if diag_y: - if aw.dist == 0.0: - aw.dist = min([self.axes["y"]["center"] - margin, self.axes["x"]["center"] - margin]) - aw.axis = "diag_y" - aw.travel = self._calc_travel(aw.dist, aw.dist) - aw.func = self._check_diag_y - rw.vals[aw.axis] = func(aw) - if x: - if aw.dist == 0.0: - aw.dist = (self.axes["x"]["center"] - margin) - aw.axis = "x" - aw.travel = aw.dist * 2 - aw.func = self._check_x - rw.vals[aw.axis] = func(aw) - if y: - if aw.dist == 0.0: - aw.dist = (self.axes["y"]["center"] - margin) - aw.axis = "y" - aw.travel = aw.dist * 2 - aw.func = self._check_y + for axis in axes: + aw.axis = axis + if axis == "diag_x": + aw.func = self._check_diag_x + if aw.dist == 0.0: + aw.dist = min([self.axis_limits["y"]["center"] - margin, self.axis_limits["x"]["center"] - margin]) + aw.travel = self._calc_travel(aw.dist, aw.dist) + elif axis == "diag_y": + aw.func = self._check_diag_y + if aw.dist == 0.0: + aw.dist = min([self.axis_limits["y"]["center"] - margin, self.axis_limits["x"]["center"] - margin]) + aw.travel = self._calc_travel(aw.dist, aw.dist) + elif axis == "x": + aw.func = self._check_x + if aw.dist == 0.0: + aw.dist = (self.axis_limits["x"]["center"] - margin) + aw.travel = aw.dist + elif axis == "y": + aw.func = self._check_y + if aw.dist == 0.0: + aw.dist = (self.axis_limits["y"]["center"] - margin) + aw.travel = aw.dist + aw.travel = round(aw.travel * 2) rw.vals[aw.axis] = func(aw) rw.duration = perf_counter() - start return rw + def _attempt(self, aw: AttemptWrapper, accel, veloc): + # if self.debug: + # self.gcode.respond_info(f"_attempt: AW: {aw}") + timeAttempt = perf_counter() + self._set_velocity(veloc, accel) + start_steps = self._pretest(x=True, y=True) + timeMove = perf_counter() + aw.func(veloc, aw.dist, aw.iterations) + self.toolhead.wait_moves() + timeMove = perf_counter() - timeMove + valid, missed_x, missed_y = self._posttest(start_steps, aw.max_missed, x=True, y=True) + timeAttempt = perf_counter() - timeAttempt + return valid, missed_x, missed_y, timeMove, timeAttempt + def _attempt_accel(self, aw: AttemptWrapper): #self.gcode.respond_info("AUTO SPEED checking accel...") measured_accel = None @@ -438,20 +573,12 @@ class AutoSpeed: accel = m_min + (m_max-m_min) // 3 veloc = aw.veloc while measuring: - start = perf_counter() tries += 1 if aw.veloc == 0.0: veloc = self._calc_velocity(accel, aw.travel)/2.5 - self._set_velocity(veloc, accel) - #self.gcode.respond_info(f"Trying min: {m_min:.0f}, max:{m_max:.0f} at {accel:.0f}") - start_steps = self._pretest(x=True, y=True) - check_start = perf_counter() - aw.func(veloc, aw.dist, aw.iterations) - self.toolhead.wait_moves() - duration = perf_counter() - check_start - valid, missed_x, missed_y = self._posttest(start_steps, aw.max_missed, x=True, y=True) - respond = f"AUTO SPEED acceleration {aw.axis} measurement {tries} ({perf_counter() - start:.2f}s)\n" - respond += f"Missed X {missed_x:.2f}, Y {missed_y:.2f} at a{accel:.0f}/v{veloc:.0f} over {duration:.2f}s" + valid, missed_x, missed_y, timeMove, timeAttempt = self._attempt(aw, accel, veloc) + respond = f"AUTO SPEED acceleration {aw.axis} measurement {tries} ({timeAttempt:.2f}s)\n" + respond += f"Missed X {missed_x:.2f}, Y {missed_y:.2f} at a{accel:.0f}/v{veloc:.0f} over {timeMove:.2f}s" self.gcode.respond_info(respond) if measured_accel is not None: if accel > measured_accel - aw.accuracy and accel < measured_accel + aw.accuracy: @@ -474,20 +601,12 @@ class AutoSpeed: veloc = m_min + (m_max-m_min) // 3 accel = aw.accel while measuring: - start = perf_counter() tries += 1 if aw.accel == 0.0: accel = self._calc_accel(veloc, aw.travel)*2.5 - self._set_velocity(veloc, accel) - #self.gcode.respond_info(f"Trying min: {m_min:.0f}, max:{m_max:.0f} at {accel:.0f}") - start_steps = self._pretest(x=True, y=True) - check_start = perf_counter() - aw.func(veloc, aw.dist, aw.iterations) - self.toolhead.wait_moves() - duration = perf_counter() - check_start - valid, missed_x, missed_y = self._posttest(start_steps, aw.max_missed, x=True, y=True) - respond = f"AUTO SPEED velocity {aw.axis} measurement {tries} ({perf_counter() - start:.2f}s)\n" - respond += f"Missed X {missed_x:.2f}, Y {missed_y:.2f} at v{veloc:.0f}/a{accel:.0f} over {duration:.2f}s" + valid, missed_x, missed_y, timeMove, timeAttempt = self._attempt(aw, accel, veloc) + respond = f"AUTO SPEED velocity {aw.axis} measurement {tries} ({timeAttempt:.2f}s)\n" + respond += f"Missed X {missed_x:.2f}, Y {missed_y:.2f} at v{veloc:.0f}/a{accel:.0f} over {timeMove:.2f}s" self.gcode.respond_info(respond) if measured_veloc is not None: if veloc > measured_veloc - aw.accuracy and veloc < measured_veloc + aw.accuracy: @@ -530,46 +649,46 @@ class AutoSpeed: return valid, missed_x, missed_y def _check_x(self, speed: float, dist: float, iterations: int = 1): - self._move([self.axes["x"]["center"], self.axes["y"]["center"], None], speed) + self._move([self.axis_limits["x"]["center"], self.axis_limits["y"]["center"], None], speed) for _ in range(iterations): - self._move([self.axes["x"]["center"] + dist, None, None], speed) - self._move([self.axes["x"]["center"], None, None], speed) - self._move([self.axes["x"]["center"] - dist, None, None], speed) + self._move([self.axis_limits["x"]["center"] + dist, None, None], speed) + self._move([self.axis_limits["x"]["center"], None, None], speed) + self._move([self.axis_limits["x"]["center"] - dist, None, None], speed) def _check_y(self, speed: float, dist: float, iterations: int = 1): - self._move([self.axes["x"]["center"], self.axes["y"]["center"], None], speed) + self._move([self.axis_limits["x"]["center"], self.axis_limits["y"]["center"], None], speed) for _ in range(iterations): - self._move([None, self.axes["y"]["center"] + dist, None], speed) - self._move([None, self.axes["y"]["center"], None], speed) - self._move([None, self.axes["y"]["center"] - dist, None], speed) + self._move([None, self.axis_limits["y"]["center"] + dist, None], speed) + self._move([None, self.axis_limits["y"]["center"], None], speed) + self._move([None, self.axis_limits["y"]["center"] - dist, None], speed) def _check_diag_x(self, speed: float, dist: float, iterations: int = 1): # B stepper - self._move([self.axes["x"]["center"], self.axes["y"]["center"], None], speed) + self._move([self.axis_limits["x"]["center"], self.axis_limits["y"]["center"], None], speed) for _ in range(iterations): - self._move([self.axes["x"]["center"] + dist, self.axes["y"]["center"] + dist, None], speed) - self._move([self.axes["x"]["center"], self.axes["y"]["center"], None], speed) - self._move([self.axes["x"]["center"] - dist, self.axes["y"]["center"] - dist, None], speed) + self._move([self.axis_limits["x"]["center"] + dist, self.axis_limits["y"]["center"] + dist, None], speed) + self._move([self.axis_limits["x"]["center"], self.axis_limits["y"]["center"], None], speed) + self._move([self.axis_limits["x"]["center"] - dist, self.axis_limits["y"]["center"] - dist, None], speed) def _check_diag_y(self, speed: float, dist: float, iterations: int = 1): # A stepper - self._move([self.axes["x"]["center"], self.axes["y"]["center"], None], speed) + self._move([self.axis_limits["x"]["center"], self.axis_limits["y"]["center"], None], speed) for _ in range(iterations): - self._move([self.axes["x"]["center"] + dist, self.axes["y"]["center"] - dist, None], speed) - self._move([self.axes["x"]["center"], self.axes["y"]["center"], None], speed) - self._move([self.axes["x"]["center"] - dist, self.axes["y"]["center"] + dist, None], speed) + self._move([self.axis_limits["x"]["center"] + dist, self.axis_limits["y"]["center"] - dist, None], speed) + self._move([self.axis_limits["x"]["center"], self.axis_limits["y"]["center"], None], speed) + self._move([self.axis_limits["x"]["center"] - dist, self.axis_limits["y"]["center"] + dist, None], speed) def _validate(self, speed, iterations, margin, small_margin, max_missed): pos = { "x": { - "min": self.axes["x"]["min"] + margin, - "max": self.axes["x"]["max"] - margin, - "center_min": self.axes["x"]["center"] - (small_margin/2), - "center_max": self.axes["x"]["center"] + (small_margin/2), + "min": self.axis_limits["x"]["min"] + margin, + "max": self.axis_limits["x"]["max"] - margin, + "center_min": self.axis_limits["x"]["center"] - (small_margin/2), + "center_max": self.axis_limits["x"]["center"] + (small_margin/2), }, "y": { - "min": self.axes["y"]["min"] + margin, - "max": self.axes["y"]["max"] - margin, - "center_min": self.axes["y"]["center"] - (small_margin/2), - "center_max": self.axes["y"]["center"] + (small_margin/2), + "min": self.axis_limits["y"]["min"] + margin, + "max": self.axis_limits["y"]["max"] - margin, + "center_min": self.axis_limits["y"]["center"] - (small_margin/2), + "center_max": self.axis_limits["y"]["center"] + (small_margin/2), } } self.toolhead.wait_moves() @@ -701,6 +820,23 @@ class AutoSpeed: def _calc_travel(self, x: float, y: float): return math.sqrt(x**2 + y**2) + + def _parse_axis(self, raw_axes): + raw_axes = raw_axes.lower() + raw_axes = raw_axes.replace(" ", "") + raw_axes = raw_axes.split(',') + axes = [] + for axis in raw_axes: + if axis in self.valid_axes: + axes.append(axis) + return axes + + def _axis_to_str(self, raw_axes): + axes = "" + for axis in raw_axes: + axes += f"{axis}," + axes = axes[:-1] + return axes def load_config(config): diff --git a/install.sh b/install.sh index 1ba11b4..0feabc2 100644 --- a/install.sh +++ b/install.sh @@ -30,6 +30,10 @@ fi echo "Linking auto speed to Klipper..." ln -sf "${SRCDIR}/auto_speed.py" "${KLIPPER_PATH}/klippy/extras/auto_speed.py" +# Install matplotlib +echo "Installing matplotlib in klippy..." +~/klippy-env/bin/python -m pip install matplotlib + # Restart klipper echo "Restarting Klipper..." sudo systemctl restart klipper \ No newline at end of file