fixed most of the bugs now as a Klipper plugin

This commit is contained in:
Félix Boisselier
2024-05-16 23:33:49 +02:00
parent dd08162616
commit 55895c1507
13 changed files with 116 additions and 106 deletions

View File

@@ -5,20 +5,3 @@ from .axes_map import axes_map_calibration as axes_map_calibration
from .belts_comparison import compare_belts_responses as compare_belts_responses
from .static_freq import excitate_axis_at_freq as excitate_axis_at_freq
from .vibrations_profile import create_vibrations_profile as create_vibrations_profile
AXIS_CONFIG = [
{'axis': 'x', 'direction': (1, 0, 0), 'label': 'axis_X'},
{'axis': 'y', 'direction': (0, 1, 0), 'label': 'axis_Y'},
{'axis': 'a', 'direction': (1, -1, 0), 'label': 'belt_A'},
{'axis': 'b', 'direction': (1, 1, 0), 'label': 'belt_B'},
]
# graph_creators = {
# 'axesmap': (AxesMapFinder, lambda gc: gc.configure(options.accel_used, options.chip_name)),
# 'belts': (BeltsGraphCreator, None),
# 'shaper': (ShaperGraphCreator, lambda gc: gc.configure(options.scv, options.max_smoothing)),
# 'vibrations': (
# VibrationsGraphCreator,
# lambda gc: gc.configure(options.kinematics, options.accel_used, options.chip_name, options.metadata),
# ),
# }

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env python3
from ..helpers.common_func import AXIS_CONFIG
from ..helpers.console_output import ConsoleOutput
from ..shaketune_thread import ShakeTuneThread
from . import AXIS_CONFIG
from .accelerometer import Accelerometer
from .resonance_test import vibrate_axis
def axes_shaper_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
def axes_shaper_calibration(gcmd, config, st_thread: ShakeTuneThread) -> None:
min_freq = gcmd.get_float('FREQ_START', default=5, minval=1)
max_freq = gcmd.get_float('FREQ_END', default=133.33, minval=1)
hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1, minval=1)
@@ -21,9 +21,11 @@ def axes_shaper_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) ->
feedrate_travel = gcmd.get_float('TRAVEL_SPEED', default=120.0, minval=20.0)
z_height = gcmd.get_float('Z_HEIGHT', default=None, minval=1)
systime = printer.get_reactor().monotonic()
printer = config.get_printer()
gcode = printer.lookup_object('gcode')
toolhead = printer.lookup_object('toolhead')
res_tester = printer.lookup_object('resonance_tester')
systime = printer.get_reactor().monotonic()
if scv is None:
toolhead_info = toolhead.get_status(systime)
@@ -92,7 +94,7 @@ def axes_shaper_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) ->
accelerometer.stop_measurement(config['label'], append_time=True)
# And finally generate the graph for each measured axis
ConsoleOutput.print(f'{config['axis'].upper()} axis frequency profile generation...')
ConsoleOutput.print(f'{config["axis"].upper()} axis frequency profile generation...')
ConsoleOutput.print('This may take some time (1-3min)')
st_thread.run()

View File

@@ -6,13 +6,18 @@ from ..shaketune_thread import ShakeTuneThread
from .accelerometer import Accelerometer
def axes_map_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
def axes_map_calibration(gcmd, config, st_thread: ShakeTuneThread) -> None:
z_height = gcmd.get_float('Z_HEIGHT', default=20.0)
speed = gcmd.get_float('SPEED', default=80.0, minval=20.0)
accel = gcmd.get_int('ACCEL', default=1500, minval=100)
feedrate_travel = gcmd.get_float('TRAVEL_SPEED', default=120.0, minval=20.0)
accel_chip = gcmd.get('ACCEL_CHIP', default=None)
printer = config.get_printer()
gcode = printer.lookup_object('gcode')
toolhead = printer.lookup_object('toolhead')
systime = printer.get_reactor().monotonic()
if accel_chip is None:
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy')
if accel_chip is None:
@@ -21,8 +26,6 @@ def axes_map_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> No
)
accelerometer = Accelerometer(printer.lookup_object(accel_chip))
systime = printer.get_reactor().monotonic()
toolhead = printer.lookup_object('toolhead')
toolhead_info = toolhead.get_status(systime)
old_accel = toolhead_info['max_accel']
old_mcr = toolhead_info['minimum_cruise_ratio']

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env python3
from ..helpers.common_func import AXIS_CONFIG
from ..helpers.console_output import ConsoleOutput
from ..shaketune_thread import ShakeTuneThread
from . import AXIS_CONFIG
from .accelerometer import Accelerometer
from .resonance_test import vibrate_axis
def compare_belts_responses(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
def compare_belts_responses(gcmd, config, st_thread: ShakeTuneThread) -> None:
min_freq = gcmd.get_float('FREQ_START', default=5.0, minval=1)
max_freq = gcmd.get_float('FREQ_END', default=133.33, minval=1)
hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1.0, minval=1)
@@ -16,9 +16,11 @@ def compare_belts_responses(gcmd, gcode, printer, st_thread: ShakeTuneThread) ->
feedrate_travel = gcmd.get_float('TRAVEL_SPEED', default=120.0, minval=20.0)
z_height = gcmd.get_float('Z_HEIGHT', default=None, minval=1)
systime = printer.get_reactor().monotonic()
printer = config.get_printer()
gcode = printer.lookup_object('gcode')
toolhead = printer.lookup_object('toolhead')
res_tester = printer.lookup_object('resonance_tester')
systime = printer.get_reactor().monotonic()
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy')
if accel_chip is None:
@@ -68,7 +70,7 @@ def compare_belts_responses(gcmd, gcode, printer, st_thread: ShakeTuneThread) ->
input_shaper = None
# Filter axis configurations to get the A and B axis only
filtered_config = [a for a in AXIS_CONFIG if a['axis'] in ('x', 'y')]
filtered_config = [a for a in AXIS_CONFIG if a['axis'] in ('a', 'b')]
for config in filtered_config:
accelerometer.start_measurement()
vibrate_axis(toolhead, gcode, config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz)

View File

@@ -0,0 +1,8 @@
# [gcode_macro AXES_MAP_CALIBRATION]
# gcode:
# {% set z_height = params.Z_HEIGHT|default(20)|int %} # z height to put the toolhead before starting the movements
# {% set speed = params.SPEED|default(80)|float * 60 %} # feedrate for the movements
# {% set accel = params.ACCEL|default(1500)|int %} # accel value used to move on the pattern
# {% set feedrate_travel = params.TRAVEL_SPEED|default(120)|int * 60 %} # travel feedrate between moves
# {% set accel_chip = params.ACCEL_CHIP|default("adxl345") %} # ADXL chip name in the config

View File

@@ -4,8 +4,7 @@
# from the Klipper configuration and the TMC registers
# Written by Frix_x#0161 #
import re
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional
TRINAMIC_DRIVERS = ['tmc2130', 'tmc2208', 'tmc2209', 'tmc2240', 'tmc2660', 'tmc5160']
MOTORS = ['stepper_x', 'stepper_y', 'stepper_x1', 'stepper_y1', 'stepper_z', 'stepper_z1', 'stepper_z2', 'stepper_z3']
@@ -17,25 +16,20 @@ class Motor:
self.name: str = name
self._registers: Dict[str, Dict[str, Any]] = {}
self._config: Dict[str, Any] = {}
self._driver: Tuple[str, Any] = ('', None)
def set_driver(self, driver_name: str, tmc_object: Any) -> None:
self._driver = (driver_name, tmc_object)
def get_driver(self) -> Tuple[str, Any]:
return self._driver
def set_register(self, register: str, value_dict: dict) -> None:
# First we filter out entries with a value of 0 to avoid having too much uneeded data
value_dict = {k: v for k, v in value_dict.items() if v != 0}
# Special parsing for CHOPCONF to extract meaningful values
if register == 'CHOPCONF':
# Add intpol=0 if missing from the register dump
if 'intpol=' not in value_dict:
# Add intpol=0 if missing from the register dump to force printing it as it's important
if 'intpol' not in value_dict:
value_dict['intpol'] = '0'
# Simplify the microstep resolution format
# Remove the microsteps entry as the format here is not easy to read and
# it's already read in the correct format directly from the Klipper config
if 'mres' in value_dict:
mres_match = re.search(r'(\d+)usteps', value_dict['mres'])
if mres_match:
value_dict['mres'] = mres_match.group(1)
del value_dict['mres']
# Special parsing for CHOPCONF to avoid pwm_ before each values
if register == 'PWMCONF':
@@ -46,7 +40,7 @@ class Motor:
new_value_dict[key] = val
value_dict = new_value_dict
# Then fill the registers while merging all the thresholds into the same THRS virtual register
# Then gets merged all the thresholds into the same THRS virtual register
if register in ['TPWMTHRS', 'TCOOLTHRS']:
existing_thrs = self._registers.get('THRS', {})
merged_values = {**existing_thrs, **value_dict}
@@ -110,35 +104,42 @@ class Motor:
class MotorsConfigParser:
def __init__(self, printer, motors: List[str] = MOTORS, drivers: List[str] = TRINAMIC_DRIVERS):
def __init__(self, config, motors: List[str] = MOTORS, drivers: List[str] = TRINAMIC_DRIVERS):
self._printer = config.get_printer()
self._motors: List[Motor] = []
self._printer = printer
for motor_name in motors:
for driver in drivers:
tmc_object = printer.lookup_object(f'{driver} {motor_name}', None)
tmc_object = self._printer.lookup_object(f'{driver} {motor_name}', None)
if tmc_object is None:
continue
motor = self._create_motor(motor_name, driver, tmc_object)
self._motors.append(motor)
pconfig = self._printer.lookup_object('configfile')
self.kinematics = pconfig.status_raw_config['printer']['kinematics']
# Create a Motor object with the given name, driver and TMC object
# and fill it with the relevant configuration and registers
def _create_motor(self, motor_name: str, driver: str, tmc_object: Any) -> Motor:
motor = Motor(motor_name)
motor.set_driver(driver.upper(), tmc_object)
motor.set_config('tmc', driver)
self._parse_klipper_config(motor, tmc_object)
self._parse_tmc_registers(motor, tmc_object)
return motor
def _parse_klipper_config(self, motor: Motor, tmc: Any) -> None:
def _parse_klipper_config(self, motor: Motor, tmc_object: Any) -> None:
# The TMCCommandHelper isn't a direct member of the TMC object... but we can still get it this way
tmc_cmdhelper = tmc.get_status.__self__
tmc_cmdhelper = tmc_object.get_status.__self__
motor_currents = tmc_cmdhelper.current_helper.get_current()
motor.set_config('run_current', motor_currents[0])
motor.set_config('hold_current', motor_currents[1])
pconfig = self._printer.lookup_object('configfile')
motor.set_config('microsteps', int(pconfig.status_raw_config[motor.name]['microsteps']))
autotune_object = self._printer.lookup_object(f'autotune_tmc {motor.name}', None)
if autotune_object is not None:
motor.set_config('autotune_enabled', True)
@@ -147,24 +148,21 @@ class MotorsConfigParser:
else:
motor.set_config('autotune_enabled', False)
def _parse_tmc_registers(self, motor: Motor, tmc: Any) -> None:
def _parse_tmc_registers(self, motor: Motor, tmc_object: Any) -> None:
# The TMCCommandHelper isn't a direct member of the TMC object... but we can still get it this way
tmc_cmdhelper = tmc.get_status.__self__
tmc_cmdhelper = tmc_object.get_status.__self__
for register in RELEVANT_TMC_REGISTERS:
# value = tmc_cmdhelper.read_register(register)
# motor.set_register(register, value)
val = tmc_cmdhelper.fields.registers.get(register)
if (val is not None) and (register not in tmc_cmdhelper.read_registers):
# write-only register
fields_string = self._extract_register_values(register, val)
fields_string = self._extract_register_values(tmc_cmdhelper, register, val)
elif register in tmc_cmdhelper.read_registers:
# readable register
val = tmc_cmdhelper.mcu_tmc.get_register(register)
if tmc_cmdhelper.read_translate is not None:
register, val = tmc_cmdhelper.read_translate(register, val)
fields_string = self._extract_register_values(register, val)
fields_string = self._extract_register_values(tmc_cmdhelper, register, val)
motor.set_register(register, fields_string)
@@ -173,7 +171,7 @@ class MotorsConfigParser:
reg_fields = tmc_cmdhelper.fields.all_fields.get(register, {})
reg_fields = sorted([(mask, name) for name, mask in reg_fields.items()])
fields = {}
for mask, field_name in reg_fields:
for _, field_name in reg_fields:
field_value = tmc_cmdhelper.fields.get_field(field_name, val, register)
fields[field_name] = field_value
return fields
@@ -181,7 +179,7 @@ class MotorsConfigParser:
# Find and return the motor by its name
def get_motor(self, motor_name: str) -> Optional[Motor]:
for motor in self._motors:
if motor._name == motor_name:
if motor.name == motor_name:
return motor
return None

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env python3
from ..helpers.common_func import AXIS_CONFIG
from ..helpers.console_output import ConsoleOutput
from . import AXIS_CONFIG
from .resonance_test import vibrate_axis
def excitate_axis_at_freq(gcmd, gcode, printer) -> None:
def excitate_axis_at_freq(gcmd, config) -> None:
freq = gcmd.get_int('FREQUENCY', default=25, minval=1)
duration = gcmd.get_int('DURATION', default=10, minval=1)
accel_per_hz = gcmd.get_float('ACCEL_PER_HZ', default=None)
@@ -19,9 +19,11 @@ def excitate_axis_at_freq(gcmd, gcode, printer) -> None:
ConsoleOutput.print(f'Excitating {axis.upper()} axis at {freq}Hz for {duration} seconds')
systime = printer.get_reactor().monotonic()
printer = config.get_printer()
gcode = printer.lookup_object('gcode')
toolhead = printer.lookup_object('toolhead')
res_tester = printer.lookup_object('resonance_tester')
systime = printer.get_reactor().monotonic()
if accel_per_hz is None:
accel_per_hz = res_tester.test.accel_per_hz

View File

@@ -11,7 +11,7 @@ from .motorsconfigparser import MotorsConfigParser
MIN_SPEED = 2 # mm/s
def create_vibrations_profile(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
def create_vibrations_profile(gcmd, config, st_thread: ShakeTuneThread) -> None:
size = gcmd.get_float('SIZE', default=100.0, minval=50.0)
z_height = gcmd.get_float('Z_HEIGHT', default=20.0)
max_speed = gcmd.get_float('MAX_SPEED', default=200.0, minval=10.0)
@@ -23,29 +23,30 @@ def create_vibrations_profile(gcmd, gcode, printer, st_thread: ShakeTuneThread)
if (size / (max_speed / 60)) < 0.25:
gcmd.error('The size of the movement is too small for the given speed! Increase SIZE or decrease MAX_SPEED!')
# Check that input shaper is already configured
printer = config.get_printer()
gcode = printer.lookup_object('gcode')
toolhead = printer.lookup_object('toolhead')
input_shaper = printer.lookup_object('input_shaper', None)
systime = printer.get_reactor().monotonic()
# Check that input shaper is already configured
if input_shaper is None:
gcmd.error('Input shaper is not configured! Please run the shaper calibration macro first.')
# TODO: Add the kinematics check to define the main_angles
# but this needs to retrieve it from the printer configuration
# {% if kinematics == "cartesian" %}
# # Cartesian motors are on X and Y axis directly
# RESPOND MSG="Cartesian kinematics mode"
# {% set main_angles = [0, 90] %}
# {% elif kinematics == "corexy" %}
# # CoreXY motors are on A and B axis (45 and 135 degrees)
# RESPOND MSG="CoreXY kinematics mode"
# {% set main_angles = [45, 135] %}
# {% else %}
# { action_raise_error("Only Cartesian and CoreXY kinematics are supported at the moment for the vibrations measurement tool!") }
# {% endif %}
kinematics = 'cartesian'
main_angles = [0, 90]
motors_config_parser = MotorsConfigParser(config, motors=['stepper_x', 'stepper_y'])
if motors_config_parser.kinematics == 'cartesian' or motors_config_parser.kinematics == 'corexz':
# Cartesian motors are on X and Y axis directly, same for CoreXZ
main_angles = [0, 90]
elif motors_config_parser.kinematics == 'corexy':
# CoreXY motors are on A and B axis (45 and 135 degrees)
main_angles = [45, 135]
else:
gcmd.error(
'Only Cartesian and CoreXY kinematics are supported at the moment for the vibrations measurement tool!'
)
ConsoleOutput.print(f'{motors_config_parser.kinematics.upper()} kinematics mode')
systime = printer.get_reactor().monotonic()
toolhead = printer.lookup_object('toolhead')
toolhead_info = toolhead.get_status(systime)
old_accel = toolhead_info['max_accel']
old_mcr = toolhead_info['minimum_cruise_ratio']
@@ -66,6 +67,7 @@ def create_vibrations_profile(gcmd, gcode, printer, st_thread: ShakeTuneThread)
nb_speed_samples = int((max_speed - MIN_SPEED) / speed_increment + 1)
for curr_angle in main_angles:
ConsoleOutput.print(f'-> Measuring angle: {curr_angle} degrees...')
radian_angle = math.radians(curr_angle)
# Find the best accelerometer chip for the current angle if not specified
@@ -86,6 +88,7 @@ def create_vibrations_profile(gcmd, gcode, printer, st_thread: ShakeTuneThread)
# Sweep the speed range to record the vibrations at different speeds
for curr_speed_sample in range(nb_speed_samples):
curr_speed = MIN_SPEED + curr_speed_sample * speed_increment
ConsoleOutput.print(f'Current speed: {curr_speed} mm/s')
# Reduce the segments length for the lower speed range (0-100mm/s). The minimum length is 1/3 of the SIZE and is gradually increased
# to the nominal SIZE at 100mm/s. No further size changes are made above this speed. The goal is to ensure that the print head moves
@@ -126,12 +129,9 @@ def create_vibrations_profile(gcmd, gcode, printer, st_thread: ShakeTuneThread)
)
toolhead.wait_moves()
# Get the motors and TMC configurations from Klipper
motors_config_parser = MotorsConfigParser(printer, motors=['stepper_x', 'stepper_y'])
# Run post-processing
ConsoleOutput.print('Machine vibrations profile generation...')
ConsoleOutput.print('This may take some time (5-8min)')
creator = st_thread.get_graph_creator()
creator.configure(kinematics, accel, motors_config_parser)
creator.configure(motors_config_parser.kinematics, accel, motors_config_parser)
st_thread.run()