Compare commits
5 Commits
smooth-acc
...
mot-res
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d59e33775 | ||
|
|
3d919898a6 | ||
|
|
c19af1c457 | ||
|
|
e3e24184be | ||
|
|
a49a571911 |
21
README.md
21
README.md
@@ -31,6 +31,27 @@ Follow these steps to install Shake&Tune on your printer:
|
|||||||
# printer.cfg file. If you want to see the macros in the webui, set this to True.
|
# printer.cfg file. If you want to see the macros in the webui, set this to True.
|
||||||
# timeout: 300
|
# timeout: 300
|
||||||
# The maximum time in seconds to let Shake&Tune process the CSV files and generate the graphs.
|
# The maximum time in seconds to let Shake&Tune process the CSV files and generate the graphs.
|
||||||
|
|
||||||
|
# motor_freq:
|
||||||
|
# /!\ This option has limitations in stock Klipper and is best used with DangerKlipper /!\
|
||||||
|
# Frequencies of X and Y motor resonances to filter them by using
|
||||||
|
# composite shapers. This requires the `[input_shaper]` config
|
||||||
|
# section to be defined in your printer.cfg file to work.
|
||||||
|
# motor_freq_x:
|
||||||
|
# motor_freq_y:
|
||||||
|
# /!\ This option has limitations in stock Klipper and is best used with DangerKlipper /!\
|
||||||
|
# If motor_freq is not set, these two parameters can be used
|
||||||
|
# to configure different filters for X and Y motors. The same
|
||||||
|
# values are supported as for motor_freq parameter.
|
||||||
|
# motor_damping_ratio: 0.05
|
||||||
|
# /!\ This option has limitations in stock Klipper and is best used with DangerKlipper /!\
|
||||||
|
# Damping ratios for X and Y motor resonances.
|
||||||
|
# motor_damping_ratio_x:
|
||||||
|
# motor_damping_ratio_y:
|
||||||
|
# /!\ This option has limitations in stock Klipper and is best used with DangerKlipper /!\
|
||||||
|
# If motor_damping_ratio is not set, these two parameters can be used
|
||||||
|
# to configure different filters for X and Y motors. The same values
|
||||||
|
# are supported as for motor_damping_ratio parameter.
|
||||||
```
|
```
|
||||||
|
|
||||||
Don't forget to check out **[Shake&Tune documentation here](./docs/README.md)**.
|
Don't forget to check out **[Shake&Tune documentation here](./docs/README.md)**.
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from ..helpers.motors_config_parser import Motor, MotorsConfigParser
|
|||||||
from ..shaketune_config import ShakeTuneConfig
|
from ..shaketune_config import ShakeTuneConfig
|
||||||
from .graph_creator import GraphCreator
|
from .graph_creator import GraphCreator
|
||||||
|
|
||||||
|
DEFAULT_LOW_FREQ_MAX = 30
|
||||||
PEAKS_DETECTION_THRESHOLD = 0.05
|
PEAKS_DETECTION_THRESHOLD = 0.05
|
||||||
PEAKS_RELATIVE_HEIGHT_THRESHOLD = 0.04
|
PEAKS_RELATIVE_HEIGHT_THRESHOLD = 0.04
|
||||||
CURVE_SIMILARITY_SIGMOID_K = 0.5
|
CURVE_SIMILARITY_SIGMOID_K = 0.5
|
||||||
@@ -114,58 +115,61 @@ def calc_freq_response(data) -> Tuple[np.ndarray, np.ndarray]:
|
|||||||
return helper.process_accelerometer_data(data)
|
return helper.process_accelerometer_data(data)
|
||||||
|
|
||||||
|
|
||||||
# Calculate motor frequency profiles based on the measured Power Spectral Density (PSD) measurements for the machine kinematics
|
def find_motor_characteristics(motor: str, freqs: np.ndarray, psd: np.ndarray) -> Tuple[float, float, int]:
|
||||||
# main angles and then create a global motor profile as a weighted average (from their own vibrations) of all calculated profiles
|
motor_fr, motor_zeta, motor_res_idx, lowfreq_max = compute_mechanical_parameters(psd, freqs, DEFAULT_LOW_FREQ_MAX)
|
||||||
def compute_motor_profiles(
|
|
||||||
freqs: np.ndarray,
|
|
||||||
psds: dict,
|
|
||||||
all_angles_energy: dict,
|
|
||||||
measured_angles: Optional[List[int]] = None,
|
|
||||||
energy_amplification_factor: int = 2,
|
|
||||||
) -> Tuple[dict, np.ndarray]:
|
|
||||||
if measured_angles is None:
|
|
||||||
measured_angles = [0, 90]
|
|
||||||
|
|
||||||
|
if lowfreq_max:
|
||||||
|
ConsoleOutput.print(
|
||||||
|
(
|
||||||
|
f'[WARNING] {motor} motor has a lot of low frequency vibrations. This is '
|
||||||
|
'probably due to the test being performed at too high an acceleration!\n'
|
||||||
|
'Try lowering ACCEL and/or increasing SIZE before restarting the macro '
|
||||||
|
'to ensure that only constant speeds are being recorded and that the '
|
||||||
|
'dynamic behavior of the machine is not affecting the measurements.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if motor_zeta is not None:
|
||||||
|
ConsoleOutput.print(
|
||||||
|
(
|
||||||
|
f'Motor {motor} have a main resonant frequency at {motor_fr:.1f}Hz '
|
||||||
|
f'with an estimated damping ratio of {motor_zeta:.3f}'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ConsoleOutput.print(
|
||||||
|
(
|
||||||
|
f'Motor {motor} have a main resonant frequency at {motor_fr:.1f}Hz '
|
||||||
|
'but it was impossible to estimate its damping ratio.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return motor_fr, motor_zeta, motor_res_idx
|
||||||
|
|
||||||
|
|
||||||
|
# Calculate motor frequency profiles based on the measured Power Spectral Density (PSD) measurements
|
||||||
|
# for the machine kinematics main angles
|
||||||
|
def compute_motor_profiles(freqs: np.ndarray, psds: dict, measured_angles: Optional[List[int]] = (0, 90)) -> dict:
|
||||||
motor_profiles = {}
|
motor_profiles = {}
|
||||||
weighted_sum_profiles = np.zeros_like(freqs)
|
|
||||||
total_weight = 0
|
|
||||||
conv_filter = np.ones(20) / 20
|
conv_filter = np.ones(20) / 20
|
||||||
|
|
||||||
# Creating the PSD motor profiles for each angles
|
# Creating the PSD motor profiles for each angle by summing the PSDs for each speed
|
||||||
for angle in measured_angles:
|
for angle in measured_angles:
|
||||||
# Calculate the sum of PSDs for the current angle and then convolve
|
|
||||||
sum_curve = np.sum(np.array([psds[angle][speed] for speed in psds[angle]]), axis=0)
|
sum_curve = np.sum(np.array([psds[angle][speed] for speed in psds[angle]]), axis=0)
|
||||||
motor_profiles[angle] = np.convolve(sum_curve / len(psds[angle]), conv_filter, mode='same')
|
motor_profiles[angle] = np.convolve(sum_curve / len(psds[angle]), conv_filter, mode='same')
|
||||||
|
|
||||||
# Calculate weights
|
return motor_profiles
|
||||||
angle_energy = (
|
|
||||||
all_angles_energy[angle] ** energy_amplification_factor
|
|
||||||
) # First weighting factor is based on the total vibrations of the machine at the specified angle
|
|
||||||
curve_area = (
|
|
||||||
np.trapz(motor_profiles[angle], freqs) ** energy_amplification_factor
|
|
||||||
) # Additional weighting factor is based on the area under the current motor profile at this specified angle
|
|
||||||
total_angle_weight = angle_energy * curve_area
|
|
||||||
|
|
||||||
# Update weighted sum profiles to get the global motor profile
|
|
||||||
weighted_sum_profiles += motor_profiles[angle] * total_angle_weight
|
|
||||||
total_weight += total_angle_weight
|
|
||||||
|
|
||||||
# Creating a global average motor profile that is the weighted average of all the PSD motor profiles
|
|
||||||
global_motor_profile = weighted_sum_profiles / total_weight if total_weight != 0 else weighted_sum_profiles
|
|
||||||
|
|
||||||
return motor_profiles, global_motor_profile
|
|
||||||
|
|
||||||
|
|
||||||
# Since it was discovered that there is no non-linear mixing in the stepper "steps" vibrations, instead of measuring
|
# Since it was discovered that there is no non-linear mixing in the stepper "steps" vibrations, instead of measuring
|
||||||
# the effects of each speeds at each angles, this function simplify it by using only the main motors axes (X/Y for Cartesian
|
# the effects of each speeds at each angles, this function simplify it by using only the main motors axes (X/Y for Cartesian
|
||||||
# printers and A/B for CoreXY) measurements and project each points on the [0,360] degrees range using trigonometry
|
# printers and A/B for CoreXY) measurements and project each points on the [0, 360] degrees range using trigonometry
|
||||||
# to "sum" the vibration impact of each axis at every points of the generated spectrogram. The result is very similar at the end.
|
# to "sum" the vibration impact of each axis at every points of the generated spectrogram. The result is very similar at the end.
|
||||||
def compute_dir_speed_spectrogram(
|
def compute_dir_speed_spectrogram(
|
||||||
measured_speeds: List[float], data: dict, kinematics: str = 'cartesian', measured_angles: Optional[List[int]] = None
|
measured_speeds: List[float],
|
||||||
|
data: dict,
|
||||||
|
kinematics: str = 'cartesian',
|
||||||
|
measured_angles: Optional[List[int]] = (0, 90),
|
||||||
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
if measured_angles is None:
|
|
||||||
measured_angles = [0, 90]
|
|
||||||
|
|
||||||
# We want to project the motor vibrations measured on their own axes on the [0, 360] range
|
# We want to project the motor vibrations measured on their own axes on the [0, 360] range
|
||||||
spectrum_angles = np.linspace(0, 360, 720) # One point every 0.5 degrees
|
spectrum_angles = np.linspace(0, 360, 720) # One point every 0.5 degrees
|
||||||
spectrum_speeds = np.linspace(min(measured_speeds), max(measured_speeds), len(measured_speeds) * 6)
|
spectrum_speeds = np.linspace(min(measured_speeds), max(measured_speeds), len(measured_speeds) * 6)
|
||||||
@@ -293,11 +297,8 @@ def filter_and_split_ranges(
|
|||||||
# This function allow the computation of a symmetry score that reflect the spectrogram apparent symmetry between
|
# This function allow the computation of a symmetry score that reflect the spectrogram apparent symmetry between
|
||||||
# measured axes on both the shape of the signal and the energy level consistency across both side of the signal
|
# measured axes on both the shape of the signal and the energy level consistency across both side of the signal
|
||||||
def compute_symmetry_analysis(
|
def compute_symmetry_analysis(
|
||||||
all_angles: np.ndarray, spectrogram_data: np.ndarray, measured_angles: Optional[List[int]] = None
|
all_angles: np.ndarray, spectrogram_data: np.ndarray, measured_angles: Optional[List[int]] = (0, 90)
|
||||||
) -> float:
|
) -> float:
|
||||||
if measured_angles is None:
|
|
||||||
measured_angles = [0, 90]
|
|
||||||
|
|
||||||
total_spectrogram_angles = len(all_angles)
|
total_spectrogram_angles = len(all_angles)
|
||||||
half_spectrogram_angles = total_spectrogram_angles // 2
|
half_spectrogram_angles = total_spectrogram_angles // 2
|
||||||
|
|
||||||
@@ -501,75 +502,40 @@ def plot_angular_speed_profiles(
|
|||||||
|
|
||||||
|
|
||||||
def plot_motor_profiles(
|
def plot_motor_profiles(
|
||||||
ax: plt.Axes,
|
ax: plt.Axes, freqs: np.ndarray, main_angles: List[int], motor_profiles: dict, max_freq: float
|
||||||
freqs: np.ndarray,
|
|
||||||
main_angles: List[int],
|
|
||||||
motor_profiles: dict,
|
|
||||||
global_motor_profile: np.ndarray,
|
|
||||||
max_freq: float,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
ax.set_title('Motor frequency profile', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
ax.set_title('Motors frequency profiles', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||||
ax.set_ylabel('Energy')
|
ax.set_ylabel('Energy')
|
||||||
ax.set_xlabel('Frequency (Hz)')
|
ax.set_xlabel('Frequency (Hz)')
|
||||||
|
|
||||||
ax2 = ax.twinx()
|
ax2 = ax.twinx()
|
||||||
ax2.yaxis.set_visible(False)
|
ax2.yaxis.set_visible(False)
|
||||||
|
|
||||||
# Global weighted average motor profile
|
|
||||||
ax.plot(freqs, global_motor_profile, label='Combined', color=KLIPPAIN_COLORS['purple'], zorder=5)
|
|
||||||
max_value = global_motor_profile.max()
|
|
||||||
|
|
||||||
# Mapping of angles to axis names
|
# Mapping of angles to axis names
|
||||||
angle_settings = {0: 'X', 90: 'Y', 45: 'A', 135: 'B'}
|
angle_settings = {0: 'X', 90: 'Y', 45: 'A', 135: 'B'}
|
||||||
|
|
||||||
# And then plot the motor profiles at each measured angles
|
# And then plot the motor profiles at each measured angles with their characteristics
|
||||||
|
max_value = 0
|
||||||
for angle in main_angles:
|
for angle in main_angles:
|
||||||
profile_max = motor_profiles[angle].max()
|
profile_max = motor_profiles[angle].max()
|
||||||
if profile_max > max_value:
|
if profile_max > max_value:
|
||||||
max_value = profile_max
|
max_value = profile_max
|
||||||
label = f'{angle_settings[angle]} ({angle} deg)' if angle in angle_settings else f'{angle} deg'
|
label = f'{angle_settings[angle]} ({angle} deg)' if angle in angle_settings else f'{angle} deg'
|
||||||
ax.plot(freqs, motor_profiles[angle], linestyle='--', label=label, zorder=2)
|
ax.plot(freqs, motor_profiles[angle], label=label, zorder=2)
|
||||||
|
|
||||||
|
motor_fr, motor_zeta, motor_res_idx = find_motor_characteristics(
|
||||||
|
angle_settings[angle], freqs, motor_profiles[angle]
|
||||||
|
)
|
||||||
|
ax2.plot([], [], ' ', label=f'{angle_settings[angle]} resonant frequency (ω0): {motor_fr:.1f}Hz')
|
||||||
|
if motor_zeta is not None:
|
||||||
|
ax2.plot([], [], ' ', label=f'{angle_settings[angle]} damping ratio (ζ): {motor_zeta:.3f}')
|
||||||
|
else:
|
||||||
|
ax2.plot([], [], ' ', label=f'{angle_settings[angle]} damping ratio (ζ): unknown')
|
||||||
|
|
||||||
ax.set_xlim([0, max_freq])
|
ax.set_xlim([0, max_freq])
|
||||||
ax.set_ylim([0, max_value * 1.1])
|
ax.set_ylim([0, max_value * 1.1])
|
||||||
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0, 0))
|
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0, 0))
|
||||||
|
|
||||||
# Then add the motor resonance peak to the graph and print some infos about it
|
|
||||||
motor_fr, motor_zeta, motor_res_idx, lowfreq_max = compute_mechanical_parameters(global_motor_profile, freqs, 30)
|
|
||||||
if lowfreq_max:
|
|
||||||
ConsoleOutput.print(
|
|
||||||
'[WARNING] There are a lot of low frequency vibrations that can alter the readings. This is probably due to the test being performed at too high an acceleration!'
|
|
||||||
)
|
|
||||||
ConsoleOutput.print(
|
|
||||||
'Try lowering the ACCEL value and/or increasing the SIZE value before restarting the macro to ensure that only constant speeds are being recorded and that the dynamic behavior of the machine is not affecting the measurements'
|
|
||||||
)
|
|
||||||
if motor_zeta is not None:
|
|
||||||
ConsoleOutput.print(
|
|
||||||
f'Motors have a main resonant frequency at {motor_fr:.1f}Hz with an estimated damping ratio of {motor_zeta:.3f}'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ConsoleOutput.print(
|
|
||||||
f'Motors have a main resonant frequency at {motor_fr:.1f}Hz but it was impossible to estimate a damping ratio.'
|
|
||||||
)
|
|
||||||
|
|
||||||
ax.plot(freqs[motor_res_idx], global_motor_profile[motor_res_idx], 'x', color='black', markersize=10)
|
|
||||||
ax.annotate(
|
|
||||||
'R',
|
|
||||||
(freqs[motor_res_idx], global_motor_profile[motor_res_idx]),
|
|
||||||
textcoords='offset points',
|
|
||||||
xytext=(15, 5),
|
|
||||||
ha='right',
|
|
||||||
fontsize=14,
|
|
||||||
color=KLIPPAIN_COLORS['red_pink'],
|
|
||||||
weight='bold',
|
|
||||||
)
|
|
||||||
|
|
||||||
ax2.plot([], [], ' ', label=f'Motor resonant frequency (ω0): {motor_fr:.1f}Hz')
|
|
||||||
if motor_zeta is not None:
|
|
||||||
ax2.plot([], [], ' ', label=f'Motor damping ratio (ζ): {motor_zeta:.3f}')
|
|
||||||
else:
|
|
||||||
ax2.plot([], [], ' ', label='No damping ratio computed')
|
|
||||||
|
|
||||||
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||||
ax.grid(which='major', color='grey')
|
ax.grid(which='major', color='grey')
|
||||||
@@ -649,7 +615,7 @@ def plot_vibration_spectrogram(
|
|||||||
def plot_motor_config_txt(fig: plt.Figure, motors: List[MotorsConfigParser], differences: Optional[str]) -> None:
|
def plot_motor_config_txt(fig: plt.Figure, motors: List[MotorsConfigParser], differences: Optional[str]) -> None:
|
||||||
motor_details = [(motors[0], 'X motor'), (motors[1], 'Y motor')]
|
motor_details = [(motors[0], 'X motor'), (motors[1], 'Y motor')]
|
||||||
|
|
||||||
distance = 0.12
|
distance = 0.15
|
||||||
if motors[0].get_config('autotune_enabled'):
|
if motors[0].get_config('autotune_enabled'):
|
||||||
distance = 0.27
|
distance = 0.27
|
||||||
config_blocks = [
|
config_blocks = [
|
||||||
@@ -732,9 +698,9 @@ def vibrations_profile(
|
|||||||
shaper_calibrate = setup_klipper_import(klipperdir)
|
shaper_calibrate = setup_klipper_import(klipperdir)
|
||||||
|
|
||||||
if kinematics == 'cartesian' or kinematics == 'corexz':
|
if kinematics == 'cartesian' or kinematics == 'corexz':
|
||||||
main_angles = [0, 90]
|
main_angles = (0, 90)
|
||||||
elif kinematics == 'corexy':
|
elif kinematics == 'corexy':
|
||||||
main_angles = [45, 135]
|
main_angles = (45, 135)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Only Cartesian, CoreXY and CoreXZ kinematics are supported by this tool at the moment!')
|
raise ValueError('Only Cartesian, CoreXY and CoreXZ kinematics are supported by this tool at the moment!')
|
||||||
|
|
||||||
@@ -775,7 +741,7 @@ def vibrations_profile(
|
|||||||
)
|
)
|
||||||
all_angles_energy = compute_angle_powers(spectrogram_data)
|
all_angles_energy = compute_angle_powers(spectrogram_data)
|
||||||
sp_min_energy, sp_max_energy, sp_variance_energy, vibration_metric = compute_speed_powers(spectrogram_data)
|
sp_min_energy, sp_max_energy, sp_variance_energy, vibration_metric = compute_speed_powers(spectrogram_data)
|
||||||
motor_profiles, global_motor_profile = compute_motor_profiles(target_freqs, psds, all_angles_energy, main_angles)
|
motor_profiles = compute_motor_profiles(target_freqs, psds, main_angles)
|
||||||
|
|
||||||
# symmetry_factor = compute_symmetry_analysis(all_angles, all_angles_energy)
|
# symmetry_factor = compute_symmetry_analysis(all_angles, all_angles_energy)
|
||||||
symmetry_factor = compute_symmetry_analysis(all_angles, spectrogram_data, main_angles)
|
symmetry_factor = compute_symmetry_analysis(all_angles, spectrogram_data, main_angles)
|
||||||
@@ -884,7 +850,7 @@ def vibrations_profile(
|
|||||||
plot_angular_speed_profiles(ax3, all_speeds, all_angles, spectrogram_data, kinematics)
|
plot_angular_speed_profiles(ax3, all_speeds, all_angles, spectrogram_data, kinematics)
|
||||||
plot_vibration_spectrogram(ax5, all_angles, all_speeds, spectrogram_data, vibration_peaks)
|
plot_vibration_spectrogram(ax5, all_angles, all_speeds, spectrogram_data, vibration_peaks)
|
||||||
|
|
||||||
plot_motor_profiles(ax6, target_freqs, main_angles, motor_profiles, global_motor_profile, max_freq)
|
plot_motor_profiles(ax6, target_freqs, main_angles, motor_profiles, max_freq)
|
||||||
|
|
||||||
# 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.924, 0.075, 0.075], anchor='NW')
|
||||||
|
|||||||
142
shaketune/motor_res_filter.py
Normal file
142
shaketune/motor_res_filter.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# Shake&Tune: 3D printer analysis tools
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 Félix Boisselier <felix@fboisselier.fr> (Frix_x on Discord)
|
||||||
|
# Licensed under the GNU General Public License v3.0 (GPL-3.0)
|
||||||
|
#
|
||||||
|
# File: motor_res_filter.py
|
||||||
|
# Description: This script defines the MotorResonanceFilter class that applies and removes motor resonance filters
|
||||||
|
# into the input shaper initial Klipper object. This is done by convolving a motor resonance targeted
|
||||||
|
# input shaper filter with the current configured axis input shapers.
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from .helpers.console_output import ConsoleOutput
|
||||||
|
|
||||||
|
|
||||||
|
class MotorResonanceFilter:
|
||||||
|
def __init__(self, printer, freq_x: float, freq_y: float, damping_x: float, damping_y: float, in_danger: bool):
|
||||||
|
self._printer = printer
|
||||||
|
self.freq_x = freq_x
|
||||||
|
self.freq_y = freq_y
|
||||||
|
self.damping_x = damping_x
|
||||||
|
self.damping_y = damping_y
|
||||||
|
self._in_danger = in_danger
|
||||||
|
|
||||||
|
self._original_shapers = {}
|
||||||
|
|
||||||
|
# Convolve two Klipper shapers into a new custom composite input shaping filter
|
||||||
|
@staticmethod
|
||||||
|
def convolve_shapers(L, R):
|
||||||
|
As = [a * b for a in L[0] for b in R[0]]
|
||||||
|
Ts = [a + b for a in L[1] for b in R[1]]
|
||||||
|
C = sorted(list(zip(Ts, As)))
|
||||||
|
return ([a for _, a in C], [t for t, _ in C])
|
||||||
|
|
||||||
|
def apply_filters(self) -> None:
|
||||||
|
input_shaper = self._printer.lookup_object('input_shaper', None)
|
||||||
|
if input_shaper is None:
|
||||||
|
raise ValueError(
|
||||||
|
'Unable to apply Shake&Tune motor resonance filters: no [input_shaper] config section found!'
|
||||||
|
)
|
||||||
|
|
||||||
|
shapers = input_shaper.get_shapers()
|
||||||
|
for shaper in shapers:
|
||||||
|
axis = shaper.axis
|
||||||
|
shaper_type = shaper.params.get_status()['shaper_type']
|
||||||
|
|
||||||
|
# Ignore the motor resonance filters for smoothers from DangerKlipper
|
||||||
|
if shaper_type.startswith('smooth_'):
|
||||||
|
ConsoleOutput.print(
|
||||||
|
(
|
||||||
|
f'Warning: {shaper_type} type shaper on {axis} axis is a smoother from DangerKlipper '
|
||||||
|
'Bleeding-Edge that already filters the motor resonance frequency range. Shake&Tune '
|
||||||
|
'motor resonance filters will be ignored for this axis...'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ignore the motor resonance filters for custom shapers as users can set their own A&T values
|
||||||
|
if shaper_type == 'custom':
|
||||||
|
ConsoleOutput.print(
|
||||||
|
(
|
||||||
|
f'Warning: custom type shaper on {axis} axis is a manually crafted filter. So you have '
|
||||||
|
'already set custom A&T values for this axis and you should be able to convolve the motor '
|
||||||
|
'resonance frequency range to this custom shaper. Shake&Tune motor resonance filters will '
|
||||||
|
'be ignored for this axis...'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# At the moment, when running stock Klipper, only ZV type shapers are supported to get combined with
|
||||||
|
# the motor resonance filters. This is due to the size of the pulse train that is too small and is not
|
||||||
|
# allowing the convolved shapers to be applied. This unless this PR is merged: https://github.com/Klipper3d/klipper/pull/6460
|
||||||
|
if not self._in_danger and shaper_type != 'zv':
|
||||||
|
ConsoleOutput.print(
|
||||||
|
(
|
||||||
|
f'Error: the {axis} axis is not a ZV type shaper. Shake&Tune motor resonance filters '
|
||||||
|
'will be ignored for this axis... This is due to the size of the pulse train being too '
|
||||||
|
'small and not allowing the convolved shapers to be applied... unless this PR is '
|
||||||
|
'merged: https://github.com/Klipper3d/klipper/pull/6460'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get the current shaper parameters and store them for later restoration
|
||||||
|
_, axis_shaper_A, axis_shaper_T = shaper.get_shaper()
|
||||||
|
self._original_shapers[axis] = (axis_shaper_A, axis_shaper_T)
|
||||||
|
|
||||||
|
# Creating the new combined shapers that contains the motor resonance filters
|
||||||
|
if axis in {'x', 'y'}:
|
||||||
|
if self._in_danger:
|
||||||
|
# In DangerKlipper, the pulse train is large enough to allow the
|
||||||
|
# convolution of any shapers in order to craft the new combined shapers
|
||||||
|
# so we can use the MZV shaper (that looks to be the best for this purpose)
|
||||||
|
df = math.sqrt(1.0 - self.damping_x**2)
|
||||||
|
K = math.exp(-0.75 * self.damping_x * math.pi / df)
|
||||||
|
t_d = 1.0 / (self.freq_x * df)
|
||||||
|
a1 = 1.0 - 1.0 / math.sqrt(2.0)
|
||||||
|
a2 = (math.sqrt(2.0) - 1.0) * K
|
||||||
|
a3 = a1 * K * K
|
||||||
|
motor_filter_A = [a1, a2, a3]
|
||||||
|
motor_filter_T = [0.0, 0.375 * t_d, 0.75 * t_d]
|
||||||
|
else:
|
||||||
|
# In stock Klipper, the pulse train is too small for most shapers
|
||||||
|
# to be convolved. So we need to use the ZV shaper instead for the
|
||||||
|
# motor resonance filters... even if it's not the best for this purpose
|
||||||
|
df = math.sqrt(1.0 - self.damping_x**2)
|
||||||
|
K = math.exp(-self.damping_x * math.pi / df)
|
||||||
|
t_d = 1.0 / (self.freq_x * df)
|
||||||
|
motor_filter_A = [1.0, K]
|
||||||
|
motor_filter_T = [0.0, 0.5 * t_d]
|
||||||
|
|
||||||
|
combined_filter_A, combined_filter_T = MotorResonanceFilter.convolve_shapers(
|
||||||
|
(axis_shaper_A, axis_shaper_T),
|
||||||
|
(motor_filter_A, motor_filter_T),
|
||||||
|
)
|
||||||
|
|
||||||
|
shaper.A = combined_filter_A
|
||||||
|
shaper.T = combined_filter_T
|
||||||
|
shaper.n = len(combined_filter_A)
|
||||||
|
|
||||||
|
# Update the running input shaper filter with the new parameters
|
||||||
|
input_shaper._update_input_shaping()
|
||||||
|
|
||||||
|
def remove_filters(self) -> None:
|
||||||
|
input_shaper = self._printer.lookup_object('input_shaper', None)
|
||||||
|
if input_shaper is None:
|
||||||
|
raise ValueError(
|
||||||
|
'Unable to deactivate Shake&Tune motor resonance filters: no [input_shaper] config section found!'
|
||||||
|
)
|
||||||
|
|
||||||
|
shapers = input_shaper.get_shapers()
|
||||||
|
for shaper in shapers:
|
||||||
|
axis = shaper.axis
|
||||||
|
if axis in self._original_shapers:
|
||||||
|
A, T = self._original_shapers[axis]
|
||||||
|
shaper.A = A
|
||||||
|
shaper.T = T
|
||||||
|
shaper.n = len(A)
|
||||||
|
|
||||||
|
# Update the running input shaper filter with the restored initial parameters
|
||||||
|
# to keep only standard axis input shapers activated
|
||||||
|
input_shaper._update_input_shaping()
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -26,166 +27,234 @@ from .graph_creators import (
|
|||||||
VibrationsGraphCreator,
|
VibrationsGraphCreator,
|
||||||
)
|
)
|
||||||
from .helpers.console_output import ConsoleOutput
|
from .helpers.console_output import ConsoleOutput
|
||||||
|
from .motor_res_filter import MotorResonanceFilter
|
||||||
from .shaketune_config import ShakeTuneConfig
|
from .shaketune_config import ShakeTuneConfig
|
||||||
from .shaketune_process import ShakeTuneProcess
|
from .shaketune_process import ShakeTuneProcess
|
||||||
|
|
||||||
IN_DANGER = False
|
DEFAULT_MOTOR_DAMPING_RATIO = 0.05
|
||||||
|
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:
|
||||||
try:
|
self._config = config
|
||||||
from extras.danger_options import get_danger_options
|
|
||||||
|
|
||||||
IN_DANGER = True # check if Shake&Tune is running in DangerKlipper
|
|
||||||
except ImportError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
res_tester = self._printer.lookup_object('resonance_tester', None)
|
self._initialize_config(config)
|
||||||
if res_tester is None:
|
self._register_commands()
|
||||||
config.error('No [resonance_tester] config section found in printer.cfg! Please add one to use Shake&Tune.')
|
self._initialize_motor_resonance_filter()
|
||||||
|
|
||||||
self.timeout = config.getfloat('timeout', 300, above=0.0)
|
# Initialize the ShakeTune object and its configuration
|
||||||
|
def _initialize_config(self, config) -> None:
|
||||||
result_folder = config.get('result_folder', default='~/printer_data/config/ShakeTune_results')
|
result_folder = config.get('result_folder', default='~/printer_data/config/ShakeTune_results')
|
||||||
result_folder_path = Path(result_folder).expanduser() if result_folder else None
|
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_n_results = config.getint('number_of_results_to_keep', default=3, minval=0)
|
||||||
keep_csv = config.getboolean('keep_raw_csv', default=False)
|
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)
|
dpi = config.getint('dpi', default=150, minval=100, maxval=500)
|
||||||
|
self._st_config = ShakeTuneConfig(result_folder_path, keep_n_results, keep_csv, dpi)
|
||||||
|
|
||||||
self._config = ShakeTuneConfig(result_folder_path, keep_n_results, keep_csv, dpi)
|
self.timeout = config.getfloat('timeout', 300, above=0.0)
|
||||||
ConsoleOutput.register_output_callback(gcode.respond_info)
|
self._show_macros = config.getboolean('show_macros_in_webui', default=True)
|
||||||
|
|
||||||
# Register Shake&Tune's measurement commands
|
motor_freq = config.getfloat('motor_freq', None, minval=0.0)
|
||||||
|
self._motor_freq_x = config.getfloat('motor_freq_x', motor_freq, minval=0.0)
|
||||||
|
self._motor_freq_y = config.getfloat('motor_freq_y', motor_freq, minval=0.0)
|
||||||
|
motor_damping = config.getfloat('motor_damping_ratio', DEFAULT_MOTOR_DAMPING_RATIO, minval=0.0)
|
||||||
|
self._motor_damping_x = config.getfloat('motor_damping_ratio_x', motor_damping, minval=0.0)
|
||||||
|
self._motor_damping_y = config.getfloat('motor_damping_ratio_y', motor_damping, minval=0.0)
|
||||||
|
|
||||||
|
# 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 = [
|
measurement_commands = [
|
||||||
(
|
('EXCITATE_AXIS_AT_FREQ', self.cmd_EXCITATE_AXIS_AT_FREQ, ST_COMMANDS['EXCITATE_AXIS_AT_FREQ']),
|
||||||
'EXCITATE_AXIS_AT_FREQ',
|
('AXES_MAP_CALIBRATION', self.cmd_AXES_MAP_CALIBRATION, ST_COMMANDS['AXES_MAP_CALIBRATION']),
|
||||||
self.cmd_EXCITATE_AXIS_AT_FREQ,
|
('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']),
|
||||||
'Maintain a specified excitation frequency for a period '
|
('CREATE_VIBRATIONS_PROFILE', self.cmd_CREATE_VIBRATIONS_PROFILE, ST_COMMANDS['CREATE_VIBRATIONS_PROFILE']),
|
||||||
'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
|
# Register Shake&Tune's measurement commands using the official Klipper API (gcode.register_command)
|
||||||
if show_macros:
|
# Doing this makes the commands available in Klipper but they are not shown in the web interfaces
|
||||||
pconfig = self._printer.lookup_object('configfile')
|
# 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())
|
||||||
|
|
||||||
|
# Register the motor resonance filters if they are defined in the config
|
||||||
|
# DangerKlipper is required for the full feature but a degraded system forcing the ZV filter for
|
||||||
|
# both input shaping and motor resonance filter will be used instead in stock Klipper. But this might
|
||||||
|
# be improved in the future if https://github.com/Klipper3d/klipper/pull/6460 get merged
|
||||||
|
# TODO: To mitigate this issue, add an automated patch to klippy/chelper/kin_shaper.c
|
||||||
|
# (using a .diff file) to enable the motor filters in stock Klipper as well.
|
||||||
|
# But this will make the Klipper repo dirty to moonraker update manager, so I'm not
|
||||||
|
# sure how to handle this. Maybe with also a command to revert the patch? Or a
|
||||||
|
# manual command to apply the patch with a required user action?
|
||||||
|
def _initialize_motor_resonance_filter(self) -> None:
|
||||||
|
if self._motor_freq_x is not None and self._motor_freq_y is not None:
|
||||||
|
self._printer.register_event_handler('klippy:ready', self._on_klippy_ready)
|
||||||
|
gcode = self._printer.lookup_object('gcode')
|
||||||
|
gcode.register_command(
|
||||||
|
'MOTOR_RESONANCE_FILTER',
|
||||||
|
self.cmd_MOTOR_RESONANCE_FILTER,
|
||||||
|
desc='Enable/disable the motor resonance filters',
|
||||||
|
)
|
||||||
|
self.motor_resonance_filter = MotorResonanceFilter(
|
||||||
|
self._printer,
|
||||||
|
self._motor_freq_x,
|
||||||
|
self._motor_freq_y,
|
||||||
|
self._motor_damping_x,
|
||||||
|
self._motor_damping_y,
|
||||||
|
self.IN_DANGER,
|
||||||
|
)
|
||||||
|
|
||||||
|
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!'
|
||||||
|
)
|
||||||
|
|
||||||
|
# In case the user has configured a motor resonance filter, we need to make sure
|
||||||
|
# that the input shaper is configured as well in order to use them. This is because
|
||||||
|
# the input shaper object is the one used to actually applies the additional filters
|
||||||
|
if self._motor_freq_x is not None and self._motor_freq_y is not None:
|
||||||
|
input_shaper = self._printer.lookup_object('input_shaper', None)
|
||||||
|
if input_shaper is None:
|
||||||
|
raise self._config.error(
|
||||||
|
(
|
||||||
|
'No [input_shaper] config section found in printer.cfg! Please add one to use Shake&Tune '
|
||||||
|
'motor resonance filters!'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_klippy_ready(self) -> None:
|
||||||
|
self.motor_resonance_filter.apply_filters()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------
|
||||||
|
# ------------------------------------------------------------------------------------------
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
def cmd_MOTOR_RESONANCE_FILTER(self, gcmd) -> None:
|
||||||
|
enable = gcmd.get_int('ENABLE', default=1, minval=0, maxval=1)
|
||||||
|
if enable:
|
||||||
|
self.motor_resonance_filter.apply_filters()
|
||||||
|
else:
|
||||||
|
self.motor_resonance_filter.remove_filters()
|
||||||
|
ConsoleOutput.print(f'Motor resonance filter {"enabled" if enable else "disabled"}.')
|
||||||
|
|||||||
Reference in New Issue
Block a user