mirror of
https://github.com/outbackdingo/klipper_auto_speed.git
synced 2026-01-27 10:19:30 +00:00
Add AUTO_SPEED_GRAPH
Clean up axes definitions
This commit is contained in:
79
README.md
79
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.
|
||||
|
||||
|
||||
368
auto_speed.py
368
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):
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user