using my own resonance tester algorithm
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Klipper Shake&Tune Module
|
# Klipper Shake&Tune plugin
|
||||||
|
|
||||||
This "Shake&Tune" repository is a standalone module from the [Klippain](https://github.com/Frix-x/klippain) ecosystem, designed to automate and calibrate the input shaper system on your Klipper 3D printer with a streamlined workflow and insightful vizualisations. This can be installed on any Klipper machine. It is not limited to those using Klippain.
|
This "Shake&Tune" repository is a standalone module from the [Klippain](https://github.com/Frix-x/klippain) ecosystem, designed to automate and calibrate the input shaper system on your Klipper 3D printer with a streamlined workflow and insightful vizualisations. This can be installed on any Klipper machine. It is not limited to those using Klippain.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Klippain Shake&Tune module documentation
|
# Klipper Shake&Tune plugin documentation
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ Here are the parameters available when calling this macro:
|
|||||||
|SPEED|80|speed of the toolhead in mm/s for the movements|
|
|SPEED|80|speed of the toolhead in mm/s for the movements|
|
||||||
|ACCEL|1500 (or max printer accel)|accel in mm/s^2 used for all the moves|
|
|ACCEL|1500 (or max printer accel)|accel in mm/s^2 used for all the moves|
|
||||||
|TRAVEL_SPEED|120|speed in mm/s used for all the travels moves|
|
|TRAVEL_SPEED|120|speed in mm/s used for all the travels moves|
|
||||||
|ACCEL_CHIP|"adxl345"|accelerometer chip name in the config|
|
|ACCEL_CHIP|None|accelerometer to use for the test. If unset, it will automatically select the proper accelerometer based on what is configured in your `[resonance_tester]` config section|
|
||||||
|
|
||||||
The machine will move slightly in +X, +Y, and +Z, and output in the console: `Detected axes_map: -z,y,x`.
|
The machine will move slightly in +X, +Y, and +Z, and output in the console: `Detected axes_map: -z,y,x`.
|
||||||
|
|
||||||
@@ -108,8 +108,11 @@ Here are the parameters available when calling this macro:
|
|||||||
| parameters | default value | description |
|
| parameters | default value | description |
|
||||||
|-----------:|---------------|-------------|
|
|-----------:|---------------|-------------|
|
||||||
|FREQUENCY|25|excitation frequency (in Hz) that you want to maintain. Usually, it's the frequency of a peak on one of the graphs|
|
|FREQUENCY|25|excitation frequency (in Hz) that you want to maintain. Usually, it's the frequency of a peak on one of the graphs|
|
||||||
|TIME|10|time in second to maintain this excitation|
|
|DURATION|10|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)|
|
||||||
|AXIS|x|axis you want to excitate. Can be set to either "x", "y", "a", "b"|
|
|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|
|
||||||
|
|
||||||
|
|
||||||
## Complementary ressources
|
## Complementary ressources
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ Then, call the `AXES_SHAPER_CALIBRATION` macro and look for the graphs in the re
|
|||||||
|
|
||||||
| parameters | default value | description |
|
| parameters | default value | description |
|
||||||
|-----------:|---------------|-------------|
|
|-----------:|---------------|-------------|
|
||||||
|FREQ_START|5|Starting excitation frequency|
|
|FREQ_START|5|starting excitation frequency|
|
||||||
|FREQ_END|133|Maximum excitation frequency|
|
|FREQ_END|133|maximum excitation frequency|
|
||||||
|HZ_PER_SEC|1|Number of Hz per seconds for the test|
|
|HZ_PER_SEC|1|number of Hz per seconds for the test|
|
||||||
|AXIS|"all"|Axis you want to test in the list of "all", "X" or "Y"|
|
|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)|
|
||||||
|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|
|
|AXIS|"all"|axis you want to test in the list of "all", "X" or "Y"|
|
||||||
|MAX_SMOOTHING|None|Max smoothing allowed when calculating shaper recommendations|
|
|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|
|
||||||
|KEEP_N_RESULTS|3|Total number of results to keep in the result folder after running the test. The older results are automatically cleaned up|
|
|MAX_SMOOTHING|None|max smoothing allowed when calculating shaper recommendations|
|
||||||
|KEEP_CSV|0|Weither or not to keep the CSV data file alonside the PNG graphs|
|
|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|
|
||||||
|
|
||||||
|
|
||||||
## Graphs description
|
## Graphs description
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ Then, call the `COMPARE_BELTS_RESPONSES` macro and look for the graphs in the re
|
|||||||
|
|
||||||
| parameters | default value | description |
|
| parameters | default value | description |
|
||||||
|-----------:|---------------|-------------|
|
|-----------:|---------------|-------------|
|
||||||
|FREQ_START|5|Starting excitation frequency|
|
|FREQ_START|5|starting excitation frequency|
|
||||||
|FREQ_END|133|Maximum excitation frequency|
|
|FREQ_END|133|maximum excitation frequency|
|
||||||
|HZ_PER_SEC|1|Number of Hz per seconds for the test|
|
|HZ_PER_SEC|1|number of Hz per seconds for the test|
|
||||||
|KEEP_N_RESULTS|3|Total number of results to keep in the result folder after running the test. The older results are automatically cleaned up|
|
|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)|
|
||||||
|KEEP_CSV|0|Weither or not to keep the CSV data files alonside the PNG graphs|
|
|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|
|
||||||
|
|
||||||
|
|
||||||
## Graphs description
|
## Graphs description
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ class GraphCreator(abc.ABC):
|
|||||||
new_file = self._folder / f'{self._type}_{self._graph_date}_{custom_name}.csv'
|
new_file = self._folder / f'{self._type}_{self._graph_date}_{custom_name}.csv'
|
||||||
# shutil.move() is needed to move the file across filesystems (mainly for BTT CB1 Pi default OS image)
|
# shutil.move() is needed to move the file across filesystems (mainly for BTT CB1 Pi default OS image)
|
||||||
shutil.move(filename, new_file)
|
shutil.move(filename, new_file)
|
||||||
fm.wait_file_ready(new_file)
|
|
||||||
lognames.append(new_file)
|
lognames.append(new_file)
|
||||||
return lognames
|
return lognames
|
||||||
|
|
||||||
@@ -98,9 +97,9 @@ class BeltsGraphCreator(GraphCreator):
|
|||||||
|
|
||||||
def create_graph(self) -> None:
|
def create_graph(self) -> None:
|
||||||
lognames = self._move_and_prepare_files(
|
lognames = self._move_and_prepare_files(
|
||||||
glob_pattern='raw_data_axis*.csv',
|
glob_pattern='shaketune-belt_*.csv',
|
||||||
min_files_required=2,
|
min_files_required=2,
|
||||||
custom_name_func=lambda f: f.stem.split('_')[3].upper(),
|
custom_name_func=lambda f: f.stem.split('_')[1].upper(),
|
||||||
)
|
)
|
||||||
fig = belts_calibration(
|
fig = belts_calibration(
|
||||||
lognames=[str(path) for path in lognames],
|
lognames=[str(path) for path in lognames],
|
||||||
@@ -245,15 +244,13 @@ class AxesMapFinder(GraphCreator):
|
|||||||
|
|
||||||
def find_axesmap(self) -> None:
|
def find_axesmap(self) -> None:
|
||||||
tmp_folder = Path('/tmp')
|
tmp_folder = Path('/tmp')
|
||||||
globbed_files = list(tmp_folder.glob(f'{self._chip_name}-*.csv'))
|
globbed_files = list(tmp_folder.glob('shaketune-axemap_*.csv'))
|
||||||
|
|
||||||
if not globbed_files:
|
if not globbed_files:
|
||||||
raise FileNotFoundError('no CSV files found in the /tmp folder to find the axes map!')
|
raise FileNotFoundError('no CSV files found in the /tmp folder to find the axes map!')
|
||||||
|
|
||||||
# Find the CSV files with the latest timestamp and wait for it to be released by Klipper
|
# Find the CSV files with the latest timestamp and process it
|
||||||
logname = sorted(globbed_files, key=lambda f: f.stat().st_mtime, reverse=True)[0]
|
logname = sorted(globbed_files, key=lambda f: f.stat().st_mtime, reverse=True)[0]
|
||||||
fm.wait_file_ready(logname)
|
|
||||||
|
|
||||||
results = axesmap_calibration(
|
results = axesmap_calibration(
|
||||||
lognames=[str(logname)],
|
lognames=[str(logname)],
|
||||||
accel=self._accel,
|
accel=self._accel,
|
||||||
@@ -271,6 +268,6 @@ class AxesMapFinder(GraphCreator):
|
|||||||
|
|
||||||
def clean_old_files(self, keep_results: int) -> None:
|
def clean_old_files(self, keep_results: int) -> None:
|
||||||
tmp_folder = Path('/tmp')
|
tmp_folder = Path('/tmp')
|
||||||
globbed_files = list(tmp_folder.glob(f'{self._chip_name}-*.csv'))
|
globbed_files = list(tmp_folder.glob('shaketune-axemap_*.csv'))
|
||||||
for csv_file in globbed_files:
|
for csv_file in globbed_files:
|
||||||
csv_file.unlink()
|
csv_file.unlink()
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ from .axes_map import axes_map_calibration as axes_map_calibration
|
|||||||
from .belts_comparison import compare_belts_responses as compare_belts_responses
|
from .belts_comparison import compare_belts_responses as compare_belts_responses
|
||||||
from .static_freq import excitate_axis_at_freq as excitate_axis_at_freq
|
from .static_freq import excitate_axis_at_freq as excitate_axis_at_freq
|
||||||
|
|
||||||
|
AXIS_CONFIG = [
|
||||||
|
{'axis': 'x', 'direction': (1, 0, 0), 'label': 'axis_X'},
|
||||||
|
{'axis': 'y', 'direction': (0, 1, 0), 'label': 'axis_Y'},
|
||||||
|
{'axis': 'a', 'direction': (1, -1, 0), 'label': 'belt_A'},
|
||||||
|
{'axis': 'b', 'direction': (1, 1, 0), 'label': 'belt_B'},
|
||||||
|
]
|
||||||
|
|
||||||
# graph_creators = {
|
# graph_creators = {
|
||||||
# 'axesmap': (AxesMapFinder, lambda gc: gc.configure(options.accel_used, options.chip_name)),
|
# 'axesmap': (AxesMapFinder, lambda gc: gc.configure(options.accel_used, options.chip_name)),
|
||||||
# 'belts': (BeltsGraphCreator, None),
|
# 'belts': (BeltsGraphCreator, None),
|
||||||
|
|||||||
@@ -1,36 +1,56 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# This file provides a custom and internal Shake&Tune Accelerometer helper that is
|
||||||
|
# an interface to Klipper's own accelerometer classes. It is used to start and
|
||||||
|
# stop accelerometer measurements and write the data to a file in a blocking manner.
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ..helpers.console_output import ConsoleOutput
|
# from ..helpers.console_output import ConsoleOutput
|
||||||
|
|
||||||
|
|
||||||
class Accelerometer:
|
class Accelerometer:
|
||||||
def __init__(self, klipper_accelerometer):
|
def __init__(self, klipper_accelerometer):
|
||||||
self._k_accelerometer = klipper_accelerometer
|
self._k_accelerometer = klipper_accelerometer
|
||||||
|
self._bg_client = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find_axis_accelerometer(printer, axis: str = 'xy'):
|
||||||
|
accel_chip_names = printer.lookup_object('resonance_tester').accel_chip_names
|
||||||
|
for chip_axis, chip_name in accel_chip_names:
|
||||||
|
if axis in ['x', 'y'] and chip_axis == 'xy':
|
||||||
|
return chip_name
|
||||||
|
elif chip_axis == axis:
|
||||||
|
return chip_name
|
||||||
|
return None
|
||||||
|
|
||||||
def start_measurement(self):
|
def start_measurement(self):
|
||||||
if self._k_accelerometer.bg_client is None:
|
if self._bg_client is None:
|
||||||
self._k_accelerometer.bg_client = self._k_accelerometer.chip.start_internal_client()
|
self._bg_client = self._k_accelerometer.start_internal_client()
|
||||||
ConsoleOutput.print('accelerometer measurements started')
|
# ConsoleOutput.print('Accelerometer measurements started')
|
||||||
else:
|
else:
|
||||||
raise ValueError('measurements already started!')
|
raise ValueError('measurements already started!')
|
||||||
|
|
||||||
def stop_measurement(self, name: str = None):
|
def stop_measurement(self, name: str = None, append_time: bool = True):
|
||||||
if self._k_accelerometer.bg_client is not None:
|
if self._bg_client is None:
|
||||||
name = name or time.strftime('%Y%m%d_%H%M%S')
|
raise ValueError('measurements need to be started first!')
|
||||||
|
|
||||||
|
timestamp = time.strftime('%Y%m%d_%H%M%S')
|
||||||
|
if name is None:
|
||||||
|
name = timestamp
|
||||||
|
elif append_time:
|
||||||
|
name += f'_{timestamp}'
|
||||||
|
|
||||||
if not name.replace('-', '').replace('_', '').isalnum():
|
if not name.replace('-', '').replace('_', '').isalnum():
|
||||||
raise ValueError('invalid file name!')
|
raise ValueError('invalid file name!')
|
||||||
|
|
||||||
bg_client = self._k_accelerometer.bg_client
|
bg_client = self._bg_client
|
||||||
self._k_accelerometer.bg_client = None
|
self._bg_client = None
|
||||||
bg_client.finish_measurements()
|
bg_client.finish_measurements()
|
||||||
|
|
||||||
filename = f'/tmp/shaketune-{name}.csv'
|
filename = f'/tmp/shaketune-{name}.csv'
|
||||||
self._write_to_file(bg_client, filename)
|
self._write_to_file(bg_client, filename)
|
||||||
ConsoleOutput.print(f'Measurements stopped. Data written to {filename}')
|
# ConsoleOutput.print(f'Accelerometer measurements stopped. Data written to {filename}')
|
||||||
else:
|
|
||||||
raise ValueError('measurements need to be started first!')
|
|
||||||
|
|
||||||
def _write_to_file(self, bg_client, filename):
|
def _write_to_file(self, bg_client, filename):
|
||||||
with open(filename, 'w') as f:
|
with open(filename, 'w') as f:
|
||||||
|
|||||||
@@ -3,33 +3,102 @@
|
|||||||
|
|
||||||
from ..helpers.console_output import ConsoleOutput
|
from ..helpers.console_output import ConsoleOutput
|
||||||
from ..shaketune_thread import ShakeTuneThread
|
from ..shaketune_thread import ShakeTuneThread
|
||||||
|
from . import AXIS_CONFIG
|
||||||
|
from .accelerometer import Accelerometer
|
||||||
|
from .resonance_test import vibrate_axis
|
||||||
|
|
||||||
|
|
||||||
def axes_shaper_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
|
def axes_shaper_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
|
||||||
min_freq = gcmd.get_float('FREQ_START', default=5, minval=1)
|
min_freq = gcmd.get_float('FREQ_START', default=5, minval=1)
|
||||||
max_freq = gcmd.get_float('FREQ_END', default=133.33, 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, minval=1)
|
hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1, minval=1)
|
||||||
axis = gcmd.get('AXIS', default='all')
|
accel_per_hz = gcmd.get_float('ACCEL_PER_HZ', default=None)
|
||||||
if axis not in ['x', 'y', 'all']:
|
axis_input = gcmd.get('AXIS', default='all').lower()
|
||||||
|
if axis_input not in ['x', 'y', 'all']:
|
||||||
gcmd.error('AXIS selection invalid. Should be either x, y, or all!')
|
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=None, minval=0)
|
||||||
max_sm = gcmd.get_float('MAX_SMOOTHING', default=None, 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)
|
||||||
|
|
||||||
if scv is None:
|
|
||||||
systime = printer.get_reactor().monotonic()
|
systime = printer.get_reactor().monotonic()
|
||||||
toolhead = printer.lookup_object('toolhead')
|
toolhead = printer.lookup_object('toolhead')
|
||||||
|
res_tester = printer.lookup_object('resonance_tester')
|
||||||
|
|
||||||
|
if scv is None:
|
||||||
toolhead_info = toolhead.get_status(systime)
|
toolhead_info = toolhead.get_status(systime)
|
||||||
scv = toolhead_info['square_corner_velocity']
|
scv = toolhead_info['square_corner_velocity']
|
||||||
|
|
||||||
|
if accel_per_hz is None:
|
||||||
|
accel_per_hz = res_tester.test.accel_per_hz
|
||||||
|
max_accel = max_freq * accel_per_hz
|
||||||
|
|
||||||
|
# Move to the starting point
|
||||||
|
test_points = res_tester.test.get_start_test_points()
|
||||||
|
if len(test_points) > 1:
|
||||||
|
gcmd.error('Only one test point in the [resonance_tester] section is supported by Shake&Tune.')
|
||||||
|
if test_points[0] == (-1, -1, -1):
|
||||||
|
if z_height is None:
|
||||||
|
gcmd.error(
|
||||||
|
'Z_HEIGHT parameter is required if the test_point in [resonance_tester] section is set to -1,-1,-1'
|
||||||
|
)
|
||||||
|
# Use center of bed in case the test point in [resonance_tester] is set to -1,-1,-1
|
||||||
|
# This is usefull to get something automatic and is also used in the Klippain modular config
|
||||||
|
kin_info = toolhead.kin.get_status(systime)
|
||||||
|
mid_x = (kin_info['axis_minimum'].x + kin_info['axis_maximum'].x) / 2
|
||||||
|
mid_y = (kin_info['axis_minimum'].y + kin_info['axis_maximum'].y) / 2
|
||||||
|
point = (mid_x, mid_y, z_height)
|
||||||
|
else:
|
||||||
|
x, y, z = test_points[0]
|
||||||
|
if z_height is not None:
|
||||||
|
z = z_height
|
||||||
|
point = (x, y, z)
|
||||||
|
|
||||||
|
toolhead.manual_move(point, feedrate_travel)
|
||||||
|
|
||||||
|
# Configure the graph creator
|
||||||
creator = st_thread.get_graph_creator()
|
creator = st_thread.get_graph_creator()
|
||||||
creator.configure(scv, max_sm)
|
creator.configure(scv, max_sm)
|
||||||
|
|
||||||
axis_flags = {'x': axis in ('x', 'all'), 'y': axis in ('y', 'all')}
|
# set the needed acceleration values for the test
|
||||||
for axis in ['x', 'y']:
|
toolhead_info = toolhead.get_status(systime)
|
||||||
if axis_flags[axis]:
|
old_accel = toolhead_info['max_accel']
|
||||||
gcode.run_script_from_command(
|
old_mcr = toolhead_info['minimum_cruise_ratio']
|
||||||
f'TEST_RESONANCES AXIS={axis.upper()} OUTPUT=raw_data NAME={axis} FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec}'
|
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel} MINIMUM_CRUISE_RATIO=0')
|
||||||
|
|
||||||
|
# Deactivate input shaper if it is active to get raw movements
|
||||||
|
input_shaper = printer.lookup_object('input_shaper', None)
|
||||||
|
if input_shaper is not None:
|
||||||
|
input_shaper.disable_shaping()
|
||||||
|
else:
|
||||||
|
input_shaper = None
|
||||||
|
|
||||||
|
# Filter axis configurations based on user input, assuming 'axis_input' can be 'x', 'y', 'all' (that means 'x' and 'y')
|
||||||
|
filtered_config = [
|
||||||
|
a for a in AXIS_CONFIG if a['axis'] == axis_input or (axis_input == 'all' and a['axis'] in ('x', 'y'))
|
||||||
|
]
|
||||||
|
for config in filtered_config:
|
||||||
|
# First we need to find the accelerometer chip suited for the axis
|
||||||
|
accel_chip = Accelerometer.find_axis_accelerometer(printer, config['axis'])
|
||||||
|
if accel_chip is None:
|
||||||
|
gcmd.error(
|
||||||
|
'No suitable accelerometer found for measurement! Multi-accelerometer configurations are not supported for this macro.'
|
||||||
)
|
)
|
||||||
ConsoleOutput.print(f'{axis.upper()} axis frequency profile generation...')
|
accelerometer = Accelerometer(printer.lookup_object(accel_chip))
|
||||||
|
|
||||||
|
# Then do the actual measurements
|
||||||
|
accelerometer.start_measurement()
|
||||||
|
vibrate_axis(toolhead, gcode, config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz)
|
||||||
|
accelerometer.stop_measurement(config['label'], append_time=True)
|
||||||
|
|
||||||
|
# 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)')
|
ConsoleOutput.print('This may take some time (1-3min)')
|
||||||
st_thread.run()
|
st_thread.run()
|
||||||
|
|
||||||
|
# Re-enable the input shaper if it was active
|
||||||
|
if input_shaper is not None:
|
||||||
|
input_shaper.enable_shaping()
|
||||||
|
|
||||||
|
# Restore the previous acceleration values
|
||||||
|
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr}')
|
||||||
|
|||||||
@@ -6,16 +6,6 @@ from ..shaketune_thread import ShakeTuneThread
|
|||||||
from .accelerometer import Accelerometer
|
from .accelerometer import Accelerometer
|
||||||
|
|
||||||
|
|
||||||
def find_axis_accelerometer(printer, axis: str = 'xy'):
|
|
||||||
accel_chip_names = printer.lookup_object('resonance_tester').accel_chip_names
|
|
||||||
for chip_axis, chip_name in accel_chip_names:
|
|
||||||
if axis in ['x', 'y'] and chip_axis == 'xy':
|
|
||||||
return chip_name
|
|
||||||
elif chip_axis == axis:
|
|
||||||
return chip_name
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def axes_map_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
|
def axes_map_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
|
||||||
z_height = gcmd.get_float('Z_HEIGHT', default=20.0)
|
z_height = gcmd.get_float('Z_HEIGHT', default=20.0)
|
||||||
speed = gcmd.get_float('SPEED', default=80.0, minval=20.0)
|
speed = gcmd.get_float('SPEED', default=80.0, minval=20.0)
|
||||||
@@ -24,11 +14,12 @@ def axes_map_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> No
|
|||||||
accel_chip = gcmd.get('ACCEL_CHIP', default=None)
|
accel_chip = gcmd.get('ACCEL_CHIP', default=None)
|
||||||
|
|
||||||
if accel_chip is None:
|
if accel_chip is None:
|
||||||
accel_chip = find_axis_accelerometer(printer, 'xy')
|
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy')
|
||||||
if accel_chip is None:
|
if accel_chip is None:
|
||||||
gcmd.error(
|
gcmd.error(
|
||||||
'No accelerometer specified for measurement! Multi-accelerometer configurations are not supported for this macro.'
|
'No accelerometer specified for measurement! Multi-accelerometer configurations are not supported for this macro.'
|
||||||
)
|
)
|
||||||
|
accelerometer = Accelerometer(printer.lookup_object(accel_chip))
|
||||||
|
|
||||||
systime = printer.get_reactor().monotonic()
|
systime = printer.get_reactor().monotonic()
|
||||||
toolhead = printer.lookup_object('toolhead')
|
toolhead = printer.lookup_object('toolhead')
|
||||||
@@ -57,10 +48,7 @@ def axes_map_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> No
|
|||||||
toolhead.dwell(0.5)
|
toolhead.dwell(0.5)
|
||||||
|
|
||||||
# Start the measurements and do the movements (+X, +Y and then +Z)
|
# Start the measurements and do the movements (+X, +Y and then +Z)
|
||||||
accelerometer = Accelerometer(printer.lookup_object(accel_chip))
|
|
||||||
accelerometer.start_measurement()
|
accelerometer.start_measurement()
|
||||||
# gcode.run_script_from_command(f'ACCELEROMETER_MEASURE CHIP={accel_chip}')
|
|
||||||
|
|
||||||
toolhead.dwell(1)
|
toolhead.dwell(1)
|
||||||
toolhead.move([mid_x + 15, mid_y - 15, z_height, E], speed)
|
toolhead.move([mid_x + 15, mid_y - 15, z_height, E], speed)
|
||||||
toolhead.dwell(1)
|
toolhead.dwell(1)
|
||||||
@@ -68,9 +56,7 @@ def axes_map_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> No
|
|||||||
toolhead.dwell(1)
|
toolhead.dwell(1)
|
||||||
toolhead.move([mid_x + 15, mid_y + 15, z_height + 15, E], speed)
|
toolhead.move([mid_x + 15, mid_y + 15, z_height + 15, E], speed)
|
||||||
toolhead.dwell(1)
|
toolhead.dwell(1)
|
||||||
|
|
||||||
accelerometer.stop_measurement('axemap')
|
accelerometer.stop_measurement('axemap')
|
||||||
# gcode.run_script_from_command(f'ACCELEROMETER_MEASURE CHIP={accel_chip} NAME=axemap')
|
|
||||||
|
|
||||||
# Re-enable the input shaper if it was active
|
# Re-enable the input shaper if it was active
|
||||||
if input_shaper is not None:
|
if input_shaper is not None:
|
||||||
|
|||||||
@@ -3,24 +3,83 @@
|
|||||||
|
|
||||||
from ..helpers.console_output import ConsoleOutput
|
from ..helpers.console_output import ConsoleOutput
|
||||||
from ..shaketune_thread import ShakeTuneThread
|
from ..shaketune_thread import ShakeTuneThread
|
||||||
|
from . import AXIS_CONFIG
|
||||||
|
from .accelerometer import Accelerometer
|
||||||
|
from .resonance_test import vibrate_axis
|
||||||
|
|
||||||
|
|
||||||
def compare_belts_responses(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
|
def compare_belts_responses(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
|
||||||
min_freq = gcmd.get_float('FREQ_START', default=5, minval=1)
|
min_freq = gcmd.get_float('FREQ_START', default=5.0, minval=1)
|
||||||
max_freq = gcmd.get_float('FREQ_END', default=133.33, 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, minval=1)
|
hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1.0, 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)
|
||||||
|
|
||||||
|
systime = printer.get_reactor().monotonic()
|
||||||
toolhead = printer.lookup_object('toolhead')
|
toolhead = printer.lookup_object('toolhead')
|
||||||
|
res_tester = printer.lookup_object('resonance_tester')
|
||||||
|
|
||||||
gcode.run_script_from_command(
|
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy')
|
||||||
f'TEST_RESONANCES AXIS=1,1 OUTPUT=raw_data NAME=b FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec}'
|
if accel_chip is None:
|
||||||
|
gcmd.error(
|
||||||
|
'No suitable accelerometer found for measurement! Multi-accelerometer configurations are not supported for this macro.'
|
||||||
)
|
)
|
||||||
toolhead.wait_moves()
|
accelerometer = Accelerometer(printer.lookup_object(accel_chip))
|
||||||
|
|
||||||
gcode.run_script_from_command(
|
if accel_per_hz is None:
|
||||||
f'TEST_RESONANCES AXIS=1,-1 OUTPUT=raw_data NAME=a FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec}'
|
accel_per_hz = res_tester.test.accel_per_hz
|
||||||
|
max_accel = max_freq * accel_per_hz
|
||||||
|
|
||||||
|
# Move to the starting point
|
||||||
|
test_points = res_tester.test.get_start_test_points()
|
||||||
|
if len(test_points) > 1:
|
||||||
|
gcmd.error('Only one test point in the [resonance_tester] section is supported by Shake&Tune.')
|
||||||
|
if test_points[0] == (-1, -1, -1):
|
||||||
|
if z_height is None:
|
||||||
|
gcmd.error(
|
||||||
|
'Z_HEIGHT parameter is required if the test_point in [resonance_tester] section is set to -1,-1,-1'
|
||||||
)
|
)
|
||||||
toolhead.wait_moves()
|
# Use center of bed in case the test point in [resonance_tester] is set to -1,-1,-1
|
||||||
|
# This is usefull to get something automatic and is also used in the Klippain modular config
|
||||||
|
kin_info = toolhead.kin.get_status(systime)
|
||||||
|
mid_x = (kin_info['axis_minimum'].x + kin_info['axis_maximum'].x) / 2
|
||||||
|
mid_y = (kin_info['axis_minimum'].y + kin_info['axis_maximum'].y) / 2
|
||||||
|
point = (mid_x, mid_y, z_height)
|
||||||
|
else:
|
||||||
|
x, y, z = test_points[0]
|
||||||
|
if z_height is not None:
|
||||||
|
z = z_height
|
||||||
|
point = (x, y, z)
|
||||||
|
|
||||||
|
toolhead.manual_move(point, feedrate_travel)
|
||||||
|
|
||||||
|
# set the needed acceleration values for the test
|
||||||
|
toolhead_info = toolhead.get_status(systime)
|
||||||
|
old_accel = toolhead_info['max_accel']
|
||||||
|
old_mcr = toolhead_info['minimum_cruise_ratio']
|
||||||
|
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel} MINIMUM_CRUISE_RATIO=0')
|
||||||
|
|
||||||
|
# Deactivate input shaper if it is active to get raw movements
|
||||||
|
input_shaper = printer.lookup_object('input_shaper', None)
|
||||||
|
if input_shaper is not None:
|
||||||
|
input_shaper.disable_shaping()
|
||||||
|
else:
|
||||||
|
input_shaper = None
|
||||||
|
|
||||||
|
# Filter axis configurations to get the A and B axis only
|
||||||
|
filtered_config = [a for a in AXIS_CONFIG if a['axis'] in ('x', 'y')]
|
||||||
|
for config in filtered_config:
|
||||||
|
accelerometer.start_measurement()
|
||||||
|
vibrate_axis(toolhead, gcode, config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz)
|
||||||
|
accelerometer.stop_measurement(config['label'], append_time=True)
|
||||||
|
|
||||||
|
# Re-enable the input shaper if it was active
|
||||||
|
if input_shaper is not None:
|
||||||
|
input_shaper.enable_shaping()
|
||||||
|
|
||||||
|
# Restore the previous acceleration values
|
||||||
|
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr}')
|
||||||
|
|
||||||
# Run post-processing
|
# Run post-processing
|
||||||
ConsoleOutput.print('Belts comparative frequency profile generation...')
|
ConsoleOutput.print('Belts comparative frequency profile generation...')
|
||||||
|
|||||||
50
shaketune/macros/resonance_test.py
Normal file
50
shaketune/macros/resonance_test.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# The logic in this file was "extracted" from Klipper's orignal resonance_tester.py file
|
||||||
|
# Courtesy of Dmitry Butyugin <dmbutyugin@google.com> for the original implementation
|
||||||
|
|
||||||
|
# This derive a bit from Klipper's implementation as there are two main changes:
|
||||||
|
# 1. Original code doesn't use euclidean distance for the moves calculation with projection. The new approach implemented here
|
||||||
|
# ensures that the vector's total length remains constant (= L), regardless of the direction components. It's especially
|
||||||
|
# important when the direction vector involves combinations of movements along multiple axes like for the diagonal belt tests.
|
||||||
|
# 2. Original code doesn't allow Z axis movement that was added here for later use
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from ..helpers.console_output import ConsoleOutput
|
||||||
|
|
||||||
|
|
||||||
|
# This function is used to vibrate the toolhead in a specific axis direction
|
||||||
|
# to test the resonance frequency of the printer and its components
|
||||||
|
def vibrate_axis(toolhead, gcode, axis_direction, min_freq, max_freq, hz_per_sec, accel_per_hz):
|
||||||
|
freq = min_freq
|
||||||
|
X, Y, Z, E = toolhead.get_position() # Get current position
|
||||||
|
sign = 1.0
|
||||||
|
|
||||||
|
while freq <= max_freq + 0.000001:
|
||||||
|
t_seg = 0.25 / freq # Time segment for one vibration cycle
|
||||||
|
accel = accel_per_hz * freq # Acceleration for each half-cycle
|
||||||
|
max_v = accel * t_seg # Max velocity for each half-cycle
|
||||||
|
toolhead.cmd_M204(gcode.create_gcode_command('M204', 'M204', {'S': accel}))
|
||||||
|
L = 0.5 * accel * t_seg**2 # Distance for each half-cycle
|
||||||
|
|
||||||
|
# Calculate move points based on axis direction (X, Y and Z)
|
||||||
|
magnitude = math.sqrt(sum([component**2 for component in axis_direction]))
|
||||||
|
normalized_direction = tuple(component / magnitude for component in axis_direction)
|
||||||
|
dX, dY, dZ = normalized_direction[0] * L, normalized_direction[1] * L, normalized_direction[2] * L
|
||||||
|
nX = X + sign * dX
|
||||||
|
nY = Y + sign * dY
|
||||||
|
nZ = Z + sign * dZ
|
||||||
|
|
||||||
|
# Execute movement
|
||||||
|
toolhead.move([nX, nY, nZ, E], max_v)
|
||||||
|
toolhead.move([X, Y, Z, E], max_v)
|
||||||
|
sign *= -1
|
||||||
|
|
||||||
|
# Increase frequency for next cycle
|
||||||
|
old_freq = freq
|
||||||
|
freq += 2 * t_seg * hz_per_sec
|
||||||
|
if int(freq) > int(old_freq):
|
||||||
|
ConsoleOutput.print(f'Testing frequency: {freq:.0f} Hz')
|
||||||
|
|
||||||
|
toolhead.wait_moves()
|
||||||
@@ -1,22 +1,55 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from ..helpers.console_output import ConsoleOutput
|
from ..helpers.console_output import ConsoleOutput
|
||||||
|
from . import AXIS_CONFIG
|
||||||
|
from .resonance_test import vibrate_axis
|
||||||
|
|
||||||
|
|
||||||
def excitate_axis_at_freq(gcmd, gcode) -> None:
|
def excitate_axis_at_freq(gcmd, printer, gcode) -> None:
|
||||||
freq = gcmd.get_int('FREQUENCY', default=25, minval=1)
|
freq = gcmd.get_int('FREQUENCY', default=25, minval=1)
|
||||||
duration = gcmd.get_int('DURATION', default=10, minval=1)
|
duration = gcmd.get_int('DURATION', default=10, minval=1)
|
||||||
axis = gcmd.get('AXIS', default='x')
|
accel_per_hz = gcmd.get_float('ACCEL_PER_HZ', default=None)
|
||||||
if axis not in ['x', 'y', 'a', 'b']:
|
axis = gcmd.get('AXIS', default='x').lower()
|
||||||
|
feedrate_travel = gcmd.get_float('TRAVEL_SPEED', default=120.0, minval=20.0)
|
||||||
|
z_height = gcmd.get_float('Z_HEIGHT', default=None, minval=1)
|
||||||
|
|
||||||
|
axis_config = next((item for item in AXIS_CONFIG if item['axis'] == axis), None)
|
||||||
|
if axis_config is None:
|
||||||
gcmd.error('AXIS selection invalid. Should be either x, y, a or b!')
|
gcmd.error('AXIS selection invalid. Should be either x, y, a or b!')
|
||||||
|
|
||||||
ConsoleOutput.print(f'Excitating {axis.upper()} axis at {freq}Hz for {duration} seconds')
|
ConsoleOutput.print(f'Excitating {axis.upper()} axis at {freq}Hz for {duration} seconds')
|
||||||
|
|
||||||
if axis == 'a':
|
systime = printer.get_reactor().monotonic()
|
||||||
axis = '1,-1'
|
toolhead = printer.lookup_object('toolhead')
|
||||||
elif axis == 'b':
|
res_tester = printer.lookup_object('resonance_tester')
|
||||||
axis = '1,1'
|
|
||||||
|
|
||||||
gcode.run_script_from_command(
|
if accel_per_hz is None:
|
||||||
f'TEST_RESONANCES OUTPUT=raw_data AXIS={axis} FREQ_START={freq-1} FREQ_END={freq+1} HZ_PER_SEC={1/(duration/3)}'
|
accel_per_hz = res_tester.test.accel_per_hz
|
||||||
|
|
||||||
|
# Move to the starting point
|
||||||
|
test_points = res_tester.test.get_start_test_points()
|
||||||
|
if len(test_points) > 1:
|
||||||
|
gcmd.error('Only one test point in the [resonance_tester] section is supported by Shake&Tune.')
|
||||||
|
if test_points[0] == (-1, -1, -1):
|
||||||
|
if z_height is None:
|
||||||
|
gcmd.error(
|
||||||
|
'Z_HEIGHT parameter is required if the test_point in [resonance_tester] section is set to -1,-1,-1'
|
||||||
)
|
)
|
||||||
|
# Use center of bed in case the test point in [resonance_tester] is set to -1,-1,-1
|
||||||
|
# This is usefull to get something automatic and is also used in the Klippain modular config
|
||||||
|
kin_info = toolhead.kin.get_status(systime)
|
||||||
|
mid_x = (kin_info['axis_minimum'].x + kin_info['axis_maximum'].x) / 2
|
||||||
|
mid_y = (kin_info['axis_minimum'].y + kin_info['axis_maximum'].y) / 2
|
||||||
|
point = (mid_x, mid_y, z_height)
|
||||||
|
else:
|
||||||
|
x, y, z = test_points[0]
|
||||||
|
if z_height is not None:
|
||||||
|
z = z_height
|
||||||
|
point = (x, y, z)
|
||||||
|
|
||||||
|
toolhead.manual_move(point, feedrate_travel)
|
||||||
|
|
||||||
|
min_freq = freq - 1
|
||||||
|
max_freq = freq + 1
|
||||||
|
hz_per_sec = 1 / (duration / 3)
|
||||||
|
vibrate_axis(toolhead, gcode, axis_config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz)
|
||||||
|
|||||||
Reference in New Issue
Block a user