Code cleanup before release (#114)
This commit is contained in:
7
shaketune/commands/__init__.py
Normal file
7
shaketune/commands/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from .axes_map_calibration import axes_map_calibration as axes_map_calibration
|
||||
from .axes_shaper_calibration import axes_shaper_calibration as axes_shaper_calibration
|
||||
from .compare_belts_responses import compare_belts_responses as compare_belts_responses
|
||||
from .create_vibrations_profile import create_vibrations_profile as create_vibrations_profile
|
||||
from .excitate_axis_at_freq import excitate_axis_at_freq as excitate_axis_at_freq
|
||||
60
shaketune/commands/accelerometer.py
Normal file
60
shaketune/commands/accelerometer.py
Normal 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))
|
||||
93
shaketune/commands/axes_map_calibration.py
Normal file
93
shaketune/commands/axes_map_calibration.py
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
from ..helpers.console_output import ConsoleOutput
|
||||
from ..shaketune_process import ShakeTuneProcess
|
||||
from .accelerometer import Accelerometer
|
||||
|
||||
SEGMENT_LENGTH = 30 # mm
|
||||
|
||||
|
||||
def axes_map_calibration(gcmd, config, st_process: ShakeTuneProcess) -> 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)
|
||||
|
||||
printer = config.get_printer()
|
||||
gcode = printer.lookup_object('gcode')
|
||||
toolhead = printer.lookup_object('toolhead')
|
||||
systime = printer.get_reactor().monotonic()
|
||||
|
||||
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy')
|
||||
k_accelerometer = printer.lookup_object(accel_chip, None)
|
||||
if k_accelerometer is None:
|
||||
raise gcmd.error('Multi-accelerometer configurations are not supported for this macro!')
|
||||
pconfig = printer.lookup_object('configfile')
|
||||
current_axes_map = pconfig.status_raw_config[accel_chip].get('axes_map', None)
|
||||
if current_axes_map is not None and current_axes_map.strip().replace(' ', '') != 'x,y,z':
|
||||
raise gcmd.error(
|
||||
f'The parameter axes_map is already set in your {accel_chip} configuration! Please remove it (or set it to "x,y,z")!'
|
||||
)
|
||||
accelerometer = Accelerometer(k_accelerometer)
|
||||
|
||||
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 - SEGMENT_LENGTH / 2, mid_y - SEGMENT_LENGTH / 2, 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(0.5)
|
||||
toolhead.move([mid_x + SEGMENT_LENGTH / 2, mid_y - SEGMENT_LENGTH / 2, z_height, E], speed)
|
||||
toolhead.dwell(0.5)
|
||||
accelerometer.stop_measurement('axesmap_X', append_time=True)
|
||||
toolhead.dwell(0.5)
|
||||
accelerometer.start_measurement()
|
||||
toolhead.dwell(0.5)
|
||||
toolhead.move([mid_x + SEGMENT_LENGTH / 2, mid_y + SEGMENT_LENGTH / 2, z_height, E], speed)
|
||||
toolhead.dwell(0.5)
|
||||
accelerometer.stop_measurement('axesmap_Y', append_time=True)
|
||||
toolhead.dwell(0.5)
|
||||
accelerometer.start_measurement()
|
||||
toolhead.dwell(0.5)
|
||||
toolhead.move([mid_x + SEGMENT_LENGTH / 2, mid_y + SEGMENT_LENGTH / 2, z_height + SEGMENT_LENGTH, E], speed)
|
||||
toolhead.dwell(0.5)
|
||||
accelerometer.stop_measurement('axesmap_Z', 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} SQUARE_CORNER_VELOCITY={old_sqv}'
|
||||
)
|
||||
toolhead.wait_moves()
|
||||
|
||||
# Run post-processing
|
||||
ConsoleOutput.print('Analysis of the movements...')
|
||||
ConsoleOutput.print('This may take some time (1-3min)')
|
||||
creator = st_process.get_graph_creator()
|
||||
creator.configure(accel, SEGMENT_LENGTH)
|
||||
st_process.run()
|
||||
st_process.wait_for_completion()
|
||||
111
shaketune/commands/axes_shaper_calibration.py
Normal file
111
shaketune/commands/axes_shaper_calibration.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
from ..helpers.common_func import AXIS_CONFIG
|
||||
from ..helpers.console_output import ConsoleOutput
|
||||
from ..helpers.resonance_test import vibrate_axis
|
||||
from ..shaketune_process import ShakeTuneProcess
|
||||
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)
|
||||
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)
|
||||
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 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
|
||||
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:
|
||||
raise 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:
|
||||
raise 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)
|
||||
toolhead.dwell(0.5)
|
||||
|
||||
# Configure the graph creator
|
||||
creator = st_process.get_graph_creator()
|
||||
creator.configure(scv, max_sm, accel_per_hz)
|
||||
|
||||
# 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:
|
||||
raise gcmd.error('No suitable accelerometer found for measurement!')
|
||||
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_process.run()
|
||||
st_process.wait_for_completion()
|
||||
toolhead.dwell(1)
|
||||
toolhead.wait_moves()
|
||||
|
||||
# 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}')
|
||||
109
shaketune/commands/compare_belts_responses.py
Normal file
109
shaketune/commands/compare_belts_responses.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
from ..helpers.common_func import AXIS_CONFIG
|
||||
from ..helpers.console_output import ConsoleOutput
|
||||
from ..helpers.motors_config_parser import MotorsConfigParser
|
||||
from ..helpers.resonance_test import vibrate_axis
|
||||
from ..shaketune_process import ShakeTuneProcess
|
||||
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)
|
||||
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)
|
||||
|
||||
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
|
||||
max_accel = max_freq * accel_per_hz
|
||||
|
||||
# Configure the graph creator
|
||||
motors_config_parser = MotorsConfigParser(config, motors=None)
|
||||
creator = st_process.get_graph_creator()
|
||||
creator.configure(motors_config_parser.kinematics, accel_per_hz)
|
||||
|
||||
if motors_config_parser.kinematics == 'corexy':
|
||||
filtered_config = [a for a in AXIS_CONFIG if a['axis'] in ('a', 'b')]
|
||||
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy')
|
||||
elif motors_config_parser.kinematics == 'corexz':
|
||||
filtered_config = [a for a in AXIS_CONFIG if a['axis'] in ('corexz_x', 'corexz_z')]
|
||||
# For CoreXZ kinematics, we can use the X axis accelerometer as most of the time they are moving bed printers
|
||||
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'x')
|
||||
else:
|
||||
raise gcmd.error('Only CoreXY and CoreXZ kinematics are supported for the belt comparison tool!')
|
||||
ConsoleOutput.print(f'{motors_config_parser.kinematics.upper()} kinematics mode')
|
||||
|
||||
if accel_chip is None:
|
||||
raise gcmd.error(
|
||||
'No suitable accelerometer found for measurement! Multi-accelerometer configurations are not supported for this macro.'
|
||||
)
|
||||
accelerometer = Accelerometer(printer.lookup_object(accel_chip))
|
||||
|
||||
# Move to the starting point
|
||||
test_points = res_tester.test.get_start_test_points()
|
||||
if len(test_points) > 1:
|
||||
raise 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:
|
||||
raise 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)
|
||||
toolhead.dwell(0.5)
|
||||
|
||||
# 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
|
||||
|
||||
# Run the test for each axis
|
||||
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 (1-3min)')
|
||||
st_process.run()
|
||||
st_process.wait_for_completion()
|
||||
138
shaketune/commands/create_vibrations_profile.py
Normal file
138
shaketune/commands/create_vibrations_profile.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
import math
|
||||
|
||||
from ..helpers.console_output import ConsoleOutput
|
||||
from ..helpers.motors_config_parser import MotorsConfigParser
|
||||
from ..shaketune_process import ShakeTuneProcess
|
||||
from .accelerometer import Accelerometer
|
||||
|
||||
MIN_SPEED = 2 # mm/s
|
||||
|
||||
|
||||
def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> None:
|
||||
size = gcmd.get_float('SIZE', default=100.0, minval=50.0)
|
||||
z_height = gcmd.get_float('Z_HEIGHT', default=20.0)
|
||||
max_speed = gcmd.get_float('MAX_SPEED', default=200.0, minval=10.0)
|
||||
speed_increment = gcmd.get_float('SPEED_INCREMENT', default=2.0, minval=1.0)
|
||||
accel = gcmd.get_int('ACCEL', default=3000, 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 == '':
|
||||
accel_chip = None
|
||||
|
||||
if (size / (max_speed / 60)) < 0.25:
|
||||
raise gcmd.error(
|
||||
'The size of the movement is too small for the given speed! Increase SIZE or decrease MAX_SPEED!'
|
||||
)
|
||||
|
||||
printer = config.get_printer()
|
||||
gcode = printer.lookup_object('gcode')
|
||||
toolhead = printer.lookup_object('toolhead')
|
||||
input_shaper = printer.lookup_object('input_shaper', None)
|
||||
systime = printer.get_reactor().monotonic()
|
||||
|
||||
# Check that input shaper is already configured
|
||||
if input_shaper is None:
|
||||
raise gcmd.error('Input shaper is not configured! Please run the shaper calibration macro first.')
|
||||
|
||||
motors_config_parser = MotorsConfigParser(config, motors=['stepper_x', 'stepper_y'])
|
||||
if motors_config_parser.kinematics == 'cartesian' or motors_config_parser.kinematics == 'corexz':
|
||||
main_angles = [0, 90] # Cartesian motors are on X and Y axis directly, same for CoreXZ
|
||||
elif motors_config_parser.kinematics == 'corexy':
|
||||
main_angles = [45, 135] # CoreXY motors are on A and B axis (45 and 135 degrees)
|
||||
else:
|
||||
raise gcmd.error(
|
||||
'Only Cartesian, CoreXY and CoreXZ kinematics are supported at the moment for the vibrations measurement tool!'
|
||||
)
|
||||
ConsoleOutput.print(f'{motors_config_parser.kinematics.upper()} kinematics mode')
|
||||
|
||||
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')
|
||||
|
||||
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
|
||||
X, Y, _, E = toolhead.get_position()
|
||||
|
||||
# Going to the start position
|
||||
toolhead.move([X, Y, z_height, E], feedrate_travel / 10)
|
||||
toolhead.move([mid_x - 15, mid_y - 15, z_height, E], feedrate_travel)
|
||||
toolhead.dwell(0.5)
|
||||
|
||||
nb_speed_samples = int((max_speed - MIN_SPEED) / speed_increment + 1)
|
||||
for curr_angle in main_angles:
|
||||
ConsoleOutput.print(f'-> Measuring angle: {curr_angle} degrees...')
|
||||
radian_angle = math.radians(curr_angle)
|
||||
|
||||
# Map angles to accelerometer axes and default to 'xy' if angle is not 0 or 90 degrees
|
||||
# and then find the best accelerometer chip for the current angle if not manually specified
|
||||
angle_to_axis = {0: 'x', 90: 'y'}
|
||||
accel_axis = angle_to_axis.get(curr_angle, 'xy')
|
||||
current_accel_chip = accel_chip # to retain the manually specified chip
|
||||
if current_accel_chip is None:
|
||||
current_accel_chip = Accelerometer.find_axis_accelerometer(printer, accel_axis)
|
||||
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}]')
|
||||
|
||||
# Sweep the speed range to record the vibrations at different speeds
|
||||
for curr_speed_sample in range(nb_speed_samples):
|
||||
curr_speed = MIN_SPEED + curr_speed_sample * speed_increment
|
||||
ConsoleOutput.print(f'Current speed: {curr_speed} mm/s')
|
||||
|
||||
# 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:
|
||||
segment_length_multiplier = 1 / 5 + 4 / 5 * curr_speed / 100
|
||||
else:
|
||||
segment_length_multiplier = 1
|
||||
|
||||
# Calculate angle coordinates using trigonometry and length multiplier and move to start point
|
||||
dX = (size / 2) * math.cos(radian_angle) * segment_length_multiplier
|
||||
dY = (size / 2) * math.sin(radian_angle) * segment_length_multiplier
|
||||
toolhead.move([mid_x - dX, mid_y - dY, z_height, E], 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.
|
||||
movements = 3
|
||||
if curr_speed < 150:
|
||||
movements = 1
|
||||
elif curr_speed < 250:
|
||||
movements = 2
|
||||
|
||||
# Back and forth movements to record the vibrations at constant speed in both direction
|
||||
accelerometer.start_measurement()
|
||||
for _ in range(movements):
|
||||
toolhead.move([mid_x + dX, mid_y + dY, z_height, E], curr_speed)
|
||||
toolhead.move([mid_x - dX, mid_y - dY, z_height, E], curr_speed)
|
||||
name = f'vib_an{curr_angle:.2f}sp{curr_speed:.2f}'.replace('.', '_')
|
||||
accelerometer.stop_measurement(name)
|
||||
|
||||
toolhead.dwell(0.3)
|
||||
toolhead.wait_moves()
|
||||
|
||||
# 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('Machine vibrations profile generation...')
|
||||
ConsoleOutput.print('This may take some time (5-8min)')
|
||||
creator = st_process.get_graph_creator()
|
||||
creator.configure(motors_config_parser.kinematics, accel, motors_config_parser)
|
||||
st_process.run()
|
||||
st_process.wait_for_completion()
|
||||
99
shaketune/commands/excitate_axis_at_freq.py
Normal file
99
shaketune/commands/excitate_axis_at_freq.py
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from ..helpers.common_func import AXIS_CONFIG
|
||||
from ..helpers.console_output import ConsoleOutput
|
||||
from ..helpers.resonance_test import vibrate_axis_at_static_freq
|
||||
from ..shaketune_process import ShakeTuneProcess
|
||||
from .accelerometer import Accelerometer
|
||||
|
||||
|
||||
def excitate_axis_at_freq(gcmd, config, st_process: ShakeTuneProcess) -> None:
|
||||
create_graph = gcmd.get_int('CREATE_GRAPH', default=0, minval=0, maxval=1) == 1
|
||||
freq = gcmd.get_int('FREQUENCY', default=25, minval=1)
|
||||
duration = gcmd.get_int('DURATION', default=30, 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)
|
||||
accel_chip = gcmd.get('ACCEL_CHIP', default=None)
|
||||
|
||||
if accel_chip == '':
|
||||
accel_chip = None
|
||||
if accel_per_hz == '':
|
||||
accel_per_hz = None
|
||||
|
||||
axis_config = next((item for item in AXIS_CONFIG if item['axis'] == axis), None)
|
||||
if axis_config is None:
|
||||
raise gcmd.error('AXIS selection invalid. Should be either x, y, a or b!')
|
||||
|
||||
if create_graph:
|
||||
printer = config.get_printer()
|
||||
if accel_chip is None:
|
||||
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy' if axis in ['a', 'b'] else axis)
|
||||
k_accelerometer = printer.lookup_object(accel_chip, None)
|
||||
if k_accelerometer is None:
|
||||
raise gcmd.error(f'Accelerometer chip [{accel_chip}] was not found!')
|
||||
accelerometer = Accelerometer(k_accelerometer)
|
||||
|
||||
ConsoleOutput.print(f'Excitating {axis.upper()} axis at {freq}Hz for {duration} seconds')
|
||||
|
||||
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
|
||||
|
||||
# Move to the starting point
|
||||
test_points = res_tester.test.get_start_test_points()
|
||||
if len(test_points) > 1:
|
||||
raise 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:
|
||||
raise 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)
|
||||
toolhead.dwell(0.5)
|
||||
|
||||
# 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
|
||||
|
||||
# If the user want to create a graph, we start accelerometer recording
|
||||
if create_graph:
|
||||
accelerometer.start_measurement()
|
||||
|
||||
toolhead.dwell(0.5)
|
||||
vibrate_axis_at_static_freq(toolhead, gcode, axis_config['direction'], freq, duration, accel_per_hz)
|
||||
toolhead.dwell(0.5)
|
||||
|
||||
# Re-enable the input shaper if it was active
|
||||
if input_shaper is not None:
|
||||
input_shaper.enable_shaping()
|
||||
|
||||
# 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)
|
||||
|
||||
creator = st_process.get_graph_creator()
|
||||
creator.configure(freq, duration, accel_per_hz)
|
||||
st_process.run()
|
||||
st_process.wait_for_completion()
|
||||
Reference in New Issue
Block a user