Compare commits
4 Commits
develop
...
smooth-acc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c951c57f4 | ||
|
|
9798e5ae19 | ||
|
|
e364b9079e | ||
|
|
ccd95e27e1 |
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
|
sudo apt-get install -y build-essential gcc-avr avr-libc
|
||||||
- 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/linux_basic.dict
|
cp ../klipper/out/klipper.dict ../dicts/atmega2560.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
|
||||||
|
|||||||
@@ -1,34 +1,4 @@
|
|||||||
CONFIG_LOW_LEVEL_OPTIONS=y
|
# Base Kconfig file for atmega2560
|
||||||
# CONFIG_MACH_AVR is not set
|
CONFIG_MACH_AVR=y
|
||||||
# CONFIG_MACH_ATSAM is not set
|
CONFIG_MACH_atmega2560=y
|
||||||
# CONFIG_MACH_ATSAMD is not set
|
CONFIG_CLOCK_FREQ=16000000
|
||||||
# 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,9 +1,85 @@
|
|||||||
|
# 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: /tmp/klipper_host_mcu
|
serial: /dev/ttyACM0
|
||||||
|
|
||||||
[printer]
|
[printer]
|
||||||
kinematics: none
|
kinematics: cartesian
|
||||||
max_velocity: 300
|
max_velocity: 300
|
||||||
max_accel: 300
|
max_accel: 3000
|
||||||
|
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
|
||||||
|
|||||||
@@ -22,13 +22,14 @@
|
|||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import Dict, 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')
|
||||||
|
|
||||||
@@ -47,7 +48,9 @@ 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',
|
||||||
@@ -112,15 +115,13 @@ 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:
|
||||||
shaper, all_shapers = helper.find_best_shaper(
|
k_shaper_choice, all_shapers = helper.find_best_shaper(
|
||||||
calibration_data,
|
calibration_data,
|
||||||
shapers=None,
|
shapers=None,
|
||||||
damping_ratio=zeta,
|
damping_ratio=zeta,
|
||||||
@@ -129,23 +130,79 @@ 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=ConsoleOutput.print,
|
logger=None,
|
||||||
|
)
|
||||||
|
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 '
|
||||||
ConsoleOutput.print(
|
'Shake&Tune features!\nShake&Tune now runs in compatibility mode: be aware that the results may be '
|
||||||
'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'
|
'slightly off, since the real damping ratio cannot be used to craft accurate filter recommendations'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
compat = True
|
compat = True
|
||||||
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, ConsoleOutput.print)
|
k_shaper_choice, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, None)
|
||||||
|
|
||||||
ConsoleOutput.print(
|
# If max_smoothing is not None, we run the same computation but without a smoothing value
|
||||||
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})'
|
# to get the max smoothing values from the filters and create the testing list
|
||||||
|
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 shaper.name, all_shapers, calibration_data, fr, zeta, compat
|
return k_shaper_choice.name, all_shapers, additional_all_shapers, calibration_data, fr, zeta, max_smoothing, compat
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
@@ -164,7 +221,7 @@ def plot_freq_response(
|
|||||||
fr: float,
|
fr: float,
|
||||||
zeta: float,
|
zeta: float,
|
||||||
max_freq: float,
|
max_freq: float,
|
||||||
) -> None:
|
) -> Dict[str, List[Dict[str, str]]]:
|
||||||
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
|
||||||
@@ -193,27 +250,40 @@ 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:
|
||||||
shaper_max_accel = round(shaper.max_accel / 100.0) * 100.0
|
ax2.plot(freqs, shaper.vals, label=shaper.name.upper(), linestyle='dotted')
|
||||||
label = f'{shaper.name.upper()} ({shaper.freq:.1f} Hz, vibr={shaper.vibrs * 100.0:.1f}%, sm~={shaper.smoothing:.2f}, accel<={shaper_max_accel:.0f})'
|
|
||||||
ax2.plot(freqs, shaper.vals, label=label, linestyle='dotted')
|
shaper_info = {
|
||||||
|
'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
|
||||||
|
|
||||||
@@ -226,32 +296,30 @@ 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
|
||||||
):
|
):
|
||||||
ax2.plot(
|
perf_shaper_string = f'Recommended for performance: {perf_shaper_choice.upper()} @ {perf_shaper_freq:.1f} Hz'
|
||||||
[],
|
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',
|
||||||
)
|
)
|
||||||
ax2.plot(
|
ax.plot(
|
||||||
[],
|
freqs,
|
||||||
[],
|
psd * klipper_shaper_vals,
|
||||||
' ',
|
label=f'With {klipper_shaper_choice.upper()} applied',
|
||||||
label=f'Recommended low vibrations shaper: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz',
|
color='lime',
|
||||||
)
|
)
|
||||||
ax.plot(freqs, psd * klipper_shaper_vals, label=f'With {klipper_shaper_choice.upper()} applied', color='lime')
|
|
||||||
else:
|
else:
|
||||||
ax2.plot(
|
shaper_string = f'Recommended best shaper: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz'
|
||||||
[],
|
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,
|
||||||
@@ -259,9 +327,6 @@ 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)
|
||||||
@@ -297,7 +362,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
|
return shaper_table_data
|
||||||
|
|
||||||
|
|
||||||
# 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
|
||||||
@@ -350,6 +415,170 @@ 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
|
||||||
######################################################################
|
######################################################################
|
||||||
@@ -375,8 +604,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, calibration_data, fr, zeta, compat = calibrate_shaper(
|
klipper_shaper_choice, shapers, additional_shapers, calibration_data, fr, zeta, max_smoothing_computed, compat = (
|
||||||
datas[0], max_smoothing, scv, max_freq
|
calibrate_shaper(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
|
||||||
@@ -400,29 +629,31 @@ 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"\nPeaks detected on the graph: {num_peaks} @ {', '.join(map(str, peak_freqs_formated))} Hz ({num_peaks_above_effect_threshold} above effect threshold)"
|
f"Peaks 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, ax2) = plt.subplots(
|
fig, ((ax1, ax3), (ax2, ax4)) = 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.085,
|
'left': 0.048,
|
||||||
'right': 0.966,
|
'right': 0.966,
|
||||||
'hspace': 0.169,
|
'hspace': 0.169,
|
||||||
'wspace': 0.200,
|
'wspace': 0.150,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
fig.set_size_inches(8.3, 11.6)
|
ax4.remove()
|
||||||
|
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.12, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold'
|
0.065, 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('_')
|
||||||
@@ -433,8 +664,11 @@ 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'| Max allowed smoothing: {max_smoothing}'
|
title_line4 = f'| Allowed smoothing: {max_smoothing_string}'
|
||||||
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]})')
|
||||||
@@ -442,19 +676,22 @@ def shaper_calibration(
|
|||||||
title_line3 = ''
|
title_line3 = ''
|
||||||
title_line4 = ''
|
title_line4 = ''
|
||||||
title_line5 = ''
|
title_line5 = ''
|
||||||
fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.065, 0.957, title_line2, ha='left', va='top', fontsize=16, 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.990, title_line3, 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.50, 0.968, title_line4, ha='left', va='top', fontsize=14, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
fig.text(0.58, 0.933, title_line5, 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'])
|
||||||
|
|
||||||
# Plot the graphs
|
# Plot the graphs
|
||||||
plot_freq_response(
|
shaper_table_data = 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.8995, 0.1, 0.1], anchor='NW')
|
ax_logo = fig.add_axes([0.001, 0.924, 0.075, 0.075], 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,6 +8,7 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -29,156 +30,176 @@ 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
|
||||||
|
|
||||||
IN_DANGER = False
|
DEFAULT_FOLDER = '~/printer_data/config/ShakeTune_results'
|
||||||
|
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._pconfig = config
|
self._config = 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')
|
||||||
|
|
||||||
res_tester = self._printer.lookup_object('resonance_tester', None)
|
|
||||||
if res_tester is None:
|
|
||||||
config.error('No [resonance_tester] config section found in printer.cfg! Please add one to use Shake&Tune.')
|
|
||||||
|
|
||||||
self.timeout = config.getfloat('timeout', 300, above=0.0)
|
|
||||||
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)
|
|
||||||
|
|
||||||
self._config = ShakeTuneConfig(result_folder_path, keep_n_results, keep_csv, dpi)
|
|
||||||
ConsoleOutput.register_output_callback(gcode.respond_info)
|
ConsoleOutput.register_output_callback(gcode.respond_info)
|
||||||
|
|
||||||
# Register Shake&Tune's measurement commands
|
self._initialize_config(config)
|
||||||
measurement_commands = [
|
self._register_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 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}
|
|
||||||
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
|
# Initialize the ShakeTune object and its configuration
|
||||||
if show_macros:
|
def _initialize_config(self, config) -> None:
|
||||||
pconfig = self._printer.lookup_object('configfile')
|
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._show_macros = config.getboolean('show_macros_in_webui', default=True)
|
||||||
|
|
||||||
|
# Create the Klipper commands to allow the user to run Shake&Tune's tools
|
||||||
|
def _register_commands(self) -> None:
|
||||||
|
gcode = self._printer.lookup_object('gcode')
|
||||||
|
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']),
|
||||||
|
('COMPARE_BELTS_RESPONSES', self.cmd_COMPARE_BELTS_RESPONSES, ST_COMMANDS['COMPARE_BELTS_RESPONSES']),
|
||||||
|
('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']),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
gcode.register_command(f'_{name}' if self._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
|
||||||
|
# interfaces. This is not a good way to do it, but it's the only way to do it for now to get
|
||||||
|
# a good user experience while using Shake&Tune (it's indeed easier to just click a macro button)
|
||||||
|
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 = pconfig.read_config(filename)
|
dummy_macros_cfg = configfile.read_config(filename)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise config.error(f'Cannot load Shake&Tune dummy macro {filename}') from err
|
raise self._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 here (to avoid code duplication and define it in only one place)
|
# Replace the dummy description by the one from ST_COMMANDS (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 = command_descriptions.get(command, 'Shake&Tune macro')
|
description = ST_COMMANDS.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 config.fileconfig.has_section(gcode_macro_name.lower()):
|
if not self._config.fileconfig.has_section(gcode_macro_name.lower()):
|
||||||
config.fileconfig.add_section(gcode_macro_name.lower())
|
self._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)
|
||||||
config.fileconfig.set(gcode_macro_name.lower(), option, value)
|
self._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
|
||||||
config.access_tracking[(gcode_macro_name.lower(), option.lower())] = 1
|
self._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(config, gcode_macro_name.lower())
|
self._printer.load_object(self._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._config)
|
static_freq_graph_creator = StaticGraphCreator(self._st_config)
|
||||||
st_process = ShakeTuneProcess(
|
st_process = ShakeTuneProcess(
|
||||||
self._config,
|
self._st_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._pconfig, st_process)
|
excitate_axis_at_freq(gcmd, self._config, 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._config)
|
axes_map_graph_creator = AxesMapGraphCreator(self._st_config)
|
||||||
st_process = ShakeTuneProcess(
|
st_process = ShakeTuneProcess(
|
||||||
self._config,
|
self._st_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._pconfig, st_process)
|
axes_map_calibration(gcmd, self._config, 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._config)
|
belt_graph_creator = BeltsGraphCreator(self._st_config)
|
||||||
st_process = ShakeTuneProcess(
|
st_process = ShakeTuneProcess(
|
||||||
self._config,
|
self._st_config,
|
||||||
self._printer.get_reactor(),
|
self._printer.get_reactor(),
|
||||||
belt_graph_creator,
|
belt_graph_creator,
|
||||||
self.timeout,
|
self.timeout,
|
||||||
)
|
)
|
||||||
compare_belts_responses(gcmd, self._pconfig, st_process)
|
compare_belts_responses(gcmd, self._config, 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._config)
|
shaper_graph_creator = ShaperGraphCreator(self._st_config)
|
||||||
st_process = ShakeTuneProcess(
|
st_process = ShakeTuneProcess(
|
||||||
self._config,
|
self._st_config,
|
||||||
self._printer.get_reactor(),
|
self._printer.get_reactor(),
|
||||||
shaper_graph_creator,
|
shaper_graph_creator,
|
||||||
self.timeout,
|
self.timeout,
|
||||||
)
|
)
|
||||||
axes_shaper_calibration(gcmd, self._pconfig, st_process)
|
axes_shaper_calibration(gcmd, self._config, 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._config)
|
vibration_profile_creator = VibrationsGraphCreator(self._st_config)
|
||||||
st_process = ShakeTuneProcess(
|
st_process = ShakeTuneProcess(
|
||||||
self._config,
|
self._st_config,
|
||||||
self._printer.get_reactor(),
|
self._printer.get_reactor(),
|
||||||
vibration_profile_creator,
|
vibration_profile_creator,
|
||||||
self.timeout,
|
self.timeout,
|
||||||
)
|
)
|
||||||
create_vibrations_profile(gcmd, self._pconfig, st_process)
|
create_vibrations_profile(gcmd, self._config, st_process)
|
||||||
|
|||||||
Reference in New Issue
Block a user