diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8f726d7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,69 @@ +name: Smoke Tests +on: + workflow_dispatch: + push: + +jobs: + klippy_testing: + name: Klippy Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + klipper_repo: + - klipper3d/klipper + - DangerKlippers/danger-klipper + steps: + - name: Checkout shaketune + uses: actions/checkout@v4 + with: + path: shaketune + - name: Checkout Klipper + uses: actions/checkout@v4 + with: + path: klipper + repository: ${{ matrix.klipper_repo }} + ref: master + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential + - name: Build klipper dict + run: | + pushd klipper + cp ../shaketune/ci/smoke-test/klipper-smoketest.kconfig .config + make olddefconfig + make out/compile_time_request.o + popd + - name: Setup klippy env + run: | + python3 -m venv --prompt klippy klippy-env + ./klippy-env/bin/python -m pip install -r klipper/scripts/klippy-requirements.txt + ./klippy-env/bin/python -m pip install -r shaketune/requirements.txt + - name: Install shaketune + run: | + ln -s $PWD/shaketune/shaketune $PWD/klipper/klippy/extras/shaketune + - name: Klipper import test + run: | + ./klippy-env/bin/python klipper/klippy/klippy.py --import-test + - name: Klipper integrated test + run: | + pushd klipper + mkdir ../dicts + cp ../klipper/out/klipper.dict ../dicts/linux_basic.dict + ../klippy-env/bin/python scripts/test_klippy.py -d ../dicts ../shaketune/ci/smoke-test/klippy-tests/simple.test + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + cache: 'pip' + - name: install ruff + run: | + pip install ruff + - name: run ruff tests + run: | + ruff check + + diff --git a/ci/smoke-test/klipper-smoketest.kconfig b/ci/smoke-test/klipper-smoketest.kconfig new file mode 100644 index 0000000..b37fbb0 --- /dev/null +++ b/ci/smoke-test/klipper-smoketest.kconfig @@ -0,0 +1,34 @@ +CONFIG_LOW_LEVEL_OPTIONS=y +# CONFIG_MACH_AVR is not set +# CONFIG_MACH_ATSAM is not set +# CONFIG_MACH_ATSAMD is not set +# CONFIG_MACH_LPC176X is not set +# CONFIG_MACH_STM32 is not set +# CONFIG_MACH_HC32F460 is not set +# CONFIG_MACH_RP2040 is not set +# CONFIG_MACH_PRU is not set +# CONFIG_MACH_AR100 is not set +CONFIG_MACH_LINUX=y +# CONFIG_MACH_SIMU is not set +CONFIG_BOARD_DIRECTORY="linux" +CONFIG_CLOCK_FREQ=50000000 +CONFIG_LINUX_SELECT=y +CONFIG_USB_VENDOR_ID=0x1d50 +CONFIG_USB_DEVICE_ID=0x614e +CONFIG_USB_SERIAL_NUMBER="12345" +CONFIG_WANT_GPIO_BITBANGING=y +CONFIG_WANT_DISPLAYS=y +CONFIG_WANT_SENSORS=y +CONFIG_WANT_LIS2DW=y +CONFIG_WANT_LDC1612=y +CONFIG_WANT_SOFTWARE_I2C=y +CONFIG_WANT_SOFTWARE_SPI=y +CONFIG_NEED_SENSOR_BULK=y +CONFIG_CANBUS_FREQUENCY=1000000 +CONFIG_INITIAL_PINS="" +CONFIG_HAVE_GPIO=y +CONFIG_HAVE_GPIO_ADC=y +CONFIG_HAVE_GPIO_SPI=y +CONFIG_HAVE_GPIO_I2C=y +CONFIG_HAVE_GPIO_HARD_PWM=y +CONFIG_INLINE_STEPPER_HACK=y diff --git a/ci/smoke-test/klippy-tests/simple.cfg b/ci/smoke-test/klippy-tests/simple.cfg new file mode 100644 index 0000000..8604aa1 --- /dev/null +++ b/ci/smoke-test/klippy-tests/simple.cfg @@ -0,0 +1,9 @@ +[mcu] +serial: /tmp/klipper_host_mcu + +[printer] +kinematics: none +max_velocity: 300 +max_accel: 300 + +[shaketune] diff --git a/ci/smoke-test/klippy-tests/simple.test b/ci/smoke-test/klippy-tests/simple.test new file mode 100644 index 0000000..3de2790 --- /dev/null +++ b/ci/smoke-test/klippy-tests/simple.test @@ -0,0 +1,4 @@ +DICTIONARY linux_basic.dict +CONFIG simple.cfg + +G4 P1000 diff --git a/pyproject.toml b/pyproject.toml index 42306ed..e2068b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "Shake&Tune" +name = "shake_n_tune" description = "Klipper streamlined input shaper workflow and calibration tools" readme = "README.md" requires-python = ">= 3.9" diff --git a/shaketune/commands/accelerometer.py b/shaketune/commands/accelerometer.py index 3fbde12..d00c0d2 100644 --- a/shaketune/commands/accelerometer.py +++ b/shaketune/commands/accelerometer.py @@ -13,10 +13,13 @@ import time from multiprocessing import Process, Queue +FILE_WRITE_TIMEOUT = 10 # seconds + class Accelerometer: - def __init__(self, klipper_accelerometer): + def __init__(self, reactor, klipper_accelerometer): self._k_accelerometer = klipper_accelerometer + self._reactor = reactor self._bg_client = None self._write_queue = Queue() @@ -70,16 +73,35 @@ def _write_to_file(self, bg_client, filename): os.nice(20) except Exception: pass + with open(filename, 'w') as f: f.write('#time,accel_x,accel_y,accel_z\n') samples = bg_client.samples or bg_client.get_samples() for t, accel_x, accel_y, accel_z in samples: f.write(f'{t:.6f},{accel_x:.6f},{accel_y:.6f},{accel_z:.6f}\n') + self._write_queue.get() def wait_for_file_writes(self): while not self._write_queue.empty(): - time.sleep(0.1) + eventtime = self._reactor.monotonic() + self._reactor.pause(eventtime + 0.1) + for proc in self._write_processes: - proc.join() + if proc is None: + continue + eventtime = self._reactor.monotonic() + endtime = eventtime + FILE_WRITE_TIMEOUT + complete = False + while eventtime < endtime: + eventtime = self._reactor.pause(eventtime + 0.05) + if not proc.is_alive(): + complete = True + break + if not complete: + raise TimeoutError( + 'Shake&Tune was not able to write the accelerometer data into the CSV file. ' + 'This might be due to a slow SD card or a busy or full filesystem.' + ) + self._write_processes = [] diff --git a/shaketune/commands/axes_map_calibration.py b/shaketune/commands/axes_map_calibration.py index b310949..9c8e433 100644 --- a/shaketune/commands/axes_map_calibration.py +++ b/shaketune/commands/axes_map_calibration.py @@ -37,7 +37,7 @@ def axes_map_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None: 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) + accelerometer = Accelerometer(printer.get_reactor(), k_accelerometer) toolhead_info = toolhead.get_status(systime) old_accel = toolhead_info['max_accel'] @@ -45,9 +45,11 @@ def axes_map_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None: # set the wanted acceleration values if 'minimum_cruise_ratio' in toolhead_info: - old_mcr = toolhead_info['minimum_cruise_ratio'] # minimum_cruise_ratio found: Klipper >= v0.12.0-239 - gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY=5.0') - else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 + old_mcr = toolhead_info['minimum_cruise_ratio'] # minimum_cruise_ratio found: Klipper >= v0.12.0-239 + gcode.run_script_from_command( + f'SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY=5.0' + ) + else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 old_mcr = None gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={accel} SQUARE_CORNER_VELOCITY=5.0') @@ -93,11 +95,13 @@ def axes_map_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None: input_shaper.enable_shaping() # Restore the previous acceleration values - if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 - gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}') - else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 + if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 + gcode.run_script_from_command( + f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}' + ) + else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} SQUARE_CORNER_VELOCITY={old_sqv}') - + toolhead.wait_moves() # Run post-processing diff --git a/shaketune/commands/axes_shaper_calibration.py b/shaketune/commands/axes_shaper_calibration.py index 051f71b..8aab716 100644 --- a/shaketune/commands/axes_shaper_calibration.py +++ b/shaketune/commands/axes_shaper_calibration.py @@ -76,10 +76,10 @@ def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None: # set the needed acceleration values for the test toolhead_info = toolhead.get_status(systime) old_accel = toolhead_info['max_accel'] - if 'minimum_cruise_ratio' in toolhead_info: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 + if 'minimum_cruise_ratio' in toolhead_info: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 old_mcr = toolhead_info['minimum_cruise_ratio'] gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel} MINIMUM_CRUISE_RATIO=0') - else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 + else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 old_mcr = None gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel}') @@ -99,7 +99,7 @@ def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None: 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)) + accelerometer = Accelerometer(printer.get_reactor(), printer.lookup_object(accel_chip)) # Then do the actual measurements accelerometer.start_measurement() @@ -119,9 +119,9 @@ def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None: # Re-enable the input shaper if it was active if input_shaper is not None: input_shaper.enable_shaping() - + # Restore the previous acceleration values - if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 + if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr}') - else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 + else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel}') diff --git a/shaketune/commands/compare_belts_responses.py b/shaketune/commands/compare_belts_responses.py index f5d578c..c114e99 100644 --- a/shaketune/commands/compare_belts_responses.py +++ b/shaketune/commands/compare_belts_responses.py @@ -60,7 +60,7 @@ def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> 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)) + accelerometer = Accelerometer(printer.get_reactor(), printer.lookup_object(accel_chip)) # Move to the starting point test_points = res_tester.test.get_start_test_points() @@ -88,11 +88,11 @@ def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> None: # set the needed acceleration values for the test toolhead_info = toolhead.get_status(systime) - old_accel = toolhead_info['max_accel'] - if 'minimum_cruise_ratio' in toolhead_info: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 + old_accel = toolhead_info['max_accel'] + if 'minimum_cruise_ratio' in toolhead_info: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 old_mcr = toolhead_info['minimum_cruise_ratio'] gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel} MINIMUM_CRUISE_RATIO=0') - else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 + else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 old_mcr = None gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={max_accel}') @@ -116,9 +116,9 @@ def compare_belts_responses(gcmd, config, st_process: ShakeTuneProcess) -> None: input_shaper.enable_shaping() # Restore the previous acceleration values - if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 + if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr}') - else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 + else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel}') # Run post-processing diff --git a/shaketune/commands/create_vibrations_profile.py b/shaketune/commands/create_vibrations_profile.py index 06bd5a7..84cd04f 100644 --- a/shaketune/commands/create_vibrations_profile.py +++ b/shaketune/commands/create_vibrations_profile.py @@ -62,10 +62,12 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non old_sqv = toolhead_info['square_corner_velocity'] # set the wanted acceleration values - if 'minimum_cruise_ratio' in toolhead_info: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 - old_mcr = toolhead_info['minimum_cruise_ratio'] - gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY=5.0') - else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 + if 'minimum_cruise_ratio' in toolhead_info: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 + old_mcr = toolhead_info['minimum_cruise_ratio'] + gcode.run_script_from_command( + f'SET_VELOCITY_LIMIT ACCEL={accel} MINIMUM_CRUISE_RATIO=0 SQUARE_CORNER_VELOCITY=5.0' + ) + else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 old_mcr = None gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={accel} SQUARE_CORNER_VELOCITY=5.0') @@ -95,7 +97,7 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non if k_accelerometer is None: raise gcmd.error(f'Accelerometer [{current_accel_chip}] not found!') ConsoleOutput.print(f'Accelerometer chip used for this angle: [{current_accel_chip}]') - accelerometer = Accelerometer(k_accelerometer) + accelerometer = Accelerometer(printer.get_reactor(), k_accelerometer) # Sweep the speed range to record the vibrations at different speeds for curr_speed_sample in range(nb_speed_samples): @@ -138,9 +140,11 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non accelerometer.wait_for_file_writes() # Restore the previous acceleration values - if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 - gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}') - else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 + if old_mcr is not None: # minimum_cruise_ratio found: Klipper >= v0.12.0-239 + gcode.run_script_from_command( + f'SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_mcr} SQUARE_CORNER_VELOCITY={old_sqv}' + ) + else: # minimum_cruise_ratio not found: Klipper < v0.12.0-239 gcode.run_script_from_command(f'SET_VELOCITY_LIMIT ACCEL={old_accel} SQUARE_CORNER_VELOCITY={old_sqv}') toolhead.wait_moves() diff --git a/shaketune/commands/excitate_axis_at_freq.py b/shaketune/commands/excitate_axis_at_freq.py index c8aa1d6..6eae2d4 100644 --- a/shaketune/commands/excitate_axis_at_freq.py +++ b/shaketune/commands/excitate_axis_at_freq.py @@ -41,7 +41,7 @@ def excitate_axis_at_freq(gcmd, config, st_process: ShakeTuneProcess) -> None: 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) + accelerometer = Accelerometer(printer.get_reactor(), k_accelerometer) ConsoleOutput.print(f'Excitating {axis.upper()} axis at {freq}Hz for {duration} seconds') diff --git a/shaketune/shaketune.py b/shaketune/shaketune.py index 8a1641d..e30bacf 100644 --- a/shaketune/shaketune.py +++ b/shaketune/shaketune.py @@ -29,6 +29,8 @@ from .shaketune_config import ShakeTuneConfig from .shaketune_process import ShakeTuneProcess +IN_DANGER = False + class ShakeTune: def __init__(self, config) -> None: @@ -51,21 +53,31 @@ def __init__(self, config) -> None: self._config = ShakeTuneConfig(result_folder_path, keep_n_results, keep_csv, dpi) ConsoleOutput.register_output_callback(gcode.respond_info) - commands = [ + # Register Shake&Tune's measurement commands + measurement_commands = [ ( 'EXCITATE_AXIS_AT_FREQ', self.cmd_EXCITATE_AXIS_AT_FREQ, - 'Maintain a specified excitation frequency for a period of time to diagnose and locate a source of vibration', + ( + 'Maintain a specified excitation frequency for a period ' + 'of time to diagnose and locate a source of vibrations' + ), ), ( 'AXES_MAP_CALIBRATION', self.cmd_AXES_MAP_CALIBRATION, - 'Perform a set of movements to measure the orientation of the accelerometer and help you set the best axes_map configuration for your printer', + ( + 'Perform a set of movements to measure the orientation of the accelerometer ' + 'and help you set the best axes_map configuration for your printer' + ), ), ( 'COMPARE_BELTS_RESPONSES', self.cmd_COMPARE_BELTS_RESPONSES, - 'Perform a custom half-axis test to analyze and compare the frequency profiles of individual belts on CoreXY printers', + ( + 'Perform a custom half-axis test to analyze and compare the ' + 'frequency profiles of individual belts on CoreXY or CoreXZ printers' + ), ), ( 'AXES_SHAPER_CALIBRATION', @@ -75,12 +87,14 @@ def __init__(self, config) -> None: ( 'CREATE_VIBRATIONS_PROFILE', self.cmd_CREATE_VIBRATIONS_PROFILE, - 'Perform a set of movements to measure the orientation of the accelerometer and help you set the best axes_map configuration for your printer', + ( + 'Run a series of motions to find speed/angle ranges where the printer could be ' + 'exposed to VFAs to optimize your slicer speed profiles and TMC driver parameters' + ), ), ] - command_descriptions = {name: desc for name, _, desc in commands} - - for name, command, description in commands: + command_descriptions = {name: desc for name, _, desc in measurement_commands} + for name, command, description in measurement_commands: gcode.register_command(f'_{name}' if show_macros else name, command, desc=description) # Load the dummy macros with their description in order to show them in the web interfaces