17 Commits
v4.0.1 ... main

Author SHA1 Message Date
af9fee36ed Update README.md
Some checks failed
Smoke Tests / Klippy Tests (DangerKlippers/danger-klipper) (push) Has been cancelled
Smoke Tests / Klippy Tests (klipper3d/klipper) (push) Has been cancelled
Smoke Tests / lint (push) Has been cancelled
2024-08-01 16:52:24 +03:00
6b2cedfa28 Обновить README.md
Some checks are pending
Smoke Tests / Klippy Tests (DangerKlippers/danger-klipper) (push) Waiting to run
Smoke Tests / Klippy Tests (klipper3d/klipper) (push) Waiting to run
Smoke Tests / lint (push) Waiting to run
2024-08-01 01:04:04 +03:00
04ff95921e Обновить install.sh
Some checks are pending
Smoke Tests / Klippy Tests (DangerKlippers/danger-klipper) (push) Waiting to run
Smoke Tests / Klippy Tests (klipper3d/klipper) (push) Waiting to run
Smoke Tests / lint (push) Waiting to run
2024-08-01 00:58:47 +03:00
871dd72a88 Обновить moonraker.conf
Some checks are pending
Smoke Tests / Klippy Tests (DangerKlippers/danger-klipper) (push) Waiting to run
Smoke Tests / Klippy Tests (klipper3d/klipper) (push) Waiting to run
Smoke Tests / lint (push) Waiting to run
2024-08-01 00:34:32 +03:00
Félix Boisselier
66f5e32e4c typo in doc 2024-07-01 12:42:59 +02:00
Félix Boisselier
c12653e1f7 Merge pull request #138 from Frix-x/develop
v4.1.0
2024-06-30 22:41:30 +02:00
Félix Boisselier
8cf81bcb44 better sync of the peaks pair for close frequencies 2024-06-30 22:41:06 +02:00
Félix Boisselier
92a651b6a6 switched to pearson coefficient for belts similarity 2024-06-30 22:27:46 +02:00
Félix Boisselier
6712506862 fixed potential out of bounds error in belt graphs 2024-06-30 20:30:05 +02:00
Félix Boisselier
6e49c2c607 inverted belts colors to revert the behavior as pre-v4 2024-06-30 11:14:14 +02:00
Félix Boisselier
4a99e95882 Merge pull request #136 from Frix-x/develop
v4.0.2
2024-06-27 22:33:20 +02:00
Félix Boisselier
f5a74c29e1 fixed pyproject.toml project name 2024-06-27 22:25:04 +02:00
Aaron Haun
f87713eacd feat: automated testing GitHub action (#134) 2024-06-27 18:35:07 +02:00
Félix Boisselier
f045b8a49e fixed a mistake about some code that shouldn't be here... 2024-06-27 18:31:41 +02:00
Félix Boisselier
37d0e39d84 updated commands descriptions 2024-06-20 21:36:14 +02:00
Félix Boisselier
50ed13ca59 using Klipper reactor for file write process handling 2024-06-20 11:59:19 +02:00
delisjr
90ed7aca3c Compatibility with Klipper < v0.12.0-239 (#129) 2024-06-19 09:59:54 +02:00
17 changed files with 267 additions and 95 deletions

69
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: Smoke Tests
on:
workflow_dispatch:
push:
jobs:
klippy_testing:
name: Klippy Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
klipper_repo:
- klipper3d/klipper
- DangerKlippers/danger-klipper
steps:
- name: Checkout shaketune
uses: actions/checkout@v4
with:
path: shaketune
- name: Checkout Klipper
uses: actions/checkout@v4
with:
path: klipper
repository: ${{ matrix.klipper_repo }}
ref: master
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential
- name: Build klipper dict
run: |
pushd klipper
cp ../shaketune/ci/smoke-test/klipper-smoketest.kconfig .config
make olddefconfig
make out/compile_time_request.o
popd
- name: Setup klippy env
run: |
python3 -m venv --prompt klippy klippy-env
./klippy-env/bin/python -m pip install -r klipper/scripts/klippy-requirements.txt
./klippy-env/bin/python -m pip install -r shaketune/requirements.txt
- name: Install shaketune
run: |
ln -s $PWD/shaketune/shaketune $PWD/klipper/klippy/extras/shaketune
- name: Klipper import test
run: |
./klippy-env/bin/python klipper/klippy/klippy.py --import-test
- name: Klipper integrated test
run: |
pushd klipper
mkdir ../dicts
cp ../klipper/out/klipper.dict ../dicts/linux_basic.dict
../klippy-env/bin/python scripts/test_klippy.py -d ../dicts ../shaketune/ci/smoke-test/klippy-tests/simple.test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
cache: 'pip'
- name: install ruff
run: |
pip install ruff
- name: run ruff tests
run: |
ruff check

View File

@@ -13,7 +13,7 @@ Follow these steps to install Shake&Tune on your printer:
1. Be sure to have a working accelerometer on your machine and a `[resonance_tester]` section defined. You can follow the official [Measuring Resonances Klipper documentation](https://www.klipper3d.org/Measuring_Resonances.html) to configure it.
1. Install Shake&Tune by running over SSH on your printer:
```bash
wget -O - https://raw.githubusercontent.com/Frix-x/klippain-shaketune/main/install.sh | bash
wget -O - https://cloud.reijii.org/gitea/reijii/klippain-shaketune-telegramm/raw/branch/main/install.sh | bash
```
1. Then, append the following to your `printer.cfg` file and restart Klipper:
```

View File

@@ -0,0 +1,34 @@
CONFIG_LOW_LEVEL_OPTIONS=y
# CONFIG_MACH_AVR is not set
# CONFIG_MACH_ATSAM is not set
# CONFIG_MACH_ATSAMD is not set
# CONFIG_MACH_LPC176X is not set
# CONFIG_MACH_STM32 is not set
# CONFIG_MACH_HC32F460 is not set
# CONFIG_MACH_RP2040 is not set
# CONFIG_MACH_PRU is not set
# CONFIG_MACH_AR100 is not set
CONFIG_MACH_LINUX=y
# CONFIG_MACH_SIMU is not set
CONFIG_BOARD_DIRECTORY="linux"
CONFIG_CLOCK_FREQ=50000000
CONFIG_LINUX_SELECT=y
CONFIG_USB_VENDOR_ID=0x1d50
CONFIG_USB_DEVICE_ID=0x614e
CONFIG_USB_SERIAL_NUMBER="12345"
CONFIG_WANT_GPIO_BITBANGING=y
CONFIG_WANT_DISPLAYS=y
CONFIG_WANT_SENSORS=y
CONFIG_WANT_LIS2DW=y
CONFIG_WANT_LDC1612=y
CONFIG_WANT_SOFTWARE_I2C=y
CONFIG_WANT_SOFTWARE_SPI=y
CONFIG_NEED_SENSOR_BULK=y
CONFIG_CANBUS_FREQUENCY=1000000
CONFIG_INITIAL_PINS=""
CONFIG_HAVE_GPIO=y
CONFIG_HAVE_GPIO_ADC=y
CONFIG_HAVE_GPIO_SPI=y
CONFIG_HAVE_GPIO_I2C=y
CONFIG_HAVE_GPIO_HARD_PWM=y
CONFIG_INLINE_STEPPER_HACK=y

View File

@@ -0,0 +1,9 @@
[mcu]
serial: /tmp/klipper_host_mcu
[printer]
kinematics: none
max_velocity: 300
max_accel: 300
[shaketune]

View File

@@ -0,0 +1,4 @@
DICTIONARY linux_basic.dict
CONFIG simple.cfg
G4 P1000

View File

@@ -39,9 +39,9 @@ Aside from the actual belt tension, the resonant frequency/amplitude of the curv
The Cross-Belts plot is an innovative cool way to compare the frequency profiles of the belts at every frequency point. In this plot, each point marks the amplitude response of each belt at different frequencies, connected point by point to trace the frequency spectrum. Ideally, these points should align on the diagonal center line, indicating that both belts have matching energy response values at each frequency.
The good zone, wider at the bottom (low-amplitude regions where the deviation doesn't matter much) and narrower at the top right (high-energy region where the main peaks lie), represents acceptable deviations. So **you want all points to be close to the ideal center line and as many as possible within the green zone**, as this means that the bands are well tuned and behave similarly.
The good zone, wider at the bottom (low-amplitude regions where the deviation doesn't matter much) and narrower at the top right (high-energy region where the main peaks lie), represents acceptable deviations. So **you want all points to be close to the ideal center line and as many as possible within the green zone**, as this means that the belts are well tuned and behave similarly.
Paired peaks of exactly the same frequency will be on the same point (labeled α1/α2, β1/β2, ...) and the distance from the center line will show the difference in energy. For paired peaks that also have a frequency delta between them, they are displayed as two points (labeled α1 and α2, ...) and the additional distance between them along the plotted line represents their frequency delta.
Paired peaks at the same frequency will be on the same point (labeled α1/α2, β1/β2, ...) and the distance from the center line will show the difference in energy. For paired peaks that also have a frequency delta between them, they are displayed as two points (labeled α1 and α2, ...) and the additional distance between them along the plotted line represents their frequency delta.
### Estimated similarity and mechanical issues indicator

View File

@@ -64,7 +64,7 @@ function check_download {
if [ ! -d "${K_SHAKETUNE_PATH}" ]; then
echo "[DOWNLOAD] Downloading Klippain Shake&Tune module repository..."
if git -C $shaketunedirname clone https://github.com/Frix-x/klippain-shaketune.git $shaketunebasename; then
if git -C $shaketunedirname clone https://cloud.reijii.org/gitea/reijii/klippain-shaketune-telegramm.git $shaketunebasename; then
chmod +x ${K_SHAKETUNE_PATH}/install.sh
printf "[DOWNLOAD] Download complete!\n\n"
else

View File

@@ -1,8 +1,7 @@
## Klippain Shake&Tune automatic update management
[update_manager Klippain-ShakeTune]
type: git_repo
origin: https://github.com/Frix-x/klippain-shaketune.git
origin: https://cloud.reijii.org/gitea/reijii/klippain-shaketune-telegramm.git
path: ~/klippain_shaketune
virtualenv: ~/klippy-env
requirements: requirements.txt

View File

@@ -1,5 +1,5 @@
[project]
name = "Shake&Tune"
name = "shake_n_tune"
description = "Klipper streamlined input shaper workflow and calibration tools"
readme = "README.md"
requires-python = ">= 3.9"

View File

@@ -13,10 +13,13 @@ import os
import time
from multiprocessing import Process, Queue
FILE_WRITE_TIMEOUT = 10 # seconds
class Accelerometer:
def __init__(self, klipper_accelerometer):
def __init__(self, reactor, klipper_accelerometer):
self._k_accelerometer = klipper_accelerometer
self._reactor = reactor
self._bg_client = None
self._write_queue = Queue()
@@ -70,16 +73,35 @@ class Accelerometer:
os.nice(20)
except Exception:
pass
with open(filename, 'w') as f:
f.write('#time,accel_x,accel_y,accel_z\n')
samples = bg_client.samples or bg_client.get_samples()
for t, accel_x, accel_y, accel_z in samples:
f.write(f'{t:.6f},{accel_x:.6f},{accel_y:.6f},{accel_z:.6f}\n')
self._write_queue.get()
def wait_for_file_writes(self):
while not self._write_queue.empty():
time.sleep(0.1)
eventtime = self._reactor.monotonic()
self._reactor.pause(eventtime + 0.1)
for proc in self._write_processes:
proc.join()
if proc is None:
continue
eventtime = self._reactor.monotonic()
endtime = eventtime + FILE_WRITE_TIMEOUT
complete = False
while eventtime < endtime:
eventtime = self._reactor.pause(eventtime + 0.05)
if not proc.is_alive():
complete = True
break
if not complete:
raise TimeoutError(
'Shake&Tune was not able to write the accelerometer data into the CSV file. '
'This might be due to a slow SD card or a busy or full filesystem.'
)
self._write_processes = []

View File

@@ -37,15 +37,21 @@ def axes_map_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
raise gcmd.error(
f'The parameter axes_map is already set in your {accel_chip} configuration! Please remove it (or set it to "x,y,z")!'
)
accelerometer = Accelerometer(k_accelerometer)
accelerometer = Accelerometer(printer.get_reactor(), k_accelerometer)
toolhead_info = toolhead.get_status(systime)
old_accel = toolhead_info['max_accel']
old_mcr = toolhead_info['minimum_cruise_ratio']
old_sqv = toolhead_info['square_corner_velocity']
# set the wanted acceleration values
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY=5.0')
if 'minimum_cruise_ratio' in toolhead_info:
old_mcr = toolhead_info['minimum_cruise_ratio'] # minimum_cruise_ratio found: Klipper >= v0.12.0-239
gcode.run_script_from_command(
f'SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY=5.0'
)
else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239
old_mcr = None
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={accel} SQUARE_CORNER_VELOCITY=5.0')
# Deactivate input shaper if it is active to get raw movements
input_shaper = printer.lookup_object('input_shaper', None)
@@ -89,9 +95,13 @@ def axes_map_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
input_shaper.enable_shaping()
# Restore the previous acceleration values
gcode.run_script_from_command(
f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}'
)
if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239
gcode.run_script_from_command(
f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}'
)
else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} SQUARE_CORNER_VELOCITY={old_sqv}')
toolhead.wait_moves()
# Run post-processing

View File

@@ -76,8 +76,12 @@ def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
# set the needed acceleration values for the test
toolhead_info = toolhead.get_status(systime)
old_accel = toolhead_info['max_accel']
old_mcr = toolhead_info['minimum_cruise_ratio']
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel} MINIMUM_CRUISE_RATIO=0')
if 'minimum_cruise_ratio' in toolhead_info: # minimum_cruise_ratio found: Klipper >= v0.12.0-239
old_mcr = toolhead_info['minimum_cruise_ratio']
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel} MINIMUM_CRUISE_RATIO=0')
else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239
old_mcr = None
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel}')
# Deactivate input shaper if it is active to get raw movements
input_shaper = printer.lookup_object('input_shaper', None)
@@ -95,7 +99,7 @@ def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
accel_chip = Accelerometer.find_axis_accelerometer(printer, config['axis'])
if accel_chip is None:
raise gcmd.error('No suitable accelerometer found for measurement!')
accelerometer = Accelerometer(printer.lookup_object(accel_chip))
accelerometer = Accelerometer(printer.get_reactor(), printer.lookup_object(accel_chip))
# Then do the actual measurements
accelerometer.start_measurement()
@@ -117,4 +121,7 @@ def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
input_shaper.enable_shaping()
# Restore the previous acceleration values
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr}')
if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr}')
else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel}')

View File

@@ -60,7 +60,7 @@ def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> None:
raise gcmd.error(
'No suitable accelerometer found for measurement! Multi-accelerometer configurations are not supported for this macro.'
)
accelerometer = Accelerometer(printer.lookup_object(accel_chip))
accelerometer = Accelerometer(printer.get_reactor(), printer.lookup_object(accel_chip))
# Move to the starting point
test_points = res_tester.test.get_start_test_points()
@@ -89,8 +89,12 @@ def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> None:
# set the needed acceleration values for the test
toolhead_info = toolhead.get_status(systime)
old_accel = toolhead_info['max_accel']
old_mcr = toolhead_info['minimum_cruise_ratio']
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel} MINIMUM_CRUISE_RATIO=0')
if 'minimum_cruise_ratio' in toolhead_info: # minimum_cruise_ratio found: Klipper >= v0.12.0-239
old_mcr = toolhead_info['minimum_cruise_ratio']
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel} MINIMUM_CRUISE_RATIO=0')
else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239
old_mcr = None
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel}')
# Deactivate input shaper if it is active to get raw movements
input_shaper = printer.lookup_object('input_shaper', None)
@@ -112,7 +116,10 @@ def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> None:
input_shaper.enable_shaping()
# Restore the previous acceleration values
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr}')
if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr}')
else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel}')
# Run post-processing
ConsoleOutput.print('Belts comparative frequency profile generation...')

View File

@@ -59,11 +59,17 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
toolhead_info = toolhead.get_status(systime)
old_accel = toolhead_info['max_accel']
old_mcr = toolhead_info['minimum_cruise_ratio']
old_sqv = toolhead_info['square_corner_velocity']
# set the wanted acceleration values
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY=5.0')
if 'minimum_cruise_ratio' in toolhead_info: # minimum_cruise_ratio found: Klipper >= v0.12.0-239
old_mcr = toolhead_info['minimum_cruise_ratio']
gcode.run_script_from_command(
f'SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY=5.0'
)
else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239
old_mcr = None
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={accel} SQUARE_CORNER_VELOCITY=5.0')
kin_info = toolhead.kin.get_status(systime)
mid_x = (kin_info['axis_minimum'].x + kin_info['axis_maximum'].x) / 2
@@ -91,7 +97,7 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
if k_accelerometer is None:
raise gcmd.error(f'Accelerometer [{current_accel_chip}] not found!')
ConsoleOutput.print(f'Accelerometer chip used for this angle: [{current_accel_chip}]')
accelerometer = Accelerometer(k_accelerometer)
accelerometer = Accelerometer(printer.get_reactor(), k_accelerometer)
# Sweep the speed range to record the vibrations at different speeds
for curr_speed_sample in range(nb_speed_samples):
@@ -133,10 +139,13 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
accelerometer.wait_for_file_writes()
# Restore the previous acceleration values
gcode.run_script_from_command(
f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}'
)
# Restore the previous acceleration values
if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239
gcode.run_script_from_command(
f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}'
)
else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} SQUARE_CORNER_VELOCITY={old_sqv}')
toolhead.wait_moves()
# Run post-processing

View File

@@ -41,7 +41,7 @@ def excitate_axis_at_freq(gcmd, config, st_process: ShakeTuneProcess) -> None:
k_accelerometer = printer.lookup_object(accel_chip, None)
if k_accelerometer is None:
raise gcmd.error(f'Accelerometer chip [{accel_chip}] was not found!')
accelerometer = Accelerometer(k_accelerometer)
accelerometer = Accelerometer(printer.get_reactor(), k_accelerometer)
ConsoleOutput.print(f'Excitating {axis.upper()} axis at {freq}Hz for {duration} seconds')

View File

@@ -19,6 +19,7 @@ import matplotlib.font_manager
import matplotlib.pyplot as plt
import matplotlib.ticker
import numpy as np
from scipy.stats import pearsonr
matplotlib.use('Agg')
@@ -210,8 +211,8 @@ def plot_compare_frequency(
ax: plt.Axes, signal1: SignalData, signal2: SignalData, signal1_belt: str, signal2_belt: str, max_freq: float
) -> None:
# Plot the two belts PSD signals
ax.plot(signal1.freqs, signal1.psd, label='Belt ' + signal1_belt, color=KLIPPAIN_COLORS['purple'])
ax.plot(signal2.freqs, signal2.psd, label='Belt ' + signal2_belt, color=KLIPPAIN_COLORS['orange'])
ax.plot(signal1.freqs, signal1.psd, label='Belt ' + signal1_belt, color=KLIPPAIN_COLORS['orange'])
ax.plot(signal2.freqs, signal2.psd, label='Belt ' + signal2_belt, color=KLIPPAIN_COLORS['purple'])
psd_highest_max = max(signal1.psd.max(), signal2.psd.max())
@@ -343,14 +344,12 @@ def plot_versus_belts(
common_freqs: np.ndarray,
signal1: SignalData,
signal2: SignalData,
interp_psd1: np.ndarray,
interp_psd2: np.ndarray,
signal1_belt: str,
signal2_belt: str,
) -> None:
ax.set_title('Cross-belts comparison plot', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
max_psd = max(np.max(interp_psd1), np.max(interp_psd2))
max_psd = max(np.max(signal1.psd), np.max(signal2.psd))
ideal_line = np.linspace(0, max_psd * 1.1, 500)
green_boundary = ideal_line + (0.35 * max_psd * np.exp(-ideal_line / (0.6 * max_psd)))
ax.fill_betweenx(ideal_line, ideal_line, green_boundary, color='green', alpha=0.15)
@@ -364,8 +363,8 @@ def plot_versus_belts(
linewidth=2,
)
ax.plot(interp_psd1, interp_psd2, color='dimgrey', marker='o', markersize=1.5)
ax.fill_betweenx(interp_psd2, interp_psd1, color=KLIPPAIN_COLORS['red_pink'], alpha=0.1)
ax.plot(signal1.psd, signal2.psd, color='dimgrey', marker='o', markersize=1.5)
ax.fill_betweenx(signal2.psd, signal1.psd, color=KLIPPAIN_COLORS['red_pink'], alpha=0.1)
paired_peak_count = 0
unpaired_peak_count = 0
@@ -374,31 +373,27 @@ def plot_versus_belts(
label = ALPHABET[paired_peak_count]
freq1 = signal1.freqs[peak1[0]]
freq2 = signal2.freqs[peak2[0]]
nearest_idx1 = np.argmin(np.abs(common_freqs - freq1))
nearest_idx2 = np.argmin(np.abs(common_freqs - freq2))
if nearest_idx1 == nearest_idx2:
psd1_peak_value = interp_psd1[nearest_idx1]
psd2_peak_value = interp_psd2[nearest_idx1]
ax.plot(psd1_peak_value, psd2_peak_value, marker='o', color='black', markersize=7)
if abs(freq1 - freq2) < 1:
ax.plot(signal1.psd[peak1[0]], signal2.psd[peak2[0]], marker='o', color='black', markersize=7)
ax.annotate(
f'{label}1/{label}2',
(psd1_peak_value, psd2_peak_value),
(signal1.psd[peak1[0]], signal2.psd[peak2[0]]),
textcoords='offset points',
xytext=(-7, 7),
fontsize=13,
color='black',
)
else:
psd1_peak_value = interp_psd1[nearest_idx1]
psd1_on_peak = interp_psd1[nearest_idx2]
psd2_peak_value = interp_psd2[nearest_idx2]
psd2_on_peak = interp_psd2[nearest_idx1]
ax.plot(psd1_on_peak, psd2_peak_value, marker='o', color=KLIPPAIN_COLORS['orange'], markersize=7)
ax.plot(psd1_peak_value, psd2_on_peak, marker='o', color=KLIPPAIN_COLORS['purple'], markersize=7)
ax.plot(
signal1.psd[peak2[0]], signal2.psd[peak2[0]], marker='o', color=KLIPPAIN_COLORS['orange'], markersize=7
)
ax.plot(
signal1.psd[peak1[0]], signal2.psd[peak1[0]], marker='o', color=KLIPPAIN_COLORS['purple'], markersize=7
)
ax.annotate(
f'{label}1',
(psd1_peak_value, psd2_on_peak),
(signal1.psd[peak1[0]], signal2.psd[peak1[0]]),
textcoords='offset points',
xytext=(0, 7),
fontsize=13,
@@ -406,7 +401,7 @@ def plot_versus_belts(
)
ax.annotate(
f'{label}2',
(psd1_on_peak, psd2_peak_value),
(signal1.psd[peak2[0]], signal2.psd[peak2[0]]),
textcoords='offset points',
xytext=(0, 7),
fontsize=13,
@@ -415,16 +410,12 @@ def plot_versus_belts(
paired_peak_count += 1
for _, peak_index in enumerate(signal1.unpaired_peaks):
freq1 = signal1.freqs[peak_index]
freq2 = signal2.freqs[peak_index]
nearest_idx1 = np.argmin(np.abs(common_freqs - freq1))
nearest_idx2 = np.argmin(np.abs(common_freqs - freq2))
psd1_peak_value = interp_psd1[nearest_idx1]
psd2_peak_value = interp_psd2[nearest_idx1]
ax.plot(psd1_peak_value, psd2_peak_value, marker='o', color=KLIPPAIN_COLORS['purple'], markersize=7)
ax.plot(
signal1.psd[peak_index], signal2.psd[peak_index], marker='o', color=KLIPPAIN_COLORS['purple'], markersize=7
)
ax.annotate(
str(unpaired_peak_count + 1),
(psd1_peak_value, psd2_peak_value),
(signal1.psd[peak_index], signal2.psd[peak_index]),
textcoords='offset points',
fontsize=13,
weight='bold',
@@ -434,16 +425,12 @@ def plot_versus_belts(
unpaired_peak_count += 1
for _, peak_index in enumerate(signal2.unpaired_peaks):
freq1 = signal1.freqs[peak_index]
freq2 = signal2.freqs[peak_index]
nearest_idx1 = np.argmin(np.abs(common_freqs - freq1))
nearest_idx2 = np.argmin(np.abs(common_freqs - freq2))
psd1_peak_value = interp_psd1[nearest_idx1]
psd2_peak_value = interp_psd2[nearest_idx1]
ax.plot(psd1_peak_value, psd2_peak_value, marker='o', color=KLIPPAIN_COLORS['orange'], markersize=7)
ax.plot(
signal1.psd[peak_index], signal2.psd[peak_index], marker='o', color=KLIPPAIN_COLORS['orange'], markersize=7
)
ax.annotate(
str(unpaired_peak_count + 1),
(psd1_peak_value, psd2_peak_value),
(signal1.psd[peak_index], signal2.psd[peak_index]),
textcoords='offset points',
fontsize=13,
weight='bold',
@@ -476,16 +463,21 @@ def plot_versus_belts(
# Original Klipper function to get the PSD data of a raw accelerometer signal
def compute_signal_data(data: np.ndarray, max_freq: float) -> SignalData:
def compute_signal_data(data: np.ndarray, common_freqs: np.ndarray, max_freq: float) -> SignalData:
helper = shaper_calibrate.ShaperCalibrate(printer=None)
calibration_data = helper.process_accelerometer_data(data)
freqs = calibration_data.freq_bins[calibration_data.freq_bins <= max_freq]
psd = calibration_data.get_psd('all')[calibration_data.freq_bins <= max_freq]
_, peaks, _ = detect_peaks(psd, freqs, PEAKS_DETECTION_THRESHOLD * psd.max())
# Re-interpolate the PSD signal to a common frequency range to be able to plot them one against the other
interp_psd = np.interp(common_freqs, freqs, psd)
return SignalData(freqs=freqs, psd=psd, peaks=peaks)
_, peaks, _ = detect_peaks(
interp_psd, common_freqs, PEAKS_DETECTION_THRESHOLD * interp_psd.max(), window_size=20, vicinity=15
)
return SignalData(freqs=common_freqs, psd=interp_psd, peaks=peaks)
######################################################################
@@ -517,8 +509,9 @@ def belts_calibration(
signal2_belt += belt_info.get(signal2_belt, '')
# Compute calibration data for the two datasets with automatic peaks detection
signal1 = compute_signal_data(datas[0], max_freq)
signal2 = compute_signal_data(datas[1], max_freq)
common_freqs = np.linspace(0, max_freq, 500)
signal1 = compute_signal_data(datas[0], common_freqs, max_freq)
signal2 = compute_signal_data(datas[1], common_freqs, max_freq)
del datas
# Pair the peaks across the two datasets
@@ -526,18 +519,13 @@ def belts_calibration(
signal1 = signal1._replace(paired_peaks=pairing_result.paired_peaks, unpaired_peaks=pairing_result.unpaired_peaks1)
signal2 = signal2._replace(paired_peaks=pairing_result.paired_peaks, unpaired_peaks=pairing_result.unpaired_peaks2)
# Re-interpolate the PSD signals to a common frequency range to be able to plot them one against the other point by point
common_freqs = np.linspace(0, max_freq, 500)
interp_psd1 = np.interp(common_freqs, signal1.freqs, signal1.psd)
interp_psd2 = np.interp(common_freqs, signal2.freqs, signal2.psd)
# Calculating R^2 to y=x line to compute the similarity between the two belts
ss_res = np.sum((interp_psd2 - interp_psd1) ** 2)
ss_tot = np.sum((interp_psd2 - np.mean(interp_psd2)) ** 2)
similarity_factor = (1 - (ss_res / ss_tot)) * 100
# R² proved to be pretty instable to compute the similarity between the two belts
# So now, we use the Pearson correlation coefficient to compute the similarity
correlation, _ = pearsonr(signal1.psd, signal2.psd)
similarity_factor = correlation * 100
similarity_factor = np.clip(similarity_factor, 0, 100)
ConsoleOutput.print(f'Belts estimated similarity: {similarity_factor:.1f}%')
# mhi = compute_mhi(similarity_factor, num_peaks, num_unpaired_peaks)
mhi = compute_mhi(similarity_factor, signal1, signal2)
ConsoleOutput.print(f'[experimental] Mechanical health: {mhi}')
@@ -582,11 +570,11 @@ def belts_calibration(
# Add the accel_per_hz value to the title
title_line5 = f'| Accel per Hz used: {accel_per_hz} mm/s²/Hz'
fig.text(0.55, 0.915, title_line5, ha='left', va='top', fontsize=14, color=KLIPPAIN_COLORS['dark_purple'])
fig.text(0.551, 0.915, title_line5, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple'])
# Plot the graphs
plot_compare_frequency(ax1, signal1, signal2, signal1_belt, signal2_belt, max_freq)
plot_versus_belts(ax3, common_freqs, signal1, signal2, interp_psd1, interp_psd2, signal1_belt, signal2_belt)
plot_versus_belts(ax3, common_freqs, signal1, signal2, signal1_belt, signal2_belt)
# Adding a small Klippain logo to the top left corner of the figure
ax_logo = fig.add_axes([0.001, 0.894, 0.105, 0.105], anchor='NW')

View File

@@ -29,6 +29,8 @@ from .helpers.console_output import ConsoleOutput
from .shaketune_config import ShakeTuneConfig
from .shaketune_process import ShakeTuneProcess
IN_DANGER = False
class ShakeTune:
def __init__(self, config) -> None:
@@ -51,21 +53,31 @@ class ShakeTune:
self._config = ShakeTuneConfig(result_folder_path, keep_n_results, keep_csv, dpi)
ConsoleOutput.register_output_callback(gcode.respond_info)
commands = [
# Register Shake&Tune's measurement commands
measurement_commands = [
(
'EXCITATE_AXIS_AT_FREQ',
self.cmd_EXCITATE_AXIS_AT_FREQ,
'Maintain a specified excitation frequency for a period of time to diagnose and locate a source of vibration',
(
'Maintain a specified excitation frequency for a period '
'of time to diagnose and locate a source of vibrations'
),
),
(
'AXES_MAP_CALIBRATION',
self.cmd_AXES_MAP_CALIBRATION,
'Perform a set of movements to measure the orientation of the accelerometer and help you set the best axes_map configuration for your printer',
(
'Perform a set of movements to measure the orientation of the accelerometer '
'and help you set the best axes_map configuration for your printer'
),
),
(
'COMPARE_BELTS_RESPONSES',
self.cmd_COMPARE_BELTS_RESPONSES,
'Perform a custom half-axis test to analyze and compare the frequency profiles of individual belts on CoreXY printers',
(
'Perform a custom half-axis test to analyze and compare the '
'frequency profiles of individual belts on CoreXY or CoreXZ printers'
),
),
(
'AXES_SHAPER_CALIBRATION',
@@ -75,12 +87,14 @@ class ShakeTune:
(
'CREATE_VIBRATIONS_PROFILE',
self.cmd_CREATE_VIBRATIONS_PROFILE,
'Perform a set of movements to measure the orientation of the accelerometer and help you set the best axes_map configuration for your printer',
(
'Run a series of motions to find speed/angle ranges where the printer could be '
'exposed to VFAs to optimize your slicer speed profiles and TMC driver parameters'
),
),
]
command_descriptions = {name: desc for name, _, desc in commands}
for name, command, description in commands:
command_descriptions = {name: desc for name, _, desc in measurement_commands}
for name, command, description in measurement_commands:
gcode.register_command(f'_{name}' if show_macros else name, command, desc=description)
# Load the dummy macros with their description in order to show them in the web interfaces