Compare commits
8 Commits
smooth-acc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| af9fee36ed | |||
| 6b2cedfa28 | |||
| 04ff95921e | |||
| 871dd72a88 | |||
|
|
66f5e32e4c | ||
|
|
c12653e1f7 | ||
|
|
6e49c2c607 | ||
|
|
4a99e95882 |
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y build-essential gcc-avr avr-libc
|
sudo apt-get install -y build-essential
|
||||||
- name: Build klipper dict
|
- name: Build klipper dict
|
||||||
run: |
|
run: |
|
||||||
pushd klipper
|
pushd klipper
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pushd klipper
|
pushd klipper
|
||||||
mkdir ../dicts
|
mkdir ../dicts
|
||||||
cp ../klipper/out/klipper.dict ../dicts/atmega2560.dict
|
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
|
../klippy-env/bin/python scripts/test_klippy.py -d ../dicts ../shaketune/ci/smoke-test/klippy-tests/simple.test
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -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. 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:
|
1. Install Shake&Tune by running over SSH on your printer:
|
||||||
```bash
|
```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:
|
1. Then, append the following to your `printer.cfg` file and restart Klipper:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,34 @@
|
|||||||
# Base Kconfig file for atmega2560
|
CONFIG_LOW_LEVEL_OPTIONS=y
|
||||||
CONFIG_MACH_AVR=y
|
# CONFIG_MACH_AVR is not set
|
||||||
CONFIG_MACH_atmega2560=y
|
# CONFIG_MACH_ATSAM is not set
|
||||||
CONFIG_CLOCK_FREQ=16000000
|
# 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
|
||||||
|
|||||||
@@ -1,85 +1,9 @@
|
|||||||
# Test config with a minimal setup to have kind
|
|
||||||
# of a machine ready with an ADXL345 and an MPU9250
|
|
||||||
# to have the required the resonance_tester section
|
|
||||||
# and allow loading and initializing Shake&Tune into Klipper
|
|
||||||
|
|
||||||
[stepper_x]
|
|
||||||
step_pin: PF0
|
|
||||||
dir_pin: PF1
|
|
||||||
enable_pin: !PD7
|
|
||||||
microsteps: 16
|
|
||||||
rotation_distance: 40
|
|
||||||
endstop_pin: ^PE5
|
|
||||||
position_endstop: 0
|
|
||||||
position_max: 200
|
|
||||||
homing_speed: 50
|
|
||||||
|
|
||||||
[stepper_y]
|
|
||||||
step_pin: PF6
|
|
||||||
dir_pin: !PF7
|
|
||||||
enable_pin: !PF2
|
|
||||||
microsteps: 16
|
|
||||||
rotation_distance: 40
|
|
||||||
endstop_pin: ^PJ1
|
|
||||||
position_endstop: 0
|
|
||||||
position_max: 200
|
|
||||||
homing_speed: 50
|
|
||||||
|
|
||||||
[stepper_z]
|
|
||||||
step_pin: PL3
|
|
||||||
dir_pin: PL1
|
|
||||||
enable_pin: !PK0
|
|
||||||
microsteps: 16
|
|
||||||
rotation_distance: 8
|
|
||||||
endstop_pin: ^PD3
|
|
||||||
position_endstop: 0.5
|
|
||||||
position_max: 200
|
|
||||||
|
|
||||||
[extruder]
|
|
||||||
step_pin: PA4
|
|
||||||
dir_pin: PA6
|
|
||||||
enable_pin: !PA2
|
|
||||||
microsteps: 16
|
|
||||||
rotation_distance: 33.5
|
|
||||||
nozzle_diameter: 0.500
|
|
||||||
filament_diameter: 3.500
|
|
||||||
heater_pin: PB4
|
|
||||||
sensor_type: EPCOS 100K B57560G104F
|
|
||||||
sensor_pin: PK5
|
|
||||||
control: pid
|
|
||||||
pid_Kp: 22.2
|
|
||||||
pid_Ki: 1.08
|
|
||||||
pid_Kd: 114
|
|
||||||
min_temp: 0
|
|
||||||
max_temp: 210
|
|
||||||
|
|
||||||
[heater_bed]
|
|
||||||
heater_pin: PH5
|
|
||||||
sensor_type: EPCOS 100K B57560G104F
|
|
||||||
sensor_pin: PK6
|
|
||||||
control: watermark
|
|
||||||
min_temp: 0
|
|
||||||
max_temp: 110
|
|
||||||
|
|
||||||
[mcu]
|
[mcu]
|
||||||
serial: /dev/ttyACM0
|
serial: /tmp/klipper_host_mcu
|
||||||
|
|
||||||
[printer]
|
[printer]
|
||||||
kinematics: cartesian
|
kinematics: none
|
||||||
max_velocity: 300
|
max_velocity: 300
|
||||||
max_accel: 3000
|
max_accel: 300
|
||||||
max_z_velocity: 5
|
|
||||||
max_z_accel: 100
|
|
||||||
|
|
||||||
[adxl345]
|
|
||||||
cs_pin: PK7
|
|
||||||
axes_map: -x,-y,z
|
|
||||||
|
|
||||||
[mpu9250 my_mpu]
|
|
||||||
|
|
||||||
[resonance_tester]
|
|
||||||
probe_points: 20,20,20
|
|
||||||
accel_chip_x: adxl345
|
|
||||||
accel_chip_y: mpu9250 my_mpu
|
|
||||||
|
|
||||||
[shaketune]
|
[shaketune]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
DICTIONARY linux_basic.dict
|
||||||
CONFIG simple.cfg
|
CONFIG simple.cfg
|
||||||
DICTIONARY atmega2560.dict
|
|
||||||
|
|
||||||
G4 P1000
|
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 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
|
### Estimated similarity and mechanical issues indicator
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function check_download {
|
|||||||
|
|
||||||
if [ ! -d "${K_SHAKETUNE_PATH}" ]; then
|
if [ ! -d "${K_SHAKETUNE_PATH}" ]; then
|
||||||
echo "[DOWNLOAD] Downloading Klippain Shake&Tune module repository..."
|
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
|
chmod +x ${K_SHAKETUNE_PATH}/install.sh
|
||||||
printf "[DOWNLOAD] Download complete!\n\n"
|
printf "[DOWNLOAD] Download complete!\n\n"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
|
||||||
## Klippain Shake&Tune automatic update management
|
## Klippain Shake&Tune automatic update management
|
||||||
[update_manager Klippain-ShakeTune]
|
[update_manager Klippain-ShakeTune]
|
||||||
type: git_repo
|
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
|
path: ~/klippain_shaketune
|
||||||
virtualenv: ~/klippy-env
|
virtualenv: ~/klippy-env
|
||||||
requirements: requirements.txt
|
requirements: requirements.txt
|
||||||
|
|||||||
@@ -211,8 +211,8 @@ def plot_compare_frequency(
|
|||||||
ax: plt.Axes, signal1: SignalData, signal2: SignalData, signal1_belt: str, signal2_belt: str, max_freq: float
|
ax: plt.Axes, signal1: SignalData, signal2: SignalData, signal1_belt: str, signal2_belt: str, max_freq: float
|
||||||
) -> None:
|
) -> None:
|
||||||
# Plot the two belts PSD signals
|
# Plot the two belts PSD signals
|
||||||
ax.plot(signal1.freqs, signal1.psd, label='Belt ' + signal1_belt, color=KLIPPAIN_COLORS['purple'])
|
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['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())
|
psd_highest_max = max(signal1.psd.max(), signal2.psd.max())
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,13 @@
|
|||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import matplotlib
|
import matplotlib
|
||||||
import matplotlib.font_manager
|
import matplotlib.font_manager
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import matplotlib.ticker
|
import matplotlib.ticker
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy.interpolate import interp1d
|
|
||||||
|
|
||||||
matplotlib.use('Agg')
|
matplotlib.use('Agg')
|
||||||
|
|
||||||
@@ -48,9 +47,7 @@ PEAKS_DETECTION_THRESHOLD = 0.05
|
|||||||
PEAKS_EFFECT_THRESHOLD = 0.12
|
PEAKS_EFFECT_THRESHOLD = 0.12
|
||||||
SPECTROGRAM_LOW_PERCENTILE_FILTER = 5
|
SPECTROGRAM_LOW_PERCENTILE_FILTER = 5
|
||||||
MAX_VIBRATIONS = 5.0
|
MAX_VIBRATIONS = 5.0
|
||||||
MAX_VIBRATIONS_PLOTTED = 80.0
|
|
||||||
MAX_VIBRATIONS_PLOTTED_ZOOM = 1.25 # 1.25x max vibs values from the standard filters selection
|
|
||||||
SMOOTHING_TESTS = 10 # Number of smoothing values to test (it will significantly increase the computation time)
|
|
||||||
KLIPPAIN_COLORS = {
|
KLIPPAIN_COLORS = {
|
||||||
'purple': '#70088C',
|
'purple': '#70088C',
|
||||||
'orange': '#FF8D32',
|
'orange': '#FF8D32',
|
||||||
@@ -115,13 +112,15 @@ def calibrate_shaper(datas: List[np.ndarray], max_smoothing: Optional[float], sc
|
|||||||
calibration_data = helper.process_accelerometer_data(datas)
|
calibration_data = helper.process_accelerometer_data(datas)
|
||||||
calibration_data.normalize_to_frequencies()
|
calibration_data.normalize_to_frequencies()
|
||||||
|
|
||||||
# We compute the damping ratio using the Klipper's default value if it fails
|
|
||||||
fr, zeta, _, _ = compute_mechanical_parameters(calibration_data.psd_sum, calibration_data.freq_bins)
|
fr, zeta, _, _ = compute_mechanical_parameters(calibration_data.psd_sum, calibration_data.freq_bins)
|
||||||
zeta = zeta if zeta is not None else 0.1
|
|
||||||
|
# If the damping ratio computation fail, we use Klipper default value instead
|
||||||
|
if zeta is None:
|
||||||
|
zeta = 0.1
|
||||||
|
|
||||||
compat = False
|
compat = False
|
||||||
try:
|
try:
|
||||||
k_shaper_choice, all_shapers = helper.find_best_shaper(
|
shaper, all_shapers = helper.find_best_shaper(
|
||||||
calibration_data,
|
calibration_data,
|
||||||
shapers=None,
|
shapers=None,
|
||||||
damping_ratio=zeta,
|
damping_ratio=zeta,
|
||||||
@@ -130,79 +129,23 @@ def calibrate_shaper(datas: List[np.ndarray], max_smoothing: Optional[float], sc
|
|||||||
max_smoothing=max_smoothing,
|
max_smoothing=max_smoothing,
|
||||||
test_damping_ratios=None,
|
test_damping_ratios=None,
|
||||||
max_freq=max_freq,
|
max_freq=max_freq,
|
||||||
logger=None,
|
logger=ConsoleOutput.print,
|
||||||
)
|
|
||||||
ConsoleOutput.print(
|
|
||||||
(
|
|
||||||
f'Detected a square corner velocity of {scv:.1f} and a damping ratio of {zeta:.3f}. '
|
|
||||||
'These values will be used to compute the input shaper filter recommendations'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(
|
||||||
(
|
'[WARNING] You seem to be using an older version of Klipper that is not compatible with all the latest Shake&Tune features!'
|
||||||
'[WARNING] You seem to be using an older version of Klipper that is not compatible with all the latest '
|
)
|
||||||
'Shake&Tune features!\nShake&Tune now runs in compatibility mode: be aware that the results may be '
|
ConsoleOutput.print(
|
||||||
'slightly off, since the real damping ratio cannot be used to craft accurate filter recommendations'
|
'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
|
compat = True
|
||||||
k_shaper_choice, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, None)
|
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, ConsoleOutput.print)
|
||||||
|
|
||||||
# If max_smoothing is not None, we run the same computation but without a smoothing value
|
ConsoleOutput.print(
|
||||||
# to get the max smoothing values from the filters and create the testing list
|
f'\n-> Recommended shaper is {shaper.name.upper()} @ {shaper.freq:.1f} Hz (when using a square corner velocity of {scv:.1f} and a damping ratio of {zeta:.3f})'
|
||||||
all_shapers_nosmoothing = None
|
|
||||||
if max_smoothing is not None:
|
|
||||||
if compat:
|
|
||||||
_, all_shapers_nosmoothing = helper.find_best_shaper(calibration_data, None, None)
|
|
||||||
else:
|
|
||||||
_, all_shapers_nosmoothing = helper.find_best_shaper(
|
|
||||||
calibration_data,
|
|
||||||
shapers=None,
|
|
||||||
damping_ratio=zeta,
|
|
||||||
scv=scv,
|
|
||||||
shaper_freqs=None,
|
|
||||||
max_smoothing=None,
|
|
||||||
test_damping_ratios=None,
|
|
||||||
max_freq=max_freq,
|
|
||||||
logger=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Then we iterate over the all_shaperts_nosmoothing list to get the max of the smoothing values
|
|
||||||
max_smoothing = 0.0
|
|
||||||
if all_shapers_nosmoothing is not None:
|
|
||||||
for shaper in all_shapers_nosmoothing:
|
|
||||||
if shaper.smoothing > max_smoothing:
|
|
||||||
max_smoothing = shaper.smoothing
|
|
||||||
else:
|
|
||||||
for shaper in all_shapers:
|
|
||||||
if shaper.smoothing > max_smoothing:
|
|
||||||
max_smoothing = shaper.smoothing
|
|
||||||
|
|
||||||
# Then we create a list of smoothing values to test (no need to test the max smoothing value as it was already tested)
|
|
||||||
smoothing_test_list = np.linspace(0.001, max_smoothing, SMOOTHING_TESTS)[:-1]
|
|
||||||
additional_all_shapers = {}
|
|
||||||
for smoothing in smoothing_test_list:
|
|
||||||
if compat:
|
|
||||||
_, all_shapers_bis = helper.find_best_shaper(calibration_data, smoothing, None)
|
|
||||||
else:
|
|
||||||
_, all_shapers_bis = helper.find_best_shaper(
|
|
||||||
calibration_data,
|
|
||||||
shapers=None,
|
|
||||||
damping_ratio=zeta,
|
|
||||||
scv=scv,
|
|
||||||
shaper_freqs=None,
|
|
||||||
max_smoothing=smoothing,
|
|
||||||
test_damping_ratios=None,
|
|
||||||
max_freq=max_freq,
|
|
||||||
logger=None,
|
|
||||||
)
|
|
||||||
additional_all_shapers[f'sm_{smoothing}'] = all_shapers_bis
|
|
||||||
additional_all_shapers['max_smoothing'] = (
|
|
||||||
all_shapers_nosmoothing if all_shapers_nosmoothing is not None else all_shapers
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return k_shaper_choice.name, all_shapers, additional_all_shapers, calibration_data, fr, zeta, max_smoothing, compat
|
return shaper.name, all_shapers, calibration_data, fr, zeta, compat
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
@@ -221,7 +164,7 @@ def plot_freq_response(
|
|||||||
fr: float,
|
fr: float,
|
||||||
zeta: float,
|
zeta: float,
|
||||||
max_freq: float,
|
max_freq: float,
|
||||||
) -> Dict[str, List[Dict[str, str]]]:
|
) -> None:
|
||||||
freqs = calibration_data.freqs
|
freqs = calibration_data.freqs
|
||||||
psd = calibration_data.psd_sum
|
psd = calibration_data.psd_sum
|
||||||
px = calibration_data.psd_x
|
px = calibration_data.psd_x
|
||||||
@@ -250,40 +193,27 @@ def plot_freq_response(
|
|||||||
ax2 = ax.twinx()
|
ax2 = ax.twinx()
|
||||||
ax2.yaxis.set_visible(False)
|
ax2.yaxis.set_visible(False)
|
||||||
|
|
||||||
shaper_table_data = {
|
|
||||||
'shapers': [],
|
|
||||||
'recommendations': [],
|
|
||||||
'damping_ratio': zeta,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Draw the shappers curves and add their specific parameters in the legend
|
# Draw the shappers curves and add their specific parameters in the legend
|
||||||
perf_shaper_choice = None
|
perf_shaper_choice = None
|
||||||
perf_shaper_vals = None
|
perf_shaper_vals = None
|
||||||
perf_shaper_freq = None
|
perf_shaper_freq = None
|
||||||
perf_shaper_accel = 0
|
perf_shaper_accel = 0
|
||||||
for shaper in shapers:
|
for shaper in shapers:
|
||||||
ax2.plot(freqs, shaper.vals, label=shaper.name.upper(), linestyle='dotted')
|
shaper_max_accel = round(shaper.max_accel / 100.0) * 100.0
|
||||||
|
label = f'{shaper.name.upper()} ({shaper.freq:.1f} Hz, vibr={shaper.vibrs * 100.0:.1f}%, sm~={shaper.smoothing:.2f}, accel<={shaper_max_accel:.0f})'
|
||||||
shaper_info = {
|
ax2.plot(freqs, shaper.vals, label=label, linestyle='dotted')
|
||||||
'type': shaper.name.upper(),
|
|
||||||
'frequency': shaper.freq,
|
|
||||||
'vibrations': shaper.vibrs,
|
|
||||||
'smoothing': shaper.smoothing,
|
|
||||||
'max_accel': shaper.max_accel,
|
|
||||||
}
|
|
||||||
shaper_table_data['shapers'].append(shaper_info)
|
|
||||||
|
|
||||||
# Get the Klipper recommended shaper (usually it's a good low vibration compromise)
|
# Get the Klipper recommended shaper (usually it's a good low vibration compromise)
|
||||||
if shaper.name == klipper_shaper_choice:
|
if shaper.name == klipper_shaper_choice:
|
||||||
klipper_shaper_freq = shaper.freq
|
klipper_shaper_freq = shaper.freq
|
||||||
klipper_shaper_vals = shaper.vals
|
klipper_shaper_vals = shaper.vals
|
||||||
klipper_shaper_accel = shaper.max_accel
|
klipper_shaper_accel = shaper_max_accel
|
||||||
|
|
||||||
# Find the shaper with the highest accel but with vibrs under MAX_VIBRATIONS as it's
|
# Find the shaper with the highest accel but with vibrs under MAX_VIBRATIONS as it's
|
||||||
# a good performance compromise when injecting the SCV and damping ratio in the computation
|
# a good performance compromise when injecting the SCV and damping ratio in the computation
|
||||||
if perf_shaper_accel < shaper.max_accel and shaper.vibrs * 100 < MAX_VIBRATIONS:
|
if perf_shaper_accel < shaper_max_accel and shaper.vibrs * 100 < MAX_VIBRATIONS:
|
||||||
perf_shaper_choice = shaper.name
|
perf_shaper_choice = shaper.name
|
||||||
perf_shaper_accel = shaper.max_accel
|
perf_shaper_accel = shaper_max_accel
|
||||||
perf_shaper_freq = shaper.freq
|
perf_shaper_freq = shaper.freq
|
||||||
perf_shaper_vals = shaper.vals
|
perf_shaper_vals = shaper.vals
|
||||||
|
|
||||||
@@ -296,30 +226,32 @@ def plot_freq_response(
|
|||||||
and perf_shaper_choice != klipper_shaper_choice
|
and perf_shaper_choice != klipper_shaper_choice
|
||||||
and perf_shaper_accel >= klipper_shaper_accel
|
and perf_shaper_accel >= klipper_shaper_accel
|
||||||
):
|
):
|
||||||
perf_shaper_string = f'Recommended for performance: {perf_shaper_choice.upper()} @ {perf_shaper_freq:.1f} Hz'
|
ax2.plot(
|
||||||
lowvibr_shaper_string = (
|
[],
|
||||||
f'Recommended for low vibrations: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz'
|
[],
|
||||||
|
' ',
|
||||||
|
label=f'Recommended performance shaper: {perf_shaper_choice.upper()} @ {perf_shaper_freq:.1f} Hz',
|
||||||
)
|
)
|
||||||
shaper_table_data['recommendations'].append(perf_shaper_string)
|
|
||||||
shaper_table_data['recommendations'].append(lowvibr_shaper_string)
|
|
||||||
ConsoleOutput.print(f'{perf_shaper_string} (with a damping ratio of {zeta:.3f})')
|
|
||||||
ConsoleOutput.print(f'{lowvibr_shaper_string} (with a damping ratio of {zeta:.3f})')
|
|
||||||
ax.plot(
|
ax.plot(
|
||||||
freqs,
|
freqs,
|
||||||
psd * perf_shaper_vals,
|
psd * perf_shaper_vals,
|
||||||
label=f'With {perf_shaper_choice.upper()} applied',
|
label=f'With {perf_shaper_choice.upper()} applied',
|
||||||
color='cyan',
|
color='cyan',
|
||||||
)
|
)
|
||||||
ax.plot(
|
ax2.plot(
|
||||||
freqs,
|
[],
|
||||||
psd * klipper_shaper_vals,
|
[],
|
||||||
label=f'With {klipper_shaper_choice.upper()} applied',
|
' ',
|
||||||
color='lime',
|
label=f'Recommended low vibrations shaper: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz',
|
||||||
)
|
)
|
||||||
|
ax.plot(freqs, psd * klipper_shaper_vals, label=f'With {klipper_shaper_choice.upper()} applied', color='lime')
|
||||||
else:
|
else:
|
||||||
shaper_string = f'Recommended best shaper: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz'
|
ax2.plot(
|
||||||
shaper_table_data['recommendations'].append(shaper_string)
|
[],
|
||||||
ConsoleOutput.print(f'{shaper_string} (with a damping ratio of {zeta:.3f})')
|
[],
|
||||||
|
' ',
|
||||||
|
label=f'Recommended performance shaper: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz',
|
||||||
|
)
|
||||||
ax.plot(
|
ax.plot(
|
||||||
freqs,
|
freqs,
|
||||||
psd * klipper_shaper_vals,
|
psd * klipper_shaper_vals,
|
||||||
@@ -327,6 +259,9 @@ def plot_freq_response(
|
|||||||
color='cyan',
|
color='cyan',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# And the estimated damping ratio is finally added at the end of the legend
|
||||||
|
ax2.plot([], [], ' ', label=f'Estimated damping ratio (ζ): {zeta:.3f}')
|
||||||
|
|
||||||
# Draw the detected peaks and name them
|
# Draw the detected peaks and name them
|
||||||
# This also draw the detection threshold and warning threshold (aka "effect zone")
|
# This also draw the detection threshold and warning threshold (aka "effect zone")
|
||||||
ax.plot(peaks_freqs, psd[peaks], 'x', color='black', markersize=8)
|
ax.plot(peaks_freqs, psd[peaks], 'x', color='black', markersize=8)
|
||||||
@@ -362,7 +297,7 @@ def plot_freq_response(
|
|||||||
ax.legend(loc='upper left', prop=fontP)
|
ax.legend(loc='upper left', prop=fontP)
|
||||||
ax2.legend(loc='upper right', prop=fontP)
|
ax2.legend(loc='upper right', prop=fontP)
|
||||||
|
|
||||||
return shaper_table_data
|
return
|
||||||
|
|
||||||
|
|
||||||
# Plot a time-frequency spectrogram to see how the system respond over time during the
|
# Plot a time-frequency spectrogram to see how the system respond over time during the
|
||||||
@@ -415,170 +350,6 @@ def plot_spectrogram(
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def plot_smoothing_vs_accel(
|
|
||||||
ax: plt.Axes,
|
|
||||||
shaper_table_data: Dict[str, List[Dict[str, str]]],
|
|
||||||
additional_shapers: Dict[str, List[Dict[str, str]]],
|
|
||||||
) -> None:
|
|
||||||
fontP = matplotlib.font_manager.FontProperties()
|
|
||||||
fontP.set_size('x-small')
|
|
||||||
|
|
||||||
ax.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(1000))
|
|
||||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
|
||||||
ax.grid(which='major', color='grey')
|
|
||||||
ax.grid(which='minor', color='lightgrey')
|
|
||||||
|
|
||||||
shaper_data = {}
|
|
||||||
|
|
||||||
# Extract data from additional_shapers first
|
|
||||||
for _, shapers in additional_shapers.items():
|
|
||||||
for shaper in shapers:
|
|
||||||
shaper_type = shaper.name.upper()
|
|
||||||
if shaper_type not in shaper_data:
|
|
||||||
shaper_data[shaper_type] = []
|
|
||||||
shaper_data[shaper_type].append(
|
|
||||||
{
|
|
||||||
'max_accel': shaper.max_accel,
|
|
||||||
'vibrs': shaper.vibrs * 100.0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract data from shaper_table_data and insert into shaper_data
|
|
||||||
max_shaper_vibrations = 0
|
|
||||||
for shaper in shaper_table_data['shapers']:
|
|
||||||
shaper_type = shaper['type']
|
|
||||||
if shaper_type not in shaper_data:
|
|
||||||
shaper_data[shaper_type] = []
|
|
||||||
max_shaper_vibrations = max(max_shaper_vibrations, float(shaper['vibrations']) * 100.0)
|
|
||||||
shaper_data[shaper_type].append(
|
|
||||||
{
|
|
||||||
'max_accel': float(shaper['max_accel']),
|
|
||||||
'vibrs': float(shaper['vibrations']) * 100.0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate the maximum `max_accel` for points below the thresholds to get a good plot with
|
|
||||||
# continuous lines and a zoom on the graph to show details at low vibrations
|
|
||||||
min_accel_limit = 99999
|
|
||||||
max_accel_limit = 0
|
|
||||||
max_accel_limit_zoom = 0
|
|
||||||
for data in shaper_data.values():
|
|
||||||
min_accel_limit = min(min_accel_limit, min(d['max_accel'] for d in data))
|
|
||||||
max_accel_limit = max(
|
|
||||||
max_accel_limit, max(d['max_accel'] for d in data if d['vibrs'] <= MAX_VIBRATIONS_PLOTTED)
|
|
||||||
)
|
|
||||||
max_accel_limit_zoom = max(
|
|
||||||
max_accel_limit_zoom,
|
|
||||||
max(d['max_accel'] for d in data if d['vibrs'] <= max_shaper_vibrations * MAX_VIBRATIONS_PLOTTED_ZOOM),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add a zoom axes on the graph to show details at low vibrations
|
|
||||||
zoomed_window = np.clip(max_shaper_vibrations * MAX_VIBRATIONS_PLOTTED_ZOOM, 0, 20)
|
|
||||||
axins = ax.inset_axes(
|
|
||||||
[0.575, 0.125, 0.40, 0.45],
|
|
||||||
xlim=(min_accel_limit * 0.95, max_accel_limit_zoom * 1.1),
|
|
||||||
ylim=(-0.5, zoomed_window),
|
|
||||||
)
|
|
||||||
ax.indicate_inset_zoom(axins, edgecolor=KLIPPAIN_COLORS['purple'], linewidth=3)
|
|
||||||
axins.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(500))
|
|
||||||
axins.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
|
||||||
axins.grid(which='major', color='grey')
|
|
||||||
axins.grid(which='minor', color='lightgrey')
|
|
||||||
|
|
||||||
# Draw the green zone on both axes to highlight the low vibrations zone
|
|
||||||
number_of_interpolated_points = 100
|
|
||||||
x_fill = np.linspace(min_accel_limit * 0.95, max_accel_limit * 1.1, number_of_interpolated_points)
|
|
||||||
y_fill = np.full_like(x_fill, 5.0)
|
|
||||||
ax.axhline(y=5.0, color='black', linestyle='--', linewidth=0.5)
|
|
||||||
ax.fill_between(x_fill, -0.5, y_fill, color='green', alpha=0.15)
|
|
||||||
if zoomed_window > 5.0:
|
|
||||||
axins.axhline(y=5.0, color='black', linestyle='--', linewidth=0.5)
|
|
||||||
axins.fill_between(x_fill, -0.5, y_fill, color='green', alpha=0.15)
|
|
||||||
|
|
||||||
# Plot each shaper remaining vibrations response over acceleration
|
|
||||||
max_vibrations = 0
|
|
||||||
for _, (shaper_type, data) in enumerate(shaper_data.items()):
|
|
||||||
max_accel_values = np.array([d['max_accel'] for d in data])
|
|
||||||
vibrs_values = np.array([d['vibrs'] for d in data])
|
|
||||||
|
|
||||||
# remove duplicate values in max_accel_values and delete the corresponding vibrs_values
|
|
||||||
# and interpolate the curves to get them smoother with more datapoints
|
|
||||||
unique_max_accel_values, unique_indices = np.unique(max_accel_values, return_index=True)
|
|
||||||
max_accel_values = unique_max_accel_values
|
|
||||||
vibrs_values = vibrs_values[unique_indices]
|
|
||||||
interp_func = interp1d(max_accel_values, vibrs_values, kind='cubic')
|
|
||||||
max_accel_fine = np.linspace(max_accel_values.min(), max_accel_values.max(), number_of_interpolated_points)
|
|
||||||
vibrs_fine = interp_func(max_accel_fine)
|
|
||||||
|
|
||||||
ax.plot(max_accel_fine, vibrs_fine, label=f'{shaper_type}', zorder=10)
|
|
||||||
axins.plot(max_accel_fine, vibrs_fine, label=f'{shaper_type}', zorder=15)
|
|
||||||
max_vibrations = max(max_vibrations, max(vibrs_fine))
|
|
||||||
|
|
||||||
ax.set_xlabel('Max Acceleration')
|
|
||||||
ax.set_ylabel('Remaining Vibrations (%)')
|
|
||||||
ax.set_xlim([min_accel_limit * 0.95, max_accel_limit * 1.1])
|
|
||||||
ax.set_ylim([-0.5, np.clip(max_vibrations * 1.05, 50, MAX_VIBRATIONS_PLOTTED)])
|
|
||||||
ax.set_title(
|
|
||||||
'Filters performances over acceleration',
|
|
||||||
fontsize=14,
|
|
||||||
color=KLIPPAIN_COLORS['dark_orange'],
|
|
||||||
weight='bold',
|
|
||||||
)
|
|
||||||
ax.legend(loc='best', prop=fontP)
|
|
||||||
|
|
||||||
|
|
||||||
def print_shaper_table(fig: plt.Figure, shaper_table_data: Dict[str, List[Dict[str, str]]]) -> None:
|
|
||||||
columns = ['Type', 'Frequency', 'Vibrations', 'Smoothing', 'Max Accel']
|
|
||||||
table_data = []
|
|
||||||
|
|
||||||
for shaper_info in shaper_table_data['shapers']:
|
|
||||||
row = [
|
|
||||||
f'{shaper_info["type"].upper()}',
|
|
||||||
f'{shaper_info["frequency"]:.1f} Hz',
|
|
||||||
f'{shaper_info["vibrations"] * 100:.1f} %',
|
|
||||||
f'{shaper_info["smoothing"]:.3f}',
|
|
||||||
f'{round(shaper_info["max_accel"] / 10) * 10:.0f}',
|
|
||||||
]
|
|
||||||
table_data.append(row)
|
|
||||||
table = plt.table(cellText=table_data, colLabels=columns, bbox=[1.130, -0.4, 0.803, 0.25], cellLoc='center')
|
|
||||||
table.auto_set_font_size(False)
|
|
||||||
table.set_fontsize(10)
|
|
||||||
table.auto_set_column_width([0, 1, 2, 3, 4])
|
|
||||||
table.set_zorder(100)
|
|
||||||
|
|
||||||
# Add the recommendations and damping ratio using fig.text
|
|
||||||
fig.text(
|
|
||||||
0.585,
|
|
||||||
0.235,
|
|
||||||
f'Estimated damping ratio (ζ): {shaper_table_data["damping_ratio"]:.3f}',
|
|
||||||
fontsize=14,
|
|
||||||
color=KLIPPAIN_COLORS['purple'],
|
|
||||||
)
|
|
||||||
if len(shaper_table_data['recommendations']) == 1:
|
|
||||||
fig.text(
|
|
||||||
0.585,
|
|
||||||
0.200,
|
|
||||||
shaper_table_data['recommendations'][0],
|
|
||||||
fontsize=14,
|
|
||||||
color=KLIPPAIN_COLORS['red_pink'],
|
|
||||||
)
|
|
||||||
elif len(shaper_table_data['recommendations']) == 2:
|
|
||||||
fig.text(
|
|
||||||
0.585,
|
|
||||||
0.200,
|
|
||||||
shaper_table_data['recommendations'][0],
|
|
||||||
fontsize=14,
|
|
||||||
color=KLIPPAIN_COLORS['red_pink'],
|
|
||||||
)
|
|
||||||
fig.text(
|
|
||||||
0.585,
|
|
||||||
0.175,
|
|
||||||
shaper_table_data['recommendations'][1],
|
|
||||||
fontsize=14,
|
|
||||||
color=KLIPPAIN_COLORS['red_pink'],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Startup and main routines
|
# Startup and main routines
|
||||||
######################################################################
|
######################################################################
|
||||||
@@ -604,8 +375,8 @@ def shaper_calibration(
|
|||||||
ConsoleOutput.print('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
|
# Compute shapers, PSD outputs and spectrogram
|
||||||
klipper_shaper_choice, shapers, additional_shapers, calibration_data, fr, zeta, max_smoothing_computed, compat = (
|
klipper_shaper_choice, shapers, calibration_data, fr, zeta, compat = calibrate_shaper(
|
||||||
calibrate_shaper(datas[0], max_smoothing, scv, max_freq)
|
datas[0], max_smoothing, scv, max_freq
|
||||||
)
|
)
|
||||||
pdata, bins, t = compute_spectrogram(datas[0])
|
pdata, bins, t = compute_spectrogram(datas[0])
|
||||||
del datas
|
del datas
|
||||||
@@ -629,31 +400,29 @@ def shaper_calibration(
|
|||||||
peak_freqs_formated = ['{:.1f}'.format(f) for f in peaks_freqs]
|
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])
|
num_peaks_above_effect_threshold = np.sum(calibration_data.psd_sum[peaks] > peaks_threshold[1])
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(
|
||||||
f"Peaks detected on the graph: {num_peaks} @ {', '.join(map(str, peak_freqs_formated))} Hz ({num_peaks_above_effect_threshold} above effect threshold)"
|
f"\nPeaks detected on the graph: {num_peaks} @ {', '.join(map(str, peak_freqs_formated))} Hz ({num_peaks_above_effect_threshold} above effect threshold)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create graph layout
|
# Create graph layout
|
||||||
fig, ((ax1, ax3), (ax2, ax4)) = plt.subplots(
|
fig, (ax1, ax2) = plt.subplots(
|
||||||
2,
|
|
||||||
2,
|
2,
|
||||||
|
1,
|
||||||
gridspec_kw={
|
gridspec_kw={
|
||||||
'height_ratios': [4, 3],
|
'height_ratios': [4, 3],
|
||||||
'width_ratios': [5, 4],
|
|
||||||
'bottom': 0.050,
|
'bottom': 0.050,
|
||||||
'top': 0.890,
|
'top': 0.890,
|
||||||
'left': 0.048,
|
'left': 0.085,
|
||||||
'right': 0.966,
|
'right': 0.966,
|
||||||
'hspace': 0.169,
|
'hspace': 0.169,
|
||||||
'wspace': 0.150,
|
'wspace': 0.200,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
ax4.remove()
|
fig.set_size_inches(8.3, 11.6)
|
||||||
fig.set_size_inches(15, 11.6)
|
|
||||||
|
|
||||||
# Add a title with some test info
|
# Add a title with some test info
|
||||||
title_line1 = 'INPUT SHAPER CALIBRATION TOOL'
|
title_line1 = 'INPUT SHAPER CALIBRATION TOOL'
|
||||||
fig.text(
|
fig.text(
|
||||||
0.065, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold'
|
0.12, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold'
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
filename_parts = (lognames[0].split('/')[-1]).split('_')
|
filename_parts = (lognames[0].split('/')[-1]).split('_')
|
||||||
@@ -664,11 +433,8 @@ def shaper_calibration(
|
|||||||
title_line4 = '| and SCV are not used for filter recommendations!'
|
title_line4 = '| and SCV are not used for filter recommendations!'
|
||||||
title_line5 = f'| Accel per Hz used: {accel_per_hz} mm/s²/Hz' if accel_per_hz is not None else ''
|
title_line5 = f'| Accel per Hz used: {accel_per_hz} mm/s²/Hz' if accel_per_hz is not None else ''
|
||||||
else:
|
else:
|
||||||
max_smoothing_string = (
|
|
||||||
f'maximum ({max_smoothing_computed:0.3f})' if max_smoothing is None else f'{max_smoothing:0.3f}'
|
|
||||||
)
|
|
||||||
title_line3 = f'| Square corner velocity: {scv} mm/s'
|
title_line3 = f'| Square corner velocity: {scv} mm/s'
|
||||||
title_line4 = f'| Allowed smoothing: {max_smoothing_string}'
|
title_line4 = f'| Max allowed smoothing: {max_smoothing}'
|
||||||
title_line5 = f'| Accel per Hz used: {accel_per_hz} mm/s²/Hz' if accel_per_hz is not None else ''
|
title_line5 = f'| Accel per Hz used: {accel_per_hz} mm/s²/Hz' if accel_per_hz is not None else ''
|
||||||
except Exception:
|
except Exception:
|
||||||
ConsoleOutput.print(f'Warning: CSV filename look to be different than expected ({lognames[0]})')
|
ConsoleOutput.print(f'Warning: CSV filename look to be different than expected ({lognames[0]})')
|
||||||
@@ -676,22 +442,19 @@ def shaper_calibration(
|
|||||||
title_line3 = ''
|
title_line3 = ''
|
||||||
title_line4 = ''
|
title_line4 = ''
|
||||||
title_line5 = ''
|
title_line5 = ''
|
||||||
fig.text(0.065, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
fig.text(0.50, 0.990, title_line3, ha='left', va='top', fontsize=14, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.58, 0.963, title_line3, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
fig.text(0.50, 0.968, title_line4, ha='left', va='top', fontsize=14, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.58, 0.948, title_line4, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
fig.text(0.501, 0.945, title_line5, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.58, 0.933, title_line5, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
|
|
||||||
# Plot the graphs
|
# Plot the graphs
|
||||||
shaper_table_data = plot_freq_response(
|
plot_freq_response(
|
||||||
ax1, calibration_data, shapers, klipper_shaper_choice, peaks, peaks_freqs, peaks_threshold, fr, zeta, max_freq
|
ax1, calibration_data, shapers, klipper_shaper_choice, peaks, peaks_freqs, peaks_threshold, fr, zeta, max_freq
|
||||||
)
|
)
|
||||||
plot_spectrogram(ax2, t, bins, pdata, peaks_freqs, max_freq)
|
plot_spectrogram(ax2, t, bins, pdata, peaks_freqs, max_freq)
|
||||||
plot_smoothing_vs_accel(ax3, shaper_table_data, additional_shapers)
|
|
||||||
|
|
||||||
print_shaper_table(fig, shaper_table_data)
|
|
||||||
|
|
||||||
# Adding a small Klippain logo to the top left corner of the figure
|
# Adding a small Klippain logo to the top left corner of the figure
|
||||||
ax_logo = fig.add_axes([0.001, 0.924, 0.075, 0.075], anchor='NW')
|
ax_logo = fig.add_axes([0.001, 0.8995, 0.1, 0.1], anchor='NW')
|
||||||
ax_logo.imshow(plt.imread(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'klippain.png')))
|
ax_logo.imshow(plt.imread(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'klippain.png')))
|
||||||
ax_logo.axis('off')
|
ax_logo.axis('off')
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
# loading of the plugin, and the registration of the tuning commands
|
# loading of the plugin, and the registration of the tuning commands
|
||||||
|
|
||||||
|
|
||||||
import importlib
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -30,176 +29,156 @@ from .helpers.console_output import ConsoleOutput
|
|||||||
from .shaketune_config import ShakeTuneConfig
|
from .shaketune_config import ShakeTuneConfig
|
||||||
from .shaketune_process import ShakeTuneProcess
|
from .shaketune_process import ShakeTuneProcess
|
||||||
|
|
||||||
DEFAULT_FOLDER = '~/printer_data/config/ShakeTune_results'
|
IN_DANGER = False
|
||||||
DEFAULT_NUMBER_OF_RESULTS = 3
|
|
||||||
DEFAULT_KEEP_RAW_CSV = False
|
|
||||||
DEFAULT_DPI = 150
|
|
||||||
DEFAULT_TIMEOUT = 300
|
|
||||||
DEFAULT_SHOW_MACROS = True
|
|
||||||
ST_COMMANDS = {
|
|
||||||
'EXCITATE_AXIS_AT_FREQ': (
|
|
||||||
'Maintain a specified excitation frequency for a period '
|
|
||||||
'of time to diagnose and locate a source of vibrations'
|
|
||||||
),
|
|
||||||
'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'
|
|
||||||
),
|
|
||||||
'COMPARE_BELTS_RESPONSES': (
|
|
||||||
'Perform a custom half-axis test to analyze and compare the '
|
|
||||||
'frequency profiles of individual belts on CoreXY or CoreXZ printers'
|
|
||||||
),
|
|
||||||
'AXES_SHAPER_CALIBRATION': 'Perform standard axis input shaper tests on one or both XY axes to select the best input shaper filter',
|
|
||||||
'CREATE_VIBRATIONS_PROFILE': (
|
|
||||||
'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'
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ShakeTune:
|
class ShakeTune:
|
||||||
def __init__(self, config) -> None:
|
def __init__(self, config) -> None:
|
||||||
self._config = config
|
self._pconfig = config
|
||||||
self._printer = config.get_printer()
|
self._printer = config.get_printer()
|
||||||
self._printer.register_event_handler('klippy:connect', self._on_klippy_connect)
|
|
||||||
|
|
||||||
# Check if Shake&Tune is running in DangerKlipper
|
|
||||||
self.IN_DANGER = importlib.util.find_spec('extras.danger_options') is not None
|
|
||||||
|
|
||||||
# Register the console print output callback to the corresponding Klipper function
|
|
||||||
gcode = self._printer.lookup_object('gcode')
|
gcode = self._printer.lookup_object('gcode')
|
||||||
ConsoleOutput.register_output_callback(gcode.respond_info)
|
|
||||||
|
|
||||||
self._initialize_config(config)
|
res_tester = self._printer.lookup_object('resonance_tester', None)
|
||||||
self._register_commands()
|
if res_tester is None:
|
||||||
|
config.error('No [resonance_tester] config section found in printer.cfg! Please add one to use Shake&Tune.')
|
||||||
# Initialize the ShakeTune object and its configuration
|
|
||||||
def _initialize_config(self, config) -> None:
|
|
||||||
result_folder = config.get('result_folder', default=DEFAULT_FOLDER)
|
|
||||||
result_folder_path = Path(result_folder).expanduser() if result_folder else None
|
|
||||||
keep_n_results = config.getint('number_of_results_to_keep', default=DEFAULT_NUMBER_OF_RESULTS, minval=0)
|
|
||||||
keep_csv = config.getboolean('keep_raw_csv', default=DEFAULT_KEEP_RAW_CSV)
|
|
||||||
dpi = config.getint('dpi', default=DEFAULT_DPI, minval=100, maxval=500)
|
|
||||||
self._st_config = ShakeTuneConfig(result_folder_path, keep_n_results, keep_csv, dpi)
|
|
||||||
|
|
||||||
self.timeout = config.getfloat('timeout', 300, above=0.0)
|
self.timeout = config.getfloat('timeout', 300, above=0.0)
|
||||||
self._show_macros = config.getboolean('show_macros_in_webui', default=True)
|
result_folder = config.get('result_folder', default='~/printer_data/config/ShakeTune_results')
|
||||||
|
result_folder_path = Path(result_folder).expanduser() if result_folder else None
|
||||||
|
keep_n_results = config.getint('number_of_results_to_keep', default=3, minval=0)
|
||||||
|
keep_csv = config.getboolean('keep_raw_csv', default=False)
|
||||||
|
show_macros = config.getboolean('show_macros_in_webui', default=True)
|
||||||
|
dpi = config.getint('dpi', default=150, minval=100, maxval=500)
|
||||||
|
|
||||||
# Create the Klipper commands to allow the user to run Shake&Tune's tools
|
self._config = ShakeTuneConfig(result_folder_path, keep_n_results, keep_csv, dpi)
|
||||||
def _register_commands(self) -> None:
|
ConsoleOutput.register_output_callback(gcode.respond_info)
|
||||||
gcode = self._printer.lookup_object('gcode')
|
|
||||||
|
# Register Shake&Tune's measurement commands
|
||||||
measurement_commands = [
|
measurement_commands = [
|
||||||
('EXCITATE_AXIS_AT_FREQ', self.cmd_EXCITATE_AXIS_AT_FREQ, ST_COMMANDS['EXCITATE_AXIS_AT_FREQ']),
|
(
|
||||||
('AXES_MAP_CALIBRATION', self.cmd_AXES_MAP_CALIBRATION, ST_COMMANDS['AXES_MAP_CALIBRATION']),
|
'EXCITATE_AXIS_AT_FREQ',
|
||||||
('COMPARE_BELTS_RESPONSES', self.cmd_COMPARE_BELTS_RESPONSES, ST_COMMANDS['COMPARE_BELTS_RESPONSES']),
|
self.cmd_EXCITATE_AXIS_AT_FREQ,
|
||||||
('AXES_SHAPER_CALIBRATION', self.cmd_AXES_SHAPER_CALIBRATION, ST_COMMANDS['AXES_SHAPER_CALIBRATION']),
|
(
|
||||||
('CREATE_VIBRATIONS_PROFILE', self.cmd_CREATE_VIBRATIONS_PROFILE, ST_COMMANDS['CREATE_VIBRATIONS_PROFILE']),
|
'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'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'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 or CoreXZ printers'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'AXES_SHAPER_CALIBRATION',
|
||||||
|
self.cmd_AXES_SHAPER_CALIBRATION,
|
||||||
|
'Perform standard axis input shaper tests on one or both XY axes to select the best input shaper filter',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'CREATE_VIBRATIONS_PROFILE',
|
||||||
|
self.cmd_CREATE_VIBRATIONS_PROFILE,
|
||||||
|
(
|
||||||
|
'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 measurement_commands}
|
||||||
# Register Shake&Tune's measurement commands using the official Klipper API (gcode.register_command)
|
|
||||||
# Doing this makes the commands available in Klipper but they are not shown in the web interfaces
|
|
||||||
# and are only available by typing the full name in the console (like all the other Klipper commands)
|
|
||||||
for name, command, description in measurement_commands:
|
for name, command, description in measurement_commands:
|
||||||
gcode.register_command(f'_{name}' if self._show_macros else name, command, desc=description)
|
gcode.register_command(f'_{name}' if show_macros else name, command, desc=description)
|
||||||
|
|
||||||
# Then, a hack to inject the macros into Klipper's config system in order to show them in the web
|
# Load the dummy macros with their description in order to show them in the web interfaces
|
||||||
# interfaces. This is not a good way to do it, but it's the only way to do it for now to get
|
if show_macros:
|
||||||
# a good user experience while using Shake&Tune (it's indeed easier to just click a macro button)
|
pconfig = self._printer.lookup_object('configfile')
|
||||||
if self._show_macros:
|
|
||||||
configfile = self._printer.lookup_object('configfile')
|
|
||||||
dirname = os.path.dirname(os.path.realpath(__file__))
|
dirname = os.path.dirname(os.path.realpath(__file__))
|
||||||
filename = os.path.join(dirname, 'dummy_macros.cfg')
|
filename = os.path.join(dirname, 'dummy_macros.cfg')
|
||||||
try:
|
try:
|
||||||
dummy_macros_cfg = configfile.read_config(filename)
|
dummy_macros_cfg = pconfig.read_config(filename)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise self._config.error(f'Cannot load Shake&Tune dummy macro {filename}') from err
|
raise config.error(f'Cannot load Shake&Tune dummy macro {filename}') from err
|
||||||
|
|
||||||
for gcode_macro in dummy_macros_cfg.get_prefix_sections('gcode_macro '):
|
for gcode_macro in dummy_macros_cfg.get_prefix_sections('gcode_macro '):
|
||||||
gcode_macro_name = gcode_macro.get_name()
|
gcode_macro_name = gcode_macro.get_name()
|
||||||
|
|
||||||
# Replace the dummy description by the one from ST_COMMANDS (to avoid code duplication and define it in only one place)
|
# Replace the dummy description by the one here (to avoid code duplication and define it in only one place)
|
||||||
command = gcode_macro_name.split(' ', 1)[1]
|
command = gcode_macro_name.split(' ', 1)[1]
|
||||||
description = ST_COMMANDS.get(command, 'Shake&Tune macro')
|
description = command_descriptions.get(command, 'Shake&Tune macro')
|
||||||
gcode_macro.fileconfig.set(gcode_macro_name, 'description', description)
|
gcode_macro.fileconfig.set(gcode_macro_name, 'description', description)
|
||||||
|
|
||||||
# Add the section to the Klipper configuration object with all its options
|
# Add the section to the Klipper configuration object with all its options
|
||||||
if not self._config.fileconfig.has_section(gcode_macro_name.lower()):
|
if not config.fileconfig.has_section(gcode_macro_name.lower()):
|
||||||
self._config.fileconfig.add_section(gcode_macro_name.lower())
|
config.fileconfig.add_section(gcode_macro_name.lower())
|
||||||
for option in gcode_macro.fileconfig.options(gcode_macro_name):
|
for option in gcode_macro.fileconfig.options(gcode_macro_name):
|
||||||
value = gcode_macro.fileconfig.get(gcode_macro_name, option)
|
value = gcode_macro.fileconfig.get(gcode_macro_name, option)
|
||||||
self._config.fileconfig.set(gcode_macro_name.lower(), option, value)
|
config.fileconfig.set(gcode_macro_name.lower(), option, value)
|
||||||
|
|
||||||
# Small trick to ensure the new injected sections are considered valid by Klipper config system
|
# Small trick to ensure the new injected sections are considered valid by Klipper config system
|
||||||
self._config.access_tracking[(gcode_macro_name.lower(), option.lower())] = 1
|
config.access_tracking[(gcode_macro_name.lower(), option.lower())] = 1
|
||||||
|
|
||||||
# Finally, load the section within the printer objects
|
# Finally, load the section within the printer objects
|
||||||
self._printer.load_object(self._config, gcode_macro_name.lower())
|
self._printer.load_object(config, gcode_macro_name.lower())
|
||||||
|
|
||||||
def _on_klippy_connect(self) -> None:
|
|
||||||
# Check if the resonance_tester object is available in the printer
|
|
||||||
# configuration as it is required for Shake&Tune to work properly
|
|
||||||
res_tester = self._printer.lookup_object('resonance_tester', None)
|
|
||||||
if res_tester is None:
|
|
||||||
raise self._config.error(
|
|
||||||
'No [resonance_tester] config section found in printer.cfg! Please add one to use Shake&Tune!'
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------
|
|
||||||
# ------------------------------------------------------------------------------------------
|
|
||||||
# Following are all the Shake&Tune commands that are registered to the Klipper console
|
|
||||||
# ------------------------------------------------------------------------------------------
|
|
||||||
# ------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def cmd_EXCITATE_AXIS_AT_FREQ(self, gcmd) -> None:
|
def cmd_EXCITATE_AXIS_AT_FREQ(self, gcmd) -> None:
|
||||||
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
|
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
|
||||||
static_freq_graph_creator = StaticGraphCreator(self._st_config)
|
static_freq_graph_creator = StaticGraphCreator(self._config)
|
||||||
st_process = ShakeTuneProcess(
|
st_process = ShakeTuneProcess(
|
||||||
self._st_config,
|
self._config,
|
||||||
self._printer.get_reactor(),
|
self._printer.get_reactor(),
|
||||||
static_freq_graph_creator,
|
static_freq_graph_creator,
|
||||||
self.timeout,
|
self.timeout,
|
||||||
)
|
)
|
||||||
excitate_axis_at_freq(gcmd, self._config, st_process)
|
excitate_axis_at_freq(gcmd, self._pconfig, st_process)
|
||||||
|
|
||||||
def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None:
|
def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None:
|
||||||
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
|
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
|
||||||
axes_map_graph_creator = AxesMapGraphCreator(self._st_config)
|
axes_map_graph_creator = AxesMapGraphCreator(self._config)
|
||||||
st_process = ShakeTuneProcess(
|
st_process = ShakeTuneProcess(
|
||||||
self._st_config,
|
self._config,
|
||||||
self._printer.get_reactor(),
|
self._printer.get_reactor(),
|
||||||
axes_map_graph_creator,
|
axes_map_graph_creator,
|
||||||
self.timeout,
|
self.timeout,
|
||||||
)
|
)
|
||||||
axes_map_calibration(gcmd, self._config, st_process)
|
axes_map_calibration(gcmd, self._pconfig, st_process)
|
||||||
|
|
||||||
def cmd_COMPARE_BELTS_RESPONSES(self, gcmd) -> None:
|
def cmd_COMPARE_BELTS_RESPONSES(self, gcmd) -> None:
|
||||||
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
|
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
|
||||||
belt_graph_creator = BeltsGraphCreator(self._st_config)
|
belt_graph_creator = BeltsGraphCreator(self._config)
|
||||||
st_process = ShakeTuneProcess(
|
st_process = ShakeTuneProcess(
|
||||||
self._st_config,
|
self._config,
|
||||||
self._printer.get_reactor(),
|
self._printer.get_reactor(),
|
||||||
belt_graph_creator,
|
belt_graph_creator,
|
||||||
self.timeout,
|
self.timeout,
|
||||||
)
|
)
|
||||||
compare_belts_responses(gcmd, self._config, st_process)
|
compare_belts_responses(gcmd, self._pconfig, st_process)
|
||||||
|
|
||||||
def cmd_AXES_SHAPER_CALIBRATION(self, gcmd) -> None:
|
def cmd_AXES_SHAPER_CALIBRATION(self, gcmd) -> None:
|
||||||
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
|
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
|
||||||
shaper_graph_creator = ShaperGraphCreator(self._st_config)
|
shaper_graph_creator = ShaperGraphCreator(self._config)
|
||||||
st_process = ShakeTuneProcess(
|
st_process = ShakeTuneProcess(
|
||||||
self._st_config,
|
self._config,
|
||||||
self._printer.get_reactor(),
|
self._printer.get_reactor(),
|
||||||
shaper_graph_creator,
|
shaper_graph_creator,
|
||||||
self.timeout,
|
self.timeout,
|
||||||
)
|
)
|
||||||
axes_shaper_calibration(gcmd, self._config, st_process)
|
axes_shaper_calibration(gcmd, self._pconfig, st_process)
|
||||||
|
|
||||||
def cmd_CREATE_VIBRATIONS_PROFILE(self, gcmd) -> None:
|
def cmd_CREATE_VIBRATIONS_PROFILE(self, gcmd) -> None:
|
||||||
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
|
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
|
||||||
vibration_profile_creator = VibrationsGraphCreator(self._st_config)
|
vibration_profile_creator = VibrationsGraphCreator(self._config)
|
||||||
st_process = ShakeTuneProcess(
|
st_process = ShakeTuneProcess(
|
||||||
self._st_config,
|
self._config,
|
||||||
self._printer.get_reactor(),
|
self._printer.get_reactor(),
|
||||||
vibration_profile_creator,
|
vibration_profile_creator,
|
||||||
self.timeout,
|
self.timeout,
|
||||||
)
|
)
|
||||||
create_vibrations_profile(gcmd, self._config, st_process)
|
create_vibrations_profile(gcmd, self._pconfig, st_process)
|
||||||
|
|||||||
Reference in New Issue
Block a user