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:
@@ -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}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[gcode_shell_command shaketune]
|
||||
command: ~/printer_data/config/K-ShakeTune/shaketune.sh
|
||||
timeout: 600.0
|
||||
verbose: True
|
||||
|
||||
[respond]
|
||||
@@ -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]
|
||||
```
|
||||
|
||||
|
||||
27
install.sh
27
install.sh
@@ -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
|
||||
|
||||
@@ -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
111
src/is_workflow.py → shaketune/__init__.py
Executable file → Normal 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
10
shaketune/__main__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from . import Config, create_graph
|
||||
|
||||
|
||||
def main() -> None:
|
||||
options = Config.parse_arguments()
|
||||
create_graph(options)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -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:
|
||||
@@ -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'])
|
||||
|
||||
@@ -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 = ''
|
||||
@@ -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)
|
||||
|
Before Width: | Height: | Size: 607 KiB After Width: | Height: | Size: 607 KiB |
@@ -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)
|
||||
24
shaketune/helpers/console_output.py
Normal file
24
shaketune/helpers/console_output.py
Normal 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())
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user