rename folders in measurement and post-processing

This commit is contained in:
Félix Boisselier
2024-05-13 17:22:05 +02:00
parent 375190610c
commit a37ece7ece
16 changed files with 2 additions and 2 deletions

View File

@@ -0,0 +1,214 @@
#########################################
###### MACHINE VIBRATIONS ANALYSIS ######
#########################################
# Written by Frix_x#0161 #
[gcode_macro CREATE_VIBRATIONS_PROFILE]
gcode:
{% set size = params.SIZE|default(100)|int %} # size of the circle where the angled lines are done
{% set z_height = params.Z_HEIGHT|default(20)|int %} # z height to put the toolhead before starting the movements
{% set max_speed = params.MAX_SPEED|default(200)|float * 60 %} # maximum feedrate for the movements
{% set speed_increment = params.SPEED_INCREMENT|default(2)|float * 60 %} # feedrate increment between each move
{% set feedrate_travel = params.TRAVEL_SPEED|default(200)|int * 60 %} # travel feedrate between moves
{% set accel = params.ACCEL|default(3000)|int %} # accel value used to move on the pattern
{% set accel_chip = params.ACCEL_CHIP|default("adxl345") %} # ADXL chip name in the config
{% set keep_results = params.KEEP_N_RESULTS|default(3)|int %}
{% set keep_csv = params.KEEP_CSV|default(0)|int %}
{% set mid_x = printer.toolhead.axis_maximum.x|float / 2 %}
{% set mid_y = printer.toolhead.axis_maximum.y|float / 2 %}
{% set min_speed = 2 * 60 %} # minimum feedrate for the movements is set to 2mm/s
{% set nb_speed_samples = ((max_speed - min_speed) / speed_increment + 1) | int %}
{% set accel = [accel, printer.configfile.settings.printer.max_accel]|min %}
{% set old_accel = printer.toolhead.max_accel %}
{% set old_cruise_ratio = printer.toolhead.minimum_cruise_ratio %}
{% set old_sqv = printer.toolhead.square_corner_velocity %}
{% set kinematics = printer.configfile.settings.printer.kinematics %}
{% if not 'xyz' in printer.toolhead.homed_axes %}
{ action_raise_error("Must Home printer first!") }
{% endif %}
{% if params.SPEED_INCREMENT|default(2)|float * 100 != (params.SPEED_INCREMENT|default(2)|float * 100)|int %}
{ action_raise_error("Only 2 decimal digits are allowed for SPEED_INCREMENT") }
{% endif %}
{% if (size / (max_speed / 60)) < 0.25 %}
{ action_raise_error("SIZE is too small for this MAX_SPEED. Increase SIZE or decrease MAX_SPEED!") }
{% endif %}
{action_respond_info("")}
{action_respond_info("Starting machine vibrations profile measurement")}
{action_respond_info("This operation can not be interrupted by normal means. Hit the \"emergency stop\" button to stop it if needed")}
{action_respond_info("")}
SAVE_GCODE_STATE NAME=CREATE_VIBRATIONS_PROFILE
G90
# Set the wanted acceleration values (not too high to avoid oscillation, not too low to be able to reach constant speed on each segments)
SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY={[(accel / 1000), 5.0]|max}
# Going to the start position
G1 Z{z_height} F{feedrate_travel / 10}
G1 X{mid_x } Y{mid_y} F{feedrate_travel}
{% if kinematics == "cartesian" %}
# Cartesian motors are on X and Y axis directly
RESPOND MSG="Cartesian kinematics mode"
{% set main_angles = [0, 90] %}
{% elif kinematics == "corexy" %}
# CoreXY motors are on A and B axis (45 and 135 degrees)
RESPOND MSG="CoreXY kinematics mode"
{% set main_angles = [45, 135] %}
{% else %}
{ action_raise_error("Only Cartesian and CoreXY kinematics are supported at the moment for the vibrations measurement tool!") }
{% endif %}
{% set pi = (3.141592653589793) | float %}
{% set tau = (pi * 2) | float %}
{% for curr_angle in main_angles %}
{% for curr_speed_sample in range(0, nb_speed_samples) %}
{% set curr_speed = min_speed + curr_speed_sample * speed_increment %}
{% set rad_angle_full = (curr_angle|float * pi / 180) %}
# -----------------------------------------------------------------------------------------------------------
# Here are some maths to approximate the sin and cos values of rad_angle in Jinja
# Thanks a lot to Aubey! for sharing the idea of using hardcoded Taylor series and
# the associated bit of code to do it easily! This is pure madness!
{% set rad_angle = ((rad_angle_full % tau) - (tau / 2)) | float %}
{% if rad_angle < (-(tau / 4)) %}
{% set rad_angle = (rad_angle + (tau / 2)) | float %}
{% set final_mult = (-1) %}
{% elif rad_angle > (tau / 4) %}
{% set rad_angle = (rad_angle - (tau / 2)) | float %}
{% set final_mult = (-1) %}
{% else %}
{% set final_mult = (1) %}
{% endif %}
{% set sin0 = (rad_angle) %}
{% set sin1 = ((rad_angle ** 3) / 6) | float %}
{% set sin2 = ((rad_angle ** 5) / 120) | float %}
{% set sin3 = ((rad_angle ** 7) / 5040) | float %}
{% set sin4 = ((rad_angle ** 9) / 362880) | float %}
{% set sin5 = ((rad_angle ** 11) / 39916800) | float %}
{% set sin6 = ((rad_angle ** 13) / 6227020800) | float %}
{% set sin7 = ((rad_angle ** 15) / 1307674368000) | float %}
{% set sin = (-(sin0 - sin1 + sin2 - sin3 + sin4 - sin5 + sin6 - sin7) * final_mult) | float %}
{% set cos0 = (1) | float %}
{% set cos1 = ((rad_angle ** 2) / 2) | float %}
{% set cos2 = ((rad_angle ** 4) / 24) | float %}
{% set cos3 = ((rad_angle ** 6) / 720) | float %}
{% set cos4 = ((rad_angle ** 8) / 40320) | float %}
{% set cos5 = ((rad_angle ** 10) / 3628800) | float %}
{% set cos6 = ((rad_angle ** 12) / 479001600) | float %}
{% set cos7 = ((rad_angle ** 14) / 87178291200) | float %}
{% set cos = (-(cos0 - cos1 + cos2 - cos3 + cos4 - cos5 + cos6 - cos7) * final_mult) | float %}
# -----------------------------------------------------------------------------------------------------------
# Reduce the segments length for the lower speed range (0-100mm/s). The minimum length is 1/3 of the SIZE and is gradually increased
# to the nominal SIZE at 100mm/s. No further size changes are made above this speed. The goal is to ensure that the print head moves
# enough to collect enough data for vibration analysis, without doing unnecessary distance to save time. At higher speeds, the full
# segments lengths are used because the head moves faster and travels more distance in the same amount of time and we want enough data
{% if curr_speed < (100 * 60) %}
{% set segment_length_multiplier = 1/5 + 4/5 * (curr_speed / 60) / 100 %}
{% else %}
{% set segment_length_multiplier = 1 %}
{% endif %}
# Calculate angle coordinates using trigonometry and length multiplier and move to start point
{% set dx = (size / 2) * cos * segment_length_multiplier %}
{% set dy = (size / 2) * sin * segment_length_multiplier %}
G1 X{mid_x - dx} Y{mid_y - dy} F{feedrate_travel}
# Adjust the number of back and forth movements based on speed to also save time on lower speed range
# 3 movements are done by default, reduced to 2 between 150-250mm/s and to 1 under 150mm/s.
{% set movements = 3 %}
{% if curr_speed < (150 * 60) %}
{% set movements = 1 %}
{% elif curr_speed < (250 * 60) %}
{% set movements = 2 %}
{% endif %}
ACCELEROMETER_MEASURE CHIP={accel_chip}
# Back and forth movements to record the vibrations at constant speed in both direction
{% for n in range(movements) %}
G1 X{mid_x + dx} Y{mid_y + dy} F{curr_speed}
G1 X{mid_x - dx} Y{mid_y - dy} F{curr_speed}
{% endfor %}
ACCELEROMETER_MEASURE CHIP={accel_chip} NAME=an{("%.2f" % curr_angle|float)|replace('.','_')}sp{("%.2f" % (curr_speed / 60)|float)|replace('.','_')}
G4 P300
M400
{% endfor %}
{% endfor %}
# Restore the previous acceleration values
SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_cruise_ratio} SQUARE_CORNER_VELOCITY={old_sqv}
# Extract the TMC names and configuration
{% set ns_x = namespace(path='') %}
{% set ns_y = namespace(path='') %}
{% for item in printer %}
{% set parts = item.split() %}
{% if parts|length == 2 and parts[0].startswith('tmc') and parts[0][3:].isdigit() %}
{% if parts[1] == 'stepper_x' %}
{% set ns_x.path = parts[0] %}
{% elif parts[1] == 'stepper_y' %}
{% set ns_y.path = parts[0] %}
{% endif %}
{% endif %}
{% endfor %}
{% if ns_x.path and ns_y.path %}
{% set metadata =
"stepper_x_tmc:" ~ ns_x.path ~ "|"
"stepper_x_run_current:" ~ (printer[ns_x.path + ' stepper_x'].run_current | round(2) | string) ~ "|"
"stepper_x_hold_current:" ~ (printer[ns_x.path + ' stepper_x'].hold_current | round(2) | string) ~ "|"
"stepper_y_tmc:" ~ ns_y.path ~ "|"
"stepper_y_run_current:" ~ (printer[ns_y.path + ' stepper_y'].run_current | round(2) | string) ~ "|"
"stepper_y_hold_current:" ~ (printer[ns_y.path + ' stepper_y'].hold_current | round(2) | string) ~ "|"
%}
{% set autotune_x = printer.configfile.config['autotune_tmc stepper_x'] if 'autotune_tmc stepper_x' in printer.configfile.config else none %}
{% set autotune_y = printer.configfile.config['autotune_tmc stepper_y'] if 'autotune_tmc stepper_y' in printer.configfile.config else none %}
{% if autotune_x and autotune_y %}
{% set stepper_x_voltage = autotune_x.voltage if autotune_x.voltage else '24.0' %}
{% set stepper_y_voltage = autotune_y.voltage if autotune_y.voltage else '24.0' %}
{% set metadata = metadata ~
"autotune_enabled:True|"
"stepper_x_motor:" ~ autotune_x.motor ~ "|"
"stepper_x_voltage:" ~ stepper_x_voltage ~ "|"
"stepper_y_motor:" ~ autotune_y.motor ~ "|"
"stepper_y_voltage:" ~ stepper_y_voltage ~ "|"
%}
{% else %}
{% set metadata = metadata ~ "autotune_enabled:False|" %}
{% endif %}
DUMP_TMC STEPPER=stepper_x
DUMP_TMC STEPPER=stepper_y
{% else %}
{ action_respond_info("No TMC drivers found for X and Y steppers") }
{% endif %}
RESPOND MSG="Machine vibrations profile generation..."
RESPOND MSG="This may take some time (3-5min)"
SHAKETUNE_POSTPROCESS PARAMS="--type vibrations --accel {accel|int} --kinematics {kinematics} {% if metadata %}--metadata {metadata}{% endif %} --chip_name {accel_chip} {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
RESTORE_GCODE_STATE NAME=CREATE_VIBRATIONS_PROFILE

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python3
from .axes_input_shaper import axes_shaper_calibration as axes_shaper_calibration
from .axes_map import axes_map_calibration as axes_map_calibration
from .belts_comparison import compare_belts_responses as compare_belts_responses
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 = {
# 'axesmap': (AxesMapFinder, lambda gc: gc.configure(options.accel_used, options.chip_name)),
# 'belts': (BeltsGraphCreator, None),
# 'shaper': (ShaperGraphCreator, lambda gc: gc.configure(options.scv, options.max_smoothing)),
# 'vibrations': (
# VibrationsGraphCreator,
# lambda gc: gc.configure(options.kinematics, options.accel_used, options.chip_name, options.metadata),
# ),
# }

View File

@@ -0,0 +1,60 @@
#!/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
# from ..helpers.console_output import ConsoleOutput
class Accelerometer:
def __init__(self, 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):
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!')
def stop_measurement(self, name: str = None, append_time: bool = True):
if self._bg_client is None:
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():
raise ValueError('invalid file name!')
bg_client = self._bg_client
self._bg_client = None
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}')
def _write_to_file(self, bg_client, filename):
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('%.6f,%.6f,%.6f,%.6f\n' % (t, accel_x, accel_y, accel_z))

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
from ..helpers.console_output import ConsoleOutput
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:
min_freq = gcmd.get_float('FREQ_START', default=5, 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)
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']:
gcmd.error('AXIS selection invalid. Should be either x, y, or all!')
scv = gcmd.get_float('SCV', 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)
systime = printer.get_reactor().monotonic()
toolhead = printer.lookup_object('toolhead')
res_tester = printer.lookup_object('resonance_tester')
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
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.configure(scv, max_sm)
# 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 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.'
)
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)')
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}')

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
from ..helpers.console_output import ConsoleOutput
from ..shaketune_thread import ShakeTuneThread
from .accelerometer import Accelerometer
def axes_map_calibration(gcmd, gcode, printer, st_thread: ShakeTuneThread) -> None:
z_height = gcmd.get_float('Z_HEIGHT', default=20.0)
speed = gcmd.get_float('SPEED', default=80.0, minval=20.0)
accel = gcmd.get_int('ACCEL', default=1500, minval=100)
feedrate_travel = gcmd.get_float('TRAVEL_SPEED', default=120.0, minval=20.0)
accel_chip = gcmd.get('ACCEL_CHIP', default=None)
if accel_chip is None:
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy')
if accel_chip is None:
gcmd.error(
'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()
toolhead = printer.lookup_object('toolhead')
toolhead_info = toolhead.get_status(systime)
old_accel = toolhead_info['max_accel']
old_mcr = toolhead_info['minimum_cruise_ratio']
old_sqv = toolhead_info['square_corner_velocity']
# set the wanted acceleration values
gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY=5.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
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
_, _, _, E = toolhead.get_position()
# Going to the start position
toolhead.move([mid_x - 15, mid_y - 15, z_height, E], feedrate_travel)
toolhead.dwell(0.5)
# Start the measurements and do the movements (+X, +Y and then +Z)
accelerometer.start_measurement()
toolhead.dwell(1)
toolhead.move([mid_x + 15, mid_y - 15, z_height, E], speed)
toolhead.dwell(1)
toolhead.move([mid_x + 15, mid_y + 15, z_height, E], speed)
toolhead.dwell(1)
toolhead.move([mid_x + 15, mid_y + 15, z_height + 15, E], speed)
toolhead.dwell(1)
accelerometer.stop_measurement('axemap')
# 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} SQUARE_CORNER_VELOCITY={old_sqv}'
)
toolhead.wait_moves()
# Run post-processing
ConsoleOutput.print('Analysis of the movements...')
creator = st_thread.get_graph_creator()
creator.configure(accel, accel_chip)
st_thread.run()

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python3
from ..helpers.console_output import ConsoleOutput
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:
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)
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')
res_tester = printer.lookup_object('resonance_tester')
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy')
if accel_chip is None:
gcmd.error(
'No suitable accelerometer found for measurement! Multi-accelerometer configurations are not supported for this macro.'
)
accelerometer = Accelerometer(printer.lookup_object(accel_chip))
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)
# 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
ConsoleOutput.print('Belts comparative frequency profile generation...')
ConsoleOutput.print('This may take some time (3-5min)')
st_thread.run()

View 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()

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
from ..helpers.console_output import ConsoleOutput
from . import AXIS_CONFIG
from .resonance_test import vibrate_axis
def excitate_axis_at_freq(gcmd, printer, gcode) -> None:
freq = gcmd.get_int('FREQUENCY', default=25, minval=1)
duration = gcmd.get_int('DURATION', default=10, minval=1)
accel_per_hz = gcmd.get_float('ACCEL_PER_HZ', default=None)
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!')
ConsoleOutput.print(f'Excitating {axis.upper()} axis at {freq}Hz for {duration} seconds')
systime = printer.get_reactor().monotonic()
toolhead = printer.lookup_object('toolhead')
res_tester = printer.lookup_object('resonance_tester')
if accel_per_hz is None:
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)