206 lines
7.7 KiB
Python
206 lines
7.7 KiB
Python
#!/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)
|