diff --git a/shaketune/helpers/motorlogparser.py b/shaketune/helpers/motorlogparser.py deleted file mode 100644 index 4e6e743..0000000 --- a/shaketune/helpers/motorlogparser.py +++ /dev/null @@ -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) diff --git a/shaketune/measurement/K-SnT_vibrations.cfg b/shaketune/measurement/K-SnT_vibrations.cfg deleted file mode 100644 index d6ebacd..0000000 --- a/shaketune/measurement/K-SnT_vibrations.cfg +++ /dev/null @@ -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 diff --git a/shaketune/measurement/__init__.py b/shaketune/measurement/__init__.py index e6338e4..8cffad7 100644 --- a/shaketune/measurement/__init__.py +++ b/shaketune/measurement/__init__.py @@ -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'}, diff --git a/shaketune/measurement/axes_map.py b/shaketune/measurement/axes_map.py index 38d1e0e..bbf0dc5 100644 --- a/shaketune/measurement/axes_map.py +++ b/shaketune/measurement/axes_map.py @@ -19,7 +19,7 @@ def axes_map_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> No gcmd.error( 'No accelerometer specified for measurement! Multi-accelerometer configurations are not supported for this macro.' ) - accelerometer = Accelerometer(printer.lookup_object(accel_chip)) + accelerometer = Accelerometer(printer.lookup_object(accel_chip)) systime = printer.get_reactor().monotonic() toolhead = printer.lookup_object('toolhead') @@ -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() diff --git a/shaketune/measurement/motorsconfigparser.py b/shaketune/measurement/motorsconfigparser.py new file mode 100644 index 0000000..3dab656 --- /dev/null +++ b/shaketune/measurement/motorsconfigparser.py @@ -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 diff --git a/shaketune/measurement/static_freq.py b/shaketune/measurement/static_freq.py index b6bbf12..7bc41b1 100644 --- a/shaketune/measurement/static_freq.py +++ b/shaketune/measurement/static_freq.py @@ -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) diff --git a/shaketune/measurement/vibrations_profile.py b/shaketune/measurement/vibrations_profile.py new file mode 100644 index 0000000..f580d9f --- /dev/null +++ b/shaketune/measurement/vibrations_profile.py @@ -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() diff --git a/shaketune/post_processing/graph_creator.py b/shaketune/post_processing/graph_creator.py index 4902d37..d8af166 100644 --- a/shaketune/post_processing/graph_creator.py +++ b/shaketune/post_processing/graph_creator.py @@ -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') diff --git a/shaketune/shaketune.py b/shaketune/shaketune.py index 1c6ffe5..6999bd2 100644 --- a/shaketune/shaketune.py +++ b/shaketune/shaketune.py @@ -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) diff --git a/shaketune/shaketune_config.py b/shaketune/shaketune_config.py index bf0e96d..057900a 100644 --- a/shaketune/shaketune_config.py +++ b/shaketune/shaketune_config.py @@ -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)