added back the vibrations profile measurement

This commit is contained in:
Félix Boisselier
2024-05-13 18:38:35 +02:00
parent a37ece7ece
commit dd08162616
10 changed files with 370 additions and 526 deletions

View File

@@ -1,205 +0,0 @@
#!/usr/bin/env python3
# Classes to parse the Klipper log and parse the TMC dump to extract the relevant information
# Written by Frix_x#0161 #
import re
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
class Motor:
def __init__(self, name: str):
self._name: str = name
self._registers: Dict[str, Dict[str, Any]] = {}
self._properties: Dict[str, Any] = {}
def set_register(self, register: str, value: Any) -> None:
# 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:
value += ' intpol=0'
# Simplify the microstep resolution format
mres_match = re.search(r'mres=\d+\((\d+)usteps\)', value)
if mres_match:
value = re.sub(r'mres=\d+\(\d+usteps\)', f'mres={mres_match.group(1)}', value)
# Special parsing for CHOPCONF to avoid pwm_ before each values
if register == 'PWMCONF':
parts = value.split()
new_parts = []
for part in parts:
key, val = part.split('=', 1)
if key.startswith('pwm_'):
key = key[4:]
new_parts.append(f'{key}={val}')
value = ' '.join(new_parts)
# General cleaning to remove extraneous labels and colons and parse the whole into Motor _registers
cleaned_values = re.sub(r'\b\w+:\s+\S+\s+', '', value)
# Then fill the registers while merging all the thresholds into the same THRS virtual register
if register in ['TPWMTHRS', 'TCOOLTHRS']:
existing_thrs = self._registers.get('THRS', {})
new_values = self._parse_register_values(cleaned_values)
merged_values = {**existing_thrs, **new_values}
self._registers['THRS'] = merged_values
else:
self._registers[register] = self._parse_register_values(cleaned_values)
def _parse_register_values(self, register_string: str) -> Dict[str, Any]:
parsed = {}
parts = register_string.split()
for part in parts:
if '=' in part:
k, v = part.split('=', 1)
parsed[k] = v
return parsed
def get_register(self, register: str) -> Optional[Dict[str, Any]]:
return self._registers.get(register)
def get_registers(self) -> Dict[str, Dict[str, Any]]:
return self._registers
def set_property(self, property: str, value: Any) -> None:
self._properties[property] = value
def get_property(self, property: str) -> Optional[Any]:
return self._properties.get(property)
def __str__(self):
return f'Stepper: {self._name}\nKlipper config: {self._properties}\nTMC Registers: {self._registers}'
# Return the other motor properties and registers that are different from the current motor
def compare_to(self, other: 'Motor') -> Optional[Dict[str, Dict[str, Any]]]:
differences = {'properties': {}, 'registers': {}}
# Compare properties
all_keys = self._properties.keys() | other._properties.keys()
for key in all_keys:
val1 = self._properties.get(key)
val2 = other._properties.get(key)
if val1 != val2:
differences['properties'][key] = val2
# Compare registers
all_keys = self._registers.keys() | other._registers.keys()
for key in all_keys:
reg1 = self._registers.get(key, {})
reg2 = other._registers.get(key, {})
if reg1 != reg2:
reg_diffs = {}
sub_keys = reg1.keys() | reg2.keys()
for sub_key in sub_keys:
reg_val1 = reg1.get(sub_key)
reg_val2 = reg2.get(sub_key)
if reg_val1 != reg_val2:
reg_diffs[sub_key] = reg_val2
if reg_diffs:
differences['registers'][key] = reg_diffs
# Clean up: remove empty sections if there are no differences
if not differences['properties']:
del differences['properties']
if not differences['registers']:
del differences['registers']
if not differences:
return None
return differences
class MotorLogParser:
_section_pattern: str = r'DUMP_TMC stepper_(x|y)'
_register_patterns: Dict[str, str] = {
'CHOPCONF': r'CHOPCONF:\s+\S+\s+(.*)',
'PWMCONF': r'PWMCONF:\s+\S+\s+(.*)',
'COOLCONF': r'COOLCONF:\s+(.*)',
'TPWMTHRS': r'TPWMTHRS:\s+\S+\s+(.*)',
'TCOOLTHRS': r'TCOOLTHRS:\s+\S+\s+(.*)',
}
def __init__(self, filepath: Path, config_string: Optional[str] = None):
self._filepath = filepath
self._motors: List[Motor] = []
self._config = self._parse_config(config_string) if config_string else {}
self._parse_registers()
def _parse_config(self, config_string: str) -> Dict[str, Any]:
config = {}
entries = config_string.split('|')
for entry in entries:
if entry:
key, value = entry.split(':')
config[key.strip()] = self._convert_value(value.strip())
return config
def _convert_value(self, value: str) -> Union[int, float, bool, str]:
if value.isdigit():
return int(value)
try:
return float(value)
except ValueError:
if value.lower() in ['true', 'false']:
return value.lower() == 'true'
return value
def _parse_registers(self) -> None:
with open(self._filepath, 'r') as file:
log_content = file.read()
sections = re.split(self._section_pattern, log_content)
# Detect only the latest dumps from the log (to ignore potential previous and outdated dumps)
last_sections: Dict[str, int] = {}
for i in range(1, len(sections), 2):
stepper_name = 'stepper_' + sections[i].strip()
last_sections[stepper_name] = i
for stepper_name, index in last_sections.items():
content = sections[index + 1]
motor = Motor(stepper_name)
# Apply general properties from config string
for key, value in self._config.items():
if stepper_name in key:
prop_key = key.replace(stepper_name + '_', '')
motor.set_property(prop_key, value)
elif 'autotune' in key:
motor.set_property(key, value)
# Parse TMC registers
for key, pattern in self._register_patterns.items():
match = re.search(pattern, content)
if match:
values = match.group(1).strip()
motor.set_register(key, values)
self._motors.append(motor)
# 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:
return motor
return None
# Get all the motor list at once
def get_motors(self) -> List[Motor]:
return self._motors
# # Usage example:
# config_string = "stepper_x_tmc:tmc2240|stepper_x_run_current:0.9|stepper_x_hold_current:0.9|stepper_y_tmc:tmc2240|stepper_y_run_current:0.9|stepper_y_hold_current:0.9|autotune_enabled:True|stepper_x_motor:ldo-35sth48-1684ah|stepper_x_voltage:|stepper_y_motor:ldo-35sth48-1684ah|stepper_y_voltage:|"
# parser = MotorLogParser('/path/to/your/logfile.log', config_string)
# stepper_x = parser.get_motor('stepper_x')
# stepper_y = parser.get_motor('stepper_y')
# print(stepper_x)
# print(stepper_y)

View File

@@ -1,214 +0,0 @@
#########################################
###### MACHINE VIBRATIONS ANALYSIS ######
#########################################
# Written by Frix_x#0161 #
[gcode_macro CREATE_VIBRATIONS_PROFILE]
gcode:
{% set size = params.SIZE|default(100)|int %} # size of the circle where the angled lines are done
{% set z_height = params.Z_HEIGHT|default(20)|int %} # z height to put the toolhead before starting the movements
{% set max_speed = params.MAX_SPEED|default(200)|float * 60 %} # maximum feedrate for the movements
{% set speed_increment = params.SPEED_INCREMENT|default(2)|float * 60 %} # feedrate increment between each move
{% set feedrate_travel = params.TRAVEL_SPEED|default(200)|int * 60 %} # travel feedrate between moves
{% set accel = params.ACCEL|default(3000)|int %} # accel value used to move on the pattern
{% set accel_chip = params.ACCEL_CHIP|default("adxl345") %} # ADXL chip name in the config
{% set keep_results = params.KEEP_N_RESULTS|default(3)|int %}
{% set keep_csv = params.KEEP_CSV|default(0)|int %}
{% set mid_x = printer.toolhead.axis_maximum.x|float / 2 %}
{% set mid_y = printer.toolhead.axis_maximum.y|float / 2 %}
{% set min_speed = 2 * 60 %} # minimum feedrate for the movements is set to 2mm/s
{% set nb_speed_samples = ((max_speed - min_speed) / speed_increment + 1) | int %}
{% set accel = [accel, printer.configfile.settings.printer.max_accel]|min %}
{% set old_accel = printer.toolhead.max_accel %}
{% set old_cruise_ratio = printer.toolhead.minimum_cruise_ratio %}
{% set old_sqv = printer.toolhead.square_corner_velocity %}
{% set kinematics = printer.configfile.settings.printer.kinematics %}
{% if not 'xyz' in printer.toolhead.homed_axes %}
{ action_raise_error("Must Home printer first!") }
{% endif %}
{% if params.SPEED_INCREMENT|default(2)|float * 100 != (params.SPEED_INCREMENT|default(2)|float * 100)|int %}
{ action_raise_error("Only 2 decimal digits are allowed for SPEED_INCREMENT") }
{% endif %}
{% if (size / (max_speed / 60)) < 0.25 %}
{ action_raise_error("SIZE is too small for this MAX_SPEED. Increase SIZE or decrease MAX_SPEED!") }
{% endif %}
{action_respond_info("")}
{action_respond_info("Starting machine vibrations profile measurement")}
{action_respond_info("This operation can not be interrupted by normal means. Hit the \"emergency stop\" button to stop it if needed")}
{action_respond_info("")}
SAVE_GCODE_STATE NAME=CREATE_VIBRATIONS_PROFILE
G90
# Set the wanted acceleration values (not too high to avoid oscillation, not too low to be able to reach constant speed on each segments)
SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY={[(accel / 1000), 5.0]|max}
# Going to the start position
G1 Z{z_height} F{feedrate_travel / 10}
G1 X{mid_x } Y{mid_y} F{feedrate_travel}
{% 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 %}
{% set pi = (3.141592653589793) | float %}
{% set tau = (pi * 2) | float %}
{% for curr_angle in main_angles %}
{% for curr_speed_sample in range(0, nb_speed_samples) %}
{% set curr_speed = min_speed + curr_speed_sample * speed_increment %}
{% set rad_angle_full = (curr_angle|float * pi / 180) %}
# -----------------------------------------------------------------------------------------------------------
# Here are some maths to approximate the sin and cos values of rad_angle in Jinja
# Thanks a lot to Aubey! for sharing the idea of using hardcoded Taylor series and
# the associated bit of code to do it easily! This is pure madness!
{% set rad_angle = ((rad_angle_full % tau) - (tau / 2)) | float %}
{% if rad_angle < (-(tau / 4)) %}
{% set rad_angle = (rad_angle + (tau / 2)) | float %}
{% set final_mult = (-1) %}
{% elif rad_angle > (tau / 4) %}
{% set rad_angle = (rad_angle - (tau / 2)) | float %}
{% set final_mult = (-1) %}
{% else %}
{% set final_mult = (1) %}
{% endif %}
{% set sin0 = (rad_angle) %}
{% set sin1 = ((rad_angle ** 3) / 6) | float %}
{% set sin2 = ((rad_angle ** 5) / 120) | float %}
{% set sin3 = ((rad_angle ** 7) / 5040) | float %}
{% set sin4 = ((rad_angle ** 9) / 362880) | float %}
{% set sin5 = ((rad_angle ** 11) / 39916800) | float %}
{% set sin6 = ((rad_angle ** 13) / 6227020800) | float %}
{% set sin7 = ((rad_angle ** 15) / 1307674368000) | float %}
{% set sin = (-(sin0 - sin1 + sin2 - sin3 + sin4 - sin5 + sin6 - sin7) * final_mult) | float %}
{% set cos0 = (1) | float %}
{% set cos1 = ((rad_angle ** 2) / 2) | float %}
{% set cos2 = ((rad_angle ** 4) / 24) | float %}
{% set cos3 = ((rad_angle ** 6) / 720) | float %}
{% set cos4 = ((rad_angle ** 8) / 40320) | float %}
{% set cos5 = ((rad_angle ** 10) / 3628800) | float %}
{% set cos6 = ((rad_angle ** 12) / 479001600) | float %}
{% set cos7 = ((rad_angle ** 14) / 87178291200) | float %}
{% set cos = (-(cos0 - cos1 + cos2 - cos3 + cos4 - cos5 + cos6 - cos7) * final_mult) | float %}
# -----------------------------------------------------------------------------------------------------------
# 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
# enough to collect enough data for vibration analysis, without doing unnecessary distance to save time. At higher speeds, the full
# segments lengths are used because the head moves faster and travels more distance in the same amount of time and we want enough data
{% if curr_speed < (100 * 60) %}
{% set segment_length_multiplier = 1/5 + 4/5 * (curr_speed / 60) / 100 %}
{% else %}
{% set segment_length_multiplier = 1 %}
{% endif %}
# Calculate angle coordinates using trigonometry and length multiplier and move to start point
{% set dx = (size / 2) * cos * segment_length_multiplier %}
{% set dy = (size / 2) * sin * segment_length_multiplier %}
G1 X{mid_x - dx} Y{mid_y - dy} F{feedrate_travel}
# Adjust the number of back and forth movements based on speed to also save time on lower speed range
# 3 movements are done by default, reduced to 2 between 150-250mm/s and to 1 under 150mm/s.
{% set movements = 3 %}
{% if curr_speed < (150 * 60) %}
{% set movements = 1 %}
{% elif curr_speed < (250 * 60) %}
{% set movements = 2 %}
{% endif %}
ACCELEROMETER_MEASURE CHIP={accel_chip}
# Back and forth movements to record the vibrations at constant speed in both direction
{% for n in range(movements) %}
G1 X{mid_x + dx} Y{mid_y + dy} F{curr_speed}
G1 X{mid_x - dx} Y{mid_y - dy} F{curr_speed}
{% endfor %}
ACCELEROMETER_MEASURE CHIP={accel_chip} NAME=an{("%.2f" % curr_angle|float)|replace('.','_')}sp{("%.2f" % (curr_speed / 60)|float)|replace('.','_')}
G4 P300
M400
{% endfor %}
{% endfor %}
# Restore the previous acceleration values
SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_cruise_ratio} SQUARE_CORNER_VELOCITY={old_sqv}
# Extract the TMC names and configuration
{% set ns_x = namespace(path='') %}
{% set ns_y = namespace(path='') %}
{% for item in printer %}
{% set parts = item.split() %}
{% if parts|length == 2 and parts[0].startswith('tmc') and parts[0][3:].isdigit() %}
{% if parts[1] == 'stepper_x' %}
{% set ns_x.path = parts[0] %}
{% elif parts[1] == 'stepper_y' %}
{% set ns_y.path = parts[0] %}
{% endif %}
{% endif %}
{% endfor %}
{% if ns_x.path and ns_y.path %}
{% set metadata =
"stepper_x_tmc:" ~ ns_x.path ~ "|"
"stepper_x_run_current:" ~ (printer[ns_x.path + ' stepper_x'].run_current | round(2) | string) ~ "|"
"stepper_x_hold_current:" ~ (printer[ns_x.path + ' stepper_x'].hold_current | round(2) | string) ~ "|"
"stepper_y_tmc:" ~ ns_y.path ~ "|"
"stepper_y_run_current:" ~ (printer[ns_y.path + ' stepper_y'].run_current | round(2) | string) ~ "|"
"stepper_y_hold_current:" ~ (printer[ns_y.path + ' stepper_y'].hold_current | round(2) | string) ~ "|"
%}
{% set autotune_x = printer.configfile.config['autotune_tmc stepper_x'] if 'autotune_tmc stepper_x' in printer.configfile.config else none %}
{% set autotune_y = printer.configfile.config['autotune_tmc stepper_y'] if 'autotune_tmc stepper_y' in printer.configfile.config else none %}
{% if autotune_x and autotune_y %}
{% set stepper_x_voltage = autotune_x.voltage if autotune_x.voltage else '24.0' %}
{% set stepper_y_voltage = autotune_y.voltage if autotune_y.voltage else '24.0' %}
{% set metadata = metadata ~
"autotune_enabled:True|"
"stepper_x_motor:" ~ autotune_x.motor ~ "|"
"stepper_x_voltage:" ~ stepper_x_voltage ~ "|"
"stepper_y_motor:" ~ autotune_y.motor ~ "|"
"stepper_y_voltage:" ~ stepper_y_voltage ~ "|"
%}
{% else %}
{% set metadata = metadata ~ "autotune_enabled:False|" %}
{% endif %}
DUMP_TMC STEPPER=stepper_x
DUMP_TMC STEPPER=stepper_y
{% else %}
{ action_respond_info("No TMC drivers found for X and Y steppers") }
{% endif %}
RESPOND MSG="Machine vibrations profile generation..."
RESPOND MSG="This may take some time (3-5min)"
SHAKETUNE_POSTPROCESS PARAMS="--type vibrations --accel {accel|int} --kinematics {kinematics} {% if metadata %}--metadata {metadata}{% endif %} --chip_name {accel_chip} {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
RESTORE_GCODE_STATE NAME=CREATE_VIBRATIONS_PROFILE

View File

@@ -4,6 +4,7 @@ from .axes_input_shaper import axes_shaper_calibration as axes_shaper_calibratio
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'},

View File

@@ -71,5 +71,5 @@ def axes_map_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> No
# Run post-processing
ConsoleOutput.print('Analysis of the movements...')
creator = st_thread.get_graph_creator()
creator.configure(accel, accel_chip)
creator.configure(accel)
st_thread.run()

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
# Classes to retrieve a couple of motors infos and extract the relevant information
# from the Klipper configuration and the TMC registers
# Written by Frix_x#0161 #
import re
from typing import Any, Dict, List, Optional, Tuple
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']
RELEVANT_TMC_REGISTERS = ['CHOPCONF', 'PWMCONF', 'COOLCONF', 'TPWMTHRS', 'TCOOLTHRS']
class Motor:
def __init__(self, name: str):
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:
# 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:
value_dict['intpol'] = '0'
# Simplify the microstep resolution format
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)
# Special parsing for CHOPCONF to avoid pwm_ before each values
if register == 'PWMCONF':
new_value_dict = {}
for key, val in value_dict.items():
if key.startswith('pwm_'):
key = key[4:]
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
if register in ['TPWMTHRS', 'TCOOLTHRS']:
existing_thrs = self._registers.get('THRS', {})
merged_values = {**existing_thrs, **value_dict}
self._registers['THRS'] = merged_values
else:
self._registers[register] = value_dict
def get_register(self, register: str) -> Optional[Dict[str, Any]]:
return self._registers.get(register)
def get_registers(self) -> Dict[str, Dict[str, Any]]:
return self._registers
def set_config(self, field: str, value: Any) -> None:
self._config[field] = value
def get_config(self, field: str) -> Optional[Any]:
return self._config.get(field)
def __str__(self):
return f'Stepper: {self.name}\nKlipper config: {self._config}\nTMC Registers: {self._registers}'
# Return the other motor config and registers that are different from the current motor
def compare_to(self, other: 'Motor') -> Optional[Dict[str, Dict[str, Any]]]:
differences = {'config': {}, 'registers': {}}
# Compare Klipper config
all_keys = self._config.keys() | other._config.keys()
for key in all_keys:
val1 = self._config.get(key)
val2 = other._config.get(key)
if val1 != val2:
differences['config'][key] = val2
# Compare TMC registers
all_keys = self._registers.keys() | other._registers.keys()
for key in all_keys:
reg1 = self._registers.get(key, {})
reg2 = other._registers.get(key, {})
if reg1 != reg2:
reg_diffs = {}
sub_keys = reg1.keys() | reg2.keys()
for sub_key in sub_keys:
reg_val1 = reg1.get(sub_key)
reg_val2 = reg2.get(sub_key)
if reg_val1 != reg_val2:
reg_diffs[sub_key] = reg_val2
if reg_diffs:
differences['registers'][key] = reg_diffs
# Clean up: remove empty sections if there are no differences
if not differences['config']:
del differences['config']
if not differences['registers']:
del differences['registers']
if not differences:
return None
return differences
class MotorsConfigParser:
def __init__(self, printer, motors: List[str] = MOTORS, drivers: List[str] = TRINAMIC_DRIVERS):
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)
if tmc_object is None:
continue
motor = self._create_motor(motor_name, driver, tmc_object)
self._motors.append(motor)
# 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)
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:
# 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__
motor_currents = tmc_cmdhelper.current_helper.get_current()
motor.set_config('run_current', motor_currents[0])
motor.set_config('hold_current', motor_currents[1])
autotune_object = self._printer.lookup_object(f'autotune_tmc {motor.name}', None)
if autotune_object is not None:
motor.set_config('autotune_enabled', True)
motor.set_config('motor', autotune_object.motor)
motor.set_config('voltage', autotune_object.voltage)
else:
motor.set_config('autotune_enabled', False)
def _parse_tmc_registers(self, motor: Motor, tmc: 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__
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)
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)
motor.set_register(register, fields_string)
def _extract_register_values(self, tmc_cmdhelper, register, val):
# Provide a dictionary of register values
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:
field_value = tmc_cmdhelper.fields.get_field(field_name, val, register)
fields[field_name] = field_value
return fields
# 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:
return motor
return None
# Get all the motor list at once
def get_motors(self) -> List[Motor]:
return self._motors

View File

@@ -5,7 +5,7 @@ from . import AXIS_CONFIG
from .resonance_test import vibrate_axis
def excitate_axis_at_freq(gcmd, printer, gcode) -> None:
def excitate_axis_at_freq(gcmd, gcode, printer) -> 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)

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python3
import math
from ..helpers.console_output import ConsoleOutput
from ..shaketune_thread import ShakeTuneThread
from .accelerometer import Accelerometer
from .motorsconfigparser import MotorsConfigParser
MIN_SPEED = 2 # mm/s
def create_vibrations_profile(gcmd, gcode, printer, 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)
speed_increment = gcmd.get_float('SPEED_INCREMENT', default=2.0, minval=1.0)
accel = gcmd.get_int('ACCEL', default=3000, minval=100)
feedrate_travel = gcmd.get_float('TRAVEL_SPEED', default=120.0, minval=20.0)
accel_chip = gcmd.get('ACCEL_CHIP', default=None)
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
input_shaper = printer.lookup_object('input_shaper', None)
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]
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']
old_sqv = toolhead_info['square_corner_velocity']
# set the wanted acceleration values
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY=5.0')
kin_info = toolhead.kin.get_status(systime)
mid_x = (kin_info['axis_minimum'].x + kin_info['axis_maximum'].x) / 2
mid_y = (kin_info['axis_minimum'].y + kin_info['axis_maximum'].y) / 2
X, Y, _, E = toolhead.get_position()
# Going to the start position
toolhead.move([X, Y, z_height, E], feedrate_travel / 10)
toolhead.move([mid_x - 15, mid_y - 15, z_height, E], feedrate_travel)
toolhead.dwell(0.5)
nb_speed_samples = int((max_speed - MIN_SPEED) / speed_increment + 1)
for curr_angle in main_angles:
radian_angle = math.radians(curr_angle)
# Find the best accelerometer chip for the current angle if not specified
if curr_angle == 0:
accel_axis = 'x'
elif curr_angle == 90:
accel_axis = 'y'
else:
accel_axis = 'xy'
if accel_chip is None:
accel_chip = Accelerometer.find_axis_accelerometer(printer, accel_axis)
if accel_chip is None:
gcmd.error(
'No accelerometer specified for measurement! Multi-accelerometer configurations are not supported for this macro.'
)
accelerometer = Accelerometer(printer.lookup_object(accel_chip))
# 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
# 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
# enough to collect enough data for vibration analysis, without doing unnecessary distance to save time. At higher speeds, the full
# segments lengths are used because the head moves faster and travels more distance in the same amount of time and we want enough data
if curr_speed < 100:
segment_length_multiplier = 1 / 5 + 4 / 5 * curr_speed / 100
else:
segment_length_multiplier = 1
# Calculate angle coordinates using trigonometry and length multiplier and move to start point
dX = (size / 2) * math.cos(radian_angle) * segment_length_multiplier
dY = (size / 2) * math.sin(radian_angle) * segment_length_multiplier
toolhead.move([mid_x - dX, mid_y - dY, z_height, E], feedrate_travel)
# Adjust the number of back and forth movements based on speed to also save time on lower speed range
# 3 movements are done by default, reduced to 2 between 150-250mm/s and to 1 under 150mm/s.
movements = 3
if curr_speed < 150:
movements = 1
elif curr_speed < 250:
movements = 2
# Back and forth movements to record the vibrations at constant speed in both direction
accelerometer.start_measurement()
for _ in range(movements):
toolhead.move([mid_x + dX, mid_y + dY, z_height, E], curr_speed)
toolhead.move([mid_x - dX, mid_y - dY, z_height, E], curr_speed)
name = f'vib_an{curr_angle:.2f}sp{curr_speed:.2f}'.replace('.', '_')
accelerometer.stop_measurement(name)
toolhead.dwell(0.3)
toolhead.wait_moves()
# Restore the previous acceleration values
gcode.run_script_from_command(
f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}'
)
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)
st_thread.run()

View File

@@ -11,7 +11,7 @@ from matplotlib.figure import Figure
from ..helpers import filemanager as fm
from ..helpers.console_output import ConsoleOutput
from ..helpers.motorlogparser import MotorLogParser
from ..measurement.motorsconfigparser import MotorsConfigParser
from ..shaketune_config import ShakeTuneConfig
from .analyze_axesmap import axesmap_calibration
from .graph_belts import belts_calibration
@@ -142,9 +142,9 @@ class ShaperGraphCreator(GraphCreator):
raise ValueError('scv must be set to create the input shaper graph!')
lognames = self._move_and_prepare_files(
glob_pattern='raw_data*.csv',
glob_pattern='shaketune-axis_*.csv',
min_files_required=1,
custom_name_func=lambda f: f.stem.split('_')[3].upper(),
custom_name_func=lambda f: f.stem.split('_')[1].upper(),
)
fig = shaper_calibration(
lognames=[str(path) for path in lognames],
@@ -175,18 +175,14 @@ class VibrationsGraphCreator(GraphCreator):
self._kinematics = None
self._accel = None
self._chip_name = None
self._motors = None
self._setup_folder('vibrations')
def configure(self, kinematics: str, accel: float, chip_name: str, metadata: str) -> None:
def configure(self, kinematics: str, accel: float, motor_config_parser: MotorsConfigParser) -> None:
self._kinematics = kinematics
self._accel = accel
self._chip_name = chip_name
parser = MotorLogParser(self._config.klipper_log_folder / 'klippy.log', metadata)
self._motors = parser.get_motors()
self._motors = motor_config_parser.get_motors()
def _archive_files(self, lognames: list[Path]) -> None:
tar_path = self._folder / f'{self._type}_{self._graph_date}.tar.gz'
@@ -195,13 +191,13 @@ class VibrationsGraphCreator(GraphCreator):
tar.add(csv_file, arcname=csv_file.name, recursive=False)
def create_graph(self) -> None:
if not self._accel or not self._chip_name or not self._kinematics:
if not self._accel or not self._kinematics:
raise ValueError('accel, chip_name and kinematics must be set to create the vibrations profile graph!')
lognames = self._move_and_prepare_files(
glob_pattern=f'{self._chip_name}-*.csv',
glob_pattern='shaketune-vib_*.csv',
min_files_required=None,
custom_name_func=lambda f: f.name.replace(self._chip_name, self._type),
custom_name_func=lambda f: f.name,
)
fig = vibrations_profile(
lognames=[str(path) for path in lognames],
@@ -236,11 +232,9 @@ class AxesMapFinder(GraphCreator):
self._folder = config.get_results_folder()
self._accel = None
self._chip_name = None
def configure(self, accel: int, chip_name: str) -> None:
def configure(self, accel: int) -> None:
self._accel = accel
self._chip_name = chip_name
def find_axesmap(self) -> None:
tmp_folder = Path('/tmp')

View File

@@ -4,8 +4,14 @@
from pathlib import Path
from .helpers.console_output import ConsoleOutput
from .measurement import axes_map_calibration, axes_shaper_calibration, compare_belts_responses, excitate_axis_at_freq
from .post_processing import AxesMapFinder, BeltsGraphCreator, ShaperGraphCreator
from .measurement import (
axes_map_calibration,
axes_shaper_calibration,
compare_belts_responses,
create_vibrations_profile,
excitate_axis_at_freq,
)
from .post_processing import AxesMapFinder, BeltsGraphCreator, ShaperGraphCreator, VibrationsGraphCreator
from .shaketune_config import ShakeTuneConfig
from .shaketune_thread import ShakeTuneThread
@@ -35,6 +41,11 @@ class ShakeTune:
self.cmd_EXCITATE_AXIS_AT_FREQ,
desc=self.cmd_EXCITATE_AXIS_AT_FREQ_help,
)
self._gcode.register_command(
'AXES_MAP_CALIBRATION',
self.cmd_AXES_MAP_CALIBRATION,
desc=self.cmd_AXES_MAP_CALIBRATION_help,
)
self._gcode.register_command(
'COMPARE_BELTS_RESPONSES',
self.cmd_COMPARE_BELTS_RESPONSES,
@@ -46,9 +57,9 @@ class ShakeTune:
desc=self.cmd_AXES_SHAPER_CALIBRATION_help,
)
self._gcode.register_command(
'AXES_MAP_CALIBRATION',
self.cmd_AXES_MAP_CALIBRATION,
desc=self.cmd_AXES_MAP_CALIBRATION_help,
'CREATE_VIBRATIONS_PROFILE',
self.cmd_CREATE_VIBRATIONS_PROFILE,
desc=self.cmd_CREATE_VIBRATIONS_PROFILE_help,
)
cmd_EXCITATE_AXIS_AT_FREQ_help = (
@@ -57,7 +68,15 @@ class ShakeTune:
def cmd_EXCITATE_AXIS_AT_FREQ(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
excitate_axis_at_freq(gcmd, self._gcode)
excitate_axis_at_freq(gcmd, self._gcode, self._printer)
cmd_AXES_MAP_CALIBRATION_help = 'Perform a set of movements to measure the orientation of the accelerometer and help you set the best axes_map configuration for your printer'
def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
axes_map_finder = AxesMapFinder(self._config)
st_thread = ShakeTuneThread(self._config, axes_map_finder, self._printer.get_reactor(), self.timeout)
axes_map_calibration(gcmd, self._gcode, self._printer, st_thread)
cmd_COMPARE_BELTS_RESPONSES_help = 'Perform a custom half-axis test to analyze and compare the frequency profiles of individual belts on CoreXY printers'
@@ -77,10 +96,10 @@ class ShakeTune:
st_thread = ShakeTuneThread(self._config, shaper_graph_creator, self._printer.get_reactor(), self.timeout)
axes_shaper_calibration(gcmd, self._gcode, self._printer, st_thread)
cmd_AXES_MAP_CALIBRATION_help = 'Perform a set of movements to measure the orientation of the accelerometer and help you set the best axes_map configuration for your printer'
cmd_CREATE_VIBRATIONS_PROFILE_help = 'Perform a set of movements to measure the orientation of the accelerometer and help you set the best axes_map configuration for your printer'
def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None:
def cmd_CREATE_VIBRATIONS_PROFILE(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
axes_map_finder = AxesMapFinder(self._config)
st_thread = ShakeTuneThread(self._config, axes_map_finder, self._printer.get_reactor(), self.timeout)
axes_map_calibration(gcmd, self._gcode, self._printer, st_thread)
vibration_profile_creator = VibrationsGraphCreator(self._config)
st_thread = ShakeTuneThread(self._config, vibration_profile_creator, self._printer.get_reactor(), self.timeout)
create_vibrations_profile(gcmd, self._gcode, self._printer, st_thread)

View File

@@ -51,81 +51,3 @@ class ShakeTuneConfig:
except Exception as e:
ConsoleOutput.print(f'Warning: unable to retrieve Shake&Tune version number: {e}')
return 'unknown'
# @staticmethod
# def parse_arguments(params: Optional[List] = None) -> argparse.Namespace:
# parser = argparse.ArgumentParser(description='Shake&Tune graphs generation script')
# parser.add_argument(
# '-t',
# '--type',
# dest='type',
# choices=['belts', 'shaper', 'vibrations', 'axesmap'],
# required=True,
# help='Type of output graph to produce',
# )
# parser.add_argument(
# '--accel',
# type=int,
# default=None,
# dest='accel_used',
# help='Accelerometion used for vibrations profile creation or axes map calibration',
# )
# parser.add_argument(
# '--chip_name',
# type=str,
# default='adxl345',
# dest='chip_name',
# help='Accelerometer chip name used for vibrations profile creation or axes map calibration',
# )
# parser.add_argument(
# '--max_smoothing',
# type=float,
# default=None,
# dest='max_smoothing',
# help='Maximum smoothing to allow for input shaper filter recommendations',
# )
# parser.add_argument(
# '--scv',
# '--square_corner_velocity',
# type=float,
# default=5.0,
# dest='scv',
# help='Square corner velocity used to compute max accel for input shapers filter recommendations',
# )
# parser.add_argument(
# '-m',
# '--kinematics',
# dest='kinematics',
# default='cartesian',
# choices=['cartesian', 'corexy'],
# help='Machine kinematics configuration used for the vibrations profile creation',
# )
# parser.add_argument(
# '--metadata',
# type=str,
# default=None,
# dest='metadata',
# help='Motor configuration metadata printed on the vibrations profiles',
# )
# parser.add_argument(
# '-c',
# '--keep_csv',
# action='store_true',
# default=False,
# dest='keep_csv',
# help='Whether to keep the raw CSV files after processing in addition to the PNG graphs',
# )
# parser.add_argument(
# '-n',
# '--keep_results',
# type=int,
# default=3,
# dest='keep_results',
# help='Number of results to keep in the result folder after each run of the script',
# )
# parser.add_argument('--dpi', type=int, default=150, dest='dpi', help='DPI of the output PNG files')
# parser.add_argument(
# '-v', '--version', action='version', version=f'Shake&Tune {ShakeTuneConfig.get_git_version()}'
# )
# return parser.parse_args(params)