Files
klippain-shaketune-telegramm/shaketune/helpers/motorlogparser.py
Oz Elentok 3a0c0c4173 Run ShakeTune as an in-process Klipper module (#100)
* feat: Run ShakeTune as an in-process Klipper module
* feat: install shaketune dependencies to klipper venv
* refactor: replace print_with_c_locale with klipper console output with stdout fallback
2024-05-08 23:02:23 +02:00

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)