Motor info added to the vibration graphs (#93)
and reduced global vibration generation time by reducing segment lenghts
This commit is contained in:
@@ -122,7 +122,7 @@ gcode:
|
||||
# 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/3 + 2/3 * (curr_speed / 60) / 100 %}
|
||||
{% set segment_length_multiplier = 1/5 + 4/5 * (curr_speed / 60) / 100 %}
|
||||
{% else %}
|
||||
{% set segment_length_multiplier = 1 %}
|
||||
{% endif %}
|
||||
@@ -156,12 +156,59 @@ gcode:
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
RESPOND MSG="Machine vibrations profile generation..."
|
||||
RESPOND MSG="This may take some time (3-5min)"
|
||||
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type vibrations --accel {accel|int} --kinematics {kinematics} --chip_name {accel_chip} {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
|
||||
|
||||
# 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)"
|
||||
RUN_SHELL_COMMAND CMD=shaketune 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
|
||||
|
||||
@@ -560,6 +560,57 @@ def plot_vibration_spectrogram(ax, angles, speeds, spectrogram_data, peaks):
|
||||
return
|
||||
|
||||
|
||||
def plot_motor_config_txt(fig, motors, differences):
|
||||
motor_details = [(motors[0], 'X motor'), (motors[1], 'Y motor')]
|
||||
|
||||
distance = 0.12
|
||||
if motors[0].get_property('autotune_enabled'):
|
||||
distance = 0.24
|
||||
config_blocks = [
|
||||
f"| {lbl}: {mot.get_property('motor').upper()} on {mot.get_property('tmc').upper()} @ {mot.get_property('voltage')}V {mot.get_property('run_current')}A"
|
||||
for mot, lbl in motor_details
|
||||
]
|
||||
config_blocks.append('| TMC Autotune enabled')
|
||||
else:
|
||||
config_blocks = [
|
||||
f"| {lbl}: {mot.get_property('tmc').upper()} @ {mot.get_property('run_current')}A"
|
||||
for mot, lbl in motor_details
|
||||
]
|
||||
config_blocks.append('| TMC Autotune not detected')
|
||||
|
||||
for idx, block in enumerate(config_blocks):
|
||||
fig.text(
|
||||
0.40, 0.990 - 0.015 * idx, block, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple']
|
||||
)
|
||||
|
||||
tmc_registers = motors[0].get_registers()
|
||||
idx = -1
|
||||
for idx, (register, settings) in enumerate(tmc_registers.items()):
|
||||
settings_str = ' '.join(f'{k}={v}' for k, v in settings.items())
|
||||
tmc_block = f'| {register.upper()}: {settings_str}'
|
||||
fig.text(
|
||||
0.40 + distance,
|
||||
0.990 - 0.015 * idx,
|
||||
tmc_block,
|
||||
ha='left',
|
||||
va='top',
|
||||
fontsize=10,
|
||||
color=KLIPPAIN_COLORS['dark_purple'],
|
||||
)
|
||||
|
||||
if differences is not None:
|
||||
differences_text = f'| Y motor diff: {differences}'
|
||||
fig.text(
|
||||
0.40 + distance,
|
||||
0.990 - 0.015 * (idx + 1),
|
||||
differences_text,
|
||||
ha='left',
|
||||
va='top',
|
||||
fontsize=10,
|
||||
color=KLIPPAIN_COLORS['dark_purple'],
|
||||
)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Startup and main routines
|
||||
######################################################################
|
||||
@@ -577,7 +628,7 @@ def extract_angle_and_speed(logname):
|
||||
|
||||
|
||||
def vibrations_profile(
|
||||
lognames, klipperdir='~/klipper', kinematics='cartesian', accel=None, max_freq=1000.0, st_version=None
|
||||
lognames, klipperdir='~/klipper', kinematics='cartesian', accel=None, max_freq=1000.0, st_version=None, motors=None
|
||||
):
|
||||
set_locale()
|
||||
global shaper_calibrate
|
||||
@@ -710,6 +761,13 @@ def vibrations_profile(
|
||||
title_line2 = lognames[0].split('/')[-1]
|
||||
fig.text(0.060, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
||||
|
||||
# Add the motors infos to the top of the graph
|
||||
if motors is not None and len(motors) == 2:
|
||||
differences = motors[0].compare_to(motors[1])
|
||||
plot_motor_config_txt(fig, motors, differences)
|
||||
if differences is not None and kinematics == 'corexy':
|
||||
print_with_c_locale(f'Warning: motors have different TMC configurations!\n{differences}')
|
||||
|
||||
# Plot the graphs
|
||||
plot_angle_profile_polar(ax1, all_angles, all_angles_energy, good_angles, symmetry_factor)
|
||||
plot_vibration_spectrogram_polar(ax4, all_angles, all_speeds, spectrogram_data)
|
||||
|
||||
205
src/helpers/motorlogparser.py
Normal file
205
src/helpers/motorlogparser.py
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/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)
|
||||
@@ -26,10 +26,12 @@ from src.graph_creators.graph_belts import belts_calibration
|
||||
from src.graph_creators.graph_shaper import shaper_calibration
|
||||
from src.graph_creators.graph_vibrations import vibrations_profile
|
||||
from src.helpers.locale_utils import print_with_c_locale
|
||||
from src.helpers.motorlogparser import MotorLogParser
|
||||
|
||||
|
||||
class Config:
|
||||
KLIPPER_FOLDER = Path.home() / 'klipper'
|
||||
KLIPPER_LOG_FOLDER = Path.home() / 'printer_data/logs'
|
||||
RESULTS_BASE_FOLDER = Path.home() / 'printer_data/config/K-ShakeTune_results'
|
||||
RESULTS_SUBFOLDERS = {'belts': 'belts', 'shaper': 'inputshaper', 'vibrations': 'vibrations'}
|
||||
|
||||
@@ -102,6 +104,13 @@ class Config:
|
||||
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',
|
||||
@@ -279,14 +288,18 @@ 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) -> None:
|
||||
def configure(self, kinematics: str, accel: float, chip_name: str, metadata: str) -> None:
|
||||
self._kinematics = kinematics
|
||||
self._accel = accel
|
||||
self._chip_name = chip_name
|
||||
|
||||
parser = MotorLogParser(Config.KLIPPER_LOG_FOLDER / 'klippy.log', metadata)
|
||||
self._motors = parser.get_motors()
|
||||
|
||||
def _archive_files(self, lognames: list[Path]) -> None:
|
||||
tar_path = self._folder / f'{self._type}_{self._graph_date}.tar.gz'
|
||||
with tarfile.open(tar_path, 'w:gz') as tar:
|
||||
@@ -308,6 +321,7 @@ class VibrationsGraphCreator(GraphCreator):
|
||||
kinematics=self._kinematics,
|
||||
accel=self._accel,
|
||||
st_version=self._version,
|
||||
motors=self._motors,
|
||||
)
|
||||
self._save_figure_and_cleanup(fig, lognames)
|
||||
|
||||
@@ -369,7 +383,7 @@ def main():
|
||||
'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),
|
||||
lambda gc: gc.configure(options.kinematics, options.accel_used, options.chip_name, options.metadata),
|
||||
),
|
||||
'axesmap': (AxesMapFinder, None),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user