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
This commit is contained in:
Oz Elentok
2024-05-09 00:02:23 +03:00
committed by GitHub
parent 303ed7060c
commit 3a0c0c4173
22 changed files with 169 additions and 133 deletions

View File

@@ -52,7 +52,7 @@ gcode:
ACCELEROMETER_MEASURE CHIP={accel_chip} NAME=axemap
RESPOND MSG="Analysis of the movements..."
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type axesmap --accel {accel|int} --chip_name {accel_chip}"
SHAKETUNE_POSTPROCESS PARAMS="--type axesmap --accel {accel|int} --chip_name {accel_chip}"
# Restore the previous acceleration values
SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_cruise_ratio} SQUARE_CORNER_VELOCITY={old_sqv}

View File

@@ -41,7 +41,7 @@ gcode:
RESPOND MSG="X axis frequency profile generation..."
RESPOND MSG="This may take some time (1-3min)"
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type shaper --scv {scv} {% if max_sm is not none %}--max_smoothing {max_sm}{% endif %} {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
SHAKETUNE_POSTPROCESS PARAMS="--type shaper --scv {scv} {% if max_sm is not none %}--max_smoothing {max_sm}{% endif %} {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
{% endif %}
{% if Y %}
@@ -50,5 +50,5 @@ gcode:
RESPOND MSG="Y axis frequency profile generation..."
RESPOND MSG="This may take some time (1-3min)"
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type shaper --scv {scv} {% if max_sm is not none %}--max_smoothing {max_sm}{% endif %} {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
SHAKETUNE_POSTPROCESS PARAMS="--type shaper --scv {scv} {% if max_sm is not none %}--max_smoothing {max_sm}{% endif %} {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
{% endif %}

View File

@@ -20,4 +20,4 @@ gcode:
RESPOND MSG="Belts comparative frequency profile generation..."
RESPOND MSG="This may take some time (3-5min)"
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type belts {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
SHAKETUNE_POSTPROCESS PARAMS="--type belts {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"

View File

@@ -209,6 +209,6 @@ gcode:
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}"
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

@@ -1,6 +0,0 @@
[gcode_shell_command shaketune]
command: ~/printer_data/config/K-ShakeTune/shaketune.sh
timeout: 600.0
verbose: True
[respond]

View File

@@ -17,9 +17,6 @@ Check out the **[detailed documentation of the Shake&Tune module here](./docs/RE
|:----------------:|:------------:|:---------------------:|
| [<img src="./docs/images/belts_example.png">](./docs/macros/belts_tuning.md) | [<img src="./docs/images/axis_example.png">](./docs/macros/axis_tuning.md) | [<img src="./docs/images/vibrations_example.png">](./docs/macros/vibrations_profile.md) |
> **Note**:
>
> Be aware that Shake&Tune uses the [Gcode shell command plugin](https://github.com/dw-0/kiauh/blob/master/docs/gcode_shell_command.md) under the hood to call the Python scripts that generate the graphs. While my scripts should be safe, the Gcode shell command plugin also has great potential for abuse if not used carefully for other purposes, since it opens shell access from Klipper.
## Installation
@@ -31,6 +28,7 @@ Follow these steps to install the Shake&Tune module in your printer:
```
1. Then, append the following to your `printer.cfg` file and restart Klipper (if prefered, you can include only the needed macros: using `*.cfg` is a convenient way to include them all at once):
```
[shaketune]
[include K-ShakeTune/*.cfg]
```

View File

@@ -3,9 +3,9 @@
USER_CONFIG_PATH="${HOME}/printer_data/config"
MOONRAKER_CONFIG="${HOME}/printer_data/config/moonraker.conf"
KLIPPER_PATH="${HOME}/klipper"
KLIPPER_VENV_PATH="${HOME}/klippy-env"
K_SHAKETUNE_PATH="${HOME}/klippain_shaketune"
K_SHAKETUNE_VENV_PATH="${HOME}/klippain_shaketune-env"
set -eu
export LC_ALL=C
@@ -39,7 +39,7 @@ function is_package_installed {
}
function install_package_requirements {
packages=("python3-venv" "libopenblas-dev" "libatlas-base-dev")
packages=("libopenblas-dev" "libatlas-base-dev")
packages_to_install=""
for package in "${packages[@]}"; do
@@ -76,14 +76,12 @@ function check_download {
}
function setup_venv {
if [ ! -d "${K_SHAKETUNE_VENV_PATH}" ]; then
echo "[SETUP] Creating Python virtual environment..."
python3 -m venv "${K_SHAKETUNE_VENV_PATH}"
else
echo "[SETUP] Virtual environment already exists. Continuing..."
if [ ! -d "${KLIPPER_VENV_PATH}" ]; then
echo "[ERROR] Klipper's Python virtual environment not found!"
exit -1
fi
source "${K_SHAKETUNE_VENV_PATH}/bin/activate"
source "${KLIPPER_VENV_PATH}/bin/activate"
echo "[SETUP] Installing/Updating K-Shake&Tune dependencies..."
pip install --upgrade pip
pip install -r "${K_SHAKETUNE_PATH}/requirements.txt"
@@ -98,16 +96,17 @@ function link_extension {
echo "[INSTALL] Klippain full installation found! Linking module to the script folder of Klippain"
ln -frsn ${K_SHAKETUNE_PATH}/K-ShakeTune ${USER_CONFIG_PATH}/scripts/K-ShakeTune
else
echo "[INSTALL] Klippain not found! Linking module to the config folder of Klipper"
ln -frsn ${K_SHAKETUNE_PATH}/K-ShakeTune ${USER_CONFIG_PATH}/K-ShakeTune
fi
}
function link_gcodeshellcommandpy {
if [ ! -f "${KLIPPER_PATH}/klippy/extras/gcode_shell_command.py" ]; then
echo "[INSTALL] Downloading gcode_shell_command.py Klipper extension needed for this module"
wget -P ${KLIPPER_PATH}/klippy/extras https://raw.githubusercontent.com/Frix-x/klippain/main/scripts/gcode_shell_command.py
function link_module {
if [ ! -d "${KLIPPER_PATH}/klippy/extras/shaketune" ]; then
echo "[INSTALL] Linking Shake&Tune module to Klipper extras"
ln -frsn ${K_SHAKETUNE_PATH}/shaketune ${KLIPPER_PATH}/klippy/extras/shaketune
else
printf "[INSTALL] gcode_shell_command.py Klipper extension is already installed. Continuing...\n\n"
printf "[INSTALL] Klippain Shake&Tune Klipper module is already installed. Continuing...\n\n"
fi
}
@@ -140,7 +139,7 @@ preflight_checks
check_download
setup_venv
link_extension
link_module
add_updater
link_gcodeshellcommandpy
restart_klipper
restart_moonraker

View File

@@ -4,7 +4,7 @@
type: git_repo
origin: https://github.com/Frix-x/klippain-shaketune.git
path: ~/klippain_shaketune
virtualenv: ~/klippain_shaketune-env
virtualenv: ~/klippy-env
requirements: requirements.txt
system_dependencies: system-dependencies.json
primary_branch: main

111
src/is_workflow.py → shaketune/__init__.py Executable file → Normal file
View File

@@ -5,29 +5,30 @@
############################################
# Written by Frix_x#0161 #
# This script is designed to be used with gcode_shell_commands directly from Klipper
# This script is designed to be run from inside Klipper Console
# Use the provided Shake&Tune macros instead!
import abc
import argparse
import os
import shutil
import tarfile
import threading
import traceback
from datetime import datetime
from pathlib import Path
from typing import Callable, Optional
from typing import Callable, List, Optional
from git import GitCommandError, Repo
from matplotlib.figure import Figure
import src.helpers.filemanager as fm
from src.graph_creators.analyze_axesmap import axesmap_calibration
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
from .graph_creators.analyze_axesmap import axesmap_calibration
from .graph_creators.graph_belts import belts_calibration
from .graph_creators.graph_shaper import shaper_calibration
from .graph_creators.graph_vibrations import vibrations_profile
from .helpers import filemanager as fm
from .helpers.motorlogparser import MotorLogParser
from .helpers.console_output import ConsoleOutput
class Config:
@@ -43,6 +44,8 @@ class Config:
@staticmethod
def get_git_version() -> str:
try:
from git import GitCommandError, Repo
# Get the absolute path of the script, resolving any symlinks
# Then get 1 times to parent dir to be at the git root folder
script_path = Path(__file__).resolve()
@@ -54,11 +57,11 @@ class Config:
version = repo.head.commit.hexsha[:7] # If no tag is found, use the simplified commit SHA instead
return version
except Exception as e:
print_with_c_locale(f'Warning: unable to retrieve Shake&Tune version number: {e}')
ConsoleOutput.print(f'Warning: unable to retrieve Shake&Tune version number: {e}')
return 'unknown'
@staticmethod
def parse_arguments() -> argparse.Namespace:
def parse_arguments(params: Optional[List] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description='Shake&Tune graphs generation script')
parser.add_argument(
'-t',
@@ -131,7 +134,7 @@ class Config:
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 {Config.get_git_version()}')
return parser.parse_args()
return parser.parse_args(params)
class GraphCreator(abc.ABC):
@@ -341,16 +344,21 @@ class VibrationsGraphCreator(GraphCreator):
tar_file.unlink(missing_ok=True)
class AxesMapFinder:
def __init__(self, accel: float, chip_name: str):
self._accel = accel
self._chip_name = chip_name
class AxesMapFinder(GraphCreator):
def __init__(self, keep_csv: bool = False, dpi: int = 150):
super().__init__(keep_csv, dpi)
self._graph_date = datetime.now().strftime('%Y%m%d_%H%M%S')
self._type = 'axesmap'
self._folder = Config.RESULTS_BASE_FOLDER
self._accel = None
self._chip_name = None
def configure(self, accel: int, chip_name: str) -> None:
self._accel = accel
self._chip_name = chip_name
def find_axesmap(self) -> None:
tmp_folder = Path('/tmp')
globbed_files = list(tmp_folder.glob(f'{self._chip_name}-*.csv'))
@@ -371,14 +379,21 @@ class AxesMapFinder:
with result_filename.open('w') as f:
f.write(results)
ConsoleOutput.print(f'Detected axes_map: {results}')
def main():
options = Config.parse_arguments()
def create_graph(self) -> None:
self.find_axesmap()
def clean_old_files(self, keep_results: int) -> None:
pass
def create_graph(options: argparse.Namespace) -> None:
fm.ensure_folders_exist(
folders=[Config.RESULTS_BASE_FOLDER / subfolder for subfolder in Config.RESULTS_SUBFOLDERS.values()]
)
print_with_c_locale(f'Shake&Tune version: {Config.get_git_version()}')
ConsoleOutput.print(f'Shake&Tune version: {Config.get_git_version()}')
graph_creators = {
'belts': (BeltsGraphCreator, None),
@@ -387,12 +402,12 @@ def main():
VibrationsGraphCreator,
lambda gc: gc.configure(options.kinematics, options.accel_used, options.chip_name, options.metadata),
),
'axesmap': (AxesMapFinder, None),
'axesmap': (AxesMapFinder, lambda gc: gc.configure(options.accel_used, options.chip_name)),
}
creator_info = graph_creators.get(options.type)
if not creator_info:
print_with_c_locale('Error: invalid graph type specified!')
ConsoleOutput.print('Error: invalid graph type specified!')
return
# Instantiate the graph creator
@@ -407,20 +422,54 @@ def main():
try:
graph_creator.create_graph()
except FileNotFoundError as e:
print_with_c_locale(f'FileNotFound error: {e}')
ConsoleOutput.print(f'FileNotFound error: {e}')
return
except TimeoutError as e:
print_with_c_locale(f'Timeout error: {e}')
ConsoleOutput.print(f'Timeout error: {e}')
return
except Exception as e:
print_with_c_locale(f'Error while generating the graphs: {e}')
traceback.print_exc()
ConsoleOutput.print(f'Error while generating the graphs: {e}\n{traceback.print_exc()}')
return
print_with_c_locale(f'{options.type} graphs created successfully!')
ConsoleOutput.print(f'{options.type} graphs created successfully!')
graph_creator.clean_old_files(options.keep_results)
print_with_c_locale(f'Cleaned output folder to keep only the last {options.keep_results} results!')
ConsoleOutput.print(f'Cleaned output folder to keep only the last {options.keep_results} results!')
if __name__ == '__main__':
main()
class ShakeTune:
def __init__(self, config) -> None:
self._printer = config.get_printer()
self._gcode = self._printer.lookup_object('gcode')
self.timeout = config.getfloat('timeout', 2.0, above=0.0)
ConsoleOutput.register_output_callback(self._gcode.respond_info)
self._gcode.register_command(
'SHAKETUNE_POSTPROCESS',
self.cmd_SHAKETUNE_POSTPROCESS,
desc='Post process data for ShakeTune graph creation',
)
def shaketune_thread(self, options):
try:
os.nice(20)
except Exception:
ConsoleOutput.print('Failed reducing ShakeTune thread priority, continuing.')
create_graph(options)
def cmd_SHAKETUNE_POSTPROCESS(self, gcmd) -> None:
options = Config.parse_arguments(gcmd.get('PARAMS').split())
t = threading.Thread(target=self.shaketune_thread, args=(options,))
t.start()
reactor = self._printer.get_reactor()
event_time = reactor.monotonic()
end_time = event_time + self.timeout
while event_time < end_time:
event_time = reactor.pause(event_time + 0.05)
if not t.is_alive():
break
def load_config(config) -> ShakeTune:
return ShakeTune(config)

10
shaketune/__main__.py Normal file
View File

@@ -0,0 +1,10 @@
from . import Config, create_graph
def main() -> None:
options = Config.parse_arguments()
create_graph(options)
if __name__ == '__main__':
main()

View File

@@ -10,7 +10,7 @@ import optparse
import numpy as np
from scipy.signal import butter, filtfilt
from ..helpers.locale_utils import print_with_c_locale
from ..helpers.console_output import ConsoleOutput
NUM_POINTS = 500
@@ -143,7 +143,7 @@ def main():
opts.error('Invalid acceleration value. It should be a numeric value.')
results = axesmap_calibration(args, accel_value)
print_with_c_locale(results)
ConsoleOutput.print(results)
if options.output is not None:
with open(options.output, 'w') as f:

View File

@@ -27,7 +27,7 @@ from ..helpers.common_func import (
parse_log,
setup_klipper_import,
)
from ..helpers.locale_utils import print_with_c_locale, set_locale
from ..helpers.console_output import ConsoleOutput
ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' # For paired peaks names
@@ -200,7 +200,7 @@ def plot_compare_frequency(ax, lognames, signal1, signal2, similarity_factor, ma
signal1_belt += ' (axis 1, 1)'
signal2_belt += ' (axis 1,-1)'
else:
print_with_c_locale(
ConsoleOutput.print(
"Warning: belts doesn't seem to have the correct name A and B (extracted from the filename.csv)"
)
@@ -453,7 +453,6 @@ def compute_signal_data(data, max_freq):
def belts_calibration(lognames, klipperdir='~/klipper', max_freq=200.0, st_version=None):
set_locale()
global shaper_calibrate
shaper_calibrate = setup_klipper_import(klipperdir)
@@ -479,13 +478,13 @@ def belts_calibration(lognames, klipperdir='~/klipper', max_freq=200.0, st_versi
similarity_factor = compute_curve_similarity_factor(
signal1.freqs, signal1.psd, signal2.freqs, signal2.psd, CURVE_SIMILARITY_SIGMOID_K
)
print_with_c_locale(f'Belts estimated similarity: {similarity_factor:.1f}%')
ConsoleOutput.print(f'Belts estimated similarity: {similarity_factor:.1f}%')
# Compute the MHI value from the differential spectrogram sum of gradient, salted with the similarity factor and the number of
# unpaired peaks from the belts frequency profile. Be careful, this value is highly opinionated and is pretty experimental!
mhi, textual_mhi = compute_mhi(
combined_sum, similarity_factor, len(signal1.unpaired_peaks) + len(signal2.unpaired_peaks)
)
print_with_c_locale(f'[experimental] Mechanical Health Indicator: {textual_mhi.lower()} ({mhi:.1f}%)')
ConsoleOutput.print(f'[experimental] Mechanical Health Indicator: {textual_mhi.lower()} ({mhi:.1f}%)')
# Create graph layout
fig, (ax1, ax2) = plt.subplots(
@@ -513,9 +512,7 @@ def belts_calibration(lognames, klipperdir='~/klipper', max_freq=200.0, st_versi
dt = datetime.strptime(f"{filename.split('_')[1]} {filename.split('_')[2]}", '%Y%m%d %H%M%S')
title_line2 = dt.strftime('%x %X')
except Exception:
print_with_c_locale(
'Warning: CSV filenames look to be different than expected (%s , %s)' % (lognames[0], lognames[1])
)
ConsoleOutput.print(f'Warning: CSV filenames look to be different than expected: {lognames}')
title_line2 = lognames[0].split('/')[-1] + ' / ' + lognames[1].split('/')[-1]
fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])

View File

@@ -27,7 +27,7 @@ from ..helpers.common_func import (
parse_log,
setup_klipper_import,
)
from ..helpers.locale_utils import print_with_c_locale, set_locale
from ..helpers.console_output import ConsoleOutput
PEAKS_DETECTION_THRESHOLD = 0.05
PEAKS_EFFECT_THRESHOLD = 0.12
@@ -72,19 +72,19 @@ def calibrate_shaper(datas, max_smoothing, scv, max_freq):
max_smoothing=max_smoothing,
test_damping_ratios=None,
max_freq=max_freq,
logger=print_with_c_locale,
logger=ConsoleOutput.print,
)
except TypeError:
print_with_c_locale(
ConsoleOutput.print(
'[WARNING] You seem to be using an older version of Klipper that is not compatible with all the latest Shake&Tune features!'
)
print_with_c_locale(
ConsoleOutput.print(
'Shake&Tune now runs in compatibility mode: be aware that the results may be slightly off, since the real damping ratio cannot be used to create the filter recommendations'
)
compat = True
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, print_with_c_locale)
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, ConsoleOutput.print)
print_with_c_locale(
ConsoleOutput.print(
'\n-> Recommended shaper is %s @ %.1f Hz (when using a square corner velocity of %.1f and a damping ratio of %.3f)'
% (shaper.name.upper(), shaper.freq, scv, zeta)
)
@@ -295,14 +295,13 @@ def plot_spectrogram(ax, t, bins, pdata, peaks, max_freq):
def shaper_calibration(lognames, klipperdir='~/klipper', max_smoothing=None, scv=5.0, max_freq=200.0, st_version=None):
set_locale()
global shaper_calibrate
shaper_calibrate = setup_klipper_import(klipperdir)
# Parse data from the log files while ignoring CSV in the wrong format
datas = [data for data in (parse_log(fn) for fn in lognames) if data is not None]
if len(datas) > 1:
print_with_c_locale('Warning: incorrect number of .csv files detected. Only the first one will be used!')
ConsoleOutput.print('Warning: incorrect number of .csv files detected. Only the first one will be used!')
# Compute shapers, PSD outputs and spectrogram
performance_shaper, shapers, calibration_data, fr, zeta, compat = calibrate_shaper(
@@ -329,7 +328,7 @@ def shaper_calibration(lognames, klipperdir='~/klipper', max_smoothing=None, scv
# Print the peaks info in the console
peak_freqs_formated = ['{:.1f}'.format(f) for f in peaks_freqs]
num_peaks_above_effect_threshold = np.sum(calibration_data.psd_sum[peaks] > peaks_threshold[1])
print_with_c_locale(
ConsoleOutput.print(
'\nPeaks detected on the graph: %d @ %s Hz (%d above effect threshold)'
% (num_peaks, ', '.join(map(str, peak_freqs_formated)), num_peaks_above_effect_threshold)
)
@@ -366,7 +365,7 @@ def shaper_calibration(lognames, klipperdir='~/klipper', max_smoothing=None, scv
title_line3 = '| Square corner velocity: ' + str(scv) + 'mm/s'
title_line4 = '| Max allowed smoothing: ' + str(max_smoothing)
except Exception:
print_with_c_locale('Warning: CSV filename look to be different than expected (%s)' % (lognames[0]))
ConsoleOutput.print('Warning: CSV filename look to be different than expected (%s)' % (lognames[0]))
title_line2 = lognames[0].split('/')[-1]
title_line3 = ''
title_line4 = ''

View File

@@ -28,7 +28,7 @@ from ..helpers.common_func import (
parse_log,
setup_klipper_import,
)
from ..helpers.locale_utils import print_with_c_locale, set_locale
from ..helpers.console_output import ConsoleOutput
PEAKS_DETECTION_THRESHOLD = 0.05
PEAKS_RELATIVE_HEIGHT_THRESHOLD = 0.04
@@ -453,19 +453,19 @@ def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_pro
# Then add the motor resonance peak to the graph and print some infos about it
motor_fr, motor_zeta, motor_res_idx, lowfreq_max = compute_mechanical_parameters(global_motor_profile, freqs, 30)
if lowfreq_max:
print_with_c_locale(
ConsoleOutput.print(
'[WARNING] There are a lot of low frequency vibrations that can alter the readings. This is probably due to the test being performed at too high an acceleration!'
)
print_with_c_locale(
ConsoleOutput.print(
'Try lowering the ACCEL value and/or increasing the SIZE value before restarting the macro to ensure that only constant speeds are being recorded and that the dynamic behavior of the machine is not affecting the measurements'
)
if motor_zeta is not None:
print_with_c_locale(
ConsoleOutput.print(
'Motors have a main resonant frequency at %.1fHz with an estimated damping ratio of %.3f'
% (motor_fr, motor_zeta)
)
else:
print_with_c_locale(
ConsoleOutput.print(
'Motors have a main resonant frequency at %.1fHz but it was impossible to estimate a damping ratio.'
% (motor_fr)
)
@@ -634,7 +634,6 @@ def extract_angle_and_speed(logname):
def vibrations_profile(
lognames, klipperdir='~/klipper', kinematics='cartesian', accel=None, max_freq=1000.0, st_version=None, motors=None
):
set_locale()
global shaper_calibrate
shaper_calibrate = setup_klipper_import(klipperdir)
@@ -686,7 +685,7 @@ def vibrations_profile(
# symmetry_factor = compute_symmetry_analysis(all_angles, all_angles_energy)
symmetry_factor = compute_symmetry_analysis(all_angles, spectrogram_data, main_angles)
print_with_c_locale(f'Machine estimated vibration symmetry: {symmetry_factor:.1f}%')
ConsoleOutput.print(f'Machine estimated vibration symmetry: {symmetry_factor:.1f}%')
# Analyze low variance ranges of vibration energy across all angles for each speed to identify clean speeds
# and highlight them. Also find the peaks to identify speeds to avoid due to high resonances
@@ -699,7 +698,7 @@ def vibrations_profile(
10,
)
formated_peaks_speeds = ['{:.1f}'.format(pspeed) for pspeed in peaks_speeds]
print_with_c_locale(
ConsoleOutput.print(
'Vibrations peaks detected: %d @ %s mm/s (avoid setting a speed near these values in your slicer print profile)'
% (num_peaks, ', '.join(map(str, formated_peaks_speeds)))
)
@@ -713,16 +712,16 @@ def vibrations_profile(
good_speeds = filter_and_split_ranges(all_speeds, good_speeds, peak_speed_indices, deletion_range)
# Add some logging about the good speeds found
print_with_c_locale(f'Lowest vibrations speeds ({len(good_speeds)} ranges sorted from best to worse):')
ConsoleOutput.print(f'Lowest vibrations speeds ({len(good_speeds)} ranges sorted from best to worse):')
for idx, (start, end, _) in enumerate(good_speeds):
print_with_c_locale(f'{idx+1}: {all_speeds[start]:.1f} to {all_speeds[end]:.1f} mm/s')
ConsoleOutput.print(f'{idx+1}: {all_speeds[start]:.1f} to {all_speeds[end]:.1f} mm/s')
# Angle low energy valleys identification (good angles ranges) and print them to the console
good_angles = identify_low_energy_zones(all_angles_energy, ANGLES_VALLEY_DETECTION_THRESHOLD)
if good_angles is not None:
print_with_c_locale(f'Lowest vibrations angles ({len(good_angles)} ranges sorted from best to worse):')
ConsoleOutput.print(f'Lowest vibrations angles ({len(good_angles)} ranges sorted from best to worse):')
for idx, (start, end, energy) in enumerate(good_angles):
print_with_c_locale(
ConsoleOutput.print(
f'{idx+1}: {all_angles[start]:.1f}° to {all_angles[end]:.1f}° (mean vibrations energy: {energy:.2f}% of max)'
)
@@ -763,7 +762,7 @@ def vibrations_profile(
if accel is not None:
title_line2 += ' at ' + str(accel) + ' mm/s² -- ' + kinematics.upper() + ' kinematics'
except Exception:
print_with_c_locale('Warning: CSV filenames appear to be different than expected (%s)' % (lognames[0]))
ConsoleOutput.print('Warning: CSV filenames appear to be different than expected (%s)' % (lognames[0]))
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'])
@@ -772,7 +771,7 @@ def vibrations_profile(
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}')
ConsoleOutput.print(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)

View File

Before

Width:  |  Height:  |  Size: 607 KiB

After

Width:  |  Height:  |  Size: 607 KiB

View File

@@ -10,8 +10,8 @@ from importlib import import_module
from pathlib import Path
import numpy as np
from git import GitCommandError, Repo
from scipy.signal import spectrogram
from .console_output import ConsoleOutput
def parse_log(logname):
@@ -23,7 +23,7 @@ def parse_log(logname):
# Check for a PSD file generated by Klipper and raise a warning
if cleaned_line.startswith('#freq,psd_x,psd_y,psd_z,psd_xyz'):
print(
ConsoleOutput.print(
'Warning: %s does not contain raw accelerometer data. '
'Please use the official Klipper script to process it instead. '
'It will be ignored by Shake&Tune!' % (logname,)
@@ -36,7 +36,7 @@ def parse_log(logname):
break
if not header:
print(
ConsoleOutput.print(
'Warning: file %s has an incorrect header and will be ignored by Shake&Tune!\n'
"Expected '#time,accel_x,accel_y,accel_z', but got '%s'." % (logname, header.strip())
)
@@ -45,7 +45,7 @@ def parse_log(logname):
# If we have the correct raw data header, proceed to load the data
data = np.loadtxt(logname, comments='#', delimiter=',', skiprows=1)
if data.ndim == 1 or data.shape[1] != 4:
print(
ConsoleOutput.print(
'Warning: %s does not have the correct data format; expected 4 columns. '
'It will be ignored by Shake&Tune!' % (logname,)
)
@@ -54,7 +54,7 @@ def parse_log(logname):
return data
except Exception as err:
print(f'Error while reading {logname}: {err}. It will be ignored by Shake&Tune!')
ConsoleOutput.print(f'Error while reading {logname}: {err}. It will be ignored by Shake&Tune!')
return None
@@ -69,6 +69,7 @@ def get_git_version():
try:
# Get the absolute path of the script, resolving any symlinks
# Then get 2 times to parent dir to be at the git root folder
from git import GitCommandError, Repo
script_path = Path(__file__).resolve()
repo_path = script_path.parents[1]
repo = Repo(repo_path)

View File

@@ -0,0 +1,24 @@
import io
from typing import Callable, Optional
class ConsoleOutput:
"""
Print output to stdout or to an alternative like the Klipper console through a callback
"""
_output_func: Optional[Callable[[str], None]] = None
@classmethod
def register_output_callback(cls, output_func: Optional[Callable[[str], None]]):
cls._output_func = output_func
@classmethod
def print(cls, *args, **kwargs):
if not cls._output_func:
print(*args, **kwargs)
return
with io.StringIO() as mem_output:
print(*args, file=mem_output, **kwargs)
cls._output_func(mem_output.getvalue())

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env python3
# Special utility functions to manage locale settings and printing
# Written by Frix_x#0161 #
import locale
# Set the best locale for time and date formating (generation of the titles)
def set_locale():
try:
current_locale = locale.getlocale(locale.LC_TIME)
if current_locale is None or current_locale[0] is None:
locale.setlocale(locale.LC_TIME, 'C')
except locale.Error:
locale.setlocale(locale.LC_TIME, 'C')
# Print function to avoid problem in Klipper console (that doesn't support special characters) due to locale settings
def print_with_c_locale(*args, **kwargs):
try:
original_locale = locale.getlocale()
locale.setlocale(locale.LC_ALL, 'C')
except locale.Error as e:
print(
'Warning: Failed to set a basic locale. Special characters may not display correctly in Klipper console:', e
)
finally:
print(*args, **kwargs) # Proceed with printing regardless of locale setting success
try:
locale.setlocale(locale.LC_ALL, original_locale)
except locale.Error as e:
print('Warning: Failed to restore the original locale setting:', e)