Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af9fee36ed | |||
| 6b2cedfa28 | |||
| 04ff95921e | |||
| 871dd72a88 | |||
|
|
66f5e32e4c | ||
|
|
c12653e1f7 | ||
|
|
8cf81bcb44 | ||
|
|
92a651b6a6 | ||
|
|
6712506862 | ||
|
|
6e49c2c607 | ||
|
|
4a99e95882 | ||
|
|
f5a74c29e1 | ||
|
|
f87713eacd | ||
|
|
f045b8a49e | ||
|
|
37d0e39d84 | ||
|
|
50ed13ca59 | ||
|
|
90ed7aca3c |
69
.github/workflows/test.yml
vendored
Normal file
69
.github/workflows/test.yml
vendored
Normal 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
|
||||
|
||||
|
||||
@@ -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:
|
||||
```
|
||||
|
||||
34
ci/smoke-test/klipper-smoketest.kconfig
Normal file
34
ci/smoke-test/klipper-smoketest.kconfig
Normal 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
|
||||
9
ci/smoke-test/klippy-tests/simple.cfg
Normal file
9
ci/smoke-test/klippy-tests/simple.cfg
Normal file
@@ -0,0 +1,9 @@
|
||||
[mcu]
|
||||
serial: /tmp/klipper_host_mcu
|
||||
|
||||
[printer]
|
||||
kinematics: none
|
||||
max_velocity: 300
|
||||
max_accel: 300
|
||||
|
||||
[shaketune]
|
||||
4
ci/smoke-test/klippy-tests/simple.test
Normal file
4
ci/smoke-test/klippy-tests/simple.test
Normal file
@@ -0,0 +1,4 @@
|
||||
DICTIONARY linux_basic.dict
|
||||
CONFIG simple.cfg
|
||||
|
||||
G4 P1000
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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']
|
||||
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
|
||||
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}')
|
||||
|
||||
@@ -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']
|
||||
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
|
||||
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...')
|
||||
|
||||
@@ -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):
|
||||
@@ -134,9 +140,12 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
|
||||
accelerometer.wait_for_file_writes()
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user