5 Commits

Author SHA1 Message Date
Félix Boisselier
69ad228356 small documentation update about default parameters 2024-06-17 19:53:13 +02:00
Zeanon
b98d103a26 Make frequencies default on [resonance_tester] settings (#119) 2024-06-17 19:49:37 +02:00
Félix Boisselier
a9c7a8491b fix random Timer too close or Move queue overflow errors (#123) 2024-06-17 19:45:20 +02:00
Félix Boisselier
fb8e1ce98f avoid returning wrong axes_map if it wasn't determined correctly 2024-06-16 18:43:16 +02:00
Félix Boisselier
8b0862a96a fixed axis frequency scale on belt graph 2024-06-13 14:21:04 +02:00
16 changed files with 239 additions and 140 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 501 KiB

After

Width:  |  Height:  |  Size: 490 KiB

View File

@@ -36,9 +36,9 @@ axes_map: -z,y,x
This plot shows the acceleration data over time for the X, Y, and Z axes after removing the gravity offset. Look for patterns in the acceleration data for each axis: you should have exactly 2 spikes for each subplot (for the start and stop of the motion) that break away from the global noise. This can help identify any anomalies or inconsistencies in your accelerometer behavior.
The detected gravity offset is printed in the legend to give some context to the readings and their scale: if it's too far from the standard 9.8-10 m/s², this means that your accelerometer is not working properly and should be fixed or calibrated.
The dynamic noise and background vibrations measured by the accelerometer are extracted from the signal (using wavelet transform decomposition) and printed in the legend. **Usually values below about 500mm/s² are ok**, but Shake&Tune will automatically add a note if too much noise is recorded. **Be careful because this value is very different from Klipper's `MEASURE_AXES_NOISE` command, as Shake&Tune measures everything during the motion**, such as accelerometer noise, but also vibrations and motor noise, axis and toolhead oscillations, etc. If you want to record your axes_map correctly, you may need to use about 10 times this value in the `ACCEL` parameter to get a good signal-to-noise ratio and allow Shake&Tune to correctly detect the toolhead acceleration and deceleration phases.
The average noise in the accelerometer measurement is calculated (using wavelet transform decomposition) and displayed at the top of the image. Usually values <500mm/s² are ok, but a note is automatically added by Shake&Tune in case your accelerometer has too much noise.
The detected gravity offset is printed in the legend to give some context to the readings and their scale: if it's too far from the standard 9.8-10 m/s², this means that your accelerometer is not working properly and should be fixed or calibrated.
### Estimated 3D movement path

View File

@@ -11,10 +11,10 @@ Then, call the `AXES_SHAPER_CALIBRATION` macro and look for the graphs in the re
| parameters | default value | description |
|-----------:|---------------|-------------|
|FREQ_START|5|starting excitation frequency|
|FREQ_END|133|maximum excitation frequency|
|FREQ_START|None (default to `[resonance_tester]` value)|starting excitation frequency|
|FREQ_END|None (default to `[resonance_tester]` value)|maximum excitation frequency|
|HZ_PER_SEC|1|number of Hz per seconds for the test|
|ACCEL_PER_HZ|None|accel per Hz value used for the test. If unset, it will use the value from your `[resonance_tester]` config section (75 is the default)|
|ACCEL_PER_HZ|None (default to `[resonance_tester]` value)|accel per Hz value used for the test|
|AXIS|"all"|axis you want to test in the list of "all", "X" or "Y"|
|SCV|printer square corner velocity|square corner velocity you want to use to calculate shaper recommendations. Using higher SCV values usually results in more smoothing and lower maximum accelerations|
|MAX_SMOOTHING|None|max smoothing allowed when calculating shaper recommendations|

View File

@@ -15,10 +15,10 @@ Then, call the `COMPARE_BELTS_RESPONSES` macro and look for the graphs in the re
| parameters | default value | description |
|-----------:|---------------|-------------|
|FREQ_START|5|starting excitation frequency|
|FREQ_END|133|maximum excitation frequency|
|FREQ_START|None (default to `[resonance_tester]` value)|starting excitation frequency|
|FREQ_END|None (default to `[resonance_tester]` value)|maximum excitation frequency|
|HZ_PER_SEC|1|number of Hz per seconds for the test|
|ACCEL_PER_HZ|None|accel per Hz value used for the test. If unset, it will use the value from your `[resonance_tester]` config section (75 is the default)|
|ACCEL_PER_HZ|None (default to `[resonance_tester]` value)|accel per Hz value used for the test|
|TRAVEL_SPEED|120|speed in mm/s used for all the travel movements (to go to the start position prior to the test)|
|Z_HEIGHT|None|Z height wanted for the test. This value can be used if needed to override the Z value of the probe_point set in your `[resonance_tester]` config section|

View File

@@ -12,7 +12,7 @@ Here are the parameters available:
|CREATE_GRAPH|0|whether or not to record the accelerometer data and create an associated graph during the excitation|
|FREQUENCY|25|excitation frequency (in Hz) that you want to maintain. Usually, it's the frequency of a peak on one of the graphs|
|DURATION|30|duration in second to maintain this excitation|
|ACCEL_PER_HZ|None|accel per Hz value used for the test. If unset, it will use the value from your `[resonance_tester]` config section (75 is the default)|
|ACCEL_PER_HZ|None (default to `[resonance_tester]` value)|accel per Hz value used for the test|
|AXIS|x|axis you want to excitate. Can be set to either "x", "y", "a", "b"|
|TRAVEL_SPEED|120|speed in mm/s used for all the travel movements (to go to the start position prior to the test)|
|Z_HEIGHT|None|Z height wanted for the test. This value can be used if needed to override the Z value of the probe_point set in your `[resonance_tester]` config section|

View File

@@ -9,15 +9,18 @@
# accelerometer measurements and write the data to a file in a blocking manner.
import os
import time
# from ..helpers.console_output import ConsoleOutput
from multiprocessing import Process, Queue
class Accelerometer:
def __init__(self, klipper_accelerometer):
self._k_accelerometer = klipper_accelerometer
self._bg_client = None
self._write_queue = Queue()
self._write_processes = []
@staticmethod
def find_axis_accelerometer(printer, axis: str = 'xy'):
@@ -32,7 +35,6 @@ class Accelerometer:
def start_measurement(self):
if self._bg_client is None:
self._bg_client = self._k_accelerometer.start_internal_client()
# ConsoleOutput.print('Accelerometer measurements started')
else:
raise ValueError('measurements already started!')
@@ -54,12 +56,30 @@ class Accelerometer:
bg_client.finish_measurements()
filename = f'/tmp/shaketune-{name}.csv'
self._write_to_file(bg_client, filename)
# ConsoleOutput.print(f'Accelerometer measurements stopped. Data written to {filename}')
self._queue_file_write(bg_client, filename)
def _queue_file_write(self, bg_client, filename):
self._write_queue.put(filename)
write_proc = Process(target=self._write_to_file, args=(bg_client, filename))
write_proc.daemon = True
write_proc.start()
self._write_processes.append(write_proc)
def _write_to_file(self, bg_client, filename):
try:
os.nice(20)
except Exception:
pass
with open(filename, 'w') as f:
f.write('#time,accel_x,accel_y,accel_z\n')
samples = bg_client.samples or bg_client.get_samples()
for t, accel_x, accel_y, accel_z in samples:
f.write(f'{t:.6f},{accel_x:.6f},{accel_y:.6f},{accel_z:.6f}\n')
self._write_queue.get()
def wait_for_file_writes(self):
while not self._write_queue.empty():
time.sleep(0.1)
for proc in self._write_processes:
proc.join()
self._write_processes = []

View File

@@ -82,6 +82,8 @@ def axes_map_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
toolhead.dwell(0.5)
accelerometer.stop_measurement('axesmap_Z', append_time=True)
accelerometer.wait_for_file_writes()
# Re-enable the input shaper if it was active
if input_shaper is not None:
input_shaper.enable_shaping()

View File

@@ -17,14 +17,20 @@ from .accelerometer import Accelerometer
def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
min_freq = gcmd.get_float('FREQ_START', default=5, minval=1)
max_freq = gcmd.get_float('FREQ_END', default=133.33, minval=1)
printer = config.get_printer()
toolhead = printer.lookup_object('toolhead')
res_tester = printer.lookup_object('resonance_tester')
systime = printer.get_reactor().monotonic()
toolhead_info = toolhead.get_status(systime)
min_freq = gcmd.get_float('FREQ_START', default=res_tester.test.min_freq, minval=1)
max_freq = gcmd.get_float('FREQ_END', default=res_tester.test.max_freq, minval=1)
hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1, minval=1)
accel_per_hz = gcmd.get_float('ACCEL_PER_HZ', default=None)
axis_input = gcmd.get('AXIS', default='all').lower()
if axis_input not in {'x', 'y', 'all'}:
raise gcmd.error('AXIS selection invalid. Should be either x, y, or all!')
scv = gcmd.get_float('SCV', default=None, minval=0)
scv = gcmd.get_float('SCV', default=toolhead_info['square_corner_velocity'], minval=0)
max_sm = gcmd.get_float('MAX_SMOOTHING', default=None, minval=0)
feedrate_travel = gcmd.get_float('TRAVEL_SPEED', default=120.0, minval=20.0)
z_height = gcmd.get_float('Z_HEIGHT', default=None, minval=1)
@@ -32,18 +38,11 @@ def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
if accel_per_hz == '':
accel_per_hz = None
printer = config.get_printer()
gcode = printer.lookup_object('gcode')
toolhead = printer.lookup_object('toolhead')
res_tester = printer.lookup_object('resonance_tester')
systime = printer.get_reactor().monotonic()
if scv is None:
toolhead_info = toolhead.get_status(systime)
scv = toolhead_info['square_corner_velocity']
if accel_per_hz is None:
accel_per_hz = res_tester.test.accel_per_hz
gcode = printer.lookup_object('gcode')
max_accel = max_freq * accel_per_hz
# Move to the starting point
@@ -103,6 +102,8 @@ def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
vibrate_axis(toolhead, gcode, config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz)
accelerometer.stop_measurement(config['label'], append_time=True)
accelerometer.wait_for_file_writes()
# And finally generate the graph for each measured axis
ConsoleOutput.print(f'{config["axis"].upper()} axis frequency profile generation...')
ConsoleOutput.print('This may take some time (1-3min)')

View File

@@ -18,9 +18,14 @@ from .accelerometer import Accelerometer
def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> None:
min_freq = gcmd.get_float('FREQ_START', default=5.0, minval=1)
max_freq = gcmd.get_float('FREQ_END', default=133.33, minval=1)
hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1.0, minval=1)
printer = config.get_printer()
toolhead = printer.lookup_object('toolhead')
res_tester = printer.lookup_object('resonance_tester')
systime = printer.get_reactor().monotonic()
min_freq = gcmd.get_float('FREQ_START', default=res_tester.test.min_freq, minval=1)
max_freq = gcmd.get_float('FREQ_END', default=res_tester.test.max_freq, minval=1)
hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1, minval=1)
accel_per_hz = gcmd.get_float('ACCEL_PER_HZ', default=None)
feedrate_travel = gcmd.get_float('TRAVEL_SPEED', default=120.0, minval=20.0)
z_height = gcmd.get_float('Z_HEIGHT', default=None, minval=1)
@@ -28,14 +33,11 @@ def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> None:
if accel_per_hz == '':
accel_per_hz = None
printer = config.get_printer()
gcode = printer.lookup_object('gcode')
toolhead = printer.lookup_object('toolhead')
res_tester = printer.lookup_object('resonance_tester')
systime = printer.get_reactor().monotonic()
if accel_per_hz is None:
accel_per_hz = res_tester.test.accel_per_hz
gcode = printer.lookup_object('gcode')
max_accel = max_freq * accel_per_hz
# Configure the graph creator
@@ -103,6 +105,8 @@ def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> None:
vibrate_axis(toolhead, gcode, config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz)
accelerometer.stop_measurement(config['label'], append_time=True)
accelerometer.wait_for_file_writes()
# Re-enable the input shaper if it was active
if input_shaper is not None:
input_shaper.enable_shaping()

View File

@@ -90,8 +90,8 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
k_accelerometer = printer.lookup_object(current_accel_chip, None)
if k_accelerometer is None:
raise gcmd.error(f'Accelerometer [{current_accel_chip}] not found!')
accelerometer = Accelerometer(k_accelerometer)
ConsoleOutput.print(f'Accelerometer chip used for this angle: [{current_accel_chip}]')
accelerometer = Accelerometer(k_accelerometer)
# Sweep the speed range to record the vibrations at different speeds
for curr_speed_sample in range(nb_speed_samples):
@@ -131,6 +131,8 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
toolhead.dwell(0.3)
toolhead.wait_moves()
accelerometer.wait_for_file_writes()
# Restore the previous acceleration values
gcode.run_script_from_command(
f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}'

View File

@@ -100,6 +100,7 @@ def excitate_axis_at_freq(gcmd, config, st_process: ShakeTuneProcess) -> None:
# If the user wanted to create a graph, we stop the recording and generate it
if create_graph:
accelerometer.stop_measurement(f'staticfreq_{axis.upper()}', append_time=True)
accelerometer.wait_for_file_writes()
creator = st_process.get_graph_creator()
creator.configure(freq, duration, accel_per_hz)

View File

@@ -45,15 +45,15 @@ gcode:
[gcode_macro COMPARE_BELTS_RESPONSES]
description: dummy
gcode:
{% set freq_start = params.FREQ_START|default(5) %}
{% set freq_end = params.FREQ_END|default(133.33) %}
{% set freq_start = params.FREQ_START %}
{% set freq_end = params.FREQ_END %}
{% set hz_per_sec = params.HZ_PER_SEC|default(1) %}
{% set accel_per_hz = params.ACCEL_PER_HZ %}
{% set travel_speed = params.TRAVEL_SPEED|default(120) %}
{% set z_height = params.Z_HEIGHT %}
{% set params_filtered = {
"FREQ_START": freq_start,
"FREQ_END": freq_end,
"FREQ_START": freq_start if freq_start is not none else '',
"FREQ_END": freq_end if freq_end is not none else '',
"HZ_PER_SEC": hz_per_sec,
"ACCEL_PER_HZ": accel_per_hz if accel_per_hz is not none else '',
"TRAVEL_SPEED": travel_speed,
@@ -65,8 +65,8 @@ gcode:
[gcode_macro AXES_SHAPER_CALIBRATION]
description: dummy
gcode:
{% set freq_start = params.FREQ_START|default(5) %}
{% set freq_end = params.FREQ_END|default(133.33) %}
{% set freq_start = params.FREQ_START %}
{% set freq_end = params.FREQ_END %}
{% set hz_per_sec = params.HZ_PER_SEC|default(1) %}
{% set accel_per_hz = params.ACCEL_PER_HZ %}
{% set axis = params.AXIS|default('all') %}
@@ -75,8 +75,8 @@ gcode:
{% set travel_speed = params.TRAVEL_SPEED|default(120) %}
{% set z_height = params.Z_HEIGHT %}
{% set params_filtered = {
"FREQ_START": freq_start,
"FREQ_END": freq_end,
"FREQ_START": freq_start if freq_start is not none else '',
"FREQ_END": freq_end if freq_end is not none else '',
"HZ_PER_SEC": hz_per_sec,
"ACCEL_PER_HZ": accel_per_hz if accel_per_hz is not none else '',
"AXIS": axis,

View File

@@ -7,7 +7,6 @@
# Description: Implements the axes map detection script for Shake&Tune, including
# calibration tools and graph creation for 3D printer vibration analysis.
import optparse
import os
from datetime import datetime
@@ -194,35 +193,39 @@ def linear_regression_direction(
def plot_compare_frequency(
ax: plt.Axes, time: np.ndarray, accel_x: np.ndarray, accel_y: np.ndarray, accel_z: np.ndarray, offset: float, i: int
ax: plt.Axes,
time_data: List[np.ndarray],
accel_data: List[Tuple[np.ndarray, np.ndarray, np.ndarray]],
offset: float,
noise_level: str,
) -> None:
# Plot acceleration data
ax.plot(
time,
accel_x,
label='X' if i == 0 else '',
color=KLIPPAIN_COLORS['purple'],
linewidth=0.5,
zorder=50 if i == 0 else 10,
)
ax.plot(
time,
accel_y,
label='Y' if i == 0 else '',
color=KLIPPAIN_COLORS['orange'],
linewidth=0.5,
zorder=50 if i == 1 else 10,
)
ax.plot(
time,
accel_z,
label='Z' if i == 0 else '',
color=KLIPPAIN_COLORS['red_pink'],
linewidth=0.5,
zorder=50 if i == 2 else 10,
)
for i, (time, (accel_x, accel_y, accel_z)) in enumerate(zip(time_data, accel_data)):
ax.plot(
time,
accel_x,
label='X' if i == 0 else '',
color=KLIPPAIN_COLORS['purple'],
linewidth=0.5,
zorder=50 if i == 0 else 10,
)
ax.plot(
time,
accel_y,
label='Y' if i == 0 else '',
color=KLIPPAIN_COLORS['orange'],
linewidth=0.5,
zorder=50 if i == 1 else 10,
)
ax.plot(
time,
accel_z,
label='Z' if i == 0 else '',
color=KLIPPAIN_COLORS['red_pink'],
linewidth=0.5,
zorder=50 if i == 2 else 10,
)
# Setting axis parameters, grid and graph title
ax.set_xlabel('Time (s)')
ax.set_ylabel('Acceleration (mm/s²)')
@@ -242,53 +245,52 @@ def plot_compare_frequency(
ax.legend(loc='upper left', prop=fontP)
# Add gravity offset to the graph
if i == 0:
ax2 = ax.twinx() # To split the legends in two box
ax2.yaxis.set_visible(False)
ax2.plot([], [], ' ', label=f'Measured gravity: {offset / 1000:0.3f} m/s²')
ax2.legend(loc='upper right', prop=fontP)
# Add the gravity and noise level to the graph legend
ax2 = ax.twinx()
ax2.yaxis.set_visible(False)
ax2.plot([], [], ' ', label=noise_level)
ax2.plot([], [], ' ', label=f'Measured gravity: {offset / 1000:0.3f} m/s²')
ax2.legend(loc='upper right', prop=fontP)
def plot_3d_path(
ax: plt.Axes,
i: int,
position_x: np.ndarray,
position_y: np.ndarray,
position_z: np.ndarray,
average_direction_vector: np.ndarray,
angle_error: float,
position_data: List[Tuple[np.ndarray, np.ndarray, np.ndarray]],
direction_vectors: List[np.ndarray],
angle_errors: List[float],
) -> None:
ax.plot(position_x, position_y, position_z, color=KLIPPAIN_COLORS['orange'], linestyle=':', linewidth=2)
ax.scatter(position_x[0], position_y[0], position_z[0], color=KLIPPAIN_COLORS['red_pink'], zorder=10)
ax.text(
position_x[0] + 1,
position_y[0],
position_z[0],
str(i + 1),
color='black',
fontsize=16,
fontweight='bold',
zorder=20,
)
# Plot the 3D path of the movement
for i, ((position_x, position_y, position_z), average_direction_vector, angle_error) in enumerate(
zip(position_data, direction_vectors, angle_errors)
):
ax.plot(position_x, position_y, position_z, color=KLIPPAIN_COLORS['orange'], linestyle=':', linewidth=2)
ax.scatter(position_x[0], position_y[0], position_z[0], color=KLIPPAIN_COLORS['red_pink'], zorder=10)
ax.text(
position_x[0] + 1,
position_y[0],
position_z[0],
str(i + 1),
color='black',
fontsize=16,
fontweight='bold',
zorder=20,
)
# Plot the average direction vector
start_position = np.array([position_x[0], position_y[0], position_z[0]])
end_position = start_position + average_direction_vector * np.linalg.norm(
[position_x[-1] - position_x[0], position_y[-1] - position_y[0], position_z[-1] - position_z[0]]
)
axes = ['X', 'Y', 'Z']
ax.plot(
[start_position[0], end_position[0]],
[start_position[1], end_position[1]],
[start_position[2], end_position[2]],
label=f'{axes[i]} angle: {angle_error:0.2f}°',
color=KLIPPAIN_COLORS['purple'],
linestyle='-',
linewidth=2,
)
# Plot the average direction vector
start_position = np.array([position_x[0], position_y[0], position_z[0]])
end_position = start_position + average_direction_vector * np.linalg.norm(
[position_x[-1] - position_x[0], position_y[-1] - position_y[0], position_z[-1] - position_z[0]]
)
ax.plot(
[start_position[0], end_position[0]],
[start_position[1], end_position[1]],
[start_position[2], end_position[2]],
label=f'{["X", "Y", "Z"][i]} angle: {angle_error:0.2f}°',
color=KLIPPAIN_COLORS['purple'],
linestyle='-',
linewidth=2,
)
# Setting axis parameters, grid and graph title
ax.set_xlabel('X Position (mm)')
ax.set_ylabel('Y Position (mm)')
ax.set_zlabel('Z Position (mm)')
@@ -311,14 +313,24 @@ def plot_3d_path(
def format_direction_vector(vectors: List[np.ndarray]) -> str:
formatted_vector = []
axes_count = {'x': 0, 'y': 0, 'z': 0}
for vector in vectors:
for i in range(len(vector)):
if vector[i] > 0:
formatted_vector.append(MACHINE_AXES[i])
axes_count[MACHINE_AXES[i]] += 1
break
elif vector[i] < 0:
formatted_vector.append(f'-{MACHINE_AXES[i]}')
axes_count[MACHINE_AXES[i]] += 1
break
# Check if all axes are present in the axes_map and return an error message if not
for _, count in axes_count.items():
if count != 1:
return 'unable to determine it correctly!'
return ', '.join(formatted_vector)
@@ -360,8 +372,12 @@ def axesmap_calibration(
cumulative_start_position = np.array([0, 0, 0])
direction_vectors = []
angle_errors = []
total_noise_intensity = 0.0
for i, machine_axis in enumerate(MACHINE_AXES):
acceleration_data = []
position_data = []
gravities = []
for _, machine_axis in enumerate(MACHINE_AXES):
if machine_axis not in raw_datas:
raise ValueError(f'Missing CSV file for axis {machine_axis}')
@@ -388,15 +404,19 @@ def axesmap_calibration(
f'Machine axis {machine_axis.upper()} -> nearest accelerometer direction vector: {direction_vector} (angle error: {angle_error:.2f}°)'
)
direction_vectors.append(direction_vector)
angle_errors.append(angle_error)
total_noise_intensity += noise_intensity
plot_compare_frequency(ax1, time, accel_x, accel_y, accel_z, gravity, i)
plot_3d_path(ax2, i, position_x, position_y, position_z, average_direction_vector, angle_error)
acceleration_data.append((time, (accel_x, accel_y, accel_z)))
position_data.append((position_x, position_y, position_z))
gravities.append(gravity)
# Update the cumulative start position for the next segment
cumulative_start_position = np.array([position_x[-1], position_y[-1], position_z[-1]])
gravity = np.mean(gravities)
average_noise_intensity = total_noise_intensity / len(raw_datas)
if average_noise_intensity <= 350:
average_noise_intensity_text = '-> OK'
@@ -405,11 +425,25 @@ def axesmap_calibration(
else:
average_noise_intensity_text = '-> ERROR: accelerometer noise is too high!'
average_noise_intensity_label = (
f'Dynamic noise level: {average_noise_intensity:.2f} mm/s² {average_noise_intensity_text}'
)
ConsoleOutput.print(average_noise_intensity_label)
ConsoleOutput.print(f'--> Detected gravity: {gravity / 1000 :.2f} m/s²')
formatted_direction_vector = format_direction_vector(direction_vectors)
ConsoleOutput.print(f'--> Detected axes_map: {formatted_direction_vector}')
ConsoleOutput.print(
f'Average accelerometer noise level: {average_noise_intensity:.2f} mm/s² {average_noise_intensity_text}'
# Plot the differents graphs
plot_compare_frequency(
ax1,
[d[0] for d in acceleration_data],
[d[1] for d in acceleration_data],
gravity,
average_noise_intensity_label,
)
plot_3d_path(ax2, position_data, direction_vectors, angle_errors)
# Add title
title_line1 = 'AXES MAP CALIBRATION TOOL'
@@ -430,9 +464,7 @@ def axesmap_calibration(
fig.text(0.060, 0.939, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
title_line3 = f'| Detected axes_map: {formatted_direction_vector}'
title_line4 = f'| Accelerometer noise level: {average_noise_intensity:.2f} mm/s² {average_noise_intensity_text}'
fig.text(0.50, 0.985, title_line3, ha='left', va='top', fontsize=14, color=KLIPPAIN_COLORS['dark_purple'])
fig.text(0.50, 0.950, title_line4, ha='left', va='top', fontsize=11, color=KLIPPAIN_COLORS['dark_purple'])
fig.text(0.50, 0.985, title_line3, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
# Adding a small Klippain logo to the top left corner of the figure
ax_logo = fig.add_axes([0.001, 0.894, 0.105, 0.105], anchor='NW')

View File

@@ -296,7 +296,7 @@ def plot_compare_frequency(
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.ticklabel_format(axis='x', style='scientific', scilimits=(0, 0))
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0, 0))
ax.grid(which='major', color='grey')
ax.grid(which='minor', color='lightgrey')
fontP = matplotlib.font_manager.FontProperties()
@@ -459,7 +459,7 @@ def plot_versus_belts(
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0, 0))
ax.ticklabel_format(style='scientific', scilimits=(0, 0))
ax.grid(which='major', color='grey')
ax.grid(which='minor', color='lightgrey')

View File

@@ -117,29 +117,54 @@ class ShakeTune:
def cmd_EXCITATE_AXIS_AT_FREQ(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
static_freq_graph_creator = StaticGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, static_freq_graph_creator, self.timeout)
st_process = ShakeTuneProcess(
self._config,
self._printer.get_reactor(),
static_freq_graph_creator,
self.timeout,
)
excitate_axis_at_freq(gcmd, self._pconfig, st_process)
def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
axes_map_graph_creator = AxesMapGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, axes_map_graph_creator, self.timeout)
st_process = ShakeTuneProcess(
self._config,
self._printer.get_reactor(),
axes_map_graph_creator,
self.timeout,
)
axes_map_calibration(gcmd, self._pconfig, st_process)
def cmd_COMPARE_BELTS_RESPONSES(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
belt_graph_creator = BeltsGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, belt_graph_creator, self.timeout)
st_process = ShakeTuneProcess(
self._config,
self._printer.get_reactor(),
belt_graph_creator,
self.timeout,
)
compare_belts_responses(gcmd, self._pconfig, st_process)
def cmd_AXES_SHAPER_CALIBRATION(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
shaper_graph_creator = ShaperGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, shaper_graph_creator, self.timeout)
st_process = ShakeTuneProcess(
self._config,
self._printer.get_reactor(),
shaper_graph_creator,
self.timeout,
)
axes_shaper_calibration(gcmd, self._pconfig, st_process)
def cmd_CREATE_VIBRATIONS_PROFILE(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
vibration_profile_creator = VibrationsGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, vibration_profile_creator, self.timeout)
st_process = ShakeTuneProcess(
self._config,
self._printer.get_reactor(),
vibration_profile_creator,
self.timeout,
)
create_vibrations_profile(gcmd, self._pconfig, st_process)

View File

@@ -8,10 +8,10 @@
# vibration analysis processes in separate system processes.
import multiprocessing
import os
import threading
import traceback
from multiprocessing import Process
from typing import Optional
from .helpers.console_output import ConsoleOutput
@@ -19,11 +19,11 @@ from .shaketune_config import ShakeTuneConfig
class ShakeTuneProcess:
def __init__(self, config: ShakeTuneConfig, graph_creator, timeout: Optional[float] = None) -> None:
self._config = config
def __init__(self, st_config: ShakeTuneConfig, reactor, graph_creator, timeout: Optional[float] = None) -> None:
self._config = st_config
self._reactor = reactor
self.graph_creator = graph_creator
self._timeout = timeout
self._process = None
def get_graph_creator(self):
@@ -31,22 +31,32 @@ class ShakeTuneProcess:
def run(self) -> None:
# Start the target function in a new process (a thread is known to cause issues with Klipper and CANbus due to the GIL)
self._process = multiprocessing.Process(
target=self._shaketune_process_wrapper, args=(self.graph_creator, self._timeout)
)
self._process = Process(target=self._shaketune_process_wrapper, args=(self.graph_creator, self._timeout))
self._process.start()
def wait_for_completion(self) -> None:
if self._process is not None:
self._process.join()
if self._process is None:
return # Nothing to wait for
eventtime = self._reactor.monotonic()
endtime = eventtime + self._timeout
complete = False
while eventtime < endtime:
eventtime = self._reactor.pause(eventtime + 0.05)
if not self._process.is_alive():
complete = True
break
if not complete:
self._handle_timeout()
# This function is a simple wrapper to start the Shake&Tune process. It's needed in order to get the timeout
# as a Timer in a thread INSIDE the Shake&Tune child process to not interfere with the main Klipper process
def _shaketune_process_wrapper(self, graph_creator, timeout) -> None:
if timeout is not None:
# Add 5 seconds to the timeout for safety. The goal is to avoid the Timer to finish before the
# Shake&Tune process is done in case we call the wait_for_completion() function that uses Klipper's reactor.
timeout += 5
timer = threading.Timer(timeout, self._handle_timeout)
timer.start()
try:
self._shaketune_process(graph_creator)
finally:
@@ -58,10 +68,12 @@ class ShakeTuneProcess:
os._exit(1) # Forcefully exit the process
def _shaketune_process(self, graph_creator) -> None:
# Trying to reduce Shake&Tune process priority to avoid slowing down the main Klipper process
# as this could lead to random "Timer too close" errors when already running CANbus, etc...
# Reducing Shake&Tune process priority by putting the scheduler into batch mode with low priority. This in order to avoid
# slowing down the main Klipper process as this can lead to random "Timer too close" or "Move queue overflow" errors
# when also already running CANbus, neopixels and other consumming stuff in Klipper's main process.
try:
os.nice(19)
param = os.sched_param(os.sched_get_priority_min(os.SCHED_BATCH))
os.sched_setscheduler(0, os.SCHED_BATCH, param)
except Exception:
ConsoleOutput.print('Warning: failed reducing Shake&Tune process priority, continuing...')