From 179a56ce9206b5e6e37ea39306f8652829fd4e1a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 31 Aug 2025 12:17:37 -0400 Subject: [PATCH 001/117] gcode_move: Fix M114 when extra axes are defined Commit d40fd219 added support for defining extra axes, however that change could break the M114 command. Update the code to fix M114. Signed-off-by: Kevin O'Connor --- klippy/extras/gcode_move.py | 4 ++-- test/klippy/manual_stepper.test | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/klippy/extras/gcode_move.py b/klippy/extras/gcode_move.py index 94a0ce422..655b8108f 100644 --- a/klippy/extras/gcode_move.py +++ b/klippy/extras/gcode_move.py @@ -92,7 +92,7 @@ class GCodeMove: def _get_gcode_position(self): p = [lp - bp for lp, bp in zip(self.last_position, self.base_position)] p[3] /= self.extrude_factor - return p + return p[:4] def _get_gcode_speed(self): return self.speed / self.speed_factor def _get_gcode_speed_override(self): @@ -107,7 +107,7 @@ class GCodeMove: 'absolute_extrude': self.absolute_extrude, 'homing_origin': self.Coord(*self.homing_position[:4]), 'position': self.Coord(*self.last_position[:4]), - 'gcode_position': self.Coord(*move_position[:4]), + 'gcode_position': self.Coord(*move_position), } def reset_last_position(self): if self.is_printer_ready: diff --git a/test/klippy/manual_stepper.test b/test/klippy/manual_stepper.test index 6ab45c2da..127264113 100644 --- a/test/klippy/manual_stepper.test +++ b/test/klippy/manual_stepper.test @@ -33,6 +33,10 @@ G28 G1 X20 Y20 Z10 G1 A10 X22 +# Verify position query commands work with extra axis +GET_POSITION +M114 + # Test unregistering MANUAL_STEPPER STEPPER=basic_stepper GCODE_AXIS= G1 X15 From bab5f8031c8a5dfcfcb20e299da5630ddca0949c Mon Sep 17 00:00:00 2001 From: JamesH1978 <87171443+JamesH1978@users.noreply.github.com> Date: Sun, 31 Aug 2025 19:26:15 +0100 Subject: [PATCH 002/117] docs: Update FAQ.md - Recomendation adjustment (#7025) This doc still says the Pi 2 is an option for Klipper, in this day and age, i am not sure it is. From anecdotal evidence, the lowest pi recommended should be the zero2w. I also changed the wording and removed some Octoprint wording in that section to better reflect how things are today, as i don't think even with virtual_sdcard these older devices will keep up. Signed-off-by: James Hartley --- docs/FAQ.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 417fb1d4f..7c8214b32 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -98,17 +98,16 @@ bootloaders. ## Can I run Klipper on something other than a Raspberry Pi 3? -The recommended hardware is a Raspberry Pi 2, Raspberry Pi 3, or -Raspberry Pi 4. +The recommended hardware is a Raspberry Pi Zero2w, Raspberry Pi 3, +Raspberry Pi 4 or Raspberry Pi 5. Klipper will also run on other SBC +devices as well as x86 hardware, as described below. -Klipper will run on a Raspberry Pi 1 and on the Raspberry Pi Zero, but -these boards don't have enough processing power to run OctoPrint +Klipper will run on a Raspberry Pi 1, 2 and on the Raspberry Pi Zero1, +but these boards don't have enough processing power to run Klipper well. It is common for print stalls to occur on these slower machines -when printing directly from OctoPrint. (The printer may move faster -than OctoPrint can send movement commands.) If you wish to run on one -one of these slower boards anyway, consider using the "virtual_sdcard" -feature when printing (see -[config reference](Config_Reference.md#virtual_sdcard) for details). +when printing (The printer may move faster than Klipper can send +movement commands.) It is not reccomended to run Kliper on these older +machines. For running on the Beaglebone, see the [Beaglebone specific installation instructions](Beaglebone.md). From e4c66452dc00aa448fc5771433121a44688684d3 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 31 Aug 2025 16:02:44 -0400 Subject: [PATCH 003/117] temperature_probe: Fix python2 incompatibility It seems python2 string.split() method does not accept a "maxsplit" parameter. Use a format compatible with both python2 and python3. Signed-off-by: Kevin O'Connor --- klippy/extras/temperature_probe.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py index c480ddae8..aebb10764 100644 --- a/klippy/extras/temperature_probe.py +++ b/klippy/extras/temperature_probe.py @@ -132,7 +132,7 @@ class TemperatureProbe: self.start_pos = [] # Register GCode Commands - pname = self.name.split(maxsplit=1)[-1] + pname = self.name.split(None, 1)[-1] self.gcode.register_mux_command( "TEMPERATURE_PROBE_CALIBRATE", "PROBE", pname, self.cmd_TEMPERATURE_PROBE_CALIBRATE, @@ -357,8 +357,8 @@ class TemperatureProbe: self._check_homed() probe = self._get_probe() probe_name = probe.get_status(None)["name"] - short_name = probe_name.split(maxsplit=1)[-1] - if short_name != self.name.split(maxsplit=1)[-1]: + short_name = probe_name.split(None, 1)[-1] + if short_name != self.name.split(None, 1)[-1]: raise self.gcode.error( "[%s] not linked to registered probe [%s]." % (self.name, probe_name) @@ -588,7 +588,7 @@ class EddyDriftCompensation: temps[idx] = cur_temp probe_samples[idx].append(sample) return True - sect_name = "probe_eddy_current " + self.name.split(maxsplit=1)[-1] + sect_name = "probe_eddy_current " + self.name.split(None, 1)[-1] self.printer.lookup_object(sect_name).add_client(_on_bulk_data_recd) for i in range(DRIFT_SAMPLE_COUNT): if i == 0: From 20d9c84a9f78f418da3b098147002a8edff02dda Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 1 Sep 2025 21:20:00 -0400 Subject: [PATCH 004/117] toolhead: Fix incorrect response message in SET_VELOCITY_LIMIT Commit f8da8099 incorrectly changed the order of variables in the log/response message of the SET_VELOCITY_LIMIT command. Restore the correct order. Reported by @berkakinci. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 21aeca3df..0b66e8184 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -662,7 +662,7 @@ class ToolHeadCommandHelper: msg = ("max_velocity: %.6f\n" "max_accel: %.6f\n" "minimum_cruise_ratio: %.6f\n" - "square_corner_velocity: %.6f" % (mv, ma, scv, mcr)) + "square_corner_velocity: %.6f" % (mv, ma, mcr, scv)) self.printer.set_rollover_info("toolhead", "toolhead: %s" % (msg,)) if (max_velocity is None and max_accel is None and square_corner_velocity is None and min_cruise_ratio is None): From 77d4cdbae42b97a3e1981e2a00d98f63350fe25d Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 7 Aug 2025 16:23:05 +0200 Subject: [PATCH 005/117] pyhelper: drop linux/prctl header for compability with musl Signed-off-by: Timofey Titovets --- klippy/chelper/pyhelper.c | 1 - 1 file changed, 1 deletion(-) diff --git a/klippy/chelper/pyhelper.c b/klippy/chelper/pyhelper.c index 60c6de9b3..8d4e4ee8c 100644 --- a/klippy/chelper/pyhelper.c +++ b/klippy/chelper/pyhelper.c @@ -10,7 +10,6 @@ #include // fprintf #include // strerror #include // struct timespec -#include // PR_SET_NAME #include // prctl #include "compiler.h" // __visible #include "pyhelper.h" // get_monotonic From 1f14e950e7e0c08dcc5553dd4c6c1071ff14eef7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 1 Sep 2025 13:34:59 -0400 Subject: [PATCH 006/117] stepper_enable: Unify explicit stepper enable/disable code There were several slightly different implementations of explicit stepper motor enabling/disabling in the force_move, stepper_enable, and manual_stepper modules. Introduce a new set_motors_enable() method and use this in all implementations. This simplifies the code and reduces the chance of obscure timing issues. This fixes a manual_stepper error introduced in commit 9399e738. That commit changed the manual_stepper class to no longer explicitly flush and clear all steps after each move, which broke the expectations of manual_stepper's custom enable code. Using the more robust implementation in stepper_enable fixes that issue. Signed-off-by: Kevin O'Connor --- klippy/extras/force_move.py | 33 +++++++++---------------- klippy/extras/manual_stepper.py | 10 ++------ klippy/extras/stepper_enable.py | 44 ++++++++++++++++++--------------- klippy/extras/z_tilt.py | 2 +- 4 files changed, 39 insertions(+), 50 deletions(-) diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py index 00f835f5b..3947292b9 100644 --- a/klippy/extras/force_move.py +++ b/klippy/extras/force_move.py @@ -1,6 +1,6 @@ # Utility for manually moving a stepper for diagnostic purposes # -# Copyright (C) 2018-2019 Kevin O'Connor +# Copyright (C) 2018-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import math, logging @@ -10,7 +10,6 @@ BUZZ_DISTANCE = 1. BUZZ_VELOCITY = BUZZ_DISTANCE / .250 BUZZ_RADIANS_DISTANCE = math.radians(1.) BUZZ_RADIANS_VELOCITY = BUZZ_RADIANS_DISTANCE / .250 -STALL_TIME = 0.100 # Calculate a move's accel_t, cruise_t, and cruise_v def calc_move_time(dist, speed, accel): @@ -56,24 +55,16 @@ class ForceMove: raise self.printer.config_error("Unknown stepper %s" % (name,)) return self.steppers[name] def _force_enable(self, stepper): - toolhead = self.printer.lookup_object('toolhead') - print_time = toolhead.get_last_move_time() + stepper_name = stepper.get_name() stepper_enable = self.printer.lookup_object('stepper_enable') - enable = stepper_enable.lookup_enable(stepper.get_name()) - was_enable = enable.is_motor_enabled() - if not was_enable: - enable.motor_enable(print_time) - toolhead.dwell(STALL_TIME) - return was_enable - def _restore_enable(self, stepper, was_enable): - if not was_enable: - toolhead = self.printer.lookup_object('toolhead') - toolhead.dwell(STALL_TIME) - print_time = toolhead.get_last_move_time() - stepper_enable = self.printer.lookup_object('stepper_enable') - enable = stepper_enable.lookup_enable(stepper.get_name()) - enable.motor_disable(print_time) - toolhead.dwell(STALL_TIME) + did_enable = stepper_enable.set_motors_enable([stepper_name], True) + return did_enable + def _restore_enable(self, stepper, did_enable): + if not did_enable: + return + stepper_name = stepper.get_name() + stepper_enable = self.printer.lookup_object('stepper_enable') + stepper_enable.set_motors_enable([stepper_name], False) def manual_move(self, stepper, dist, speed, accel=0.): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() @@ -100,7 +91,7 @@ class ForceMove: def cmd_STEPPER_BUZZ(self, gcmd): stepper = self._lookup_stepper(gcmd) logging.info("Stepper buzz %s", stepper.get_name()) - was_enable = self._force_enable(stepper) + did_enable = self._force_enable(stepper) toolhead = self.printer.lookup_object('toolhead') dist, speed = BUZZ_DISTANCE, BUZZ_VELOCITY if stepper.units_in_radians(): @@ -110,7 +101,7 @@ class ForceMove: toolhead.dwell(.050) self.manual_move(stepper, -dist, speed) toolhead.dwell(.450) - self._restore_enable(stepper, was_enable) + self._restore_enable(stepper, did_enable) cmd_FORCE_MOVE_help = "Manually move a stepper; invalidates kinematics" def cmd_FORCE_MOVE(self, gcmd): stepper = self._lookup_stepper(gcmd) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 3d3e614a6..a2ce57da1 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -50,15 +50,9 @@ class ManualStepper: self.next_cmd_time = print_time def do_enable(self, enable): self.sync_print_time() + stepper_names = [s.get_name() for s in self.steppers] stepper_enable = self.printer.lookup_object('stepper_enable') - if enable: - for s in self.steppers: - se = stepper_enable.lookup_enable(s.get_name()) - se.motor_enable(self.next_cmd_time) - else: - for s in self.steppers: - se = stepper_enable.lookup_enable(s.get_name()) - se.motor_disable(self.next_cmd_time) + stepper_enable.set_motors_enable(stepper_names, enable) self.sync_print_time() def do_set_position(self, setpos): toolhead = self.printer.lookup_object('toolhead') diff --git a/klippy/extras/stepper_enable.py b/klippy/extras/stepper_enable.py index 2bad75552..cd3f4f731 100644 --- a/klippy/extras/stepper_enable.py +++ b/klippy/extras/stepper_enable.py @@ -88,30 +88,30 @@ class PrinterStepperEnable: name = mcu_stepper.get_name() enable = setup_enable_pin(self.printer, config.get('enable_pin', None)) self.enable_lines[name] = EnableTracking(mcu_stepper, enable) + def set_motors_enable(self, stepper_names, enable): + toolhead = self.printer.lookup_object('toolhead') + toolhead.dwell(DISABLE_STALL_TIME) + print_time = toolhead.get_last_move_time() + did_change = False + for stepper_name in stepper_names: + el = self.enable_lines[stepper_name] + was_enabled = el.is_motor_enabled() + if enable: + el.motor_enable(print_time) + else: + el.motor_disable(print_time) + if was_enabled != el.is_motor_enabled(): + did_change = True + toolhead.dwell(DISABLE_STALL_TIME) + return did_change def motor_off(self): + self.set_motors_enable(self.get_steppers(), False) toolhead = self.printer.lookup_object('toolhead') - toolhead.dwell(DISABLE_STALL_TIME) - print_time = toolhead.get_last_move_time() - for el in self.enable_lines.values(): - el.motor_disable(print_time) toolhead.get_kinematics().clear_homing_state("xyz") - self.printer.send_event("stepper_enable:motor_off", print_time) - toolhead.dwell(DISABLE_STALL_TIME) - def motor_debug_enable(self, stepper, enable): - toolhead = self.printer.lookup_object('toolhead') - toolhead.dwell(DISABLE_STALL_TIME) - print_time = toolhead.get_last_move_time() - el = self.enable_lines[stepper] - if enable: - el.motor_enable(print_time) - logging.info("%s has been manually enabled", stepper) - else: - el.motor_disable(print_time) - logging.info("%s has been manually disabled", stepper) - toolhead.dwell(DISABLE_STALL_TIME) + self.printer.send_event("stepper_enable:motor_off") def get_status(self, eventtime): steppers = { name: et.is_motor_enabled() - for (name, et) in self.enable_lines.items() } + for (name, et) in self.enable_lines.items() } return {'steppers': steppers} def _handle_request_restart(self, print_time): self.motor_off() @@ -126,7 +126,11 @@ class PrinterStepperEnable: % (stepper_name,)) return stepper_enable = gcmd.get_int('ENABLE', 1) - self.motor_debug_enable(stepper_name, stepper_enable) + self.set_motors_enable([stepper_name], stepper_enable) + if stepper_enable: + logging.info("%s has been manually enabled", stepper_name) + else: + logging.info("%s has been manually disabled", stepper_name) def lookup_enable(self, name): if name not in self.enable_lines: raise self.printer.config_error("Unknown stepper '%s'" % (name,)) diff --git a/klippy/extras/z_tilt.py b/klippy/extras/z_tilt.py index 9f5ea0b9c..0316ee721 100644 --- a/klippy/extras/z_tilt.py +++ b/klippy/extras/z_tilt.py @@ -79,7 +79,7 @@ class ZAdjustStatus: self.applied = False def get_status(self, eventtime): return {'applied': self.applied} - def _motor_off(self, print_time): + def _motor_off(self): self.reset() class RetryHelper: From 5056e1031c92f279caafe49fb8e2fb961dcbb9ad Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 1 Sep 2025 14:22:36 -0400 Subject: [PATCH 007/117] stepper_enable: Improve timing of manual stepper enable/disable commands Invoke flush_step_generation() prior to checking motor enable state as this is the best way to ensure all stepper active callbacks have been invoked (which could change the enable line state). Also, there is no longer a reason to add additional toolhead dwells when enabling a stepper motor. Signed-off-by: Kevin O'Connor --- klippy/extras/manual_stepper.py | 2 -- klippy/extras/stepper_enable.py | 22 +++++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index a2ce57da1..3c1b29b65 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -49,11 +49,9 @@ class ManualStepper: else: self.next_cmd_time = print_time def do_enable(self, enable): - self.sync_print_time() stepper_names = [s.get_name() for s in self.steppers] stepper_enable = self.printer.lookup_object('stepper_enable') stepper_enable.set_motors_enable(stepper_names, enable) - self.sync_print_time() def do_set_position(self, setpos): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() diff --git a/klippy/extras/stepper_enable.py b/klippy/extras/stepper_enable.py index cd3f4f731..926e95922 100644 --- a/klippy/extras/stepper_enable.py +++ b/klippy/extras/stepper_enable.py @@ -1,6 +1,6 @@ # Support for enable pins on stepper motor drivers # -# Copyright (C) 2019-2021 Kevin O'Connor +# Copyright (C) 2019-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import logging @@ -90,19 +90,27 @@ class PrinterStepperEnable: self.enable_lines[name] = EnableTracking(mcu_stepper, enable) def set_motors_enable(self, stepper_names, enable): toolhead = self.printer.lookup_object('toolhead') - toolhead.dwell(DISABLE_STALL_TIME) - print_time = toolhead.get_last_move_time() + # Flush steps to ensure all auto enable callbacks invoked + toolhead.flush_step_generation() + print_time = None did_change = False for stepper_name in stepper_names: el = self.enable_lines[stepper_name] - was_enabled = el.is_motor_enabled() + if el.is_motor_enabled() == enable: + continue + if print_time is None: + # Dwell for sufficient delay from any previous auto enable + if not enable: + toolhead.dwell(DISABLE_STALL_TIME) + print_time = toolhead.get_last_move_time() if enable: el.motor_enable(print_time) else: el.motor_disable(print_time) - if was_enabled != el.is_motor_enabled(): - did_change = True - toolhead.dwell(DISABLE_STALL_TIME) + did_change = True + # Dwell to ensure sufficient delay prior to a future auto enable + if did_change and not enable: + toolhead.dwell(DISABLE_STALL_TIME) return did_change def motor_off(self): self.set_motors_enable(self.get_steppers(), False) From a5218619b725c849c3c371b4d3d4549e636db5e3 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 11 Aug 2025 20:01:33 -0400 Subject: [PATCH 008/117] motion_queuing: Track kin_flush_delay locally Track the kin_flush_delay in both toolhead.py and motion_queuing.py . Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 15 ++++++++++++--- klippy/toolhead.py | 5 ++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index a06b556e1..49fafd6b0 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -7,22 +7,29 @@ import logging import chelper MOVE_HISTORY_EXPIRE = 30. +SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c class PrinterMotionQueuing: def __init__(self, config): self.printer = config.get_printer() + # Low level C allocations self.trapqs = [] self.stepcompress = [] self.steppersyncs = [] - self.flush_callbacks = [] + # Low-level C flushing calls ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves self.steppersync_generate_steps = ffi_lib.steppersync_generate_steps self.steppersync_flush = ffi_lib.steppersync_flush self.steppersync_history_expire = ffi_lib.steppersync_history_expire + # Flush notification callbacks + self.flush_callbacks = [] + # History expiration self.clear_history_time = 0. is_debug = self.printer.get_start_args().get('debugoutput') is not None self.is_debugoutput = is_debug + # Kinematic step generation scan window time tracking + self.kin_flush_delay = SDS_CHECK_TIME def allocate_trapq(self): ffi_main, ffi_lib = chelper.get_ffi() trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) @@ -53,8 +60,7 @@ class PrinterMotionQueuing: fcbs = list(self.flush_callbacks) fcbs.remove(callback) self.flush_callbacks = fcbs - def flush_motion_queues(self, must_flush_time, max_step_gen_time, - trapq_free_time): + def flush_motion_queues(self, must_flush_time, max_step_gen_time): # Invoke flush callbacks (if any) for cb in self.flush_callbacks: cb(must_flush_time, max_step_gen_time) @@ -72,6 +78,7 @@ class PrinterMotionQueuing: raise mcu.error("Internal error in MCU '%s' stepcompress" % (mcu.get_name(),)) # Determine maximum history to keep + trapq_free_time = max_step_gen_time - self.kin_flush_delay clear_history_time = self.clear_history_time if self.is_debugoutput: clear_history_time = trapq_free_time - MOVE_HISTORY_EXPIRE @@ -90,6 +97,8 @@ class PrinterMotionQueuing: def lookup_trapq_append(self): ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append + def set_step_generate_scan_time(self, delay): + self.kin_flush_delay = delay def stats(self, eventtime): mcu = self.printer.lookup_object('mcu') est_print_time = mcu.estimated_print_time(eventtime) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 0b66e8184..f68d49726 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -275,9 +275,7 @@ class ToolHead: sg_flush_want = min(flush_time + STEPCOMPRESS_FLUSH_TIME, self.print_time - self.kin_flush_delay) sg_flush_time = max(sg_flush_want, flush_time) - trapq_free_time = sg_flush_time - self.kin_flush_delay - self.motion_queuing.flush_motion_queues(flush_time, sg_flush_time, - trapq_free_time) + self.motion_queuing.flush_motion_queues(flush_time, sg_flush_time) self.min_restart_time = max(self.min_restart_time, sg_flush_time) self.last_flush_time = flush_time def _advance_move_time(self, next_print_time): @@ -596,6 +594,7 @@ class ToolHead: self.kin_flush_times.append(delay) new_delay = max(self.kin_flush_times + [SDS_CHECK_TIME]) self.kin_flush_delay = new_delay + self.motion_queuing.set_step_generate_scan_time(new_delay) def register_lookahead_callback(self, callback): last_move = self.lookahead.get_last() if last_move is None: From 1e7c67919ec4a9f8716426b8e9e024a2ae0076fe Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 11:38:16 -0400 Subject: [PATCH 009/117] toolhead: Rework min_restart_time to last_step_gen_time Commit 3d3b87f9 renamed last_sg_flush_time to min_restart_time to ensure that flush_step_generation() would fully flush out moves generated from the force_move module. However, now that force_move calls note_mcu_movequeue_activity() with is_step_gen=True, this is no longer necessary. Rework min_restart_time to last_step_gen_time. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index f68d49726..2f9909f51 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -239,7 +239,7 @@ class ToolHead: # Flush tracking self.flush_timer = self.reactor.register_timer(self._flush_handler) self.do_kick_flush_timer = True - self.last_flush_time = self.min_restart_time = 0. + self.last_flush_time = self.last_step_gen_time = 0. self.need_flush_time = self.step_gen_time = 0. # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME @@ -274,10 +274,10 @@ class ToolHead: # Generate steps via itersolve sg_flush_want = min(flush_time + STEPCOMPRESS_FLUSH_TIME, self.print_time - self.kin_flush_delay) - sg_flush_time = max(sg_flush_want, flush_time) - self.motion_queuing.flush_motion_queues(flush_time, sg_flush_time) - self.min_restart_time = max(self.min_restart_time, sg_flush_time) + step_gen_time = max(self.last_step_gen_time, sg_flush_want, flush_time) + self.motion_queuing.flush_motion_queues(flush_time, step_gen_time) self.last_flush_time = flush_time + self.last_step_gen_time = step_gen_time def _advance_move_time(self, next_print_time): pt_delay = self.kin_flush_delay + STEPCOMPRESS_FLUSH_TIME flush_time = max(self.last_flush_time, self.print_time - pt_delay) @@ -291,7 +291,7 @@ class ToolHead: def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - kin_time = max(est_print_time + MIN_KIN_TIME, self.min_restart_time) + kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) kin_time += self.kin_flush_delay min_print_time = max(est_print_time + BUFFER_TIME_START, kin_time) if min_print_time > self.print_time: @@ -338,7 +338,6 @@ class ToolHead: def flush_step_generation(self): self._flush_lookahead() self._advance_flush_time(self.step_gen_time) - self.min_restart_time = max(self.min_restart_time, self.print_time) def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() From b0a642a8ea9beb1d76c4aaaf5aee4923f20c8243 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 11:43:26 -0400 Subject: [PATCH 010/117] toolhead: Add kin_flush_delay in note_mcu_movequeue_activity() Automatically add kin_flush_delay to the requested flush time if is_step_gen=True. This simplifies the callers. Also, rename self.step_gen_time to self.need_step_gen_time. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 2f9909f51..167e43c0c 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -240,7 +240,7 @@ class ToolHead: self.flush_timer = self.reactor.register_timer(self._flush_handler) self.do_kick_flush_timer = True self.last_flush_time = self.last_step_gen_time = 0. - self.need_flush_time = self.step_gen_time = 0. + self.need_flush_time = self.need_step_gen_time = 0. # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME self.kin_flush_times = [] @@ -326,7 +326,7 @@ class ToolHead: for cb in move.timing_callbacks: cb(next_move_time) # Generate steps for moves - self.note_mcu_movequeue_activity(next_move_time + self.kin_flush_delay) + self.note_mcu_movequeue_activity(next_move_time) self._advance_move_time(next_move_time) def _flush_lookahead(self): # Transit from "NeedPrime"/"Priming"/"Drip"/main state to "NeedPrime" @@ -337,7 +337,7 @@ class ToolHead: self.check_stall_time = 0. def flush_step_generation(self): self._flush_lookahead() - self._advance_flush_time(self.step_gen_time) + self._advance_flush_time(self.need_step_gen_time) def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() @@ -509,7 +509,7 @@ class ToolHead: drip_completion.wait(curtime + wait_time) continue npt = min(self.print_time + DRIP_SEGMENT_TIME, next_print_time) - self.note_mcu_movequeue_activity(npt + self.kin_flush_delay) + self.note_mcu_movequeue_activity(npt) self._advance_move_time(npt) # Exit "Drip" state self.reactor.update_timer(self.flush_timer, self.reactor.NOW) @@ -601,9 +601,10 @@ class ToolHead: return last_move.timing_callbacks.append(callback) def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True): - self.need_flush_time = max(self.need_flush_time, mq_time) if is_step_gen: - self.step_gen_time = max(self.step_gen_time, mq_time) + mq_time += self.kin_flush_delay + self.need_step_gen_time = max(self.need_step_gen_time, mq_time) + self.need_flush_time = max(self.need_flush_time, mq_time) if self.do_kick_flush_timer: self.do_kick_flush_timer = False self.reactor.update_timer(self.flush_timer, self.reactor.NOW) From a64207aac3aad0bf3679c7619a3941382700e689 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 12:23:27 -0400 Subject: [PATCH 011/117] toolhead: Implement flush "waves" in _advance_flush_time() Move the code that implements flushing in waves from _advance_move_time() to _advance_flush_time(). This also separates print_time tracking from the _advance_flush_time() implementation. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 167e43c0c..e9746b54c 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -269,25 +269,38 @@ class ToolHead: self.printer.register_event_handler("klippy:shutdown", self._handle_shutdown) # Print time and flush tracking - def _advance_flush_time(self, flush_time): - flush_time = max(flush_time, self.last_flush_time) - # Generate steps via itersolve - sg_flush_want = min(flush_time + STEPCOMPRESS_FLUSH_TIME, - self.print_time - self.kin_flush_delay) - step_gen_time = max(self.last_step_gen_time, sg_flush_want, flush_time) - self.motion_queuing.flush_motion_queues(flush_time, step_gen_time) - self.last_flush_time = flush_time - self.last_step_gen_time = step_gen_time - def _advance_move_time(self, next_print_time): - pt_delay = self.kin_flush_delay + STEPCOMPRESS_FLUSH_TIME - flush_time = max(self.last_flush_time, self.print_time - pt_delay) - self.print_time = max(self.print_time, next_print_time) - want_flush_time = max(flush_time, self.print_time - pt_delay) + def _advance_flush_time(self, target_time=None, lazy_target=False): + if target_time is None: + # This is a full flush + target_time = self.need_step_gen_time + want_flush_time = want_step_gen_time = target_time + if lazy_target: + # Account for step gen scan windows and optimize step compression + want_step_gen_time -= self.kin_flush_delay + want_flush_time = want_step_gen_time - STEPCOMPRESS_FLUSH_TIME + want_flush_time = max(want_flush_time, self.last_flush_time) + flush_time = self.last_flush_time + if want_flush_time > flush_time + 10. * MOVE_BATCH_TIME: + # Use closer startup time when coming out of idle state + curtime = self.reactor.monotonic() + est_print_time = self.mcu.estimated_print_time(curtime) + flush_time = max(flush_time, est_print_time) + flush_motion_queues = self.motion_queuing.flush_motion_queues while 1: flush_time = min(flush_time + MOVE_BATCH_TIME, want_flush_time) - self._advance_flush_time(flush_time) + # Generate steps via itersolve + want_sg_wave = min(flush_time + STEPCOMPRESS_FLUSH_TIME, + want_step_gen_time) + step_gen_time = max(self.last_step_gen_time, want_sg_wave, + flush_time) + flush_motion_queues(flush_time, step_gen_time) + self.last_flush_time = flush_time + self.last_step_gen_time = step_gen_time if flush_time >= want_flush_time: break + def _advance_move_time(self, next_print_time): + self.print_time = max(self.print_time, next_print_time) + self._advance_flush_time(self.print_time, lazy_target=True) def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) @@ -337,7 +350,7 @@ class ToolHead: self.check_stall_time = 0. def flush_step_generation(self): self._flush_lookahead() - self._advance_flush_time(self.need_step_gen_time) + self._advance_flush_time() def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() From 4b9a0b4f82bbebf77a6475e7d4b3dca224ad45be Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 14:27:21 -0400 Subject: [PATCH 012/117] toolhead: Separate lookahead timer flushing to new _check_flush_lookahead() Separate out the lookahead specific flushing logic from the _flush_handler() code. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index e9746b54c..dbafdb924 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -401,21 +401,29 @@ class ToolHead: logging.exception("Exception in priming_handler") self.printer.invoke_shutdown("Exception in priming_handler") return self.reactor.NEVER + def _check_flush_lookahead(self, eventtime): + if self.special_queuing_state: + return None + # In "main" state - flush lookahead if buffer runs low + est_print_time = self.mcu.estimated_print_time(eventtime) + print_time = self.print_time + buffer_time = print_time - est_print_time + if buffer_time > BUFFER_TIME_LOW: + # Running normally - reschedule check + return eventtime + buffer_time - BUFFER_TIME_LOW + # Under ran low buffer mark - flush lookahead queue + self._flush_lookahead() + if print_time != self.print_time: + self.check_stall_time = self.print_time + return None def _flush_handler(self, eventtime): try: + # Check if flushing is done via lookahead queue + ret = self._check_flush_lookahead(eventtime) + if ret is not None: + return ret + # Flush motion queues est_print_time = self.mcu.estimated_print_time(eventtime) - if not self.special_queuing_state: - # In "main" state - flush lookahead if buffer runs low - print_time = self.print_time - buffer_time = print_time - est_print_time - if buffer_time > BUFFER_TIME_LOW: - # Running normally - reschedule check - return eventtime + buffer_time - BUFFER_TIME_LOW - # Under ran low buffer mark - flush lookahead queue - self._flush_lookahead() - if print_time != self.print_time: - self.check_stall_time = self.print_time - # In "NeedPrime"/"Priming" state - flush queues if needed while 1: end_flush = self.need_flush_time + BGFLUSH_EXTRA_TIME if self.last_flush_time >= end_flush: From 872615cfcf3939473fc4a081bb7e75a66e8e0bd8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 14:28:18 -0400 Subject: [PATCH 013/117] toolhead: Add new _calc_step_gen_restart() helper Separate out step generation specific handling from _calc_print_time() code. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index dbafdb924..cf8ee583a 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -298,14 +298,16 @@ class ToolHead: self.last_step_gen_time = step_gen_time if flush_time >= want_flush_time: break + def _calc_step_gen_restart(self, est_print_time): + kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) + return kin_time + self.kin_flush_delay def _advance_move_time(self, next_print_time): self.print_time = max(self.print_time, next_print_time) self._advance_flush_time(self.print_time, lazy_target=True) def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) - kin_time += self.kin_flush_delay + kin_time = self._calc_step_gen_restart(est_print_time) min_print_time = max(est_print_time + BUFFER_TIME_START, kin_time) if min_print_time > self.print_time: self.print_time = min_print_time From db5cbe56d330e87918be2a3c24b51e4be3e4adfd Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 13 Aug 2025 14:07:03 -0400 Subject: [PATCH 014/117] toolhead: Do not modify print_time in drip_update_time() Implement drip_update_time() using _advance_flush_time() instead of _advance_move_time(). Signed-off-by: Kevin O'Connor --- klippy/extras/manual_stepper.py | 7 +++--- klippy/toolhead.py | 42 ++++++++++++++------------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 3c1b29b65..f57bbbbb1 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -197,11 +197,12 @@ class ManualStepper: def drip_move(self, newpos, speed, drip_completion): # Submit move to trapq self.sync_print_time() - maxtime = self._submit_move(self.next_cmd_time, newpos[0], - speed, self.homing_accel) + start_time = self.next_cmd_time + end_time = self._submit_move(start_time, newpos[0], + speed, self.homing_accel) # Drip updates to motors toolhead = self.printer.lookup_object('toolhead') - toolhead.drip_update_time(maxtime, drip_completion) + toolhead.drip_update_time(start_time, end_time, drip_completion) # Clear trapq of any remaining parts of movement reactor = self.printer.get_reactor() self.motion_queuing.wipe_trapq(self.trapq) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index cf8ee583a..62756dfdc 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -344,7 +344,7 @@ class ToolHead: self.note_mcu_movequeue_activity(next_move_time) self._advance_move_time(next_move_time) def _flush_lookahead(self): - # Transit from "NeedPrime"/"Priming"/"Drip"/main state to "NeedPrime" + # Transit from "NeedPrime"/"Priming"/main state to "NeedPrime" self._process_lookahead() self.special_queuing_state = "NeedPrime" self.need_check_pause = -1. @@ -511,32 +511,29 @@ class ToolHead: def get_extra_axes(self): return [None, None, None] + self.extra_axes # Homing "drip move" handling - def drip_update_time(self, next_print_time, drip_completion): - # Transition from "NeedPrime"/"Priming"/main state to "Drip" state - self.special_queuing_state = "Drip" - self.need_check_pause = self.reactor.NEVER + def drip_update_time(self, start_time, end_time, drip_completion): + # Disable background flushing from timer self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) self.do_kick_flush_timer = False - self.lookahead.set_flush_time(BUFFER_TIME_HIGH) - self.check_stall_time = 0. - # Update print_time in segments until drip_completion signal + # Flush in segments until drip_completion signal flush_delay = DRIP_TIME + STEPCOMPRESS_FLUSH_TIME + self.kin_flush_delay - while self.print_time < next_print_time: + flush_time = start_time + while flush_time < end_time: if drip_completion.test(): break curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - wait_time = self.print_time - est_print_time - flush_delay + wait_time = flush_time - est_print_time - flush_delay if wait_time > 0. and self.can_pause: # Pause before sending more steps drip_completion.wait(curtime + wait_time) continue - npt = min(self.print_time + DRIP_SEGMENT_TIME, next_print_time) - self.note_mcu_movequeue_activity(npt) - self._advance_move_time(npt) - # Exit "Drip" state + flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) + self.note_mcu_movequeue_activity(flush_time) + self._advance_flush_time(flush_time, lazy_target=True) + # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) - self.flush_step_generation() + self._advance_flush_time() def _drip_load_trapq(self, submit_move): # Queue move into trapezoid motion queue (trapq) if submit_move.move_d: @@ -544,18 +541,17 @@ class ToolHead: self.lookahead.add_move(submit_move) moves = self.lookahead.flush() self._calc_print_time() - next_move_time = self.print_time + start_time = end_time = self.print_time for move in moves: self.trapq_append( - self.trapq, next_move_time, + self.trapq, end_time, move.accel_t, move.cruise_t, move.decel_t, move.start_pos[0], move.start_pos[1], move.start_pos[2], move.axes_r[0], move.axes_r[1], move.axes_r[2], move.start_v, move.cruise_v, move.accel) - next_move_time = (next_move_time + move.accel_t - + move.cruise_t + move.decel_t) + end_time = end_time + move.accel_t + move.cruise_t + move.decel_t self.lookahead.reset() - return next_move_time + return start_time, end_time def drip_move(self, newpos, speed, drip_completion): # Create and verify move is valid newpos = newpos[:3] + self.commanded_pos[3:] @@ -566,8 +562,8 @@ class ToolHead: self.dwell(self.kin_flush_delay) # Transmit move in "drip" mode self._process_lookahead() - next_move_time = self._drip_load_trapq(move) - self.drip_update_time(next_move_time, drip_completion) + start_time, end_time = self._drip_load_trapq(move) + self.drip_update_time(start_time, end_time, drip_completion) # Move finished; cleanup any remnants on trapq self.motion_queuing.wipe_trapq(self.trapq) # Misc commands @@ -578,8 +574,6 @@ class ToolHead: est_print_time = self.mcu.estimated_print_time(eventtime) buffer_time = self.print_time - est_print_time is_active = buffer_time > -60. or not self.special_queuing_state - if self.special_queuing_state == "Drip": - buffer_time = 0. return is_active, "print_time=%.3f buffer_time=%.3f print_stall=%d" % ( self.print_time, max(buffer_time, 0.), self.print_stall) def check_busy(self, eventtime): From 8c13811c3bbf1b4e1a48413775b60c0d50e6a5f4 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 13 Aug 2025 15:28:28 -0400 Subject: [PATCH 015/117] toolhead: Avoid using print_time when calling mcu.check_active() Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 62756dfdc..bd9f67da0 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -568,9 +568,8 @@ class ToolHead: self.motion_queuing.wipe_trapq(self.trapq) # Misc commands def stats(self, eventtime): - max_queue_time = max(self.print_time, self.last_flush_time) for m in self.all_mcus: - m.check_active(max_queue_time, eventtime) + m.check_active(self.last_step_gen_time, eventtime) est_print_time = self.mcu.estimated_print_time(eventtime) buffer_time = self.print_time - est_print_time is_active = buffer_time > -60. or not self.special_queuing_state From d1974c0d3d743e0d0ab9b30b699309d9082bbb14 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 20:07:08 -0400 Subject: [PATCH 016/117] motion_queuing: Move flushing logic from toolhead to motion_queuing module Move low-level step generation timing code to the motion_queing module. This helps simplify the toolhead module. It also helps centralize the step generation code into the motion_queing module. Signed-off-by: Kevin O'Connor --- klippy/extras/force_move.py | 2 +- klippy/extras/manual_stepper.py | 7 +- klippy/extras/motion_queuing.py | 131 +++++++++++++++++++++++++++++--- klippy/extras/output_pin.py | 11 +-- klippy/extras/pwm_tool.py | 12 +-- klippy/toolhead.py | 121 +++-------------------------- 6 files changed, 149 insertions(+), 135 deletions(-) diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py index 3947292b9..277c68e3c 100644 --- a/klippy/extras/force_move.py +++ b/klippy/extras/force_move.py @@ -76,7 +76,7 @@ class ForceMove: self.trapq_append(self.trapq, print_time, accel_t, cruise_t, accel_t, 0., 0., 0., axis_r, 0., 0., 0., cruise_v, accel) print_time = print_time + accel_t + cruise_t + accel_t - toolhead.note_mcu_movequeue_activity(print_time) + self.motion_queuing.note_mcu_movequeue_activity(print_time) toolhead.dwell(accel_t + cruise_t + accel_t) toolhead.flush_step_generation() stepper.set_trapq(prev_trapq) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index f57bbbbb1..9c775567f 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -72,8 +72,7 @@ class ManualStepper: self.sync_print_time() self.next_cmd_time = self._submit_move(self.next_cmd_time, movepos, speed, accel) - toolhead = self.printer.lookup_object('toolhead') - toolhead.note_mcu_movequeue_activity(self.next_cmd_time) + self.motion_queuing.note_mcu_movequeue_activity(self.next_cmd_time) if sync: self.sync_print_time() def do_homing_move(self, movepos, speed, accel, triggered, check_trigger): @@ -201,8 +200,8 @@ class ManualStepper: end_time = self._submit_move(start_time, newpos[0], speed, self.homing_accel) # Drip updates to motors - toolhead = self.printer.lookup_object('toolhead') - toolhead.drip_update_time(start_time, end_time, drip_completion) + self.motion_queuing.drip_update_time(start_time, end_time, + drip_completion) # Clear trapq of any remaining parts of movement reactor = self.printer.get_reactor() self.motion_queuing.wipe_trapq(self.trapq) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 49fafd6b0..226aa9f2a 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -6,12 +6,22 @@ import logging import chelper +BGFLUSH_LOW_TIME = 0.200 +BGFLUSH_BATCH_TIME = 0.200 +BGFLUSH_EXTRA_TIME = 0.250 MOVE_HISTORY_EXPIRE = 30. +MIN_KIN_TIME = 0.100 +MOVE_BATCH_TIME = 0.500 +STEPCOMPRESS_FLUSH_TIME = 0.050 SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c +DRIP_SEGMENT_TIME = 0.050 +DRIP_TIME = 0.100 + class PrinterMotionQueuing: def __init__(self, config): - self.printer = config.get_printer() + self.printer = printer = config.get_printer() + self.reactor = printer.get_reactor() # Low level C allocations self.trapqs = [] self.stepcompress = [] @@ -26,10 +36,22 @@ class PrinterMotionQueuing: self.flush_callbacks = [] # History expiration self.clear_history_time = 0. - is_debug = self.printer.get_start_args().get('debugoutput') is not None - self.is_debugoutput = is_debug + # Flush tracking + self.flush_timer = self.reactor.register_timer(self._flush_handler) + self.do_kick_flush_timer = True + self.last_flush_time = self.last_step_gen_time = 0. + self.need_flush_time = self.need_step_gen_time = 0. + self.check_flush_lookahead_cb = (lambda e: None) + # MCU tracking + self.all_mcus = [m for n, m in printer.lookup_objects(module='mcu')] + self.mcu = self.all_mcus[0] + self.can_pause = True + if self.mcu.is_fileoutput(): + self.can_pause = False # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME + # Register handlers + printer.register_event_handler("klippy:shutdown", self._handle_shutdown) def allocate_trapq(self): ffi_main, ffi_lib = chelper.get_ffi() trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) @@ -60,7 +82,7 @@ class PrinterMotionQueuing: fcbs = list(self.flush_callbacks) fcbs.remove(callback) self.flush_callbacks = fcbs - def flush_motion_queues(self, must_flush_time, max_step_gen_time): + def _flush_motion_queues(self, must_flush_time, max_step_gen_time): # Invoke flush callbacks (if any) for cb in self.flush_callbacks: cb(must_flush_time, max_step_gen_time) @@ -80,7 +102,7 @@ class PrinterMotionQueuing: # Determine maximum history to keep trapq_free_time = max_step_gen_time - self.kin_flush_delay clear_history_time = self.clear_history_time - if self.is_debugoutput: + if not self.can_pause: clear_history_time = trapq_free_time - MOVE_HISTORY_EXPIRE # Move processed trapq moves to history list, and expire old history for trapq in self.trapqs: @@ -92,18 +114,109 @@ class PrinterMotionQueuing: self.steppersync_history_expire(ss, clock) def wipe_trapq(self, trapq): # Expire any remaining movement in the trapq (force to history list) - NEVER = 9999999999999999. - self.trapq_finalize_moves(trapq, NEVER, 0.) + self.trapq_finalize_moves(trapq, self.reactor.NEVER, 0.) def lookup_trapq_append(self): ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append def set_step_generate_scan_time(self, delay): self.kin_flush_delay = delay def stats(self, eventtime): - mcu = self.printer.lookup_object('mcu') - est_print_time = mcu.estimated_print_time(eventtime) + # Hack to globally invoke mcu check_active() + for m in self.all_mcus: + m.check_active(self.last_step_gen_time, eventtime) + # Calculate history expiration + est_print_time = self.mcu.estimated_print_time(eventtime) self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE return False, "" + # Flush tracking + def _handle_shutdown(self): + self.can_pause = False + def setup_lookahead_flush_callback(self, check_flush_lookahead_cb): + self.check_flush_lookahead_cb = check_flush_lookahead_cb + def advance_flush_time(self, target_time=None, lazy_target=False): + if target_time is None: + # This is a full flush + target_time = self.need_step_gen_time + want_flush_time = want_step_gen_time = target_time + if lazy_target: + # Account for step gen scan windows and optimize step compression + want_step_gen_time -= self.kin_flush_delay + want_flush_time = want_step_gen_time - STEPCOMPRESS_FLUSH_TIME + want_flush_time = max(want_flush_time, self.last_flush_time) + flush_time = self.last_flush_time + if want_flush_time > flush_time + 10. * MOVE_BATCH_TIME: + # Use closer startup time when coming out of idle state + curtime = self.reactor.monotonic() + est_print_time = self.mcu.estimated_print_time(curtime) + flush_time = max(flush_time, est_print_time) + while 1: + flush_time = min(flush_time + MOVE_BATCH_TIME, want_flush_time) + # Generate steps via itersolve + want_sg_wave = min(flush_time + STEPCOMPRESS_FLUSH_TIME, + want_step_gen_time) + step_gen_time = max(self.last_step_gen_time, want_sg_wave, + flush_time) + self._flush_motion_queues(flush_time, step_gen_time) + self.last_flush_time = flush_time + self.last_step_gen_time = step_gen_time + if flush_time >= want_flush_time: + break + def calc_step_gen_restart(self, est_print_time): + kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) + return kin_time + self.kin_flush_delay + def _flush_handler(self, eventtime): + try: + # Check if flushing is done via lookahead queue + ret = self.check_flush_lookahead_cb(eventtime) + if ret is not None: + return ret + # Flush motion queues + est_print_time = self.mcu.estimated_print_time(eventtime) + while 1: + end_flush = self.need_flush_time + BGFLUSH_EXTRA_TIME + if self.last_flush_time >= end_flush: + self.do_kick_flush_timer = True + return self.reactor.NEVER + buffer_time = self.last_flush_time - est_print_time + if buffer_time > BGFLUSH_LOW_TIME: + return eventtime + buffer_time - BGFLUSH_LOW_TIME + ftime = est_print_time + BGFLUSH_LOW_TIME + BGFLUSH_BATCH_TIME + self.advance_flush_time(min(end_flush, ftime)) + except: + logging.exception("Exception in flush_handler") + self.printer.invoke_shutdown("Exception in flush_handler") + return self.reactor.NEVER + def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True): + if is_step_gen: + mq_time += self.kin_flush_delay + self.need_step_gen_time = max(self.need_step_gen_time, mq_time) + self.need_flush_time = max(self.need_flush_time, mq_time) + if self.do_kick_flush_timer: + self.do_kick_flush_timer = False + self.reactor.update_timer(self.flush_timer, self.reactor.NOW) + def drip_update_time(self, start_time, end_time, drip_completion): + # Disable background flushing from timer + self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) + self.do_kick_flush_timer = False + # Flush in segments until drip_completion signal + flush_delay = DRIP_TIME + STEPCOMPRESS_FLUSH_TIME + self.kin_flush_delay + flush_time = start_time + while flush_time < end_time: + if drip_completion.test(): + break + curtime = self.reactor.monotonic() + est_print_time = self.mcu.estimated_print_time(curtime) + wait_time = flush_time - est_print_time - flush_delay + if wait_time > 0. and self.can_pause: + # Pause before sending more steps + drip_completion.wait(curtime + wait_time) + continue + flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) + self.note_mcu_movequeue_activity(flush_time) + self.advance_flush_time(flush_time, lazy_target=True) + # Restore background flushing + self.reactor.update_timer(self.flush_timer, self.reactor.NOW) + self.advance_flush_time() def load_config(config): return PrinterMotionQueuing(config) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 63862d978..a51292990 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -20,8 +20,8 @@ class GCodeRequestQueue: self.rqueue = [] self.next_min_flush_time = 0. self.toolhead = None - motion_queuing = printer.load_object(config, 'motion_queuing') - motion_queuing.register_flush_callback(self._flush_notification) + self.motion_queuing = printer.load_object(config, 'motion_queuing') + self.motion_queuing.register_flush_callback(self._flush_notification) printer.register_event_handler("klippy:connect", self._handle_connect) def _handle_connect(self): self.toolhead = self.printer.lookup_object('toolhead') @@ -51,11 +51,12 @@ class GCodeRequestQueue: del rqueue[:pos+1] self.next_min_flush_time = next_time + max(min_wait, min_sched_time) # Ensure following queue items are flushed - self.toolhead.note_mcu_movequeue_activity(self.next_min_flush_time, - is_step_gen=False) + self.motion_queuing.note_mcu_movequeue_activity( + self.next_min_flush_time, is_step_gen=False) def _queue_request(self, print_time, value): self.rqueue.append((print_time, value)) - self.toolhead.note_mcu_movequeue_activity(print_time, is_step_gen=False) + self.motion_queuing.note_mcu_movequeue_activity( + print_time, is_step_gen=False) def queue_gcode_request(self, value): self.toolhead.register_lookahead_callback( (lambda pt: self._queue_request(pt, value))) diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index 6d401c0b0..d9e72c5e1 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -16,8 +16,8 @@ class MCU_queued_pwm: self._max_duration = 2. self._oid = oid = mcu.create_oid() printer = mcu.get_printer() - motion_queuing = printer.load_object(config, 'motion_queuing') - self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid) + self._motion_queuing = printer.load_object(config, 'motion_queuing') + self._stepqueue = self._motion_queuing.allocate_stepcompress(mcu, oid) ffi_main, ffi_lib = chelper.get_ffi() self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg mcu.register_config_callback(self._build_config) @@ -62,8 +62,8 @@ class MCU_queued_pwm: if self._duration_ticks >= 1<<31: raise config_error("PWM pin max duration too large") if self._duration_ticks: - motion_queuing = printer.lookup_object('motion_queuing') - motion_queuing.register_flush_callback(self._flush_notification) + self._motion_queuing.register_flush_callback( + self._flush_notification) if self._hardware_pwm: self._pwm_max = self._mcu.get_constant_float("PWM_MAX") self._default_value = self._shutdown_value * self._pwm_max @@ -116,8 +116,8 @@ class MCU_queued_pwm: # Continue flushing to resend time wakeclock += self._duration_ticks wake_print_time = self._mcu.clock_to_print_time(wakeclock) - self._toolhead.note_mcu_movequeue_activity(wake_print_time, - is_step_gen=False) + self._motion_queuing.note_mcu_movequeue_activity(wake_print_time, + is_step_gen=False) def set_pwm(self, print_time, value): clock = self._mcu.print_time_to_clock(print_time) if self._invert: diff --git a/klippy/toolhead.py b/klippy/toolhead.py index bd9f67da0..3eadbb3d3 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -193,25 +193,14 @@ class LookAheadQueue: BUFFER_TIME_LOW = 1.0 BUFFER_TIME_HIGH = 2.0 BUFFER_TIME_START = 0.250 -BGFLUSH_LOW_TIME = 0.200 -BGFLUSH_BATCH_TIME = 0.200 -BGFLUSH_EXTRA_TIME = 0.250 -MIN_KIN_TIME = 0.100 -MOVE_BATCH_TIME = 0.500 -STEPCOMPRESS_FLUSH_TIME = 0.050 SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c -DRIP_SEGMENT_TIME = 0.050 -DRIP_TIME = 0.100 - # Main code to track events (and their timing) on the printer toolhead class ToolHead: def __init__(self, config): self.printer = config.get_printer() self.reactor = self.printer.get_reactor() - self.all_mcus = [ - m for n, m in self.printer.lookup_objects(module='mcu')] - self.mcu = self.all_mcus[0] + self.mcu = self.printer.lookup_object('mcu') self.lookahead = LookAheadQueue() self.lookahead.set_flush_time(BUFFER_TIME_HIGH) self.commanded_pos = [0., 0., 0., 0.] @@ -236,16 +225,13 @@ class ToolHead: self.print_time = 0. self.special_queuing_state = "NeedPrime" self.priming_timer = None - # Flush tracking - self.flush_timer = self.reactor.register_timer(self._flush_handler) - self.do_kick_flush_timer = True - self.last_flush_time = self.last_step_gen_time = 0. - self.need_flush_time = self.need_step_gen_time = 0. # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME self.kin_flush_times = [] # Setup for generating moves self.motion_queuing = self.printer.load_object(config, 'motion_queuing') + self.motion_queuing.setup_lookahead_flush_callback( + self._check_flush_lookahead) self.trapq = self.motion_queuing.allocate_trapq() self.trapq_append = self.motion_queuing.lookup_trapq_append() # Create kinematics class @@ -268,46 +254,15 @@ class ToolHead: # Register handlers self.printer.register_event_handler("klippy:shutdown", self._handle_shutdown) - # Print time and flush tracking - def _advance_flush_time(self, target_time=None, lazy_target=False): - if target_time is None: - # This is a full flush - target_time = self.need_step_gen_time - want_flush_time = want_step_gen_time = target_time - if lazy_target: - # Account for step gen scan windows and optimize step compression - want_step_gen_time -= self.kin_flush_delay - want_flush_time = want_step_gen_time - STEPCOMPRESS_FLUSH_TIME - want_flush_time = max(want_flush_time, self.last_flush_time) - flush_time = self.last_flush_time - if want_flush_time > flush_time + 10. * MOVE_BATCH_TIME: - # Use closer startup time when coming out of idle state - curtime = self.reactor.monotonic() - est_print_time = self.mcu.estimated_print_time(curtime) - flush_time = max(flush_time, est_print_time) - flush_motion_queues = self.motion_queuing.flush_motion_queues - while 1: - flush_time = min(flush_time + MOVE_BATCH_TIME, want_flush_time) - # Generate steps via itersolve - want_sg_wave = min(flush_time + STEPCOMPRESS_FLUSH_TIME, - want_step_gen_time) - step_gen_time = max(self.last_step_gen_time, want_sg_wave, - flush_time) - flush_motion_queues(flush_time, step_gen_time) - self.last_flush_time = flush_time - self.last_step_gen_time = step_gen_time - if flush_time >= want_flush_time: - break - def _calc_step_gen_restart(self, est_print_time): - kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) - return kin_time + self.kin_flush_delay + # Print time tracking def _advance_move_time(self, next_print_time): self.print_time = max(self.print_time, next_print_time) - self._advance_flush_time(self.print_time, lazy_target=True) + self.motion_queuing.advance_flush_time(self.print_time, + lazy_target=True) def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - kin_time = self._calc_step_gen_restart(est_print_time) + kin_time = self.motion_queuing.calc_step_gen_restart(est_print_time) min_print_time = max(est_print_time + BUFFER_TIME_START, kin_time) if min_print_time > self.print_time: self.print_time = min_print_time @@ -341,7 +296,7 @@ class ToolHead: for cb in move.timing_callbacks: cb(next_move_time) # Generate steps for moves - self.note_mcu_movequeue_activity(next_move_time) + self.motion_queuing.note_mcu_movequeue_activity(next_move_time) self._advance_move_time(next_move_time) def _flush_lookahead(self): # Transit from "NeedPrime"/"Priming"/main state to "NeedPrime" @@ -352,7 +307,7 @@ class ToolHead: self.check_stall_time = 0. def flush_step_generation(self): self._flush_lookahead() - self._advance_flush_time() + self.motion_queuing.advance_flush_time() def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() @@ -418,28 +373,6 @@ class ToolHead: if print_time != self.print_time: self.check_stall_time = self.print_time return None - def _flush_handler(self, eventtime): - try: - # Check if flushing is done via lookahead queue - ret = self._check_flush_lookahead(eventtime) - if ret is not None: - return ret - # Flush motion queues - est_print_time = self.mcu.estimated_print_time(eventtime) - while 1: - end_flush = self.need_flush_time + BGFLUSH_EXTRA_TIME - if self.last_flush_time >= end_flush: - self.do_kick_flush_timer = True - return self.reactor.NEVER - buffer_time = self.last_flush_time - est_print_time - if buffer_time > BGFLUSH_LOW_TIME: - return eventtime + buffer_time - BGFLUSH_LOW_TIME - ftime = est_print_time + BGFLUSH_LOW_TIME + BGFLUSH_BATCH_TIME - self._advance_flush_time(min(end_flush, ftime)) - except: - logging.exception("Exception in flush_handler") - self.printer.invoke_shutdown("Exception in flush_handler") - return self.reactor.NEVER # Movement commands def get_position(self): return list(self.commanded_pos) @@ -511,29 +444,6 @@ class ToolHead: def get_extra_axes(self): return [None, None, None] + self.extra_axes # Homing "drip move" handling - def drip_update_time(self, start_time, end_time, drip_completion): - # Disable background flushing from timer - self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) - self.do_kick_flush_timer = False - # Flush in segments until drip_completion signal - flush_delay = DRIP_TIME + STEPCOMPRESS_FLUSH_TIME + self.kin_flush_delay - flush_time = start_time - while flush_time < end_time: - if drip_completion.test(): - break - curtime = self.reactor.monotonic() - est_print_time = self.mcu.estimated_print_time(curtime) - wait_time = flush_time - est_print_time - flush_delay - if wait_time > 0. and self.can_pause: - # Pause before sending more steps - drip_completion.wait(curtime + wait_time) - continue - flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) - self.note_mcu_movequeue_activity(flush_time) - self._advance_flush_time(flush_time, lazy_target=True) - # Restore background flushing - self.reactor.update_timer(self.flush_timer, self.reactor.NOW) - self._advance_flush_time() def _drip_load_trapq(self, submit_move): # Queue move into trapezoid motion queue (trapq) if submit_move.move_d: @@ -563,13 +473,12 @@ class ToolHead: # Transmit move in "drip" mode self._process_lookahead() start_time, end_time = self._drip_load_trapq(move) - self.drip_update_time(start_time, end_time, drip_completion) + self.motion_queuing.drip_update_time(start_time, end_time, + drip_completion) # Move finished; cleanup any remnants on trapq self.motion_queuing.wipe_trapq(self.trapq) # Misc commands def stats(self, eventtime): - for m in self.all_mcus: - m.check_active(self.last_step_gen_time, eventtime) est_print_time = self.mcu.estimated_print_time(eventtime) buffer_time = self.print_time - est_print_time is_active = buffer_time > -60. or not self.special_queuing_state @@ -616,14 +525,6 @@ class ToolHead: callback(self.get_last_move_time()) return last_move.timing_callbacks.append(callback) - def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True): - if is_step_gen: - mq_time += self.kin_flush_delay - self.need_step_gen_time = max(self.need_step_gen_time, mq_time) - self.need_flush_time = max(self.need_flush_time, mq_time) - if self.do_kick_flush_timer: - self.do_kick_flush_timer = False - self.reactor.update_timer(self.flush_timer, self.reactor.NOW) def get_max_velocity(self): return self.max_velocity, self.max_accel def _calc_junction_deviation(self): From 5426943501bf716903d4797d334634c4b0e28e49 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 29 Nov 2023 01:04:28 -0500 Subject: [PATCH 017/117] motion_queuing: Automatically detect changes to kin_flush_delay Remove the toolhead note_step_generation_scan_time() code and automatically detect the itersolve scan windows that are in use. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 7 +++++-- klippy/chelper/itersolve.c | 18 ++++++++++++++++++ klippy/chelper/itersolve.h | 3 +++ klippy/chelper/kin_shaper.c | 8 -------- klippy/chelper/stepcompress.c | 7 +++++++ klippy/chelper/stepcompress.h | 2 ++ klippy/extras/input_shaper.py | 11 +---------- klippy/extras/motion_queuing.py | 24 ++++++++++++++++++++++-- klippy/kinematics/extruder.py | 14 ++++++++------ klippy/toolhead.py | 16 ++-------------- 10 files changed, 68 insertions(+), 42 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 60ba91e7d..59971c1c4 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -56,6 +56,8 @@ defs_stepcompress = """ , uint64_t start_clock, uint64_t end_clock); void stepcompress_set_stepper_kinematics(struct stepcompress *sc , struct stepper_kinematics *sk); + struct stepper_kinematics *stepcompress_get_stepper_kinematics( + struct stepcompress *sc); """ defs_steppersync = """ @@ -76,11 +78,14 @@ defs_itersolve = """ int32_t itersolve_is_active_axis(struct stepper_kinematics *sk, char axis); void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq , double step_dist); + struct trapq *itersolve_get_trapq(struct stepper_kinematics *sk); double itersolve_calc_position_from_coord(struct stepper_kinematics *sk , double x, double y, double z); void itersolve_set_position(struct stepper_kinematics *sk , double x, double y, double z); double itersolve_get_commanded_pos(struct stepper_kinematics *sk); + double itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk); + double itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk); """ defs_trapq = """ @@ -157,8 +162,6 @@ defs_kin_extruder = """ """ defs_kin_shaper = """ - double input_shaper_get_step_generation_window( - struct stepper_kinematics *sk); int input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis , int n, double a[], double t[]); int input_shaper_set_sk(struct stepper_kinematics *sk diff --git a/klippy/chelper/itersolve.c b/klippy/chelper/itersolve.c index eba1deef4..9b1206249 100644 --- a/klippy/chelper/itersolve.c +++ b/klippy/chelper/itersolve.c @@ -248,6 +248,12 @@ itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq sk->step_dist = step_dist; } +struct trapq * __visible +itersolve_get_trapq(struct stepper_kinematics *sk) +{ + return sk->tq; +} + double __visible itersolve_calc_position_from_coord(struct stepper_kinematics *sk , double x, double y, double z) @@ -273,3 +279,15 @@ itersolve_get_commanded_pos(struct stepper_kinematics *sk) { return sk->commanded_pos; } + +double __visible +itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk) +{ + return sk->gen_steps_pre_active; +} + +double __visible +itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk) +{ + return sk->gen_steps_post_active; +} diff --git a/klippy/chelper/itersolve.h b/klippy/chelper/itersolve.h index e2e46ebe3..50a30f7da 100644 --- a/klippy/chelper/itersolve.h +++ b/klippy/chelper/itersolve.h @@ -31,10 +31,13 @@ double itersolve_check_active(struct stepper_kinematics *sk, double flush_time); int32_t itersolve_is_active_axis(struct stepper_kinematics *sk, char axis); void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq , double step_dist); +struct trapq *itersolve_get_trapq(struct stepper_kinematics *sk); double itersolve_calc_position_from_coord(struct stepper_kinematics *sk , double x, double y, double z); void itersolve_set_position(struct stepper_kinematics *sk , double x, double y, double z); double itersolve_get_commanded_pos(struct stepper_kinematics *sk); +double itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk); +double itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk); #endif // itersolve.h diff --git a/klippy/chelper/kin_shaper.c b/klippy/chelper/kin_shaper.c index 42d572d02..d5138ff04 100644 --- a/klippy/chelper/kin_shaper.c +++ b/klippy/chelper/kin_shaper.c @@ -239,14 +239,6 @@ input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis return status; } -double __visible -input_shaper_get_step_generation_window(struct stepper_kinematics *sk) -{ - struct input_shaper *is = container_of(sk, struct input_shaper, sk); - return is->sk.gen_steps_pre_active > is->sk.gen_steps_post_active - ? is->sk.gen_steps_pre_active : is->sk.gen_steps_post_active; -} - struct stepper_kinematics * __visible input_shaper_alloc(void) { diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 2889570dc..52dd40773 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -674,6 +674,13 @@ stepcompress_set_stepper_kinematics(struct stepcompress *sc sc->sk = sk; } +// Report current stepper_kinematics +struct stepper_kinematics * __visible +stepcompress_get_stepper_kinematics(struct stepcompress *sc) +{ + return sc->sk; +} + // Generate steps (via itersolve) and flush int32_t stepcompress_generate_steps(struct stepcompress *sc, double gen_steps_time diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index e21c4fd96..7ca0f2e43 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -41,6 +41,8 @@ int stepcompress_extract_old(struct stepcompress *sc struct stepper_kinematics; void stepcompress_set_stepper_kinematics(struct stepcompress *sc , struct stepper_kinematics *sk); +struct stepper_kinematics *stepcompress_get_stepper_kinematics( + struct stepcompress *sc); int32_t stepcompress_generate_steps(struct stepcompress *sc , double gen_steps_time , uint64_t flush_clock); diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py index 67a287cdd..cb9027d98 100644 --- a/klippy/extras/input_shaper.py +++ b/klippy/extras/input_shaper.py @@ -146,12 +146,8 @@ class InputShaper: is_sk = self._get_input_shaper_stepper_kinematics(s) if is_sk is None: continue - old_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk) + self.toolhead.flush_step_generation() ffi_lib.input_shaper_update_sk(is_sk) - new_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk) - if old_delay != new_delay: - self.toolhead.note_step_generation_scan_time(new_delay, - old_delay) def _update_input_shaping(self, error=None): self.toolhead.flush_step_generation() ffi_main, ffi_lib = chelper.get_ffi() @@ -163,16 +159,11 @@ class InputShaper: is_sk = self._get_input_shaper_stepper_kinematics(s) if is_sk is None: continue - old_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk) for shaper in self.shapers: if shaper in failed_shapers: continue if not shaper.set_shaper_kinematics(is_sk): failed_shapers.append(shaper) - new_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk) - if old_delay != new_delay: - self.toolhead.note_step_generation_scan_time(new_delay, - old_delay) if failed_shapers: error = error or self.printer.command_error raise error("Failed to configure shaper(s) %s with given parameters" diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 226aa9f2a..0b0981f1c 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -49,6 +49,7 @@ class PrinterMotionQueuing: if self.mcu.is_fileoutput(): self.can_pause = False # Kinematic step generation scan window time tracking + self.need_calc_kin_flush_delay = True self.kin_flush_delay = SDS_CHECK_TIME # Register handlers printer.register_event_handler("klippy:shutdown", self._handle_shutdown) @@ -118,8 +119,6 @@ class PrinterMotionQueuing: def lookup_trapq_append(self): ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append - def set_step_generate_scan_time(self, delay): - self.kin_flush_delay = delay def stats(self, eventtime): # Hack to globally invoke mcu check_active() for m in self.all_mcus: @@ -128,6 +127,24 @@ class PrinterMotionQueuing: est_print_time = self.mcu.estimated_print_time(eventtime) self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE return False, "" + # Kinematic step generation scan window time tracking + def get_kin_flush_delay(self): + return self.kin_flush_delay + def _calc_kin_flush_delay(self): + self.need_calc_kin_flush_delay = False + ffi_main, ffi_lib = chelper.get_ffi() + kin_flush_delay = SDS_CHECK_TIME + for mcu, sc in self.stepcompress: + sk = ffi_lib.stepcompress_get_stepper_kinematics(sc) + if sk == ffi_main.NULL: + continue + trapq = ffi_lib.itersolve_get_trapq(sk) + if trapq == ffi_main.NULL: + continue + pre_active = ffi_lib.itersolve_get_gen_steps_pre_active(sk) + post_active = ffi_lib.itersolve_get_gen_steps_post_active(sk) + kin_flush_delay = max(kin_flush_delay, pre_active, post_active) + self.kin_flush_delay = kin_flush_delay # Flush tracking def _handle_shutdown(self): self.can_pause = False @@ -137,6 +154,7 @@ class PrinterMotionQueuing: if target_time is None: # This is a full flush target_time = self.need_step_gen_time + self.need_calc_kin_flush_delay = True want_flush_time = want_step_gen_time = target_time if lazy_target: # Account for step gen scan windows and optimize step compression @@ -162,6 +180,8 @@ class PrinterMotionQueuing: if flush_time >= want_flush_time: break def calc_step_gen_restart(self, est_print_time): + if self.need_calc_kin_flush_delay: + self._calc_kin_flush_delay() kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) return kin_time + self.kin_flush_delay def _flush_handler(self, eventtime): diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index a89e3bdfa..4e6f14e41 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -69,14 +69,16 @@ class ExtruderStepper: if not pressure_advance: new_smooth_time = 0. toolhead = self.printer.lookup_object("toolhead") - if new_smooth_time != old_smooth_time: - toolhead.note_step_generation_scan_time( - new_smooth_time * .5, old_delay=old_smooth_time * .5) ffi_main, ffi_lib = chelper.get_ffi() espa = ffi_lib.extruder_set_pressure_advance - toolhead.register_lookahead_callback( - lambda print_time: espa(self.sk_extruder, print_time, - pressure_advance, new_smooth_time)) + if new_smooth_time != old_smooth_time: + # Need full kinematic flush to change the smooth time + toolhead.flush_step_generation() + espa(self.sk_extruder, 0., pressure_advance, new_smooth_time) + else: + toolhead.register_lookahead_callback( + lambda print_time: espa(self.sk_extruder, print_time, + pressure_advance, new_smooth_time)) self.pressure_advance = pressure_advance self.pressure_advance_smooth_time = smooth_time cmd_SET_PRESSURE_ADVANCE_help = "Set pressure advance parameters" diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 3eadbb3d3..877e4f34c 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -193,7 +193,6 @@ class LookAheadQueue: BUFFER_TIME_LOW = 1.0 BUFFER_TIME_HIGH = 2.0 BUFFER_TIME_START = 0.250 -SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c # Main code to track events (and their timing) on the printer toolhead class ToolHead: @@ -225,9 +224,6 @@ class ToolHead: self.print_time = 0. self.special_queuing_state = "NeedPrime" self.priming_timer = None - # Kinematic step generation scan window time tracking - self.kin_flush_delay = SDS_CHECK_TIME - self.kin_flush_times = [] # Setup for generating moves self.motion_queuing = self.printer.load_object(config, 'motion_queuing') self.motion_queuing.setup_lookahead_flush_callback( @@ -469,7 +465,8 @@ class ToolHead: if move.move_d: self.kin.check_move(move) # Make sure stepper movement doesn't start before nominal start time - self.dwell(self.kin_flush_delay) + kin_flush_delay = self.motion_queuing.get_kin_flush_delay() + self.dwell(kin_flush_delay) # Transmit move in "drip" mode self._process_lookahead() start_time, end_time = self._drip_load_trapq(move) @@ -510,15 +507,6 @@ class ToolHead: return self.kin def get_trapq(self): return self.trapq - def note_step_generation_scan_time(self, delay, old_delay=0.): - self.flush_step_generation() - if old_delay: - self.kin_flush_times.pop(self.kin_flush_times.index(old_delay)) - if delay: - self.kin_flush_times.append(delay) - new_delay = max(self.kin_flush_times + [SDS_CHECK_TIME]) - self.kin_flush_delay = new_delay - self.motion_queuing.set_step_generate_scan_time(new_delay) def register_lookahead_callback(self, callback): last_move = self.lookahead.get_last() if last_move is None: From 8a833175a534fcb33e5c78f8d8787a5d18c1274f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 2 Sep 2025 13:23:02 -0400 Subject: [PATCH 018/117] motion_queuing: Introduce flush_all_steps() helper Move the "full flush" code from advance_flush_time() to a new flush_all_steps() method. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 11 +++++------ klippy/toolhead.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 0b0981f1c..9d2ef5137 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -150,11 +150,7 @@ class PrinterMotionQueuing: self.can_pause = False def setup_lookahead_flush_callback(self, check_flush_lookahead_cb): self.check_flush_lookahead_cb = check_flush_lookahead_cb - def advance_flush_time(self, target_time=None, lazy_target=False): - if target_time is None: - # This is a full flush - target_time = self.need_step_gen_time - self.need_calc_kin_flush_delay = True + def advance_flush_time(self, target_time, lazy_target=False): want_flush_time = want_step_gen_time = target_time if lazy_target: # Account for step gen scan windows and optimize step compression @@ -179,6 +175,9 @@ class PrinterMotionQueuing: self.last_step_gen_time = step_gen_time if flush_time >= want_flush_time: break + def flush_all_steps(self): + self.need_calc_kin_flush_delay = True + self.advance_flush_time(self.need_step_gen_time) def calc_step_gen_restart(self, est_print_time): if self.need_calc_kin_flush_delay: self._calc_kin_flush_delay() @@ -236,7 +235,7 @@ class PrinterMotionQueuing: self.advance_flush_time(flush_time, lazy_target=True) # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) - self.advance_flush_time() + self.advance_flush_time(self.need_step_gen_time) def load_config(config): return PrinterMotionQueuing(config) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 877e4f34c..cf648695a 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -303,7 +303,7 @@ class ToolHead: self.check_stall_time = 0. def flush_step_generation(self): self._flush_lookahead() - self.motion_queuing.advance_flush_time() + self.motion_queuing.flush_all_steps() def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() From 93ea9ddfa95665fc0619f3311c93e6e07ddea51f Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 3 Sep 2025 03:27:13 +0200 Subject: [PATCH 019/117] extruder_stepper: define missing public methods methods Other modules could access the extruderN by the printer lookup_object(). That would return this wrapper class. Specifically, filament_motion_sensor will. They can try to access missing methods and klippy would crash. Signed-off-by: Timofey Titovets --- klippy/extras/extruder_stepper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/klippy/extras/extruder_stepper.py b/klippy/extras/extruder_stepper.py index 4ac5289f8..28293a3c0 100644 --- a/klippy/extras/extruder_stepper.py +++ b/klippy/extras/extruder_stepper.py @@ -15,6 +15,8 @@ class PrinterExtruderStepper: self.handle_connect) def handle_connect(self): self.extruder_stepper.sync_to_extruder(self.extruder_name) + def find_past_position(self, print_time): + return self.extruder_stepper.find_past_position(print_time) def get_status(self, eventtime): return self.extruder_stepper.get_status(eventtime) From 58e179d1281f92d3f1f73448c0eab961f5c8a958 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 3 Sep 2025 03:48:57 +0200 Subject: [PATCH 020/117] filament_motion_sensor: define tests Signed-off-by: Timofey Titovets --- test/klippy/extruders.cfg | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/klippy/extruders.cfg b/test/klippy/extruders.cfg index d7123d08e..7384617ef 100644 --- a/test/klippy/extruders.cfg +++ b/test/klippy/extruders.cfg @@ -66,3 +66,19 @@ max_velocity: 300 max_accel: 3000 max_z_velocity: 5 max_z_accel: 100 + +[filament_switch_sensor runout_switch] +switch_pin = PD4 + +[filament_motion_sensor runout_encoder] +switch_pin = PD5 +detection_length = 4 +extruder = extruder + +[filament_switch_sensor runout_switch1] +switch_pin = PL4 + +[filament_motion_sensor runout_encoder1] +switch_pin = PL6 +detection_length = 4 +extruder = extruder_stepper my_extra_stepper From 6c1a4a825d4e2fdbc0a5c9aed580e297c0d1601b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 4 Sep 2025 14:21:47 -0400 Subject: [PATCH 021/117] docs: Note filemant_motion_sensor can be associated with extruder_stepper Signed-off-by: Kevin O'Connor --- docs/Config_Reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 83de96096..ca21bcf21 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -4944,8 +4944,8 @@ detection_length: 7.0 # a state change on the switch_pin # Default is 7 mm. extruder: -# The name of the extruder section this sensor is associated with. -# This parameter must be provided. +# The name of the extruder or extruder_stepper section this sensor +# is associated with. This parameter must be provided. switch_pin: #pause_on_runout: #runout_gcode: From aa59b32031ce72dcfd8c8e96dc933f2ad37b16ed Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 3 Sep 2025 13:45:13 -0400 Subject: [PATCH 022/117] reactor: Prevent update_timer() from running a single timer multiple times The "lazy" greenlet implementation could allow the same timer to run multiple times in parallel if the first timer instance calls pause() and another task calls update_timer(). This is confusing and can cause hard to debug errors. Add a new timer_is_running flag to prevent it. Signed-off-by: Kevin O'Connor --- klippy/reactor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/klippy/reactor.py b/klippy/reactor.py index 412d53edf..f9bedcf3f 100644 --- a/klippy/reactor.py +++ b/klippy/reactor.py @@ -1,6 +1,6 @@ # File descriptor and timer event helper # -# Copyright (C) 2016-2020 Kevin O'Connor +# Copyright (C) 2016-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import os, gc, select, math, time, logging, queue @@ -14,6 +14,7 @@ class ReactorTimer: def __init__(self, callback, waketime): self.callback = callback self.waketime = waketime + self.timer_is_running = False class ReactorCompletion: class sentinel: pass @@ -118,6 +119,8 @@ class SelectReactor: return tuple(self._last_gc_times) # Timers def update_timer(self, timer_handler, waketime): + if timer_handler.timer_is_running: + return timer_handler.waketime = waketime self._next_timer = min(self._next_timer, waketime) def register_timer(self, callback, waketime=NEVER): @@ -155,7 +158,9 @@ class SelectReactor: waketime = t.waketime if eventtime >= waketime: t.waketime = self.NEVER + t.timer_is_running = True t.waketime = waketime = t.callback(eventtime) + t.timer_is_running = False if g_dispatch is not self._g_dispatch: self._next_timer = min(self._next_timer, waketime) self._end_greenlet(g_dispatch) From 68b67a16d6d7f006ac165419f5dddae16a970dd7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 4 Sep 2025 10:08:41 -0400 Subject: [PATCH 023/117] display: Check for redraw_request_pending at end of screen_update_event() Signed-off-by: Kevin O'Connor --- klippy/extras/display/display.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/klippy/extras/display/display.py b/klippy/extras/display/display.py index e9ba31d6d..cc33bc154 100644 --- a/klippy/extras/display/display.py +++ b/klippy/extras/display/display.py @@ -236,6 +236,8 @@ class PrinterLCD: except: logging.exception("Error during display screen update") self.lcd_chip.flush() + if self.redraw_request_pending: + return self.redraw_time return eventtime + REDRAW_TIME def request_redraw(self): if self.redraw_request_pending: From 96c3ca160e881dbeef8ccbe80f804572b9170d8c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 6 Sep 2025 14:06:25 -0400 Subject: [PATCH 024/117] gcode: Fix out-of-order check for M112 when read from gcode pseudo-tty Make sure to check for an out-of-order M112 command on the gcode pseudo-tty even if there is no pending commands being processed from that gcode pseudo-tty. There could be long running commands pending from webhooks, virtual_sdcard, or similar. Signed-off-by: Kevin O'Connor --- klippy/gcode.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/klippy/gcode.py b/klippy/gcode.py index 975da792b..1c50695d2 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -430,18 +430,17 @@ class GCodeIO: self.gcode.request_restart('exit') pending_commands.append("") # Handle case where multiple commands pending - if self.is_processing_data or len(pending_commands) > 1: - if len(pending_commands) < 20: - # Check for M112 out-of-order - for line in lines: - if self.m112_r.match(line) is not None: - self.gcode.cmd_M112(None) - if self.is_processing_data: - if len(pending_commands) >= 20: - # Stop reading input - self.reactor.unregister_fd(self.fd_handle) - self.fd_handle = None - return + if len(pending_commands) < 20: + # Check for M112 out-of-order + for line in lines: + if self.m112_r.match(line) is not None: + self.gcode.cmd_M112(None) + if self.is_processing_data: + if len(pending_commands) >= 20: + # Stop reading input + self.reactor.unregister_fd(self.fd_handle) + self.fd_handle = None + return # Process commands self.is_processing_data = True while pending_commands: From a89694ac68dbfe5b150a46aa90f542ba1b53ee04 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 3 Sep 2025 15:29:24 -0400 Subject: [PATCH 025/117] stepcompress: Generate steps in a per-stepper background thread Create a thread for each stepper and use it for step generation and step compression. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 9 +-- klippy/chelper/stepcompress.c | 121 +++++++++++++++++++++++++++++++- klippy/chelper/stepcompress.h | 8 +-- klippy/chelper/steppersync.c | 46 +++++++----- klippy/chelper/steppersync.h | 7 +- klippy/extras/motion_queuing.py | 21 +++--- klippy/extras/pwm_tool.py | 4 +- klippy/stepper.py | 3 +- 8 files changed, 175 insertions(+), 44 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 59971c1c4..b9ad9747d 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -36,7 +36,7 @@ defs_stepcompress = """ int step_count, interval, add; }; - struct stepcompress *stepcompress_alloc(uint32_t oid); + struct stepcompress *stepcompress_alloc(uint32_t oid, char name[16]); void stepcompress_fill(struct stepcompress *sc, uint32_t max_error , int32_t queue_step_msgtag, int32_t set_next_step_dir_msgtag); void stepcompress_set_invert_sdir(struct stepcompress *sc @@ -66,10 +66,11 @@ defs_steppersync = """ void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); - int32_t steppersync_generate_steps(struct steppersync *ss - , double gen_steps_time, uint64_t flush_clock); void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); - int steppersync_flush(struct steppersync *ss, uint64_t move_clock); + void steppersync_start_gen_steps(struct steppersync *ss + , double gen_steps_time, uint64_t flush_clock); + int32_t steppersync_finalize_gen_steps(struct steppersync *ss + , uint64_t flush_clock); """ defs_itersolve = """ diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 52dd40773..ab261d129 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -15,6 +15,7 @@ // efficiency - the repetitive integer math is vastly faster in C. #include // sqrt +#include // pthread_mutex_lock #include // offsetof #include // uint32_t #include // fprintf @@ -47,8 +48,16 @@ struct stepcompress { // History tracking int64_t last_position; struct list_head history_list; - // Itersolve reference + // Thread for step generation struct stepper_kinematics *sk; + char name[16]; + pthread_t tid; + pthread_mutex_t lock; // protects variables below + pthread_cond_t cond; + int have_work; + double bg_gen_steps_time; + uint64_t bg_flush_clock; + int32_t bg_result; }; struct step_move { @@ -244,9 +253,12 @@ check_line(struct stepcompress *sc, struct step_move move) * Step compress interface ****************************************************************/ +static int sc_thread_alloc(struct stepcompress *sc, char name[16]); +static void sc_thread_free(struct stepcompress *sc); + // Allocate a new 'stepcompress' object struct stepcompress * __visible -stepcompress_alloc(uint32_t oid) +stepcompress_alloc(uint32_t oid, char name[16]) { struct stepcompress *sc = malloc(sizeof(*sc)); memset(sc, 0, sizeof(*sc)); @@ -254,6 +266,10 @@ stepcompress_alloc(uint32_t oid) list_init(&sc->history_list); sc->oid = oid; sc->sdir = -1; + + int ret = sc_thread_alloc(sc, name); + if (ret) + return NULL; return sc; } @@ -299,6 +315,7 @@ stepcompress_free(struct stepcompress *sc) { if (!sc) return; + sc_thread_free(sc); free(sc->queue); message_queue_free(&sc->msg_queue); stepcompress_history_expire(sc, UINT64_MAX); @@ -666,6 +683,11 @@ stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p return res; } + +/**************************************************************** + * Step generation thread + ****************************************************************/ + // Store a reference to stepper_kinematics void __visible stepcompress_set_stepper_kinematics(struct stepcompress *sc @@ -682,7 +704,7 @@ stepcompress_get_stepper_kinematics(struct stepcompress *sc) } // Generate steps (via itersolve) and flush -int32_t +static int32_t stepcompress_generate_steps(struct stepcompress *sc, double gen_steps_time , uint64_t flush_clock) { @@ -695,3 +717,96 @@ stepcompress_generate_steps(struct stepcompress *sc, double gen_steps_time // Flush steps return stepcompress_flush(sc, flush_clock); } + +// Main background thread for generating steps +static void * +sc_background_thread(void *data) +{ + struct stepcompress *sc = data; + set_thread_name(sc->name); + + pthread_mutex_lock(&sc->lock); + for (;;) { + if (!sc->have_work) { + pthread_cond_wait(&sc->cond, &sc->lock); + continue; + } + if (sc->have_work < 0) + // Exit request + break; + + // Request to generate steps + sc->bg_result = stepcompress_generate_steps(sc, sc->bg_gen_steps_time + , sc->bg_flush_clock); + sc->have_work = 0; + pthread_cond_signal(&sc->cond); + } + pthread_mutex_unlock(&sc->lock); + + return NULL; +} + +// Signal background thread to start step generation +void +stepcompress_start_gen_steps(struct stepcompress *sc, double gen_steps_time + , uint64_t flush_clock) +{ + if (!sc->sk) + return; + pthread_mutex_lock(&sc->lock); + while (sc->have_work) + pthread_cond_wait(&sc->cond, &sc->lock); + sc->bg_gen_steps_time = gen_steps_time; + sc->bg_flush_clock = flush_clock; + sc->have_work = 1; + pthread_mutex_unlock(&sc->lock); + pthread_cond_signal(&sc->cond); +} + +// Wait for background thread to complete last step generation request +int32_t +stepcompress_finalize_gen_steps(struct stepcompress *sc) +{ + pthread_mutex_lock(&sc->lock); + while (sc->have_work) + pthread_cond_wait(&sc->cond, &sc->lock); + int32_t res = sc->bg_result; + pthread_mutex_unlock(&sc->lock); + return res; +} + +// Internal helper to start thread +static int +sc_thread_alloc(struct stepcompress *sc, char name[16]) +{ + strncpy(sc->name, name, sizeof(sc->name)); + sc->name[sizeof(sc->name)-1] = '\0'; + int ret = pthread_mutex_init(&sc->lock, NULL); + if (ret) + goto fail; + ret = pthread_cond_init(&sc->cond, NULL); + if (ret) + goto fail; + ret = pthread_create(&sc->tid, NULL, sc_background_thread, sc); + if (ret) + goto fail; + return 0; +fail: + report_errno("sc init", ret); + return -1; +} + +// Request background thread to exit +static void +sc_thread_free(struct stepcompress *sc) +{ + pthread_mutex_lock(&sc->lock); + while (sc->have_work) + pthread_cond_wait(&sc->cond, &sc->lock); + sc->have_work = -1; + pthread_cond_signal(&sc->cond); + pthread_mutex_unlock(&sc->lock); + int ret = pthread_join(sc->tid, NULL); + if (ret) + report_errno("sc pthread_join", ret); +} diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 7ca0f2e43..5ebf8bf08 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -11,7 +11,7 @@ struct pull_history_steps { int step_count, interval, add; }; -struct stepcompress *stepcompress_alloc(uint32_t oid); +struct stepcompress *stepcompress_alloc(uint32_t oid, char name[16]); void stepcompress_fill(struct stepcompress *sc, uint32_t max_error , int32_t queue_step_msgtag , int32_t set_next_step_dir_msgtag); @@ -43,8 +43,8 @@ void stepcompress_set_stepper_kinematics(struct stepcompress *sc , struct stepper_kinematics *sk); struct stepper_kinematics *stepcompress_get_stepper_kinematics( struct stepcompress *sc); -int32_t stepcompress_generate_steps(struct stepcompress *sc - , double gen_steps_time - , uint64_t flush_clock); +void stepcompress_start_gen_steps(struct stepcompress *sc, double gen_steps_time + , uint64_t flush_clock); +int32_t stepcompress_finalize_gen_steps(struct stepcompress *sc); #endif // stepcompress.h diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 745578c75..0ff5bcab1 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -76,22 +76,6 @@ steppersync_set_time(struct steppersync *ss, double time_offset } } -// Generate steps and flush stepcompress objects -int32_t __visible -steppersync_generate_steps(struct steppersync *ss, double gen_steps_time - , uint64_t flush_clock) -{ - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - int32_t ret = stepcompress_generate_steps(sc, gen_steps_time - , flush_clock); - if (ret) - return ret; - } - return 0; -} - // Expire the stepcompress history before the given clock time void __visible steppersync_history_expire(struct steppersync *ss, uint64_t end_clock) @@ -129,7 +113,7 @@ heap_replace(struct steppersync *ss, uint64_t req_clock) } // Find and transmit any scheduled steps prior to the given 'move_clock' -int __visible +static void steppersync_flush(struct steppersync *ss, uint64_t move_clock) { // Order commands by the reqclock of each pending command @@ -172,6 +156,34 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock) // Transmit commands if (!list_empty(&msgs)) serialqueue_send_batch(ss->sq, ss->cq, &msgs); +} +// Start generating steps in stepcompress objects +void __visible +steppersync_start_gen_steps(struct steppersync *ss, double gen_steps_time + , uint64_t flush_clock) +{ + int i; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + stepcompress_start_gen_steps(sc, gen_steps_time, flush_clock); + } +} + +// Finalize step generation and flush +int32_t __visible +steppersync_finalize_gen_steps(struct steppersync *ss, uint64_t flush_clock) +{ + int i; + int32_t res = 0; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + int32_t ret = stepcompress_finalize_gen_steps(sc); + if (ret) + res = ret; + } + if (res) + return res; + steppersync_flush(ss, flush_clock); return 0; } diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index 1320bbaa0..41cd03bbd 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -10,9 +10,10 @@ struct steppersync *steppersync_alloc( void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); -int32_t steppersync_generate_steps(struct steppersync *ss, double gen_steps_time - , uint64_t flush_clock); void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); -int steppersync_flush(struct steppersync *ss, uint64_t move_clock); +void steppersync_start_gen_steps(struct steppersync *ss, double gen_steps_time + , uint64_t flush_clock); +int32_t steppersync_finalize_gen_steps(struct steppersync *ss + , uint64_t flush_clock); #endif // steppersync.h diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 9d2ef5137..a61ba5cc0 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -29,8 +29,9 @@ class PrinterMotionQueuing: # Low-level C flushing calls ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves - self.steppersync_generate_steps = ffi_lib.steppersync_generate_steps - self.steppersync_flush = ffi_lib.steppersync_flush + self.steppersync_start_gen_steps = ffi_lib.steppersync_start_gen_steps + self.steppersync_finalize_gen_steps = \ + ffi_lib.steppersync_finalize_gen_steps self.steppersync_history_expire = ffi_lib.steppersync_history_expire # Flush notification callbacks self.flush_callbacks = [] @@ -58,9 +59,10 @@ class PrinterMotionQueuing: trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) self.trapqs.append(trapq) return trapq - def allocate_stepcompress(self, mcu, oid): + def allocate_stepcompress(self, mcu, oid, name): + name = name.encode("utf-8")[:15] ffi_main, ffi_lib = chelper.get_ffi() - sc = ffi_main.gc(ffi_lib.stepcompress_alloc(oid), + sc = ffi_main.gc(ffi_lib.stepcompress_alloc(oid, name), ffi_lib.stepcompress_free) self.stepcompress.append((mcu, sc)) return sc @@ -90,13 +92,10 @@ class PrinterMotionQueuing: # Generate stepper movement and transmit for mcu, ss in self.steppersyncs: clock = max(0, mcu.print_time_to_clock(must_flush_time)) - # Generate steps - ret = self.steppersync_generate_steps(ss, max_step_gen_time, clock) - if ret: - raise mcu.error("Internal error in MCU '%s' stepcompress" - % (mcu.get_name(),)) - # Flush steps from steppersync - ret = self.steppersync_flush(ss, clock) + self.steppersync_start_gen_steps(ss, max_step_gen_time, clock) + for mcu, ss in self.steppersyncs: + clock = max(0, mcu.print_time_to_clock(must_flush_time)) + ret = self.steppersync_finalize_gen_steps(ss, clock) if ret: raise mcu.error("Internal error in MCU '%s' stepcompress" % (mcu.get_name(),)) diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index d9e72c5e1..cec7e3791 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -16,8 +16,10 @@ class MCU_queued_pwm: self._max_duration = 2. self._oid = oid = mcu.create_oid() printer = mcu.get_printer() + sname = config.get_name().split()[-1] self._motion_queuing = printer.load_object(config, 'motion_queuing') - self._stepqueue = self._motion_queuing.allocate_stepcompress(mcu, oid) + self._stepqueue = self._motion_queuing.allocate_stepcompress( + mcu, oid, sname) ffi_main, ffi_lib = chelper.get_ffi() self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg mcu.register_config_callback(self._build_config) diff --git a/klippy/stepper.py b/klippy/stepper.py index d5b3cecde..046b5280d 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -44,7 +44,8 @@ class MCU_stepper: self._reset_cmd_tag = self._get_position_cmd = None self._active_callbacks = [] motion_queuing = printer.load_object(config, 'motion_queuing') - self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid) + sname = self._name.split()[-1] + self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid, sname) ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir) self._stepper_kinematics = None From bb88985b8d48fa7505fee116eec1c4902361f95d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 5 Sep 2025 13:34:48 -0400 Subject: [PATCH 026/117] reactor: Unify handling of fd events The SelectReactor has a different event dispatch system from the PollReactor and EPollReactor. However, in practice the PollReactor code is always used, so there is no reason to maintain a different implementation for SelectReactor. Rework the code so that SelectReactor file dispatch handling is done the same way as PollReactor (and EPollReactor). This simplfiies the code. Introduce a new _check_fds() method that is shared between Reactor implementations. Also, fix some cut-and-paste bugs in SelectReactor and EPollReactor. Signed-off-by: Kevin O'Connor --- klippy/reactor.py | 129 ++++++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 74 deletions(-) diff --git a/klippy/reactor.py b/klippy/reactor.py index f9bedcf3f..db6a089e3 100644 --- a/klippy/reactor.py +++ b/klippy/reactor.py @@ -55,8 +55,6 @@ class ReactorFileHandler: self.fd = fd self.read_callback = read_callback self.write_callback = write_callback - def fileno(self): - return self.fd class ReactorGreenlet(greenlet.greenlet): def __init__(self, run): @@ -109,8 +107,13 @@ class SelectReactor: self._pipe_fds = None self._async_queue = queue.Queue() # File descriptors + self._dummy_fd_hdl = ReactorFileHandler(-1, (lambda e: None), + (lambda e: None)) + self._fds = {} self._read_fds = [] self._write_fds = [] + self._READ = 1 + self._WRITE = 2 # Greenlets self._g_dispatch = None self._greenlets = [] @@ -245,48 +248,54 @@ class SelectReactor: # File descriptors def register_fd(self, fd, read_callback, write_callback=None): file_handler = ReactorFileHandler(fd, read_callback, write_callback) + self._fds[fd] = file_handler self.set_fd_wake(file_handler, True, False) return file_handler def unregister_fd(self, file_handler): - if file_handler in self._read_fds: - self._read_fds.pop(self._read_fds.index(file_handler)) - if file_handler in self._write_fds: - self._write_fds.pop(self._write_fds.index(file_handler)) + self.set_fd_wake(file_handler, False, False) + del self._fds[file_handler.fd] def set_fd_wake(self, file_handler, is_readable=True, is_writeable=False): - if file_handler in self._read_fds: + fd = file_handler.fd + if fd in self._read_fds: if not is_readable: - self._read_fds.pop(self._read_fds.index(file_handler)) + self._read_fds.remove(fd) elif is_readable: - self._read_fds.append(file_handler) - if file_handler in self._write_fds: + self._read_fds.append(fd) + if fd in self._write_fds: if not is_writeable: - self._write_fds.pop(self._write_fds.index(file_handler)) + self._write_fds.remove(fd) elif is_writeable: - self._write_fds.append(file_handler) + self._write_fds.append(fd) + def _check_fds(self, eventtime, hdls): + g_dispatch = self._g_dispatch + for fd, event in hdls: + hdl = self._fds.get(fd, self._dummy_fd_hdl) + if event & self._READ: + hdl.read_callback(eventtime) + if g_dispatch is not self._g_dispatch: + self._end_greenlet(g_dispatch) + return self.monotonic() + if event & self._WRITE: + hdl.write_callback(eventtime) + if g_dispatch is not self._g_dispatch: + self._end_greenlet(g_dispatch) + return self.monotonic() + return eventtime # Main loop def _dispatch_loop(self): - self._g_dispatch = g_dispatch = greenlet.getcurrent() + self._g_dispatch = greenlet.getcurrent() busy = True eventtime = self.monotonic() while self._process: timeout = self._check_timers(eventtime, busy) busy = False - res = select.select(self._read_fds, self.write_fds, [], timeout) + res = select.select(self._read_fds, self._write_fds, [], timeout) eventtime = self.monotonic() - for fd in res[0]: + if res[0] or res[1]: busy = True - fd.read_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break - for fd in res[1]: - busy = True - fd.write_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break + hdls = ([(fd, self._READ) for fd in res[0]] + + [(fd, self._WRITE) for fd in res[1]]) + eventtime = self._check_fds(eventtime, hdls) self._g_dispatch = None def run(self): if self._pipe_fds is None: @@ -315,30 +324,27 @@ class PollReactor(SelectReactor): def __init__(self, gc_checking=False): SelectReactor.__init__(self, gc_checking) self._poll = select.poll() - self._fds = {} + self._READ = select.POLLIN | select.POLLHUP + self._WRITE = select.POLLOUT # File descriptors def register_fd(self, fd, read_callback, write_callback=None): file_handler = ReactorFileHandler(fd, read_callback, write_callback) - fds = self._fds.copy() - fds[fd] = file_handler - self._fds = fds - self._poll.register(file_handler, select.POLLIN | select.POLLHUP) + self._fds[fd] = file_handler + self._poll.register(file_handler.fd, select.POLLIN | select.POLLHUP) return file_handler def unregister_fd(self, file_handler): - self._poll.unregister(file_handler) - fds = self._fds.copy() - del fds[file_handler.fd] - self._fds = fds + self._poll.unregister(file_handler.fd) + del self._fds[file_handler.fd] def set_fd_wake(self, file_handler, is_readable=True, is_writeable=False): flags = select.POLLHUP if is_readable: flags |= select.POLLIN if is_writeable: flags |= select.POLLOUT - self._poll.modify(file_handler, flags) + self._poll.modify(file_handler.fd, flags) # Main loop def _dispatch_loop(self): - self._g_dispatch = g_dispatch = greenlet.getcurrent() + self._g_dispatch = greenlet.getcurrent() busy = True eventtime = self.monotonic() while self._process: @@ -346,50 +352,36 @@ class PollReactor(SelectReactor): busy = False res = self._poll.poll(int(math.ceil(timeout * 1000.))) eventtime = self.monotonic() - for fd, event in res: + if res: busy = True - if event & (select.POLLIN | select.POLLHUP): - self._fds[fd].read_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break - if event & select.POLLOUT: - self._fds[fd].write_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break + eventtime = self._check_fds(eventtime, res) self._g_dispatch = None class EPollReactor(SelectReactor): def __init__(self, gc_checking=False): SelectReactor.__init__(self, gc_checking) self._epoll = select.epoll() - self._fds = {} + self._READ = select.EPOLLIN | select.EPOLLHUP + self._WRITE = select.EPOLLOUT # File descriptors def register_fd(self, fd, read_callback, write_callback=None): file_handler = ReactorFileHandler(fd, read_callback, write_callback) - fds = self._fds.copy() - fds[fd] = read_callback - self._fds = fds + self._fds[fd] = file_handler self._epoll.register(fd, select.EPOLLIN | select.EPOLLHUP) return file_handler def unregister_fd(self, file_handler): self._epoll.unregister(file_handler.fd) - fds = self._fds.copy() - del fds[file_handler.fd] - self._fds = fds + del self._fds[file_handler.fd] def set_fd_wake(self, file_handler, is_readable=True, is_writeable=False): - flags = select.POLLHUP + flags = select.EPOLLHUP if is_readable: flags |= select.EPOLLIN if is_writeable: flags |= select.EPOLLOUT - self._epoll.modify(file_handler, flags) + self._epoll.modify(file_handler.fd, flags) # Main loop def _dispatch_loop(self): - self._g_dispatch = g_dispatch = greenlet.getcurrent() + self._g_dispatch = greenlet.getcurrent() busy = True eventtime = self.monotonic() while self._process: @@ -397,20 +389,9 @@ class EPollReactor(SelectReactor): busy = False res = self._epoll.poll(timeout) eventtime = self.monotonic() - for fd, event in res: + if res: busy = True - if event & (select.EPOLLIN | select.EPOLLHUP): - self._fds[fd].read_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break - if event & select.EPOLLOUT: - self._fds[fd].write_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break + eventtime = self._check_fds(eventtime, res) self._g_dispatch = None # Use the poll based reactor if it is available From cde57bdcfd7b5eda44da4ad7c263cbb40789e139 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 6 Sep 2025 22:22:12 -0400 Subject: [PATCH 027/117] toolhead: Set check_stall_time from _flush_lookahead() Add a new is_runout parameter to _flush_lookahead() and use that in places that could set check_stall_time. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index cf648695a..9cb82388e 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -294,13 +294,16 @@ class ToolHead: # Generate steps for moves self.motion_queuing.note_mcu_movequeue_activity(next_move_time) self._advance_move_time(next_move_time) - def _flush_lookahead(self): + def _flush_lookahead(self, is_runout=False): # Transit from "NeedPrime"/"Priming"/main state to "NeedPrime" + prev_print_time = self.print_time self._process_lookahead() self.special_queuing_state = "NeedPrime" self.need_check_pause = -1. self.lookahead.set_flush_time(BUFFER_TIME_HIGH) self.check_stall_time = 0. + if is_runout and prev_print_time != self.print_time: + self.check_stall_time = self.print_time def flush_step_generation(self): self._flush_lookahead() self.motion_queuing.flush_all_steps() @@ -348,8 +351,7 @@ class ToolHead: self.priming_timer = None try: if self.special_queuing_state == "Priming": - self._flush_lookahead() - self.check_stall_time = self.print_time + self._flush_lookahead(is_runout=True) except: logging.exception("Exception in priming_handler") self.printer.invoke_shutdown("Exception in priming_handler") @@ -359,15 +361,12 @@ class ToolHead: return None # In "main" state - flush lookahead if buffer runs low est_print_time = self.mcu.estimated_print_time(eventtime) - print_time = self.print_time - buffer_time = print_time - est_print_time + buffer_time = self.print_time - est_print_time if buffer_time > BUFFER_TIME_LOW: # Running normally - reschedule check return eventtime + buffer_time - BUFFER_TIME_LOW # Under ran low buffer mark - flush lookahead queue - self._flush_lookahead() - if print_time != self.print_time: - self.check_stall_time = self.print_time + self._flush_lookahead(is_runout=True) return None # Movement commands def get_position(self): From 22db9bb84e26eeef5b1281707fd536829c5933af Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 7 Sep 2025 12:18:28 -0400 Subject: [PATCH 028/117] motion_queuing: Require explicit notification on a scan window change Don't try to infer when the step generation scan window may change. Instead, require the input_shaper and pressure_advance code call motion_queuing.check_step_generation_scan_windows() any time a scanning window may change. Signed-off-by: Kevin O'Connor --- klippy/extras/input_shaper.py | 6 +++++- klippy/extras/motion_queuing.py | 7 +------ klippy/kinematics/extruder.py | 2 ++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py index cb9027d98..c79cdcf2c 100644 --- a/klippy/extras/input_shaper.py +++ b/klippy/extras/input_shaper.py @@ -138,6 +138,7 @@ class InputShaper: if self.toolhead is None: # Klipper initialization is not yet completed return + self.toolhead.flush_step_generation() ffi_main, ffi_lib = chelper.get_ffi() kin = self.toolhead.get_kinematics() for s in kin.get_steppers(): @@ -146,8 +147,9 @@ class InputShaper: is_sk = self._get_input_shaper_stepper_kinematics(s) if is_sk is None: continue - self.toolhead.flush_step_generation() ffi_lib.input_shaper_update_sk(is_sk) + motion_queuing = self.printer.lookup_object("motion_queuing") + motion_queuing.check_step_generation_scan_windows() def _update_input_shaping(self, error=None): self.toolhead.flush_step_generation() ffi_main, ffi_lib = chelper.get_ffi() @@ -164,6 +166,8 @@ class InputShaper: continue if not shaper.set_shaper_kinematics(is_sk): failed_shapers.append(shaper) + motion_queuing = self.printer.lookup_object("motion_queuing") + motion_queuing.check_step_generation_scan_windows() if failed_shapers: error = error or self.printer.command_error raise error("Failed to configure shaper(s) %s with given parameters" diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index a61ba5cc0..99035f06f 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -50,7 +50,6 @@ class PrinterMotionQueuing: if self.mcu.is_fileoutput(): self.can_pause = False # Kinematic step generation scan window time tracking - self.need_calc_kin_flush_delay = True self.kin_flush_delay = SDS_CHECK_TIME # Register handlers printer.register_event_handler("klippy:shutdown", self._handle_shutdown) @@ -129,8 +128,7 @@ class PrinterMotionQueuing: # Kinematic step generation scan window time tracking def get_kin_flush_delay(self): return self.kin_flush_delay - def _calc_kin_flush_delay(self): - self.need_calc_kin_flush_delay = False + def check_step_generation_scan_windows(self): ffi_main, ffi_lib = chelper.get_ffi() kin_flush_delay = SDS_CHECK_TIME for mcu, sc in self.stepcompress: @@ -175,11 +173,8 @@ class PrinterMotionQueuing: if flush_time >= want_flush_time: break def flush_all_steps(self): - self.need_calc_kin_flush_delay = True self.advance_flush_time(self.need_step_gen_time) def calc_step_gen_restart(self, est_print_time): - if self.need_calc_kin_flush_delay: - self._calc_kin_flush_delay() kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) return kin_time + self.kin_flush_delay def _flush_handler(self, eventtime): diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 4e6f14e41..9b1ec2d20 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -75,6 +75,8 @@ class ExtruderStepper: # Need full kinematic flush to change the smooth time toolhead.flush_step_generation() espa(self.sk_extruder, 0., pressure_advance, new_smooth_time) + motion_queuing = self.printer.lookup_object('motion_queuing') + motion_queuing.check_step_generation_scan_windows() else: toolhead.register_lookahead_callback( lambda print_time: espa(self.sk_extruder, print_time, From 3bed65f10ffff5dd61f666acf974e05c59e498e5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 4 Sep 2025 12:54:03 -0400 Subject: [PATCH 029/117] motion_queuing: Move remaining steppersync logic from mcu module Move the last parts of the steppersync logic into the motion_queuing module. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 15 ++++++++++----- klippy/mcu.py | 17 ++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 99035f06f..ab3836fee 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -65,7 +65,8 @@ class PrinterMotionQueuing: ffi_lib.stepcompress_free) self.stepcompress.append((mcu, sc)) return sc - def allocate_steppersync(self, mcu, serialqueue, move_count): + def setup_mcu_movequeue(self, mcu, serialqueue, move_count): + # Setup steppersync object for the mcu's main movequeue stepqueues = [] for sc_mcu, sc in self.stepcompress: if sc_mcu is mcu: @@ -76,7 +77,8 @@ class PrinterMotionQueuing: move_count), ffi_lib.steppersync_free) self.steppersyncs.append((mcu, ss)) - return ss + mcu_freq = float(mcu.seconds_to_clock(1.)) + ffi_lib.steppersync_set_time(ss, 0., mcu_freq) def register_flush_callback(self, callback): self.flush_callbacks.append(callback) def unregister_flush_callback(self, callback): @@ -118,9 +120,12 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append def stats(self, eventtime): - # Hack to globally invoke mcu check_active() - for m in self.all_mcus: - m.check_active(self.last_step_gen_time, eventtime) + # Globally calibrate mcu clocks (and step generation clocks) + sync_time = self.last_step_gen_time + ffi_main, ffi_lib = chelper.get_ffi() + for mcu, ss in self.steppersyncs: + offset, freq = mcu.calibrate_clock(sync_time, eventtime) + ffi_lib.steppersync_set_time(ss, offset, freq) # Calculate history expiration est_print_time = self.mcu.estimated_print_time(eventtime) self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE diff --git a/klippy/mcu.py b/klippy/mcu.py index 146b5baca..74e2164e7 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -604,11 +604,9 @@ class MCU: self._init_cmds = [] self._mcu_freq = 0. # Move command queuing - ffi_main, self._ffi_lib = chelper.get_ffi() self._max_stepper_error = config.getfloat('max_stepper_error', 0.000025, minval=0.) self._reserved_move_slots = 0 - self._steppersync = None # Stats self._get_status_info = {} self._stats_sumsq_base = 0. @@ -773,10 +771,8 @@ class MCU: raise error("Too few moves available on MCU '%s'" % (self._name,)) ss_move_count = move_count - self._reserved_move_slots motion_queuing = self._printer.lookup_object('motion_queuing') - self._steppersync = motion_queuing.allocate_steppersync( + motion_queuing.setup_mcu_movequeue( self, self._serial.get_serialqueue(), ss_move_count) - self._ffi_lib.steppersync_set_time(self._steppersync, - 0., self._mcu_freq) # Log config information move_msg = "Configured MCU '%s' (%d moves)" % (self._name, move_count) logging.info(move_msg) @@ -919,7 +915,6 @@ class MCU: # Restarts def _disconnect(self): self._serial.disconnect() - self._steppersync = None def _shutdown(self, force=False): if (self._emergency_stop_cmd is None or (self._is_shutdown and not force)): @@ -974,11 +969,7 @@ class MCU: # Move queue tracking def request_move_queue_slot(self): self._reserved_move_slots += 1 - def check_active(self, print_time, eventtime): - if self._steppersync is None: - return - offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) - self._ffi_lib.steppersync_set_time(self._steppersync, offset, freq) + def _check_timeout(self, eventtime): if (self._clocksync.is_active() or self.is_fileoutput() or self._is_timeout): return @@ -987,6 +978,10 @@ class MCU: self._name, eventtime) self._printer.invoke_shutdown("Lost communication with MCU '%s'" % ( self._name,)) + def calibrate_clock(self, print_time, eventtime): + offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) + self._check_timeout(eventtime) + return offset, freq # Misc external commands def is_fileoutput(self): return self._printer.get_start_args().get('debugoutput') is not None From 32bd03703b3cff2ade12c76fd8c2b11d0b185488 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 2 Sep 2025 13:03:00 -0400 Subject: [PATCH 030/117] motion_queuing: Don't use lazy_target in drip_update_time() Using separate flush_time and step_gen_time is a minor optimization. Using it in drip_update_time() complicates the code and may reduce the time needed to schedule post homing/probing movements. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index ab3836fee..fe93b6207 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -217,24 +217,23 @@ class PrinterMotionQueuing: self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) self.do_kick_flush_timer = False # Flush in segments until drip_completion signal - flush_delay = DRIP_TIME + STEPCOMPRESS_FLUSH_TIME + self.kin_flush_delay flush_time = start_time while flush_time < end_time: if drip_completion.test(): break curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - wait_time = flush_time - est_print_time - flush_delay + wait_time = flush_time - est_print_time - DRIP_TIME if wait_time > 0. and self.can_pause: # Pause before sending more steps drip_completion.wait(curtime + wait_time) continue flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) self.note_mcu_movequeue_activity(flush_time) - self.advance_flush_time(flush_time, lazy_target=True) + self.advance_flush_time(flush_time) # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) - self.advance_flush_time(self.need_step_gen_time) + self.advance_flush_time(flush_time + self.kin_flush_delay) def load_config(config): return PrinterMotionQueuing(config) From b60804bb662a56bcbb61a0ae5271756d70b24e91 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 3 Sep 2025 21:59:26 -0400 Subject: [PATCH 031/117] trapq: Set the head sentinel to a negative print_time If a stepper kinematics has a "scan window" defined during its first flush then the iterative solver may walk past the head sentinel. Set a small negative print_time for the head sentinel to avoid this corner case. Signed-off-by: Kevin O'Connor --- klippy/chelper/trapq.c | 1 + 1 file changed, 1 insertion(+) diff --git a/klippy/chelper/trapq.c b/klippy/chelper/trapq.c index b9930e997..a21969414 100644 --- a/klippy/chelper/trapq.c +++ b/klippy/chelper/trapq.c @@ -49,6 +49,7 @@ trapq_alloc(void) list_init(&tq->moves); list_init(&tq->history); struct move *head_sentinel = move_alloc(), *tail_sentinel = move_alloc(); + head_sentinel->print_time = -1.0; tail_sentinel->print_time = tail_sentinel->move_t = NEVER_TIME; list_add_head(&head_sentinel->node, &tq->moves); list_add_tail(&tail_sentinel->node, &tq->moves); From 7ea5f5d25ecc1c6acd30873c035f150054bbdfe0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 2 Sep 2025 13:45:29 -0400 Subject: [PATCH 032/117] motion_queuing: Generate steps from timer instead of from lookahead Don't tie the step generation logic to the toolhead lookahead logic. Instead, use regular timers to generate steps with a goal of staying 500-750ms ahead of the micro-controllers. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 111 ++++++++++++++++++++++---------- klippy/toolhead.py | 26 +++----- 2 files changed, 85 insertions(+), 52 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index fe93b6207..67609eee7 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -7,8 +7,11 @@ import logging import chelper BGFLUSH_LOW_TIME = 0.200 -BGFLUSH_BATCH_TIME = 0.200 +BGFLUSH_HIGH_TIME = 0.400 +BGFLUSH_SG_LOW_TIME = 0.450 +BGFLUSH_SG_HIGH_TIME = 0.700 BGFLUSH_EXTRA_TIME = 0.250 + MOVE_HISTORY_EXPIRE = 30. MIN_KIN_TIME = 0.100 MOVE_BATCH_TIME = 0.500 @@ -37,18 +40,20 @@ class PrinterMotionQueuing: self.flush_callbacks = [] # History expiration self.clear_history_time = 0. - # Flush tracking - self.flush_timer = self.reactor.register_timer(self._flush_handler) - self.do_kick_flush_timer = True - self.last_flush_time = self.last_step_gen_time = 0. - self.need_flush_time = self.need_step_gen_time = 0. - self.check_flush_lookahead_cb = (lambda e: None) # MCU tracking self.all_mcus = [m for n, m in printer.lookup_objects(module='mcu')] self.mcu = self.all_mcus[0] self.can_pause = True if self.mcu.is_fileoutput(): self.can_pause = False + # Flush tracking + flush_handler = self._flush_handler + if not self.can_pause: + flush_handler = self._flush_handler_debug + self.flush_timer = self.reactor.register_timer(flush_handler) + self.do_kick_flush_timer = True + self.last_flush_time = self.last_step_gen_time = 0. + self.need_flush_time = self.need_step_gen_time = 0. # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME # Register handlers @@ -79,8 +84,11 @@ class PrinterMotionQueuing: self.steppersyncs.append((mcu, ss)) mcu_freq = float(mcu.seconds_to_clock(1.)) ffi_lib.steppersync_set_time(ss, 0., mcu_freq) - def register_flush_callback(self, callback): - self.flush_callbacks.append(callback) + def register_flush_callback(self, callback, can_add_trapq=False): + if can_add_trapq: + self.flush_callbacks = [callback] + self.flush_callbacks + else: + self.flush_callbacks = self.flush_callbacks + [callback] def unregister_flush_callback(self, callback): if callback in self.flush_callbacks: fcbs = list(self.flush_callbacks) @@ -150,15 +158,10 @@ class PrinterMotionQueuing: # Flush tracking def _handle_shutdown(self): self.can_pause = False - def setup_lookahead_flush_callback(self, check_flush_lookahead_cb): - self.check_flush_lookahead_cb = check_flush_lookahead_cb - def advance_flush_time(self, target_time, lazy_target=False): - want_flush_time = want_step_gen_time = target_time - if lazy_target: - # Account for step gen scan windows and optimize step compression - want_step_gen_time -= self.kin_flush_delay - want_flush_time = want_step_gen_time - STEPCOMPRESS_FLUSH_TIME - want_flush_time = max(want_flush_time, self.last_flush_time) + def _advance_flush_time(self, want_flush_time, want_step_gen_time=0.): + want_flush_time = max(want_flush_time, self.last_flush_time, + want_step_gen_time - STEPCOMPRESS_FLUSH_TIME) + want_step_gen_time = max(want_step_gen_time, want_flush_time) flush_time = self.last_flush_time if want_flush_time > flush_time + 10. * MOVE_BATCH_TIME: # Use closer startup time when coming out of idle state @@ -178,32 +181,70 @@ class PrinterMotionQueuing: if flush_time >= want_flush_time: break def flush_all_steps(self): - self.advance_flush_time(self.need_step_gen_time) + self._advance_flush_time(self.need_step_gen_time) def calc_step_gen_restart(self, est_print_time): kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) return kin_time + self.kin_flush_delay def _flush_handler(self, eventtime): try: - # Check if flushing is done via lookahead queue - ret = self.check_flush_lookahead_cb(eventtime) - if ret is not None: - return ret - # Flush motion queues est_print_time = self.mcu.estimated_print_time(eventtime) - while 1: - end_flush = self.need_flush_time + BGFLUSH_EXTRA_TIME - if self.last_flush_time >= end_flush: - self.do_kick_flush_timer = True + aggr_sg_time = self.need_step_gen_time - 2.*self.kin_flush_delay + if self.last_step_gen_time < aggr_sg_time: + # Actively stepping - want more aggressive flushing + want_sg_time = est_print_time + BGFLUSH_SG_HIGH_TIME + want_sg_time = min(want_sg_time, aggr_sg_time) + # Try improving run-to-run reproducibility by batching from last + batch_time = BGFLUSH_SG_HIGH_TIME - BGFLUSH_SG_LOW_TIME + next_batch_time = self.last_step_gen_time + batch_time + if next_batch_time > est_print_time + BGFLUSH_SG_LOW_TIME: + want_sg_time = min(want_sg_time, next_batch_time) + # Flush motion queues (if needed) + if want_sg_time > self.last_step_gen_time: + self._advance_flush_time(0., want_sg_time) + else: + # Not stepping (or only step remnants) - use relaxed flushing + want_flush_time = est_print_time + BGFLUSH_HIGH_TIME + max_flush_time = self.need_flush_time + BGFLUSH_EXTRA_TIME + want_flush_time = min(want_flush_time, max_flush_time) + # Flush motion queues (if needed) + if want_flush_time > self.last_flush_time: + self._advance_flush_time(want_flush_time) + # Reschedule timer + aggr_sg_time = self.need_step_gen_time - 2.*self.kin_flush_delay + if self.last_step_gen_time < aggr_sg_time: + waketime = self.last_step_gen_time - BGFLUSH_SG_LOW_TIME + else: + self.do_kick_flush_timer = True + max_flush_time = self.need_flush_time + BGFLUSH_EXTRA_TIME + if self.last_flush_time >= max_flush_time: return self.reactor.NEVER - buffer_time = self.last_flush_time - est_print_time - if buffer_time > BGFLUSH_LOW_TIME: - return eventtime + buffer_time - BGFLUSH_LOW_TIME - ftime = est_print_time + BGFLUSH_LOW_TIME + BGFLUSH_BATCH_TIME - self.advance_flush_time(min(end_flush, ftime)) + waketime = self.last_flush_time - BGFLUSH_LOW_TIME + return eventtime + waketime - est_print_time except: logging.exception("Exception in flush_handler") self.printer.invoke_shutdown("Exception in flush_handler") return self.reactor.NEVER + def _flush_handler_debug(self, eventtime): + # Use custom flushing code when in batch output mode + try: + faux_time = self.need_flush_time - 1.5 + batch_time = BGFLUSH_SG_HIGH_TIME - BGFLUSH_SG_LOW_TIME + flush_count = 0 + while self.last_step_gen_time < faux_time: + target = self.last_step_gen_time + batch_time + if flush_count > 100.: + target = faux_time + self._advance_flush_time(0., target) + flush_count += 1 + if flush_count: + return self.reactor.NOW + self._advance_flush_time(self.need_flush_time + BGFLUSH_EXTRA_TIME) + self.do_kick_flush_timer = True + return self.reactor.NEVER + except: + logging.exception("Exception in flush_handler_debug") + self.printer.invoke_shutdown("Exception in flush_handler_debug") + return self.reactor.NEVER def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True): if is_step_gen: mq_time += self.kin_flush_delay @@ -230,10 +271,10 @@ class PrinterMotionQueuing: continue flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) self.note_mcu_movequeue_activity(flush_time) - self.advance_flush_time(flush_time) + self._advance_flush_time(flush_time) # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) - self.advance_flush_time(flush_time + self.kin_flush_delay) + self._advance_flush_time(flush_time + self.kin_flush_delay) def load_config(config): return PrinterMotionQueuing(config) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 9cb82388e..c4264051d 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -190,7 +190,6 @@ class LookAheadQueue: # Check if enough moves have been queued to reach the target flush time. return self.junction_flush <= 0. -BUFFER_TIME_LOW = 1.0 BUFFER_TIME_HIGH = 2.0 BUFFER_TIME_START = 0.250 @@ -226,8 +225,8 @@ class ToolHead: self.priming_timer = None # Setup for generating moves self.motion_queuing = self.printer.load_object(config, 'motion_queuing') - self.motion_queuing.setup_lookahead_flush_callback( - self._check_flush_lookahead) + self.motion_queuing.register_flush_callback(self._handle_step_flush, + can_add_trapq=True) self.trapq = self.motion_queuing.allocate_trapq() self.trapq_append = self.motion_queuing.lookup_trapq_append() # Create kinematics class @@ -253,8 +252,6 @@ class ToolHead: # Print time tracking def _advance_move_time(self, next_print_time): self.print_time = max(self.print_time, next_print_time) - self.motion_queuing.advance_flush_time(self.print_time, - lazy_target=True) def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) @@ -292,8 +289,8 @@ class ToolHead: for cb in move.timing_callbacks: cb(next_move_time) # Generate steps for moves - self.motion_queuing.note_mcu_movequeue_activity(next_move_time) self._advance_move_time(next_move_time) + self.motion_queuing.note_mcu_movequeue_activity(next_move_time) def _flush_lookahead(self, is_runout=False): # Transit from "NeedPrime"/"Priming"/main state to "NeedPrime" prev_print_time = self.print_time @@ -330,7 +327,7 @@ class ToolHead: if self.priming_timer is None: self.priming_timer = self.reactor.register_timer( self._priming_handler) - wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_LOW) + wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_HIGH) self.reactor.update_timer(self.priming_timer, wtime) # Check if there are lots of queued moves and pause if so while 1: @@ -356,18 +353,13 @@ class ToolHead: logging.exception("Exception in priming_handler") self.printer.invoke_shutdown("Exception in priming_handler") return self.reactor.NEVER - def _check_flush_lookahead(self, eventtime): + def _handle_step_flush(self, flush_time, step_gen_time): if self.special_queuing_state: - return None + return # In "main" state - flush lookahead if buffer runs low - est_print_time = self.mcu.estimated_print_time(eventtime) - buffer_time = self.print_time - est_print_time - if buffer_time > BUFFER_TIME_LOW: - # Running normally - reschedule check - return eventtime + buffer_time - BUFFER_TIME_LOW - # Under ran low buffer mark - flush lookahead queue - self._flush_lookahead(is_runout=True) - return None + kin_flush_delay = self.motion_queuing.get_kin_flush_delay() + if step_gen_time >= self.print_time - kin_flush_delay: + self._flush_lookahead(is_runout=True) # Movement commands def get_position(self): return list(self.commanded_pos) From 42d149b40fc3a4443133a2369c2a2b0a184660d0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 6 Sep 2025 23:37:48 -0400 Subject: [PATCH 033/117] motion_queuing: Avoid flushing far into the future If a flush_all_steps() request is for a time far in the future, then wait for that time to become close prior to flushing steps. This avoids committing to a step schedule that is far in the future. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 67609eee7..d85f173c4 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -180,8 +180,20 @@ class PrinterMotionQueuing: self.last_step_gen_time = step_gen_time if flush_time >= want_flush_time: break + def _await_flush_time(self, want_flush_time): + while 1: + if self.last_flush_time >= want_flush_time or not self.can_pause: + return + systime = self.reactor.monotonic() + est_print_time = self.mcu.estimated_print_time(systime) + wait = want_flush_time - BGFLUSH_HIGH_TIME - est_print_time + if wait <= 0.: + return + self.reactor.pause(systime + min(1., wait)) def flush_all_steps(self): - self._advance_flush_time(self.need_step_gen_time) + flush_time = self.need_step_gen_time + self._await_flush_time(flush_time) + self._advance_flush_time(flush_time) def calc_step_gen_restart(self, est_print_time): kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) return kin_time + self.kin_flush_delay @@ -254,9 +266,11 @@ class PrinterMotionQueuing: self.do_kick_flush_timer = False self.reactor.update_timer(self.flush_timer, self.reactor.NOW) def drip_update_time(self, start_time, end_time, drip_completion): + self._await_flush_time(start_time) # Disable background flushing from timer self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) self.do_kick_flush_timer = False + self._advance_flush_time(start_time) # Flush in segments until drip_completion signal flush_time = start_time while flush_time < end_time: From 950aa103e46204395dbe035f12356cb5d9d21b74 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 6 Sep 2025 23:52:12 -0400 Subject: [PATCH 034/117] motion_queuing: It is no longer necessary to loop in _advance_flush_time() Now that the host code does not flush far into the future, it is no longer necessary to flush in waves. Integrate _advance_flush_time() into _flush_motion_queues(). Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 41 ++++++++++----------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index d85f173c4..a22598093 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -14,7 +14,6 @@ BGFLUSH_EXTRA_TIME = 0.250 MOVE_HISTORY_EXPIRE = 30. MIN_KIN_TIME = 0.100 -MOVE_BATCH_TIME = 0.500 STEPCOMPRESS_FLUSH_TIME = 0.050 SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c @@ -94,22 +93,28 @@ class PrinterMotionQueuing: fcbs = list(self.flush_callbacks) fcbs.remove(callback) self.flush_callbacks = fcbs - def _flush_motion_queues(self, must_flush_time, max_step_gen_time): + def _advance_flush_time(self, want_flush_time, want_step_gen_time=0.): + flush_time = max(want_flush_time, self.last_flush_time, + want_step_gen_time - STEPCOMPRESS_FLUSH_TIME) + step_gen_time = max(want_step_gen_time, self.last_step_gen_time, + flush_time) # Invoke flush callbacks (if any) for cb in self.flush_callbacks: - cb(must_flush_time, max_step_gen_time) + cb(flush_time, step_gen_time) # Generate stepper movement and transmit for mcu, ss in self.steppersyncs: - clock = max(0, mcu.print_time_to_clock(must_flush_time)) - self.steppersync_start_gen_steps(ss, max_step_gen_time, clock) + clock = max(0, mcu.print_time_to_clock(flush_time)) + self.steppersync_start_gen_steps(ss, step_gen_time, clock) for mcu, ss in self.steppersyncs: - clock = max(0, mcu.print_time_to_clock(must_flush_time)) + clock = max(0, mcu.print_time_to_clock(flush_time)) ret = self.steppersync_finalize_gen_steps(ss, clock) if ret: raise mcu.error("Internal error in MCU '%s' stepcompress" % (mcu.get_name(),)) + self.last_flush_time = flush_time + self.last_step_gen_time = step_gen_time # Determine maximum history to keep - trapq_free_time = max_step_gen_time - self.kin_flush_delay + trapq_free_time = step_gen_time - self.kin_flush_delay clear_history_time = self.clear_history_time if not self.can_pause: clear_history_time = trapq_free_time - MOVE_HISTORY_EXPIRE @@ -158,28 +163,6 @@ class PrinterMotionQueuing: # Flush tracking def _handle_shutdown(self): self.can_pause = False - def _advance_flush_time(self, want_flush_time, want_step_gen_time=0.): - want_flush_time = max(want_flush_time, self.last_flush_time, - want_step_gen_time - STEPCOMPRESS_FLUSH_TIME) - want_step_gen_time = max(want_step_gen_time, want_flush_time) - flush_time = self.last_flush_time - if want_flush_time > flush_time + 10. * MOVE_BATCH_TIME: - # Use closer startup time when coming out of idle state - curtime = self.reactor.monotonic() - est_print_time = self.mcu.estimated_print_time(curtime) - flush_time = max(flush_time, est_print_time) - while 1: - flush_time = min(flush_time + MOVE_BATCH_TIME, want_flush_time) - # Generate steps via itersolve - want_sg_wave = min(flush_time + STEPCOMPRESS_FLUSH_TIME, - want_step_gen_time) - step_gen_time = max(self.last_step_gen_time, want_sg_wave, - flush_time) - self._flush_motion_queues(flush_time, step_gen_time) - self.last_flush_time = flush_time - self.last_step_gen_time = step_gen_time - if flush_time >= want_flush_time: - break def _await_flush_time(self, want_flush_time): while 1: if self.last_flush_time >= want_flush_time or not self.can_pause: From f34103183453093f432b8d5565068cdd5b2c7999 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Sep 2025 13:42:32 -0400 Subject: [PATCH 035/117] motion_queuing: Try harder to use next_batch_time when flushing Use the next_batch_time even if it is slightly past or before the ideal flushing window. This should improve run to run reproducibility of flush timing. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index a22598093..c2b306436 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -187,12 +187,13 @@ class PrinterMotionQueuing: if self.last_step_gen_time < aggr_sg_time: # Actively stepping - want more aggressive flushing want_sg_time = est_print_time + BGFLUSH_SG_HIGH_TIME - want_sg_time = min(want_sg_time, aggr_sg_time) - # Try improving run-to-run reproducibility by batching from last batch_time = BGFLUSH_SG_HIGH_TIME - BGFLUSH_SG_LOW_TIME next_batch_time = self.last_step_gen_time + batch_time - if next_batch_time > est_print_time + BGFLUSH_SG_LOW_TIME: - want_sg_time = min(want_sg_time, next_batch_time) + if (next_batch_time > est_print_time + and next_batch_time < want_sg_time + 0.005): + # Improve run-to-run reproducibility by batching from last + want_sg_time = next_batch_time + want_sg_time = min(want_sg_time, aggr_sg_time) # Flush motion queues (if needed) if want_sg_time > self.last_step_gen_time: self._advance_flush_time(0., want_sg_time) From 4c46b80f38670e0b0547db5aa88ca3579a4ceaf7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Sep 2025 14:19:02 -0400 Subject: [PATCH 036/117] motion_queuing: Further improve step flushing in batches Further encourage flushing steps in batches by delaying flushing if a batch isn't needed yet. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index c2b306436..2371e9a2c 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -189,9 +189,11 @@ class PrinterMotionQueuing: want_sg_time = est_print_time + BGFLUSH_SG_HIGH_TIME batch_time = BGFLUSH_SG_HIGH_TIME - BGFLUSH_SG_LOW_TIME next_batch_time = self.last_step_gen_time + batch_time - if (next_batch_time > est_print_time - and next_batch_time < want_sg_time + 0.005): + if next_batch_time > est_print_time: # Improve run-to-run reproducibility by batching from last + if next_batch_time > want_sg_time: + # Delay flushing until next wakeup + next_batch_time = self.last_step_gen_time want_sg_time = next_batch_time want_sg_time = min(want_sg_time, aggr_sg_time) # Flush motion queues (if needed) From df29a380110651919464a694491f33703f5916b7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Sep 2025 14:30:27 -0400 Subject: [PATCH 037/117] motion_queuing: Further tune flushing in batches Avoid unnecessary reactor wakeups if a batch is close to being ready. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 2371e9a2c..ec279a698 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -191,7 +191,7 @@ class PrinterMotionQueuing: next_batch_time = self.last_step_gen_time + batch_time if next_batch_time > est_print_time: # Improve run-to-run reproducibility by batching from last - if next_batch_time > want_sg_time: + if next_batch_time > want_sg_time + 0.005: # Delay flushing until next wakeup next_batch_time = self.last_step_gen_time want_sg_time = next_batch_time From 636380e4f3cacf89b777f580d69ae672e612f501 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Sep 2025 19:20:00 -0400 Subject: [PATCH 038/117] toolhead: Avoid numerical stability in _handle_step_flush() comparison Don't rely on an exact floating point number match to detect when a forced lookahead flush is needed. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index c4264051d..f54743b0a 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -358,7 +358,7 @@ class ToolHead: return # In "main" state - flush lookahead if buffer runs low kin_flush_delay = self.motion_queuing.get_kin_flush_delay() - if step_gen_time >= self.print_time - kin_flush_delay: + if step_gen_time >= self.print_time - kin_flush_delay - 0.001: self._flush_lookahead(is_runout=True) # Movement commands def get_position(self): From c7365c8c585bb3127280651b95b0e9b87e5bdf77 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 16 Sep 2025 11:28:16 -0400 Subject: [PATCH 039/117] extruder: Recheck the step generation scan windows on sync_to_extruder() Signed-off-by: Kevin O'Connor --- klippy/kinematics/extruder.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 9b1ec2d20..684f4be71 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -50,9 +50,11 @@ class ExtruderStepper: def sync_to_extruder(self, extruder_name): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() + motion_queuing = self.printer.lookup_object('motion_queuing') if not extruder_name: self.stepper.set_trapq(None) self.motion_queue = None + motion_queuing.check_step_generation_scan_windows() return extruder = self.printer.lookup_object(extruder_name, None) if extruder is None or not isinstance(extruder, PrinterExtruder): @@ -61,6 +63,7 @@ class ExtruderStepper: self.stepper.set_position([extruder.last_position, 0., 0.]) self.stepper.set_trapq(extruder.get_trapq()) self.motion_queue = extruder_name + motion_queuing.check_step_generation_scan_windows() def _set_pressure_advance(self, pressure_advance, smooth_time): old_smooth_time = self.pressure_advance_smooth_time if not self.pressure_advance: From 8db5d254e077e10583cfaff0d9e70e08263383e5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 16 Sep 2025 11:35:11 -0400 Subject: [PATCH 040/117] docs: Update Code_Overview.md with recent motion_queuing changes Update the documentation to reflect the new threads and new movement code flow. Signed-off-by: Kevin O'Connor --- docs/Code_Overview.md | 84 +++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index fd0e90a38..6b5df5595 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -102,20 +102,35 @@ some functionality in C code. Initial execution starts in **klippy/klippy.py**. This reads the command-line arguments, opens the printer config file, instantiates the main printer objects, and starts the serial connection. The main -execution of G-code commands is in the process_commands() method in +execution of G-code commands is in the _process_commands() method in **klippy/gcode.py**. This code translates the G-code commands into printer object calls, which frequently translate the actions to commands to be executed on the micro-controller (as declared via the DECL_COMMAND macro in the micro-controller code). -There are four threads in the Klippy host code. The main thread -handles incoming gcode commands. A second thread (which resides -entirely in the **klippy/chelper/serialqueue.c** C code) handles -low-level IO with the serial port. The third thread is used to process -response messages from the micro-controller in the Python code (see -**klippy/serialhdl.py**). The fourth thread writes debug messages to -the log (see **klippy/queuelogger.py**) so that the other threads -never block on log writes. +There are several threads in the Klipper host code: +* There is a Python "main thread" that handles incoming G-Code + commands and is the starting point for most actions. This thread + runs the [reactor](https://en.wikipedia.org/wiki/Reactor_pattern) + (**klippy/reactor.py**) and most high-level actions originate from + IO and timer event callbacks from that reactor. +* A thread for writing messages to the log so that the other threads + do not block on log writes. This thread resides entirely in the + **klippy/queuelogger.py** code and its operation is generally not + exposed to the main Python thread. +* A thread per micro-controller that performs the low-level reading + and writing of messages to that micro-controller. It resides in the + **klippy/chelper/serialqueue.c** C code and its operation is + generally not exposed to the Python code. +* A thread per micro-controller for processing messages received from + that micro-controller in the Python code. This thread is created in + **klippy/serialhdl.py**. Care must be taken in Python callbacks + invoked from this thread as this thread may directly interact with + the main Python thread. +* A thread per stepper motor that calculates the timing of stepper + motor step pulses and compresses those times. This thread resides in + the **klippy/chelper/stepcompress.c** C code and its operation is + generally not exposed to the Python code. ## Code flow of a move command @@ -138,7 +153,7 @@ provides further information on the mechanics of moves. the timing of printing actions. The main codepath for a move is: `ToolHead.move() -> LookAheadQueue.add_move() -> LookAheadQueue.flush() -> Move.set_junction() -> - ToolHead._process_moves()`. + ToolHead._process_moves() -> trapq_append()`. * ToolHead.move() creates a Move() object with the parameters of the move (in cartesian space and in units of seconds and millimeters). * The kinematics class is given the opportunity to audit each move @@ -163,34 +178,41 @@ provides further information on the mechanics of moves. during acceleration/cruising/deceleration. All the information is stored in the Move() class and is in cartesian space in units of millimeters and seconds. - -* Klipper uses an - [iterative solver](https://en.wikipedia.org/wiki/Root-finding_algorithm) - to generate the step times for each stepper. For efficiency reasons, - the stepper pulse times are generated in C code. The moves are first - placed on a "trapezoid motion queue": `ToolHead._process_moves() -> - trapq_append()` (in klippy/chelper/trapq.c). The step times are then - generated: `ToolHead._process_moves() -> - ToolHead._advance_move_time() -> ToolHead._advance_flush_time() -> - MCU_Stepper.generate_steps() -> itersolve_generate_steps() -> - itersolve_gen_steps_range()` (in klippy/chelper/itersolve.c). The - goal of the iterative solver is to find step times given a function - that calculates a stepper position from a time. This is done by - repeatedly "guessing" various times until the stepper position - formula returns the desired position of the next step on the - stepper. The feedback produced from each guess is used to improve - future guesses so that the process rapidly converges to the desired - time. The kinematic stepper position formulas are located in the - klippy/chelper/ directory (eg, kin_cart.c, kin_corexy.c, - kin_delta.c, kin_extruder.c). + * The moves are then placed on a "trapezoid motion queue" via + trapq_append() (in klippy/chelper/trapq.c). The trapq stores all the + information in the Move() class in a C struct accessible to the host + C code. * Note that the extruder is handled in its own kinematic class: - `ToolHead._process_moves() -> PrinterExtruder.move()`. Since + `ToolHead._process_moves() -> PrinterExtruder.process_move()`. Since the Move() class specifies the exact movement time and since step pulses are sent to the micro-controller with specific timing, stepper movements produced by the extruder class will be in sync with head movement even though the code is kept separate. +* Klipper uses an + [iterative solver](https://en.wikipedia.org/wiki/Root-finding_algorithm) + to generate the step times for each stepper. For efficiency reasons, + the stepper pulse times are generated in C code in a thread per + stepper motor. The threads are notified of new activity by the + motion_queuing module (klippy/extras/motion_queuing.py): + `PrinterMotionQueuing._flush_handler() -> + PrinterMotionQueuing._advance_move_time() -> + steppersync_start_gen_steps() -> + stepcompress_start_gen_steps()`. The step times are then generated + from that thread (klippy/chelper/stepcompress.c): + `sc_background_thread() -> stepcompress_generate_steps() -> + itersolve_generate_steps() -> itersolve_gen_steps_range()` (in + klippy/chelper/itersolve.c). The goal of the iterative solver is to + find step times given a function that calculates a stepper position + from a time. This is done by repeatedly "guessing" various times + until the stepper position formula returns the desired position of + the next step on the stepper. The feedback produced from each guess + is used to improve future guesses so that the process rapidly + converges to the desired time. The kinematic stepper position + formulas are located in the klippy/chelper/ directory (eg, + kin_cart.c, kin_corexy.c, kin_delta.c, kin_extruder.c). + * After the iterative solver calculates the step times they are added to an array: `itersolve_gen_steps_range() -> stepcompress_append()` (in klippy/chelper/stepcompress.c). The array (struct From e8e88415ea097dc4e3873cd4ed85d26c07939784 Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Tue, 2 Sep 2025 19:08:00 +0800 Subject: [PATCH 041/117] stm32: Clean up SPI code on stm32h7_spi.c Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- src/stm32/stm32h7_spi.c | 56 +++++++++++------------------------------ 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/src/stm32/stm32h7_spi.c b/src/stm32/stm32h7_spi.c index d1e514e7a..1d8c2afdf 100644 --- a/src/stm32/stm32h7_spi.c +++ b/src/stm32/stm32h7_spi.c @@ -16,67 +16,41 @@ struct spi_info { uint8_t miso_pin, mosi_pin, sck_pin, function; }; -DECL_ENUMERATION("spi_bus", "spi2", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi2", 0); DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); - -DECL_ENUMERATION("spi_bus", "spi1", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi1", 1); DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); -DECL_ENUMERATION("spi_bus", "spi1a", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi1a", 2); DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); - -#if !CONFIG_MACH_STM32F1 -DECL_ENUMERATION("spi_bus", "spi2a", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi2a", 3); DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); -#endif - -#ifdef SPI3 -DECL_ENUMERATION("spi_bus", "spi3a", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi3a", 4); DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10"); -#endif - -#ifdef SPI4 -DECL_ENUMERATION("spi_bus", "spi4", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi4", 5); DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12"); -#endif - +DECL_ENUMERATION("spi_bus", "spi5", 6); +DECL_CONSTANT_STR("BUS_PINS_spi5", "PF8,PF9,PF7"); +DECL_ENUMERATION("spi_bus", "spi5a", 7); +DECL_CONSTANT_STR("BUS_PINS_spi5a", "PH7,PF11,PH6"); +DECL_ENUMERATION("spi_bus", "spi6", 8); +DECL_CONSTANT_STR("BUS_PINS_spi6", "PG12,PG14,PG13"); #ifdef GPIOI -DECL_ENUMERATION("spi_bus", "spi2b", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi2b", 9); DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); #endif -#ifdef SPI5 -DECL_ENUMERATION("spi_bus", "spi5", __COUNTER__); -DECL_CONSTANT_STR("BUS_PINS_spi5", "PF8,PF9,PF7"); -DECL_ENUMERATION("spi_bus", "spi5a", __COUNTER__); -DECL_CONSTANT_STR("BUS_PINS_spi5a", "PH7,PF11,PH6"); -#endif - -#ifdef SPI6 -DECL_ENUMERATION("spi_bus", "spi6", __COUNTER__); -DECL_CONSTANT_STR("BUS_PINS_spi6", "PG12,PG14,PG13"); -#endif - - static const struct spi_info spi_bus[] = { { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION(5) }, { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION(5) }, { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION(5) }, -#if !CONFIG_MACH_STM32F1 { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION(5) }, -#endif -#ifdef SPI3 { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION(6) }, -#endif -#ifdef SPI4 { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION(5) }, -#endif - { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION(5) }, -#ifdef SPI5 { SPI5, GPIO('F', 8), GPIO('F', 9), GPIO('F', 7), GPIO_FUNCTION(5) }, { SPI5, GPIO('H', 7), GPIO('F', 11), GPIO('H', 6), GPIO_FUNCTION(5) }, -#endif -#ifdef SPI6 { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), GPIO_FUNCTION(5)}, +#ifdef GPIOI + { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION(5) }, #endif }; From 61252819e3aaf9fb5995242ce132860f83578c42 Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Wed, 10 Sep 2025 15:39:51 +0800 Subject: [PATCH 042/117] stm32: Clean up SPI code on spi.c Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- src/stm32/spi.c | 324 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 266 insertions(+), 58 deletions(-) diff --git a/src/stm32/spi.c b/src/stm32/spi.c index f64326813..0e8034a9c 100644 --- a/src/stm32/spi.c +++ b/src/stm32/spi.c @@ -15,79 +15,287 @@ struct spi_info { uint8_t miso_pin, mosi_pin, sck_pin, miso_af, mosi_af, sck_af; }; -DECL_ENUMERATION("spi_bus", "spi2", 0); -DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); -DECL_ENUMERATION("spi_bus", "spi1", 1); -DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); -DECL_ENUMERATION("spi_bus", "spi1a", 2); -DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); - -#if CONFIG_MACH_STM32G4 - DECL_ENUMERATION("spi_bus", "spi2_PA10_PA11_PF1", 3); - DECL_CONSTANT_STR("BUS_PINS_spi2_PA10_PA11_PF1", "PA10,PA11,PF1"); -#elif !CONFIG_MACH_STM32F1 - DECL_ENUMERATION("spi_bus", "spi2a", 3); - DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); -#endif -#ifdef SPI3 - DECL_ENUMERATION("spi_bus", "spi3", 4); - DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); - #if CONFIG_MACH_STM32F4 || CONFIG_MACH_STM32G4 +#if CONFIG_MACH_STM32F0 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); +#elif CONFIG_MACH_STM32F1 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 3); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3", 3); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); +#elif CONFIG_MACH_STM32F2 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); +#elif CONFIG_MACH_STM32F4 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 5); + DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10"); + #ifdef GPIOI + DECL_ENUMERATION("spi_bus", "spi2_PI2_PI3_PI1", 6); + DECL_CONSTANT_STR("BUS_PINS_spi2_PI2_PI3_PI1", "PI2,PI3,PI1"); + #define SPI4_INDEX (1 + 6) + #else + #define SPI4_INDEX (0 + 6) + #endif + #ifdef SPI4 + DECL_ENUMERATION("spi_bus", "spi4_PE13_PE14_PE12", SPI4_INDEX); + DECL_CONSTANT_STR("BUS_PINS_spi4_PE13_PE14_PE12", "PE13,PE14,PE12"); + #define SPI6_INDEX (1 + SPI4_INDEX) + #else + #define SPI6_INDEX (0 + SPI4_INDEX) + #endif + #ifdef SPI6 + DECL_ENUMERATION("spi_bus", "spi6_PG12_PG14_PG13", SPI6_INDEX); + DECL_CONSTANT_STR("BUS_PINS_spi6_PG12_PG14_PG13", "PG12,PG14,PG13"); + #endif + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3a", 5); + DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10"); + #ifdef GPIOI + DECL_ENUMERATION("spi_bus", "spi2b", 6); + DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); + #endif + #ifdef SPI4 + DECL_ENUMERATION("spi_bus", "spi4", SPI4_INDEX); + DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12"); + #endif +#elif CONFIG_MACH_STM32F7 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); +#elif CONFIG_MACH_STM32G0 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi2_PB2_PB11_PB10", 4); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB2_PB11_PB10", "PB2,PB11,PB10"); + #ifdef SPI3 + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 5); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + #endif + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); + #ifdef SPI3 + DECL_ENUMERATION("spi_bus", "spi3", 5); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); + #endif +#elif CONFIG_MACH_STM32G4 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PA10_PA11_PF1", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PA10_PA11_PF1", "PA10,PA11,PF1"); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 5); + DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10"); + #ifdef SPI4 + DECL_ENUMERATION("spi_bus", "spi4_PE13_PE14_PE12", 6); + DECL_CONSTANT_STR("BUS_PINS_spi4_PE13_PE14_PE12", "PE13,PE14,PE12"); + #endif + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); DECL_ENUMERATION("spi_bus", "spi3a", 5); DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10"); #ifdef SPI4 - DECL_ENUMERATION("spi_bus", "spi4", 6); - DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12"); - #elif defined(GPIOI) - DECL_ENUMERATION("spi_bus", "spi2b", 6); - DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); + DECL_ENUMERATION("spi_bus", "spi4", 6); + DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12"); #endif - #ifdef SPI6 - DECL_ENUMERATION("spi_bus", "spi6_PG12_PG14_PG13", 7); - DECL_CONSTANT_STR("BUS_PINS_spi6_PG12_PG14_PG13", "PG12,PG14,PG13"); - #endif - #endif - #if CONFIG_MACH_STM32G0B1 - DECL_ENUMERATION("spi_bus", "spi2_PB2_PB11_PB10", 5); - DECL_CONSTANT_STR("BUS_PINS_spi2_PB2_PB11_PB10", "PB2,PB11,PB10"); -#endif +#elif CONFIG_MACH_STM32L4 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); #endif #define GPIO_FUNCTION_ALL(fn) GPIO_FUNCTION(fn), \ GPIO_FUNCTION(fn), GPIO_FUNCTION(fn) -#if CONFIG_MACH_STM32F0 || CONFIG_MACH_STM32G0 - #define SPI_FUNCTION_ALL GPIO_FUNCTION_ALL(0) -#else - #define SPI_FUNCTION_ALL GPIO_FUNCTION_ALL(5) -#endif - static const struct spi_info spi_bus[] = { - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION_ALL }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION_ALL }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION_ALL }, -#if CONFIG_MACH_STM32G4 - { SPI2, GPIO('A', 10), GPIO('A', 11), GPIO('F', 1), SPI_FUNCTION_ALL }, -#else - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION_ALL }, -#endif -#ifdef SPI3 +#if CONFIG_MACH_STM32F0 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), + GPIO_FUNCTION(1), GPIO_FUNCTION(1), GPIO_FUNCTION(5) }, +#elif CONFIG_MACH_STM32F1 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, +#elif CONFIG_MACH_STM32F2 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, +#elif CONFIG_MACH_STM32F4 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, + { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION_ALL(6) }, + #ifdef GPIOI + { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION_ALL(5) }, + #endif + #ifdef SPI4 + { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION_ALL(5) }, + #endif + #ifdef SPI6 + { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), GPIO_FUNCTION_ALL(5)}, + #endif +#elif CONFIG_MACH_STM32F7 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, +#elif CONFIG_MACH_STM32G0 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), + GPIO_FUNCTION(1), GPIO_FUNCTION(1), GPIO_FUNCTION(5) }, + { SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), + GPIO_FUNCTION(1), GPIO_FUNCTION(0), GPIO_FUNCTION(5) }, + #ifdef SPI3 + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(9) }, + #endif +#elif CONFIG_MACH_STM32G4 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('A', 10), GPIO('A', 11), GPIO('F', 1), GPIO_FUNCTION_ALL(5) }, { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, - #if CONFIG_MACH_STM32F4 || CONFIG_MACH_STM32G4 { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION_ALL(6) }, #ifdef SPI4 { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION_ALL(5) }, - #elif defined(GPIOI) - { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION_ALL(5) }, #endif - #ifdef SPI6 - { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), SPI_FUNCTION_ALL} - #endif - #endif - #if CONFIG_MACH_STM32G0B1 - { SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), - GPIO_FUNCTION(1), GPIO_FUNCTION(0), GPIO_FUNCTION(5) }, - #endif +#elif CONFIG_MACH_STM32L4 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, #endif }; From 1be6c0fce0a53b314f43a934d66cf64cf95e6176 Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Wed, 10 Sep 2025 18:20:06 +0800 Subject: [PATCH 043/117] stm32: change `GPIO_FUNCTION_ALL` to `SPI_FUNCTION` Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- src/stm32/spi.c | 115 +++++++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/src/stm32/spi.c b/src/stm32/spi.c index 0e8034a9c..5e3e428be 100644 --- a/src/stm32/spi.c +++ b/src/stm32/spi.c @@ -227,75 +227,72 @@ struct spi_info { DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); #endif -#define GPIO_FUNCTION_ALL(fn) GPIO_FUNCTION(fn), \ - GPIO_FUNCTION(fn), GPIO_FUNCTION(fn) +#define SPI_FUNCTION(miso, mosi, sck) GPIO_FUNCTION(miso), \ + GPIO_FUNCTION(mosi), GPIO_FUNCTION(sck) static const struct spi_info spi_bus[] = { #if CONFIG_MACH_STM32F0 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), - GPIO_FUNCTION(1), GPIO_FUNCTION(1), GPIO_FUNCTION(5) }, + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(1, 1, 5) }, #elif CONFIG_MACH_STM32F1 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) }, #elif CONFIG_MACH_STM32F2 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) }, #elif CONFIG_MACH_STM32F4 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, - { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION_ALL(6) }, - #ifdef GPIOI - { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION_ALL(5) }, - #endif - #ifdef SPI4 - { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION_ALL(5) }, - #endif - #ifdef SPI6 - { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), GPIO_FUNCTION_ALL(5)}, - #endif + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) }, + { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(6, 6, 6) }, + #ifdef GPIOI + { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), SPI_FUNCTION(5, 5, 5) }, + #endif + #ifdef SPI4 + { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), SPI_FUNCTION(5, 5, 5) }, + #endif + #ifdef SPI6 + { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), SPI_FUNCTION(5, 5, 5)}, + #endif #elif CONFIG_MACH_STM32F7 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) }, #elif CONFIG_MACH_STM32G0 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), - GPIO_FUNCTION(1), GPIO_FUNCTION(1), GPIO_FUNCTION(5) }, - { SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), - GPIO_FUNCTION(1), GPIO_FUNCTION(0), GPIO_FUNCTION(5) }, - #ifdef SPI3 - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(9) }, - #endif + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(1, 1, 5) }, + { SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), SPI_FUNCTION(1, 0, 5) }, + #ifdef SPI3 + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(9, 9, 9) }, + #endif #elif CONFIG_MACH_STM32G4 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI2, GPIO('A', 10), GPIO('A', 11), GPIO('F', 1), GPIO_FUNCTION_ALL(5) }, - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, - { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION_ALL(6) }, - #ifdef SPI4 - { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION_ALL(5) }, - #endif + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI2, GPIO('A', 10), GPIO('A', 11), GPIO('F', 1), SPI_FUNCTION(5, 5, 5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) }, + { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(6, 6, 6) }, + #ifdef SPI4 + { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), SPI_FUNCTION(5, 5, 5) }, + #endif #elif CONFIG_MACH_STM32L4 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) }, #endif }; From 5da026a337a681c29c66e47ca6d30dc52e9acf58 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Wed, 14 Feb 2024 01:55:21 +0100 Subject: [PATCH 044/117] input_shaper: Updated definitions of *EI input shapers Signed-off-by: Dmitry Butyugin --- docs/Config_Changes.md | 4 ++ klippy/extras/shaper_defs.py | 82 +++++++++++++++++++++--------------- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 838b23273..296343b12 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,10 @@ All dates in this document are approximate. ## Changes +20250916: The definitions of EI, 2HUMP_EI, and 3HUMP_EI input shapers +were updated. For best performance it is recommended to recalibrate +input shapers, especially if some of these shapers are currently used. + 20250811: Support for the `max_accel_to_decel` parameter in the `[printer]` config section has been removed and support for the `ACCEL_TO_DECEL` parameter in the `SET_VELOCITY_LIMIT` command has diff --git a/klippy/extras/shaper_defs.py b/klippy/extras/shaper_defs.py index 611fed16a..8a0d93962 100644 --- a/klippy/extras/shaper_defs.py +++ b/klippy/extras/shaper_defs.py @@ -46,50 +46,62 @@ def get_mzv_shaper(shaper_freq, damping_ratio): def get_ei_shaper(shaper_freq, damping_ratio): v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance df = math.sqrt(1. - damping_ratio**2) - K = math.exp(-damping_ratio * math.pi / df) t_d = 1. / (shaper_freq * df) + dr = damping_ratio - a1 = .25 * (1. + v_tol) - a2 = .5 * (1. - v_tol) * K - a3 = a1 * K * K + a1 = (0.24968 + 0.24961 * v_tol) + (( 0.80008 + 1.23328 * v_tol) + + ( 0.49599 + 3.17316 * v_tol) * dr) * dr + a3 = (0.25149 + 0.21474 * v_tol) + ((-0.83249 + 1.41498 * v_tol) + + ( 0.85181 - 4.90094 * v_tol) * dr) * dr + a2 = 1. - a1 - a3 + + t2 = 0.4999 + ((( 0.46159 + 8.57843 * v_tol) * v_tol) + + (((4.26169 - 108.644 * v_tol) * v_tol) + + ((1.75601 + 336.989 * v_tol) * v_tol) * dr) * dr) * dr A = [a1, a2, a3] - T = [0., .5*t_d, t_d] + T = [0., t2 * t_d, t_d] + return (A, T) + +def _get_shaper_from_expansion_coeffs(shaper_freq, damping_ratio, t, a): + tau = 1. / shaper_freq + T = [] + A = [] + n = len(a) + k = len(a[0]) + for i in range(n): + u = t[i][k-1] + v = a[i][k-1] + for j in range(k-1): + u = u * damping_ratio + t[i][k-j-2] + v = v * damping_ratio + a[i][k-j-2] + T.append(u * tau) + A.append(v) return (A, T) def get_2hump_ei_shaper(shaper_freq, damping_ratio): - v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance - df = math.sqrt(1. - damping_ratio**2) - K = math.exp(-damping_ratio * math.pi / df) - t_d = 1. / (shaper_freq * df) - - V2 = v_tol**2 - X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.) - a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X) - a2 = (.5 - a1) * K - a3 = a2 * K - a4 = a1 * K * K * K - - A = [a1, a2, a3, a4] - T = [0., .5*t_d, t_d, 1.5*t_d] - return (A, T) + t = [[0., 0., 0., 0.], + [0.49890, 0.16270, -0.54262, 6.16180], + [0.99748, 0.18382, -1.58270, 8.17120], + [1.49920, -0.09297, -0.28338, 1.85710]] + a = [[0.16054, 0.76699, 2.26560, -1.22750], + [0.33911, 0.45081, -2.58080, 1.73650], + [0.34089, -0.61533, -0.68765, 0.42261], + [0.15997, -0.60246, 1.00280, -0.93145]] + return _get_shaper_from_expansion_coeffs(shaper_freq, damping_ratio, t, a) def get_3hump_ei_shaper(shaper_freq, damping_ratio): - v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance - df = math.sqrt(1. - damping_ratio**2) - K = math.exp(-damping_ratio * math.pi / df) - t_d = 1. / (shaper_freq * df) - - K2 = K*K - a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol)) - a2 = 0.25 * (1. - v_tol) * K - a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2 - a4 = a2 * K2 - a5 = a1 * K2 * K2 - - A = [a1, a2, a3, a4, a5] - T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d] - return (A, T) + t = [[0., 0., 0., 0.], + [0.49974, 0.23834, 0.44559, 12.4720], + [0.99849, 0.29808, -2.36460, 23.3990], + [1.49870, 0.10306, -2.01390, 17.0320], + [1.99960, -0.28231, 0.61536, 5.40450]] + a = [[0.11275, 0.76632, 3.29160 -1.44380], + [0.23698, 0.61164, -2.57850, 4.85220], + [0.30008, -0.19062, -2.14560, 0.13744], + [0.23775, -0.73297, 0.46885, -2.08650], + [0.11244, -0.45439, 0.96382, -1.46000]] + return _get_shaper_from_expansion_coeffs(shaper_freq, damping_ratio, t, a) # min_freq for each shaper is chosen to have projected max_accel ~= 1500 INPUT_SHAPERS = [ From 599dcd176c1eed673b418d95501f4aba19e09e9c Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sun, 21 Sep 2025 01:50:54 +0200 Subject: [PATCH 045/117] resonance_tester: Correctly handle incorrect accelerometer chip names Signed-off-by: Dmitry Butyugin --- klippy/extras/resonance_tester.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index c6171dc42..3e49b7b15 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -223,9 +223,13 @@ class ResonanceTester: self.printer.register_event_handler("klippy:connect", self.connect) def connect(self): - self.accel_chips = [ - (chip_axis, self.printer.lookup_object(chip_name)) - for chip_axis, chip_name in self.accel_chip_names] + self.accel_chips = [] + for chip_axis, chip_name in self.accel_chip_names: + chip = self.printer.lookup_object(chip_name) + if not hasattr(chip, 'start_internal_client'): + raise self.printer.config_error( + "'%s' is not an accelerometer" % chip_name) + self.accel_chips.append((chip_axis, chip)) def _run_test(self, gcmd, axes, helper, raw_name_suffix=None, accel_chips=None, test_point=None): @@ -288,7 +292,13 @@ class ResonanceTester: def _parse_chips(self, accel_chips): parsed_chips = [] for chip_name in accel_chips.split(','): - chip = self.printer.lookup_object(chip_name.strip()) + chip = self.printer.lookup_object(chip_name.strip(), None) + if chip is None: + raise self.printer.command_error("Name '%s' is not valid for" + " CHIPS parameter" % chip_name) + if not hasattr(chip, 'start_internal_client'): + raise self.printer.command_error( + "'%s' is not an accelerometer" % chip_name) parsed_chips.append(chip) return parsed_chips def _get_max_calibration_freq(self): From 1c76ed1dc90fdd346e783e6c24c4aafefdbbd5a8 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sun, 21 Sep 2025 17:10:26 +0200 Subject: [PATCH 046/117] resonance_tester: Gracefully handle zero accelerations during the test Signed-off-by: Dmitry Butyugin --- klippy/extras/resonance_tester.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index 3e49b7b15..4f4ef7bb4 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -145,19 +145,27 @@ class ResonanceTestExecutor: gcmd.respond_info("Disabled [input_shaper] for resonance testing") else: input_shaper = None - last_v = last_t = last_accel = last_freq = 0. + last_v = last_t = last_freq = 0. for next_t, accel, freq in test_seq: t_seg = next_t - last_t - toolhead.set_max_velocities(None, abs(accel), None, None) - v = last_v + accel * t_seg - abs_v = abs(v) - if abs_v < 0.000001: - v = abs_v = 0. abs_last_v = abs(last_v) - v2 = v * v last_v2 = last_v * last_v - half_inv_accel = .5 / accel - d = (v2 - last_v2) * half_inv_accel + if abs(accel) < 0.000001: + v, abs_v = last_v, abs_last_v + if abs_v < 0.000001: + toolhead.dwell(t_seg) + last_t, last_freq = next_t, freq + continue + half_inv_accel = 0. + d = v * t_seg + else: + toolhead.set_max_velocities(None, abs(accel), None, None) + v = last_v + accel * t_seg + abs_v = abs(v) + if abs_v < 0.000001: + v = abs_v = 0. + half_inv_accel = .5 / accel + d = (v * v - last_v2) * half_inv_accel dX, dY = axis.get_point(d) nX = X + dX nY = Y + dY @@ -176,7 +184,6 @@ class ResonanceTestExecutor: X, Y = nX, nY last_t = next_t last_v = v - last_accel = accel last_freq = freq if last_v: d_decel = -.5 * last_v2 / old_max_accel From 13cfdf57114f2915724d85c8182360601e0622b5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 14:58:36 -0400 Subject: [PATCH 047/117] motion_queuing: Reorganize code into sections Only code movement - no code changes. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 93 +++++++++++++++++---------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index ec279a698..4679e801d 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -24,21 +24,23 @@ class PrinterMotionQueuing: def __init__(self, config): self.printer = printer = config.get_printer() self.reactor = printer.get_reactor() - # Low level C allocations + # C trapq tracking self.trapqs = [] - self.stepcompress = [] - self.steppersyncs = [] - # Low-level C flushing calls ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves + # C steppersync tracking + self.stepcompress = [] + self.steppersyncs = [] self.steppersync_start_gen_steps = ffi_lib.steppersync_start_gen_steps self.steppersync_finalize_gen_steps = \ ffi_lib.steppersync_finalize_gen_steps self.steppersync_history_expire = ffi_lib.steppersync_history_expire - # Flush notification callbacks - self.flush_callbacks = [] # History expiration self.clear_history_time = 0. + # Flush notification callbacks + self.flush_callbacks = [] + # Kinematic step generation scan window time tracking + self.kin_flush_delay = SDS_CHECK_TIME # MCU tracking self.all_mcus = [m for n, m in printer.lookup_objects(module='mcu')] self.mcu = self.all_mcus[0] @@ -53,15 +55,21 @@ class PrinterMotionQueuing: self.do_kick_flush_timer = True self.last_flush_time = self.last_step_gen_time = 0. self.need_flush_time = self.need_step_gen_time = 0. - # Kinematic step generation scan window time tracking - self.kin_flush_delay = SDS_CHECK_TIME # Register handlers printer.register_event_handler("klippy:shutdown", self._handle_shutdown) + # C trapq tracking def allocate_trapq(self): ffi_main, ffi_lib = chelper.get_ffi() trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) self.trapqs.append(trapq) return trapq + def wipe_trapq(self, trapq): + # Expire any remaining movement in the trapq (force to history list) + self.trapq_finalize_moves(trapq, self.reactor.NEVER, 0.) + def lookup_trapq_append(self): + ffi_main, ffi_lib = chelper.get_ffi() + return ffi_lib.trapq_append + # C steppersync tracking def allocate_stepcompress(self, mcu, oid, name): name = name.encode("utf-8")[:15] ffi_main, ffi_lib = chelper.get_ffi() @@ -83,6 +91,18 @@ class PrinterMotionQueuing: self.steppersyncs.append((mcu, ss)) mcu_freq = float(mcu.seconds_to_clock(1.)) ffi_lib.steppersync_set_time(ss, 0., mcu_freq) + def stats(self, eventtime): + # Globally calibrate mcu clocks (and step generation clocks) + sync_time = self.last_step_gen_time + ffi_main, ffi_lib = chelper.get_ffi() + for mcu, ss in self.steppersyncs: + offset, freq = mcu.calibrate_clock(sync_time, eventtime) + ffi_lib.steppersync_set_time(ss, offset, freq) + # Calculate history expiration + est_print_time = self.mcu.estimated_print_time(eventtime) + self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE + return False, "" + # Flush notification callbacks def register_flush_callback(self, callback, can_add_trapq=False): if can_add_trapq: self.flush_callbacks = [callback] + self.flush_callbacks @@ -93,6 +113,26 @@ class PrinterMotionQueuing: fcbs = list(self.flush_callbacks) fcbs.remove(callback) self.flush_callbacks = fcbs + # Kinematic step generation scan window time tracking + def get_kin_flush_delay(self): + return self.kin_flush_delay + def check_step_generation_scan_windows(self): + ffi_main, ffi_lib = chelper.get_ffi() + kin_flush_delay = SDS_CHECK_TIME + for mcu, sc in self.stepcompress: + sk = ffi_lib.stepcompress_get_stepper_kinematics(sc) + if sk == ffi_main.NULL: + continue + trapq = ffi_lib.itersolve_get_trapq(sk) + if trapq == ffi_main.NULL: + continue + pre_active = ffi_lib.itersolve_get_gen_steps_pre_active(sk) + post_active = ffi_lib.itersolve_get_gen_steps_post_active(sk) + kin_flush_delay = max(kin_flush_delay, pre_active, post_active) + self.kin_flush_delay = kin_flush_delay + # Flush tracking + def _handle_shutdown(self): + self.can_pause = False def _advance_flush_time(self, want_flush_time, want_step_gen_time=0.): flush_time = max(want_flush_time, self.last_flush_time, want_step_gen_time - STEPCOMPRESS_FLUSH_TIME) @@ -126,43 +166,6 @@ class PrinterMotionQueuing: for mcu, ss in self.steppersyncs: clock = max(0, mcu.print_time_to_clock(clear_history_time)) self.steppersync_history_expire(ss, clock) - def wipe_trapq(self, trapq): - # Expire any remaining movement in the trapq (force to history list) - self.trapq_finalize_moves(trapq, self.reactor.NEVER, 0.) - def lookup_trapq_append(self): - ffi_main, ffi_lib = chelper.get_ffi() - return ffi_lib.trapq_append - def stats(self, eventtime): - # Globally calibrate mcu clocks (and step generation clocks) - sync_time = self.last_step_gen_time - ffi_main, ffi_lib = chelper.get_ffi() - for mcu, ss in self.steppersyncs: - offset, freq = mcu.calibrate_clock(sync_time, eventtime) - ffi_lib.steppersync_set_time(ss, offset, freq) - # Calculate history expiration - est_print_time = self.mcu.estimated_print_time(eventtime) - self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE - return False, "" - # Kinematic step generation scan window time tracking - def get_kin_flush_delay(self): - return self.kin_flush_delay - def check_step_generation_scan_windows(self): - ffi_main, ffi_lib = chelper.get_ffi() - kin_flush_delay = SDS_CHECK_TIME - for mcu, sc in self.stepcompress: - sk = ffi_lib.stepcompress_get_stepper_kinematics(sc) - if sk == ffi_main.NULL: - continue - trapq = ffi_lib.itersolve_get_trapq(sk) - if trapq == ffi_main.NULL: - continue - pre_active = ffi_lib.itersolve_get_gen_steps_pre_active(sk) - post_active = ffi_lib.itersolve_get_gen_steps_post_active(sk) - kin_flush_delay = max(kin_flush_delay, pre_active, post_active) - self.kin_flush_delay = kin_flush_delay - # Flush tracking - def _handle_shutdown(self): - self.can_pause = False def _await_flush_time(self, want_flush_time): while 1: if self.last_flush_time >= want_flush_time or not self.can_pause: From a66f5cec520e2959d22be3a870e63e23f5032144 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 15:11:20 -0400 Subject: [PATCH 048/117] msgblock: Add new clock_fill() function Add a new function for filling the fields of 'struct clock_estimate'. Signed-off-by: Kevin O'Connor --- klippy/chelper/msgblock.c | 11 +++++++++++ klippy/chelper/msgblock.h | 2 ++ klippy/chelper/serialqueue.c | 5 +---- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/klippy/chelper/msgblock.c b/klippy/chelper/msgblock.c index e6cb298b4..a7385c893 100644 --- a/klippy/chelper/msgblock.c +++ b/klippy/chelper/msgblock.c @@ -207,3 +207,14 @@ clock_from_time(struct clock_estimate *ce, double time) { return (int64_t)((time - ce->conv_time)*ce->est_freq + .5) + ce->conv_clock; } + +// Fill the fields of a 'struct clock_estimate' +void +clock_fill(struct clock_estimate *ce, double est_freq, double conv_time + , uint64_t conv_clock, uint64_t last_clock) +{ + ce->est_freq = est_freq; + ce->conv_time = conv_time; + ce->conv_clock = conv_clock; + ce->last_clock = last_clock; +} diff --git a/klippy/chelper/msgblock.h b/klippy/chelper/msgblock.h index 43ee95325..9bb066313 100644 --- a/klippy/chelper/msgblock.h +++ b/klippy/chelper/msgblock.h @@ -50,5 +50,7 @@ void message_queue_free(struct list_head *root); uint64_t clock_from_clock32(struct clock_estimate *ce, uint32_t clock32); double clock_to_time(struct clock_estimate *ce, uint64_t clock); uint64_t clock_from_time(struct clock_estimate *ce, double time); +void clock_fill(struct clock_estimate *ce, double est_freq, double conv_time + , uint64_t conv_clock, uint64_t last_clock); #endif // msgblock.h diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index ed1215df9..914d4c395 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -909,10 +909,7 @@ serialqueue_set_clock_est(struct serialqueue *sq, double est_freq , uint64_t last_clock) { pthread_mutex_lock(&sq->lock); - sq->ce.est_freq = est_freq; - sq->ce.conv_time = conv_time; - sq->ce.conv_clock = conv_clock; - sq->ce.last_clock = last_clock; + clock_fill(&sq->ce, est_freq, conv_time, conv_clock, last_clock); pthread_mutex_unlock(&sq->lock); } From bd747872c3c917cd6677305c412a2e6c2ae34b87 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 15:13:14 -0400 Subject: [PATCH 049/117] steppersync: Add new 'struct steppersyncmgr' Add a new C based mechanism for tracking all the 'struct steppersync' instances. This simplifies memory management. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 8 +++-- klippy/chelper/steppersync.c | 56 +++++++++++++++++++++++++++++++-- klippy/chelper/steppersync.h | 13 +++++--- klippy/extras/motion_queuing.py | 9 +++--- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index b9ad9747d..efc2d303b 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -61,9 +61,6 @@ defs_stepcompress = """ """ defs_steppersync = """ - struct steppersync *steppersync_alloc(struct serialqueue *sq - , struct stepcompress **sc_list, int sc_num, int move_num); - void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); @@ -71,6 +68,11 @@ defs_steppersync = """ , double gen_steps_time, uint64_t flush_clock); int32_t steppersync_finalize_gen_steps(struct steppersync *ss , uint64_t flush_clock); + struct steppersyncmgr *steppersyncmgr_alloc(void); + void steppersyncmgr_free(struct steppersyncmgr *ssm); + struct steppersync *steppersyncmgr_alloc_steppersync( + struct steppersyncmgr *ssm, struct serialqueue *sq + , struct stepcompress **sc_list, int sc_num, int move_num); """ defs_itersolve = """ diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 0ff5bcab1..35010322d 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -19,7 +19,14 @@ #include "stepcompress.h" // stepcompress_flush #include "steppersync.h" // steppersync_alloc + +/**************************************************************** + * StepperSync - sort move queue for a micro-controller + ****************************************************************/ + struct steppersync { + // List node for storage in steppersyncmgr list + struct list_node ssm_node; // Serial port struct serialqueue *sq; struct command_queue *cq; @@ -32,7 +39,7 @@ struct steppersync { }; // Allocate a new 'steppersync' object -struct steppersync * __visible +static struct steppersync * steppersync_alloc(struct serialqueue *sq, struct stepcompress **sc_list , int sc_num, int move_num) { @@ -53,7 +60,7 @@ steppersync_alloc(struct serialqueue *sq, struct stepcompress **sc_list } // Free memory associated with a 'steppersync' object -void __visible +static void steppersync_free(struct steppersync *ss) { if (!ss) @@ -187,3 +194,48 @@ steppersync_finalize_gen_steps(struct steppersync *ss, uint64_t flush_clock) steppersync_flush(ss, flush_clock); return 0; } + + +/**************************************************************** + * StepperSyncMgr - manage a list of steppersync + ****************************************************************/ + +struct steppersyncmgr { + struct list_head ss_list; +}; + +// Allocate a new 'steppersyncmgr' object +struct steppersyncmgr * __visible +steppersyncmgr_alloc(void) +{ + struct steppersyncmgr *ssm = malloc(sizeof(*ssm)); + memset(ssm, 0, sizeof(*ssm)); + list_init(&ssm->ss_list); + return ssm; +} + +// Free memory associated with a 'steppersync' object +void __visible +steppersyncmgr_free(struct steppersyncmgr *ssm) +{ + if (!ssm) + return; + while (!list_empty(&ssm->ss_list)) { + struct steppersync *ss = list_first_entry( + &ssm->ss_list, struct steppersync, ssm_node); + list_del(&ss->ssm_node); + steppersync_free(ss); + } + free(ssm); +} + +// Allocate a new 'steppersync' object +struct steppersync * __visible +steppersyncmgr_alloc_steppersync( + struct steppersyncmgr *ssm, struct serialqueue *sq + , struct stepcompress **sc_list, int sc_num, int move_num) +{ + struct steppersync *ss = steppersync_alloc(sq, sc_list, sc_num, move_num); + list_add_tail(&ss->ssm_node, &ssm->ss_list); + return ss; +} diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index 41cd03bbd..0931d9240 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -3,11 +3,7 @@ #include // uint64_t -struct serialqueue; -struct steppersync *steppersync_alloc( - struct serialqueue *sq, struct stepcompress **sc_list, int sc_num - , int move_num); -void steppersync_free(struct steppersync *ss); +struct steppersync; void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); @@ -16,4 +12,11 @@ void steppersync_start_gen_steps(struct steppersync *ss, double gen_steps_time int32_t steppersync_finalize_gen_steps(struct steppersync *ss , uint64_t flush_clock); +struct steppersyncmgr *steppersyncmgr_alloc(void); +void steppersyncmgr_free(struct steppersyncmgr *ssm); +struct serialqueue; +struct steppersync *steppersyncmgr_alloc_steppersync( + struct steppersyncmgr *ssm, struct serialqueue *sq + , struct stepcompress **sc_list, int sc_num, int move_num); + #endif // steppersync.h diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 4679e801d..a44cb625a 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -29,6 +29,8 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves # C steppersync tracking + self.steppersyncmgr = ffi_main.gc(ffi_lib.steppersyncmgr_alloc(), + ffi_lib.steppersyncmgr_free) self.stepcompress = [] self.steppersyncs = [] self.steppersync_start_gen_steps = ffi_lib.steppersync_start_gen_steps @@ -84,10 +86,9 @@ class PrinterMotionQueuing: if sc_mcu is mcu: stepqueues.append(sc) ffi_main, ffi_lib = chelper.get_ffi() - ss = ffi_main.gc( - ffi_lib.steppersync_alloc(serialqueue, stepqueues, len(stepqueues), - move_count), - ffi_lib.steppersync_free) + ss = ffi_lib.steppersyncmgr_alloc_steppersync( + self.steppersyncmgr, serialqueue, stepqueues, len(stepqueues), + move_count) self.steppersyncs.append((mcu, ss)) mcu_freq = float(mcu.seconds_to_clock(1.)) ffi_lib.steppersync_set_time(ss, 0., mcu_freq) From f21cca049f6f0ec45ba1c9b7e8a96a135f057d26 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 15:31:44 -0400 Subject: [PATCH 050/117] steppersync: Add new steppersyncmgr_gen_steps() function Generate and flush all the steppersync instances from a single steppersyncmgr_gen_steps() call. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 7 +-- klippy/chelper/steppersync.c | 88 ++++++++++++++++++--------------- klippy/chelper/steppersync.h | 8 ++- klippy/extras/motion_queuing.py | 34 +++++-------- 4 files changed, 63 insertions(+), 74 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index efc2d303b..3a7ddf4eb 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -63,16 +63,13 @@ defs_stepcompress = """ defs_steppersync = """ void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); - void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); - void steppersync_start_gen_steps(struct steppersync *ss - , double gen_steps_time, uint64_t flush_clock); - int32_t steppersync_finalize_gen_steps(struct steppersync *ss - , uint64_t flush_clock); struct steppersyncmgr *steppersyncmgr_alloc(void); void steppersyncmgr_free(struct steppersyncmgr *ssm); struct steppersync *steppersyncmgr_alloc_steppersync( struct steppersyncmgr *ssm, struct serialqueue *sq , struct stepcompress **sc_list, int sc_num, int move_num); + int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm + , double flush_time, double gen_steps_time, double clear_history_time); """ defs_itersolve = """ diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 35010322d..ef2df415b 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -33,6 +33,8 @@ struct steppersync { // Storage for associated stepcompress objects struct stepcompress **sc_list; int sc_num; + // Convert from time to clock + struct clock_estimate ce; // Storage for list of pending move clocks uint64_t *move_clocks; int num_move_clocks; @@ -76,6 +78,7 @@ void __visible steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq) { + clock_fill(&ss->ce, mcu_freq, time_offset, 0, 0); int i; for (i=0; isc_num; i++) { struct stepcompress *sc = ss->sc_list[i]; @@ -83,17 +86,6 @@ steppersync_set_time(struct steppersync *ss, double time_offset } } -// Expire the stepcompress history before the given clock time -void __visible -steppersync_history_expire(struct steppersync *ss, uint64_t end_clock) -{ - int i; - for (i = 0; i < ss->sc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_history_expire(sc, end_clock); - } -} - // Implement a binary heap algorithm to track when the next available // 'struct move' in the mcu will be available static void @@ -165,36 +157,6 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock) serialqueue_send_batch(ss->sq, ss->cq, &msgs); } -// Start generating steps in stepcompress objects -void __visible -steppersync_start_gen_steps(struct steppersync *ss, double gen_steps_time - , uint64_t flush_clock) -{ - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_start_gen_steps(sc, gen_steps_time, flush_clock); - } -} - -// Finalize step generation and flush -int32_t __visible -steppersync_finalize_gen_steps(struct steppersync *ss, uint64_t flush_clock) -{ - int i; - int32_t res = 0; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - int32_t ret = stepcompress_finalize_gen_steps(sc); - if (ret) - res = ret; - } - if (res) - return res; - steppersync_flush(ss, flush_clock); - return 0; -} - /**************************************************************** * StepperSyncMgr - manage a list of steppersync @@ -239,3 +201,47 @@ steppersyncmgr_alloc_steppersync( list_add_tail(&ss->ssm_node, &ssm->ss_list); return ss; } + +// Generate and flush steps +int32_t __visible +steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time + , double gen_steps_time, double clear_history_time) +{ + struct steppersync *ss; + // Start step generation threads + list_for_each_entry(ss, &ssm->ss_list, ssm_node) { + uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); + int i; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + stepcompress_start_gen_steps(sc, gen_steps_time, flush_clock); + } + } + // Wait for step generation threads to complete + int32_t res = 0; + list_for_each_entry(ss, &ssm->ss_list, ssm_node) { + int i; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + int32_t ret = stepcompress_finalize_gen_steps(sc); + if (ret) + res = ret; + } + if (res) + continue; + uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); + steppersync_flush(ss, flush_clock); + } + if (res) + return res; + // Clear history + list_for_each_entry(ss, &ssm->ss_list, ssm_node) { + uint64_t end_clock = clock_from_time(&ss->ce, clear_history_time); + int i; + for (i = 0; i < ss->sc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + stepcompress_history_expire(sc, end_clock); + } + } + return 0; +} diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index 0931d9240..f7a89230d 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -6,11 +6,6 @@ struct steppersync; void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); -void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); -void steppersync_start_gen_steps(struct steppersync *ss, double gen_steps_time - , uint64_t flush_clock); -int32_t steppersync_finalize_gen_steps(struct steppersync *ss - , uint64_t flush_clock); struct steppersyncmgr *steppersyncmgr_alloc(void); void steppersyncmgr_free(struct steppersyncmgr *ssm); @@ -18,5 +13,8 @@ struct serialqueue; struct steppersync *steppersyncmgr_alloc_steppersync( struct steppersyncmgr *ssm, struct serialqueue *sq , struct stepcompress **sc_list, int sc_num, int move_num); +int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time + , double gen_steps_time + , double clear_history_time); #endif // steppersync.h diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index a44cb625a..d981dcb58 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -33,10 +33,7 @@ class PrinterMotionQueuing: ffi_lib.steppersyncmgr_free) self.stepcompress = [] self.steppersyncs = [] - self.steppersync_start_gen_steps = ffi_lib.steppersync_start_gen_steps - self.steppersync_finalize_gen_steps = \ - ffi_lib.steppersync_finalize_gen_steps - self.steppersync_history_expire = ffi_lib.steppersync_history_expire + self.steppersyncmgr_gen_steps = ffi_lib.steppersyncmgr_gen_steps # History expiration self.clear_history_time = 0. # Flush notification callbacks @@ -101,7 +98,7 @@ class PrinterMotionQueuing: ffi_lib.steppersync_set_time(ss, offset, freq) # Calculate history expiration est_print_time = self.mcu.estimated_print_time(eventtime) - self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE + self.clear_history_time = max(0., est_print_time - MOVE_HISTORY_EXPIRE) return False, "" # Flush notification callbacks def register_flush_callback(self, callback, can_add_trapq=False): @@ -142,31 +139,22 @@ class PrinterMotionQueuing: # Invoke flush callbacks (if any) for cb in self.flush_callbacks: cb(flush_time, step_gen_time) - # Generate stepper movement and transmit - for mcu, ss in self.steppersyncs: - clock = max(0, mcu.print_time_to_clock(flush_time)) - self.steppersync_start_gen_steps(ss, step_gen_time, clock) - for mcu, ss in self.steppersyncs: - clock = max(0, mcu.print_time_to_clock(flush_time)) - ret = self.steppersync_finalize_gen_steps(ss, clock) - if ret: - raise mcu.error("Internal error in MCU '%s' stepcompress" - % (mcu.get_name(),)) - self.last_flush_time = flush_time - self.last_step_gen_time = step_gen_time # Determine maximum history to keep trapq_free_time = step_gen_time - self.kin_flush_delay clear_history_time = self.clear_history_time if not self.can_pause: - clear_history_time = trapq_free_time - MOVE_HISTORY_EXPIRE - # Move processed trapq moves to history list, and expire old history + clear_history_time = max(0., trapq_free_time - MOVE_HISTORY_EXPIRE) + # Generate stepper movement and transmit + ret = self.steppersyncmgr_gen_steps(self.steppersyncmgr, flush_time, + step_gen_time, clear_history_time) + if ret: + raise self.mcu.error("Internal error in stepcompress") + self.last_flush_time = flush_time + self.last_step_gen_time = step_gen_time + # Move processed trapq entries to history list, and expire old history for trapq in self.trapqs: self.trapq_finalize_moves(trapq, trapq_free_time, clear_history_time) - # Clean up old history entries in stepcompress objects - for mcu, ss in self.steppersyncs: - clock = max(0, mcu.print_time_to_clock(clear_history_time)) - self.steppersync_history_expire(ss, clock) def _await_flush_time(self, want_flush_time): while 1: if self.last_flush_time >= want_flush_time or not self.can_pause: From a29cfc170148f92adee6ec2fb226b5ded627eb71 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 18:45:18 -0400 Subject: [PATCH 051/117] stepcompress: Pass oid in stepcompress_fill() instead of stepcompress_alloc() Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 7 ++++--- klippy/chelper/stepcompress.c | 6 +++--- klippy/chelper/stepcompress.h | 4 ++-- klippy/extras/motion_queuing.py | 4 ++-- klippy/extras/pwm_tool.py | 5 ++--- klippy/stepper.py | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 3a7ddf4eb..d3366044d 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -36,9 +36,10 @@ defs_stepcompress = """ int step_count, interval, add; }; - struct stepcompress *stepcompress_alloc(uint32_t oid, char name[16]); - void stepcompress_fill(struct stepcompress *sc, uint32_t max_error - , int32_t queue_step_msgtag, int32_t set_next_step_dir_msgtag); + struct stepcompress *stepcompress_alloc(char name[16]); + void stepcompress_fill(struct stepcompress *sc, uint32_t oid + , uint32_t max_error, int32_t queue_step_msgtag + , int32_t set_next_step_dir_msgtag); void stepcompress_set_invert_sdir(struct stepcompress *sc , uint32_t invert_sdir); void stepcompress_free(struct stepcompress *sc); diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index ab261d129..1a592173e 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -258,13 +258,12 @@ static void sc_thread_free(struct stepcompress *sc); // Allocate a new 'stepcompress' object struct stepcompress * __visible -stepcompress_alloc(uint32_t oid, char name[16]) +stepcompress_alloc(char name[16]) { struct stepcompress *sc = malloc(sizeof(*sc)); memset(sc, 0, sizeof(*sc)); list_init(&sc->msg_queue); list_init(&sc->history_list); - sc->oid = oid; sc->sdir = -1; int ret = sc_thread_alloc(sc, name); @@ -275,9 +274,10 @@ stepcompress_alloc(uint32_t oid, char name[16]) // Fill message id information void __visible -stepcompress_fill(struct stepcompress *sc, uint32_t max_error +stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error , int32_t queue_step_msgtag, int32_t set_next_step_dir_msgtag) { + sc->oid = oid; sc->max_error = max_error; sc->queue_step_msgtag = queue_step_msgtag; sc->set_next_step_dir_msgtag = set_next_step_dir_msgtag; diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 5ebf8bf08..413446daf 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -11,8 +11,8 @@ struct pull_history_steps { int step_count, interval, add; }; -struct stepcompress *stepcompress_alloc(uint32_t oid, char name[16]); -void stepcompress_fill(struct stepcompress *sc, uint32_t max_error +struct stepcompress *stepcompress_alloc(char name[16]); +void stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error , int32_t queue_step_msgtag , int32_t set_next_step_dir_msgtag); void stepcompress_set_invert_sdir(struct stepcompress *sc diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index d981dcb58..8fa562621 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -69,10 +69,10 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append # C steppersync tracking - def allocate_stepcompress(self, mcu, oid, name): + def allocate_stepcompress(self, mcu, name): name = name.encode("utf-8")[:15] ffi_main, ffi_lib = chelper.get_ffi() - sc = ffi_main.gc(ffi_lib.stepcompress_alloc(oid, name), + sc = ffi_main.gc(ffi_lib.stepcompress_alloc(name), ffi_lib.stepcompress_free) self.stepcompress.append((mcu, sc)) return sc diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index cec7e3791..dfa5c682b 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -14,12 +14,11 @@ class MCU_queued_pwm: self._hardware_pwm = False self._cycle_time = 0.100 self._max_duration = 2. - self._oid = oid = mcu.create_oid() + self._oid = mcu.create_oid() printer = mcu.get_printer() sname = config.get_name().split()[-1] self._motion_queuing = printer.load_object(config, 'motion_queuing') - self._stepqueue = self._motion_queuing.allocate_stepcompress( - mcu, oid, sname) + self._stepqueue = self._motion_queuing.allocate_stepcompress(mcu, sname) ffi_main, ffi_lib = chelper.get_ffi() self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg mcu.register_config_callback(self._build_config) diff --git a/klippy/stepper.py b/klippy/stepper.py index 046b5280d..deb73769e 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -29,7 +29,7 @@ class MCU_stepper: self._units_in_radians = units_in_radians self._step_dist = rotation_dist / steps_per_rotation self._mcu = mcu = step_pin_params['chip'] - self._oid = oid = mcu.create_oid() + self._oid = mcu.create_oid() mcu.register_config_callback(self._build_config) self._step_pin = step_pin_params['pin'] self._invert_step = step_pin_params['invert'] @@ -45,7 +45,7 @@ class MCU_stepper: self._active_callbacks = [] motion_queuing = printer.load_object(config, 'motion_queuing') sname = self._name.split()[-1] - self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid, sname) + self._stepqueue = motion_queuing.allocate_stepcompress(mcu, sname) ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir) self._stepper_kinematics = None @@ -123,7 +123,7 @@ class MCU_stepper: max_error = self._mcu.get_max_stepper_error() max_error_ticks = self._mcu.seconds_to_clock(max_error) ffi_main, ffi_lib = chelper.get_ffi() - ffi_lib.stepcompress_fill(self._stepqueue, max_error_ticks, + ffi_lib.stepcompress_fill(self._stepqueue, self._oid, max_error_ticks, step_cmd_tag, dir_cmd_tag) def get_oid(self): return self._oid From d831d66c114efd3df051e6586030b15544d1e951 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 19:43:00 -0400 Subject: [PATCH 052/117] steppersync: Introduce new 'struct syncemitter' Create a new 'struct syncemitter' for each object that can generate messages for a 'struct steppersync' and store in a regular linked list. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 10 ++- klippy/chelper/stepcompress.c | 4 +- klippy/chelper/steppersync.c | 128 +++++++++++++++++++------------- klippy/chelper/steppersync.h | 10 ++- klippy/extras/motion_queuing.py | 33 ++++---- 5 files changed, 111 insertions(+), 74 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index d3366044d..e7340b204 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -36,13 +36,11 @@ defs_stepcompress = """ int step_count, interval, add; }; - struct stepcompress *stepcompress_alloc(char name[16]); void stepcompress_fill(struct stepcompress *sc, uint32_t oid , uint32_t max_error, int32_t queue_step_msgtag , int32_t set_next_step_dir_msgtag); void stepcompress_set_invert_sdir(struct stepcompress *sc , uint32_t invert_sdir); - void stepcompress_free(struct stepcompress *sc); int stepcompress_reset(struct stepcompress *sc, uint64_t last_step_clock); int stepcompress_set_last_position(struct stepcompress *sc , uint64_t clock, int64_t last_position); @@ -62,13 +60,17 @@ defs_stepcompress = """ """ defs_steppersync = """ + struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); + struct syncemitter *steppersync_alloc_syncemitter(struct steppersync *ss + , char name[16], int alloc_stepcompress); + void steppersync_setup_movequeue(struct steppersync *ss + , struct serialqueue *sq, int move_num); void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); struct steppersyncmgr *steppersyncmgr_alloc(void); void steppersyncmgr_free(struct steppersyncmgr *ssm); struct steppersync *steppersyncmgr_alloc_steppersync( - struct steppersyncmgr *ssm, struct serialqueue *sq - , struct stepcompress **sc_list, int sc_num, int move_num); + struct steppersyncmgr *ssm); int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm , double flush_time, double gen_steps_time, double clear_history_time); """ diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 1a592173e..1c74380fb 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -257,7 +257,7 @@ static int sc_thread_alloc(struct stepcompress *sc, char name[16]); static void sc_thread_free(struct stepcompress *sc); // Allocate a new 'stepcompress' object -struct stepcompress * __visible +struct stepcompress * stepcompress_alloc(char name[16]) { struct stepcompress *sc = malloc(sizeof(*sc)); @@ -310,7 +310,7 @@ stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock) } // Free memory associated with a 'stepcompress' object -void __visible +void stepcompress_free(struct stepcompress *sc) { if (!sc) diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index ef2df415b..047fde3c3 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -20,6 +20,24 @@ #include "steppersync.h" // steppersync_alloc +/**************************************************************** + * SyncEmitter - message generation for each stepper + ****************************************************************/ + +struct syncemitter { + // List node for storage in steppersync list + struct list_node ss_node; + // Step compression and generation + struct stepcompress *sc; +}; + +struct stepcompress * __visible +syncemitter_get_stepcompress(struct syncemitter *se) +{ + return se->sc; +} + + /**************************************************************** * StepperSync - sort move queue for a micro-controller ****************************************************************/ @@ -30,9 +48,8 @@ struct steppersync { // Serial port struct serialqueue *sq; struct command_queue *cq; - // Storage for associated stepcompress objects - struct stepcompress **sc_list; - int sc_num; + // The syncemitters that generate messages on this mcu + struct list_head se_list; // Convert from time to clock struct clock_estimate ce; // Storage for list of pending move clocks @@ -40,37 +57,33 @@ struct steppersync { int num_move_clocks; }; -// Allocate a new 'steppersync' object -static struct steppersync * -steppersync_alloc(struct serialqueue *sq, struct stepcompress **sc_list - , int sc_num, int move_num) +// Allocate a new syncemitter instance +struct syncemitter * __visible +steppersync_alloc_syncemitter(struct steppersync *ss, char name[16] + , int alloc_stepcompress) { - struct steppersync *ss = malloc(sizeof(*ss)); - memset(ss, 0, sizeof(*ss)); + struct syncemitter *se = malloc(sizeof(*se)); + memset(se, 0, sizeof(*se)); + list_add_tail(&se->ss_node, &ss->se_list); + if (alloc_stepcompress) + se->sc = stepcompress_alloc(name); + return se; +} + +// Fill information on mcu move queue +void __visible +steppersync_setup_movequeue(struct steppersync *ss, struct serialqueue *sq + , int move_num) +{ + serialqueue_free_commandqueue(ss->cq); + free(ss->move_clocks); + ss->sq = sq; ss->cq = serialqueue_alloc_commandqueue(); - ss->sc_list = malloc(sizeof(*sc_list)*sc_num); - memcpy(ss->sc_list, sc_list, sizeof(*sc_list)*sc_num); - ss->sc_num = sc_num; - ss->move_clocks = malloc(sizeof(*ss->move_clocks)*move_num); memset(ss->move_clocks, 0, sizeof(*ss->move_clocks)*move_num); ss->num_move_clocks = move_num; - - return ss; -} - -// Free memory associated with a 'steppersync' object -static void -steppersync_free(struct steppersync *ss) -{ - if (!ss) - return; - free(ss->sc_list); - free(ss->move_clocks); - serialqueue_free_commandqueue(ss->cq); - free(ss); } // Set the conversion rate of 'print_time' to mcu clock @@ -79,10 +92,10 @@ steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq) { clock_fill(&ss->ce, mcu_freq, time_offset, 0, 0); - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_set_time(sc, time_offset, mcu_freq); + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + if (se->sc) + stepcompress_set_time(se->sc, time_offset, mcu_freq); } } @@ -122,10 +135,9 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock) // Find message with lowest reqclock uint64_t req_clock = MAX_CLOCK; struct queue_message *qm = NULL; - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - struct list_head *sc_mq = stepcompress_get_msg_queue(sc); + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + struct list_head *sc_mq = stepcompress_get_msg_queue(se->sc); if (!list_empty(sc_mq)) { struct queue_message *m = list_first_entry( sc_mq, struct queue_message, node); @@ -186,18 +198,27 @@ steppersyncmgr_free(struct steppersyncmgr *ssm) struct steppersync *ss = list_first_entry( &ssm->ss_list, struct steppersync, ssm_node); list_del(&ss->ssm_node); - steppersync_free(ss); + free(ss->move_clocks); + serialqueue_free_commandqueue(ss->cq); + while (!list_empty(&ss->se_list)) { + struct syncemitter *se = list_first_entry( + &ss->se_list, struct syncemitter, ss_node); + list_del(&se->ss_node); + stepcompress_free(se->sc); + free(se); + } + free(ss); } free(ssm); } // Allocate a new 'steppersync' object struct steppersync * __visible -steppersyncmgr_alloc_steppersync( - struct steppersyncmgr *ssm, struct serialqueue *sq - , struct stepcompress **sc_list, int sc_num, int move_num) +steppersyncmgr_alloc_steppersync(struct steppersyncmgr *ssm) { - struct steppersync *ss = steppersync_alloc(sq, sc_list, sc_num, move_num); + struct steppersync *ss = malloc(sizeof(*ss)); + memset(ss, 0, sizeof(*ss)); + list_init(&ss->se_list); list_add_tail(&ss->ssm_node, &ssm->ss_list); return ss; } @@ -211,19 +232,21 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time // Start step generation threads list_for_each_entry(ss, &ssm->ss_list, ssm_node) { uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_start_gen_steps(sc, gen_steps_time, flush_clock); + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + if (!se->sc) + continue; + stepcompress_start_gen_steps(se->sc, gen_steps_time, flush_clock); } } // Wait for step generation threads to complete int32_t res = 0; list_for_each_entry(ss, &ssm->ss_list, ssm_node) { - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - int32_t ret = stepcompress_finalize_gen_steps(sc); + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + if (!se->sc) + continue; + int32_t ret = stepcompress_finalize_gen_steps(se->sc); if (ret) res = ret; } @@ -237,10 +260,11 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time // Clear history list_for_each_entry(ss, &ssm->ss_list, ssm_node) { uint64_t end_clock = clock_from_time(&ss->ce, clear_history_time); - int i; - for (i = 0; i < ss->sc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_history_expire(sc, end_clock); + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + if (!se->sc) + continue; + stepcompress_history_expire(se->sc, end_clock); } } return 0; diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index f7a89230d..f42d3c85d 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -3,7 +3,14 @@ #include // uint64_t +struct syncemitter; +struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); + struct steppersync; +struct syncemitter *steppersync_alloc_syncemitter( + struct steppersync *ss, char name[16], int alloc_stepcompress); +void steppersync_setup_movequeue(struct steppersync *ss, struct serialqueue *sq + , int move_num); void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); @@ -11,8 +18,7 @@ struct steppersyncmgr *steppersyncmgr_alloc(void); void steppersyncmgr_free(struct steppersyncmgr *ssm); struct serialqueue; struct steppersync *steppersyncmgr_alloc_steppersync( - struct steppersyncmgr *ssm, struct serialqueue *sq - , struct stepcompress **sc_list, int sc_num, int move_num); + struct steppersyncmgr *ssm); int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time , double gen_steps_time , double clear_history_time); diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 8fa562621..da1a5a33b 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -31,7 +31,7 @@ class PrinterMotionQueuing: # C steppersync tracking self.steppersyncmgr = ffi_main.gc(ffi_lib.steppersyncmgr_alloc(), ffi_lib.steppersyncmgr_free) - self.stepcompress = [] + self.syncemitters = [] self.steppersyncs = [] self.steppersyncmgr_gen_steps = ffi_lib.steppersyncmgr_gen_steps # History expiration @@ -69,24 +69,26 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append # C steppersync tracking + def _lookup_steppersync(self, mcu): + for ss_mcu, ss in self.steppersyncs: + if ss_mcu is mcu: + return ss + ffi_main, ffi_lib = chelper.get_ffi() + ss = ffi_lib.steppersyncmgr_alloc_steppersync(self.steppersyncmgr) + self.steppersyncs.append((mcu, ss)) + return ss def allocate_stepcompress(self, mcu, name): name = name.encode("utf-8")[:15] + ss = self._lookup_steppersync(mcu) ffi_main, ffi_lib = chelper.get_ffi() - sc = ffi_main.gc(ffi_lib.stepcompress_alloc(name), - ffi_lib.stepcompress_free) - self.stepcompress.append((mcu, sc)) - return sc + se = ffi_lib.steppersync_alloc_syncemitter(ss, name, True) + self.syncemitters.append(se) + return ffi_lib.syncemitter_get_stepcompress(se) def setup_mcu_movequeue(self, mcu, serialqueue, move_count): # Setup steppersync object for the mcu's main movequeue - stepqueues = [] - for sc_mcu, sc in self.stepcompress: - if sc_mcu is mcu: - stepqueues.append(sc) ffi_main, ffi_lib = chelper.get_ffi() - ss = ffi_lib.steppersyncmgr_alloc_steppersync( - self.steppersyncmgr, serialqueue, stepqueues, len(stepqueues), - move_count) - self.steppersyncs.append((mcu, ss)) + ss = self._lookup_steppersync(mcu) + ffi_lib.steppersync_setup_movequeue(ss, serialqueue, move_count) mcu_freq = float(mcu.seconds_to_clock(1.)) ffi_lib.steppersync_set_time(ss, 0., mcu_freq) def stats(self, eventtime): @@ -117,7 +119,10 @@ class PrinterMotionQueuing: def check_step_generation_scan_windows(self): ffi_main, ffi_lib = chelper.get_ffi() kin_flush_delay = SDS_CHECK_TIME - for mcu, sc in self.stepcompress: + for se in self.syncemitters: + sc = ffi_lib.syncemitter_get_stepcompress(se) + if sc == ffi_main.NULL: + continue sk = ffi_lib.stepcompress_get_stepper_kinematics(sc) if sk == ffi_main.NULL: continue From e78d11bc6fe9d78c5c108e056d28cc2525ed66a9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 21:03:39 -0400 Subject: [PATCH 053/117] steppersync: Support sending messages directly from syncemitter Move msg_queue allocation from stepcompress to syncemitter. With this change the pwm_tool module does not need to allocate a stepcompress object. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 4 ++-- klippy/chelper/stepcompress.c | 34 ++++++--------------------------- klippy/chelper/stepcompress.h | 7 +++---- klippy/chelper/steppersync.c | 22 +++++++++++++++++---- klippy/chelper/steppersync.h | 2 ++ klippy/extras/motion_queuing.py | 6 +++--- klippy/extras/pwm_tool.py | 10 ++++------ klippy/stepper.py | 3 ++- 8 files changed, 40 insertions(+), 48 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index e7340b204..c0324a07c 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -48,8 +48,6 @@ defs_stepcompress = """ , uint64_t clock); int stepcompress_queue_msg(struct stepcompress *sc , uint32_t *data, int len); - int stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock - , uint32_t *data, int len); int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); @@ -61,6 +59,8 @@ defs_stepcompress = """ defs_steppersync = """ struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); + void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock + , uint32_t *data, int len); struct syncemitter *steppersync_alloc_syncemitter(struct steppersync *ss , char name[16], int alloc_stepcompress); void steppersync_setup_movequeue(struct steppersync *ss diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 1c74380fb..1426d963e 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -38,7 +38,7 @@ struct stepcompress { double mcu_time_offset, mcu_freq, last_step_print_time; // Message generation uint64_t last_step_clock; - struct list_head msg_queue; + struct list_head *msg_queue; uint32_t oid; int32_t queue_step_msgtag, set_next_step_dir_msgtag; int sdir, invert_sdir; @@ -258,13 +258,13 @@ static void sc_thread_free(struct stepcompress *sc); // Allocate a new 'stepcompress' object struct stepcompress * -stepcompress_alloc(char name[16]) +stepcompress_alloc(char name[16], struct list_head *msg_queue) { struct stepcompress *sc = malloc(sizeof(*sc)); memset(sc, 0, sizeof(*sc)); - list_init(&sc->msg_queue); list_init(&sc->history_list); sc->sdir = -1; + sc->msg_queue = msg_queue; int ret = sc_thread_alloc(sc, name); if (ret) @@ -317,7 +317,6 @@ stepcompress_free(struct stepcompress *sc) return; sc_thread_free(sc); free(sc->queue); - message_queue_free(&sc->msg_queue); stepcompress_history_expire(sc, UINT64_MAX); free(sc); } @@ -334,12 +333,6 @@ stepcompress_get_step_dir(struct stepcompress *sc) return sc->next_step_dir; } -struct list_head * -stepcompress_get_msg_queue(struct stepcompress *sc) -{ - return &sc->msg_queue; -} - // Determine the "print time" of the last_step_clock static void calc_last_step_print_time(struct stepcompress *sc) @@ -377,7 +370,7 @@ add_move(struct stepcompress *sc, uint64_t first_clock, struct step_move *move) qm->min_clock = qm->req_clock = sc->last_step_clock; if (move->count == 1 && first_clock >= sc->last_step_clock + CLOCK_DIFF_MAX) qm->req_clock = first_clock; - list_add_tail(&qm->node, &sc->msg_queue); + list_add_tail(&qm->node, sc->msg_queue); sc->last_step_clock = last_clock; // Create and store move in history tracking @@ -441,7 +434,7 @@ set_next_step_dir(struct stepcompress *sc, int sdir) }; struct queue_message *qm = message_alloc_and_encode(msg, 3); qm->req_clock = sc->last_step_clock; - list_add_tail(&qm->node, &sc->msg_queue); + list_add_tail(&qm->node, sc->msg_queue); return 0; } @@ -640,22 +633,7 @@ stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len) struct queue_message *qm = message_alloc_and_encode(data, len); qm->req_clock = sc->last_step_clock; - list_add_tail(&qm->node, &sc->msg_queue); - return 0; -} - -// Queue an mcu command that will consume space in the mcu move queue -int __visible -stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock - , uint32_t *data, int len) -{ - int ret = stepcompress_flush(sc, UINT64_MAX); - if (ret) - return ret; - - struct queue_message *qm = message_alloc_and_encode(data, len); - qm->min_clock = qm->req_clock = req_clock; - list_add_tail(&qm->node, &sc->msg_queue); + list_add_tail(&qm->node, sc->msg_queue); return 0; } diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 413446daf..25521fa0c 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -11,7 +11,9 @@ struct pull_history_steps { int step_count, interval, add; }; -struct stepcompress *stepcompress_alloc(char name[16]); +struct list_head; +struct stepcompress *stepcompress_alloc(char name[16] + , struct list_head *msg_queue); void stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error , int32_t queue_step_msgtag , int32_t set_next_step_dir_msgtag); @@ -21,7 +23,6 @@ void stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock); void stepcompress_free(struct stepcompress *sc); uint32_t stepcompress_get_oid(struct stepcompress *sc); int stepcompress_get_step_dir(struct stepcompress *sc); -struct list_head *stepcompress_get_msg_queue(struct stepcompress *sc); void stepcompress_set_time(struct stepcompress *sc , double time_offset, double mcu_freq); int stepcompress_append(struct stepcompress *sc, int sdir @@ -33,8 +34,6 @@ int stepcompress_set_last_position(struct stepcompress *sc, uint64_t clock int64_t stepcompress_find_past_position(struct stepcompress *sc , uint64_t clock); int stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len); -int stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock - , uint32_t *data, int len); int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 047fde3c3..bf3a55393 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -27,16 +27,29 @@ struct syncemitter { // List node for storage in steppersync list struct list_node ss_node; + // Transmit message queue + struct list_head msg_queue; // Step compression and generation struct stepcompress *sc; }; +// Return this emitters 'struct stepcompress' (or NULL if not allocated) struct stepcompress * __visible syncemitter_get_stepcompress(struct syncemitter *se) { return se->sc; } +// Queue an mcu command that will consume space in the mcu move queue +void __visible +syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock + , uint32_t *data, int len) +{ + struct queue_message *qm = message_alloc_and_encode(data, len); + qm->min_clock = qm->req_clock = req_clock; + list_add_tail(&qm->node, &se->msg_queue); +} + /**************************************************************** * StepperSync - sort move queue for a micro-controller @@ -65,8 +78,9 @@ steppersync_alloc_syncemitter(struct steppersync *ss, char name[16] struct syncemitter *se = malloc(sizeof(*se)); memset(se, 0, sizeof(*se)); list_add_tail(&se->ss_node, &ss->se_list); + list_init(&se->msg_queue); if (alloc_stepcompress) - se->sc = stepcompress_alloc(name); + se->sc = stepcompress_alloc(name, &se->msg_queue); return se; } @@ -137,10 +151,9 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock) struct queue_message *qm = NULL; struct syncemitter *se; list_for_each_entry(se, &ss->se_list, ss_node) { - struct list_head *sc_mq = stepcompress_get_msg_queue(se->sc); - if (!list_empty(sc_mq)) { + if (!list_empty(&se->msg_queue)) { struct queue_message *m = list_first_entry( - sc_mq, struct queue_message, node); + &se->msg_queue, struct queue_message, node); if (m->req_clock < req_clock) { qm = m; req_clock = m->req_clock; @@ -205,6 +218,7 @@ steppersyncmgr_free(struct steppersyncmgr *ssm) &ss->se_list, struct syncemitter, ss_node); list_del(&se->ss_node); stepcompress_free(se->sc); + message_queue_free(&se->msg_queue); free(se); } free(ss); diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index f42d3c85d..6acbef28c 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -5,6 +5,8 @@ struct syncemitter; struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); +void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock + , uint32_t *data, int len); struct steppersync; struct syncemitter *steppersync_alloc_syncemitter( diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index da1a5a33b..459b6fa72 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -77,13 +77,13 @@ class PrinterMotionQueuing: ss = ffi_lib.steppersyncmgr_alloc_steppersync(self.steppersyncmgr) self.steppersyncs.append((mcu, ss)) return ss - def allocate_stepcompress(self, mcu, name): + def allocate_syncemitter(self, mcu, name, alloc_stepcompress=True): name = name.encode("utf-8")[:15] ss = self._lookup_steppersync(mcu) ffi_main, ffi_lib = chelper.get_ffi() - se = ffi_lib.steppersync_alloc_syncemitter(ss, name, True) + se = ffi_lib.steppersync_alloc_syncemitter(ss, name, alloc_stepcompress) self.syncemitters.append(se) - return ffi_lib.syncemitter_get_stepcompress(se) + return se def setup_mcu_movequeue(self, mcu, serialqueue, move_count): # Setup steppersync object for the mcu's main movequeue ffi_main, ffi_lib = chelper.get_ffi() diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index dfa5c682b..a069dd2d8 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -18,9 +18,10 @@ class MCU_queued_pwm: printer = mcu.get_printer() sname = config.get_name().split()[-1] self._motion_queuing = printer.load_object(config, 'motion_queuing') - self._stepqueue = self._motion_queuing.allocate_stepcompress(mcu, sname) + self._syncemitter = self._motion_queuing.allocate_syncemitter( + mcu, sname, alloc_stepcompress=False) ffi_main, ffi_lib = chelper.get_ffi() - self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg + self._syncemitter_queue_msg = ffi_lib.syncemitter_queue_msg mcu.register_config_callback(self._build_config) self._pin = pin_params['pin'] self._invert = pin_params['invert'] @@ -107,10 +108,7 @@ class MCU_queued_pwm: self._last_clock = clock = max(self._last_clock, clock) self._last_value = val data = (self._set_cmd_tag, self._oid, clock & 0xffffffff, val) - ret = self._stepcompress_queue_mq_msg(self._stepqueue, clock, - data, len(data)) - if ret: - raise error("Internal error in stepcompress") + self._syncemitter_queue_msg(self._syncemitter, clock, data, len(data)) # Notify toolhead so that it will flush this update wakeclock = clock if self._last_value != self._default_value: diff --git a/klippy/stepper.py b/klippy/stepper.py index deb73769e..5fc20d0fa 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -45,8 +45,9 @@ class MCU_stepper: self._active_callbacks = [] motion_queuing = printer.load_object(config, 'motion_queuing') sname = self._name.split()[-1] - self._stepqueue = motion_queuing.allocate_stepcompress(mcu, sname) + syncemitter = motion_queuing.allocate_syncemitter(mcu, sname) ffi_main, ffi_lib = chelper.get_ffi() + self._stepqueue = ffi_lib.syncemitter_get_stepcompress(syncemitter) ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir) self._stepper_kinematics = None self._itersolve_check_active = ffi_lib.itersolve_check_active From 3ef4702e06882a5a53f99ba4cf860f958fad6725 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 21:49:12 -0400 Subject: [PATCH 054/117] steppersync: Move step generation thread from stepcompress.c to steppersync.c Implement step generation from 'struct syncemitter' instead of in the stepcompress code. This simplifies the stepcompress code and simplifies the overall interface. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 8 +- klippy/chelper/stepcompress.c | 152 +--------------------------- klippy/chelper/stepcompress.h | 12 +-- klippy/chelper/steppersync.c | 173 +++++++++++++++++++++++++++++--- klippy/chelper/steppersync.h | 4 + klippy/extras/motion_queuing.py | 5 +- klippy/stepper.py | 7 +- 7 files changed, 174 insertions(+), 187 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index c0324a07c..1c1f73b99 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -51,14 +51,14 @@ defs_stepcompress = """ int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); - void stepcompress_set_stepper_kinematics(struct stepcompress *sc - , struct stepper_kinematics *sk); - struct stepper_kinematics *stepcompress_get_stepper_kinematics( - struct stepcompress *sc); """ defs_steppersync = """ struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); + void syncemitter_set_stepper_kinematics(struct syncemitter *se + , struct stepper_kinematics *sk); + struct stepper_kinematics *syncemitter_get_stepper_kinematics( + struct syncemitter *se); void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock , uint32_t *data, int len); struct syncemitter *steppersync_alloc_syncemitter(struct steppersync *ss diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 1426d963e..0f2c4ef74 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -15,14 +15,12 @@ // efficiency - the repetitive integer math is vastly faster in C. #include // sqrt -#include // pthread_mutex_lock #include // offsetof #include // uint32_t #include // fprintf #include // malloc #include // memset #include "compiler.h" // DIV_ROUND_UP -#include "itersolve.h" // itersolve_generate_steps #include "pyhelper.h" // errorf #include "serialqueue.h" // struct queue_message #include "stepcompress.h" // stepcompress_alloc @@ -48,16 +46,6 @@ struct stepcompress { // History tracking int64_t last_position; struct list_head history_list; - // Thread for step generation - struct stepper_kinematics *sk; - char name[16]; - pthread_t tid; - pthread_mutex_t lock; // protects variables below - pthread_cond_t cond; - int have_work; - double bg_gen_steps_time; - uint64_t bg_flush_clock; - int32_t bg_result; }; struct step_move { @@ -253,22 +241,15 @@ check_line(struct stepcompress *sc, struct step_move move) * Step compress interface ****************************************************************/ -static int sc_thread_alloc(struct stepcompress *sc, char name[16]); -static void sc_thread_free(struct stepcompress *sc); - // Allocate a new 'stepcompress' object struct stepcompress * -stepcompress_alloc(char name[16], struct list_head *msg_queue) +stepcompress_alloc(struct list_head *msg_queue) { struct stepcompress *sc = malloc(sizeof(*sc)); memset(sc, 0, sizeof(*sc)); list_init(&sc->history_list); sc->sdir = -1; sc->msg_queue = msg_queue; - - int ret = sc_thread_alloc(sc, name); - if (ret) - return NULL; return sc; } @@ -315,7 +296,6 @@ stepcompress_free(struct stepcompress *sc) { if (!sc) return; - sc_thread_free(sc); free(sc->queue); stepcompress_history_expire(sc, UINT64_MAX); free(sc); @@ -551,7 +531,7 @@ stepcompress_commit(struct stepcompress *sc) } // Flush pending steps -static int +int stepcompress_flush(struct stepcompress *sc, uint64_t move_clock) { if (sc->next_step_clock && move_clock >= sc->next_step_clock) { @@ -660,131 +640,3 @@ stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p } return res; } - - -/**************************************************************** - * Step generation thread - ****************************************************************/ - -// Store a reference to stepper_kinematics -void __visible -stepcompress_set_stepper_kinematics(struct stepcompress *sc - , struct stepper_kinematics *sk) -{ - sc->sk = sk; -} - -// Report current stepper_kinematics -struct stepper_kinematics * __visible -stepcompress_get_stepper_kinematics(struct stepcompress *sc) -{ - return sc->sk; -} - -// Generate steps (via itersolve) and flush -static int32_t -stepcompress_generate_steps(struct stepcompress *sc, double gen_steps_time - , uint64_t flush_clock) -{ - if (!sc->sk) - return 0; - // Generate steps - int32_t ret = itersolve_generate_steps(sc->sk, sc, gen_steps_time); - if (ret) - return ret; - // Flush steps - return stepcompress_flush(sc, flush_clock); -} - -// Main background thread for generating steps -static void * -sc_background_thread(void *data) -{ - struct stepcompress *sc = data; - set_thread_name(sc->name); - - pthread_mutex_lock(&sc->lock); - for (;;) { - if (!sc->have_work) { - pthread_cond_wait(&sc->cond, &sc->lock); - continue; - } - if (sc->have_work < 0) - // Exit request - break; - - // Request to generate steps - sc->bg_result = stepcompress_generate_steps(sc, sc->bg_gen_steps_time - , sc->bg_flush_clock); - sc->have_work = 0; - pthread_cond_signal(&sc->cond); - } - pthread_mutex_unlock(&sc->lock); - - return NULL; -} - -// Signal background thread to start step generation -void -stepcompress_start_gen_steps(struct stepcompress *sc, double gen_steps_time - , uint64_t flush_clock) -{ - if (!sc->sk) - return; - pthread_mutex_lock(&sc->lock); - while (sc->have_work) - pthread_cond_wait(&sc->cond, &sc->lock); - sc->bg_gen_steps_time = gen_steps_time; - sc->bg_flush_clock = flush_clock; - sc->have_work = 1; - pthread_mutex_unlock(&sc->lock); - pthread_cond_signal(&sc->cond); -} - -// Wait for background thread to complete last step generation request -int32_t -stepcompress_finalize_gen_steps(struct stepcompress *sc) -{ - pthread_mutex_lock(&sc->lock); - while (sc->have_work) - pthread_cond_wait(&sc->cond, &sc->lock); - int32_t res = sc->bg_result; - pthread_mutex_unlock(&sc->lock); - return res; -} - -// Internal helper to start thread -static int -sc_thread_alloc(struct stepcompress *sc, char name[16]) -{ - strncpy(sc->name, name, sizeof(sc->name)); - sc->name[sizeof(sc->name)-1] = '\0'; - int ret = pthread_mutex_init(&sc->lock, NULL); - if (ret) - goto fail; - ret = pthread_cond_init(&sc->cond, NULL); - if (ret) - goto fail; - ret = pthread_create(&sc->tid, NULL, sc_background_thread, sc); - if (ret) - goto fail; - return 0; -fail: - report_errno("sc init", ret); - return -1; -} - -// Request background thread to exit -static void -sc_thread_free(struct stepcompress *sc) -{ - pthread_mutex_lock(&sc->lock); - while (sc->have_work) - pthread_cond_wait(&sc->cond, &sc->lock); - sc->have_work = -1; - pthread_cond_signal(&sc->cond); - pthread_mutex_unlock(&sc->lock); - int ret = pthread_join(sc->tid, NULL); - if (ret) - report_errno("sc pthread_join", ret); -} diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 25521fa0c..91d1bc11d 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -12,8 +12,7 @@ struct pull_history_steps { }; struct list_head; -struct stepcompress *stepcompress_alloc(char name[16] - , struct list_head *msg_queue); +struct stepcompress *stepcompress_alloc(struct list_head *msg_queue); void stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error , int32_t queue_step_msgtag , int32_t set_next_step_dir_msgtag); @@ -28,6 +27,7 @@ void stepcompress_set_time(struct stepcompress *sc int stepcompress_append(struct stepcompress *sc, int sdir , double print_time, double step_time); int stepcompress_commit(struct stepcompress *sc); +int stepcompress_flush(struct stepcompress *sc, uint64_t move_clock); int stepcompress_reset(struct stepcompress *sc, uint64_t last_step_clock); int stepcompress_set_last_position(struct stepcompress *sc, uint64_t clock , int64_t last_position); @@ -37,13 +37,5 @@ int stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len); int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); -struct stepper_kinematics; -void stepcompress_set_stepper_kinematics(struct stepcompress *sc - , struct stepper_kinematics *sk); -struct stepper_kinematics *stepcompress_get_stepper_kinematics( - struct stepcompress *sc); -void stepcompress_start_gen_steps(struct stepcompress *sc, double gen_steps_time - , uint64_t flush_clock); -int32_t stepcompress_finalize_gen_steps(struct stepcompress *sc); #endif // stepcompress.h diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index bf3a55393..1c2a4a73d 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -11,10 +11,13 @@ // mcu step queue is ordered between steppers so that no stepper // starves the other steppers of space in the mcu step queue. +#include // pthread_mutex_lock #include // offsetof #include // malloc #include // memset #include "compiler.h" // __visible +#include "pyhelper.h" // set_thread_name +#include "itersolve.h" // itersolve_generate_steps #include "serialqueue.h" // struct queue_message #include "stepcompress.h" // stepcompress_flush #include "steppersync.h" // steppersync_alloc @@ -29,8 +32,17 @@ struct syncemitter { struct list_node ss_node; // Transmit message queue struct list_head msg_queue; - // Step compression and generation + // Thread for step generation struct stepcompress *sc; + struct stepper_kinematics *sk; + char name[16]; + pthread_t tid; + pthread_mutex_t lock; // protects variables below + pthread_cond_t cond; + int have_work; + double bg_gen_steps_time; + uint64_t bg_flush_clock; + int32_t bg_result; }; // Return this emitters 'struct stepcompress' (or NULL if not allocated) @@ -40,6 +52,21 @@ syncemitter_get_stepcompress(struct syncemitter *se) return se->sc; } +// Store a reference to stepper_kinematics +void __visible +syncemitter_set_stepper_kinematics(struct syncemitter *se + , struct stepper_kinematics *sk) +{ + se->sk = sk; +} + +// Report current stepper_kinematics +struct stepper_kinematics * __visible +syncemitter_get_stepper_kinematics(struct syncemitter *se) +{ + return se->sk; +} + // Queue an mcu command that will consume space in the mcu move queue void __visible syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock @@ -50,6 +77,129 @@ syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock list_add_tail(&qm->node, &se->msg_queue); } +// Generate steps (via itersolve) and flush +static int32_t +se_generate_steps(struct syncemitter *se, double gen_steps_time + , uint64_t flush_clock) +{ + if (!se->sc || !se->sk) + return 0; + // Generate steps + int32_t ret = itersolve_generate_steps(se->sk, se->sc, gen_steps_time); + if (ret) + return ret; + // Flush steps + return stepcompress_flush(se->sc, flush_clock); +} + +// Main background thread for generating steps +static void * +se_background_thread(void *data) +{ + struct syncemitter *se = data; + set_thread_name(se->name); + + pthread_mutex_lock(&se->lock); + for (;;) { + if (!se->have_work) { + pthread_cond_wait(&se->cond, &se->lock); + continue; + } + if (se->have_work < 0) + // Exit request + break; + + // Request to generate steps + se->bg_result = se_generate_steps(se, se->bg_gen_steps_time + , se->bg_flush_clock); + se->have_work = 0; + pthread_cond_signal(&se->cond); + } + pthread_mutex_unlock(&se->lock); + + return NULL; +} + +// Signal background thread to start step generation +static void +se_start_gen_steps(struct syncemitter *se, double gen_steps_time + , uint64_t flush_clock) +{ + if (!se->sc || !se->sk) + return; + pthread_mutex_lock(&se->lock); + while (se->have_work) + pthread_cond_wait(&se->cond, &se->lock); + se->bg_gen_steps_time = gen_steps_time; + se->bg_flush_clock = flush_clock; + se->have_work = 1; + pthread_mutex_unlock(&se->lock); + pthread_cond_signal(&se->cond); +} + +// Wait for background thread to complete last step generation request +static int32_t +se_finalize_gen_steps(struct syncemitter *se) +{ + if (!se->sc || !se->sk) + return 0; + pthread_mutex_lock(&se->lock); + while (se->have_work) + pthread_cond_wait(&se->cond, &se->lock); + int32_t res = se->bg_result; + pthread_mutex_unlock(&se->lock); + return res; +} + +// Allocate syncemitter and start thread +static struct syncemitter * +syncemitter_alloc(char name[16], int alloc_stepcompress) +{ + struct syncemitter *se = malloc(sizeof(*se)); + memset(se, 0, sizeof(*se)); + list_init(&se->msg_queue); + strncpy(se->name, name, sizeof(se->name)); + se->name[sizeof(se->name)-1] = '\0'; + if (!alloc_stepcompress) + return se; + se->sc = stepcompress_alloc(&se->msg_queue); + int ret = pthread_mutex_init(&se->lock, NULL); + if (ret) + goto fail; + ret = pthread_cond_init(&se->cond, NULL); + if (ret) + goto fail; + ret = pthread_create(&se->tid, NULL, se_background_thread, se); + if (ret) + goto fail; + return se; +fail: + report_errno("se alloc", ret); + return NULL; +} + +// Free syncemitter and exit background thread +static void +syncemitter_free(struct syncemitter *se) +{ + if (!se) + return; + if (se->sc) { + pthread_mutex_lock(&se->lock); + while (se->have_work) + pthread_cond_wait(&se->cond, &se->lock); + se->have_work = -1; + pthread_cond_signal(&se->cond); + pthread_mutex_unlock(&se->lock); + int ret = pthread_join(se->tid, NULL); + if (ret) + report_errno("se pthread_join", ret); + stepcompress_free(se->sc); + } + message_queue_free(&se->msg_queue); + free(se); +} + /**************************************************************** * StepperSync - sort move queue for a micro-controller @@ -75,12 +225,9 @@ struct syncemitter * __visible steppersync_alloc_syncemitter(struct steppersync *ss, char name[16] , int alloc_stepcompress) { - struct syncemitter *se = malloc(sizeof(*se)); - memset(se, 0, sizeof(*se)); - list_add_tail(&se->ss_node, &ss->se_list); - list_init(&se->msg_queue); - if (alloc_stepcompress) - se->sc = stepcompress_alloc(name, &se->msg_queue); + struct syncemitter *se = syncemitter_alloc(name, alloc_stepcompress); + if (se) + list_add_tail(&se->ss_node, &ss->se_list); return se; } @@ -217,9 +364,7 @@ steppersyncmgr_free(struct steppersyncmgr *ssm) struct syncemitter *se = list_first_entry( &ss->se_list, struct syncemitter, ss_node); list_del(&se->ss_node); - stepcompress_free(se->sc); - message_queue_free(&se->msg_queue); - free(se); + syncemitter_free(se); } free(ss); } @@ -248,9 +393,7 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); struct syncemitter *se; list_for_each_entry(se, &ss->se_list, ss_node) { - if (!se->sc) - continue; - stepcompress_start_gen_steps(se->sc, gen_steps_time, flush_clock); + se_start_gen_steps(se, gen_steps_time, flush_clock); } } // Wait for step generation threads to complete @@ -258,9 +401,7 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time list_for_each_entry(ss, &ssm->ss_list, ssm_node) { struct syncemitter *se; list_for_each_entry(se, &ss->se_list, ss_node) { - if (!se->sc) - continue; - int32_t ret = stepcompress_finalize_gen_steps(se->sc); + int32_t ret = se_finalize_gen_steps(se); if (ret) res = ret; } diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index 6acbef28c..7ad6fcae6 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -5,6 +5,10 @@ struct syncemitter; struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); +void syncemitter_set_stepper_kinematics(struct syncemitter *se + , struct stepper_kinematics *sk); +struct stepper_kinematics *syncemitter_get_stepper_kinematics( + struct syncemitter *se); void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock , uint32_t *data, int len); diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 459b6fa72..3491545a8 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -120,10 +120,7 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() kin_flush_delay = SDS_CHECK_TIME for se in self.syncemitters: - sc = ffi_lib.syncemitter_get_stepcompress(se) - if sc == ffi_main.NULL: - continue - sk = ffi_lib.stepcompress_get_stepper_kinematics(sc) + sk = ffi_lib.syncemitter_get_stepper_kinematics(se) if sk == ffi_main.NULL: continue trapq = ffi_lib.itersolve_get_trapq(sk) diff --git a/klippy/stepper.py b/klippy/stepper.py index 5fc20d0fa..3bcca565c 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -45,9 +45,10 @@ class MCU_stepper: self._active_callbacks = [] motion_queuing = printer.load_object(config, 'motion_queuing') sname = self._name.split()[-1] - syncemitter = motion_queuing.allocate_syncemitter(mcu, sname) + self._syncemitter = motion_queuing.allocate_syncemitter(mcu, sname) ffi_main, ffi_lib = chelper.get_ffi() - self._stepqueue = ffi_lib.syncemitter_get_stepcompress(syncemitter) + self._stepqueue = ffi_lib.syncemitter_get_stepcompress( + self._syncemitter) ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir) self._stepper_kinematics = None self._itersolve_check_active = ffi_lib.itersolve_check_active @@ -194,7 +195,7 @@ class MCU_stepper: mcu_pos = self.get_mcu_position() self._stepper_kinematics = sk ffi_main, ffi_lib = chelper.get_ffi() - ffi_lib.stepcompress_set_stepper_kinematics(self._stepqueue, sk); + ffi_lib.syncemitter_set_stepper_kinematics(self._syncemitter, sk); self.set_trapq(self._trapq) self._set_mcu_position(mcu_pos) return old_sk From 414679ac99192371c95cd223b3393f0a15f3b949 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 22:03:29 -0400 Subject: [PATCH 055/117] steppersync: Move history clearing to background thread Signed-off-by: Kevin O'Connor --- klippy/chelper/steppersync.c | 38 ++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 1c2a4a73d..6a01083f1 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -41,7 +41,7 @@ struct syncemitter { pthread_cond_t cond; int have_work; double bg_gen_steps_time; - uint64_t bg_flush_clock; + uint64_t bg_flush_clock, bg_clear_history_clock; int32_t bg_result; }; @@ -79,17 +79,24 @@ syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock // Generate steps (via itersolve) and flush static int32_t -se_generate_steps(struct syncemitter *se, double gen_steps_time - , uint64_t flush_clock) +se_generate_steps(struct syncemitter *se) { if (!se->sc || !se->sk) return 0; + double gen_steps_time = se->bg_gen_steps_time; + uint64_t flush_clock = se->bg_flush_clock; + uint64_t clear_history_clock = se->bg_clear_history_clock; // Generate steps int32_t ret = itersolve_generate_steps(se->sk, se->sc, gen_steps_time); if (ret) return ret; // Flush steps - return stepcompress_flush(se->sc, flush_clock); + ret = stepcompress_flush(se->sc, flush_clock); + if (ret) + return ret; + // Clear history + stepcompress_history_expire(se->sc, clear_history_clock); + return 0; } // Main background thread for generating steps @@ -110,8 +117,7 @@ se_background_thread(void *data) break; // Request to generate steps - se->bg_result = se_generate_steps(se, se->bg_gen_steps_time - , se->bg_flush_clock); + se->bg_result = se_generate_steps(se); se->have_work = 0; pthread_cond_signal(&se->cond); } @@ -123,7 +129,7 @@ se_background_thread(void *data) // Signal background thread to start step generation static void se_start_gen_steps(struct syncemitter *se, double gen_steps_time - , uint64_t flush_clock) + , uint64_t flush_clock, uint64_t clear_history_clock) { if (!se->sc || !se->sk) return; @@ -132,6 +138,7 @@ se_start_gen_steps(struct syncemitter *se, double gen_steps_time pthread_cond_wait(&se->cond, &se->lock); se->bg_gen_steps_time = gen_steps_time; se->bg_flush_clock = flush_clock; + se->bg_clear_history_clock = clear_history_clock; se->have_work = 1; pthread_mutex_unlock(&se->lock); pthread_cond_signal(&se->cond); @@ -391,9 +398,10 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time // Start step generation threads list_for_each_entry(ss, &ssm->ss_list, ssm_node) { uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); + uint64_t clear_clock = clock_from_time(&ss->ce, clear_history_time); struct syncemitter *se; list_for_each_entry(se, &ss->se_list, ss_node) { - se_start_gen_steps(se, gen_steps_time, flush_clock); + se_start_gen_steps(se, gen_steps_time, flush_clock, clear_clock); } } // Wait for step generation threads to complete @@ -410,17 +418,5 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); steppersync_flush(ss, flush_clock); } - if (res) - return res; - // Clear history - list_for_each_entry(ss, &ssm->ss_list, ssm_node) { - uint64_t end_clock = clock_from_time(&ss->ce, clear_history_time); - struct syncemitter *se; - list_for_each_entry(se, &ss->se_list, ss_node) { - if (!se->sc) - continue; - stepcompress_history_expire(se->sc, end_clock); - } - } - return 0; + return res; } From 546976b1fe2eb3a7fa46042f1f0495d374e36174 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 22:08:53 -0400 Subject: [PATCH 056/117] steppersync: Print the thread name on a stepcompress error Signed-off-by: Kevin O'Connor --- klippy/chelper/steppersync.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 6a01083f1..4dede38fb 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -118,6 +118,8 @@ se_background_thread(void *data) // Request to generate steps se->bg_result = se_generate_steps(se); + if (se->bg_result) + errorf("Error in syncemitter '%s' step generation", se->name); se->have_work = 0; pthread_cond_signal(&se->cond); } From 56fb4d2b04242bb4e9f84f6cecb97a49cdbaca62 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 22:38:59 -0400 Subject: [PATCH 057/117] docs: Update Code_Overview.md to reflect recent steppersync changes Signed-off-by: Kevin O'Connor --- docs/Code_Overview.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index 6b5df5595..e8a61068c 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -190,18 +190,19 @@ provides further information on the mechanics of moves. stepper movements produced by the extruder class will be in sync with head movement even though the code is kept separate. -* Klipper uses an - [iterative solver](https://en.wikipedia.org/wiki/Root-finding_algorithm) - to generate the step times for each stepper. For efficiency reasons, - the stepper pulse times are generated in C code in a thread per - stepper motor. The threads are notified of new activity by the - motion_queuing module (klippy/extras/motion_queuing.py): +* For efficiency reasons, stepper motion is generated in the C code in + a thread per stepper motor. The threads are notified when steps + should be generated by the motion_queuing module + (klippy/extras/motion_queuing.py): `PrinterMotionQueuing._flush_handler() -> PrinterMotionQueuing._advance_move_time() -> - steppersync_start_gen_steps() -> - stepcompress_start_gen_steps()`. The step times are then generated - from that thread (klippy/chelper/stepcompress.c): - `sc_background_thread() -> stepcompress_generate_steps() -> + steppersyncmgr_gen_steps() -> se_start_gen_steps()`. + +* Klipper uses an + [iterative solver](https://en.wikipedia.org/wiki/Root-finding_algorithm) + to generate the step times for each stepper. The step times are + generated from the background thread (klippy/chelper/steppersync.c): + `se_background_thread() -> se_generate_steps() -> itersolve_generate_steps() -> itersolve_gen_steps_range()` (in klippy/chelper/itersolve.c). The goal of the iterative solver is to find step times given a function that calculates a stepper position @@ -228,7 +229,7 @@ provides further information on the mechanics of moves. commands that correspond to the list of stepper step times built in the previous stage. These "queue_step" commands are then queued, prioritized, and sent to the micro-controller (via - stepcompress.c:steppersync and serialqueue.c:serialqueue). + steppersync.c:steppersync and serialqueue.c:serialqueue). * Processing of the queue_step commands on the micro-controller starts in src/command.c which parses the command and calls From dfa666d9c1ef2cb9ce9545ba778bc71245fe2b95 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 00:01:24 -0400 Subject: [PATCH 058/117] stepcompress: Remove stepcompress_queue_msg() Callers can use syncemitter_queue_msg() instead. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 2 -- klippy/chelper/stepcompress.c | 14 -------------- klippy/chelper/stepcompress.h | 1 - klippy/stepper.py | 4 +--- 4 files changed, 1 insertion(+), 20 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 1c1f73b99..c26196e66 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -46,8 +46,6 @@ defs_stepcompress = """ , uint64_t clock, int64_t last_position); int64_t stepcompress_find_past_position(struct stepcompress *sc , uint64_t clock); - int stepcompress_queue_msg(struct stepcompress *sc - , uint32_t *data, int len); int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 0f2c4ef74..141c8ac35 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -603,20 +603,6 @@ stepcompress_find_past_position(struct stepcompress *sc, uint64_t clock) return last_position; } -// Queue an mcu command to go out in order with stepper commands -int __visible -stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len) -{ - int ret = stepcompress_flush(sc, UINT64_MAX); - if (ret) - return ret; - - struct queue_message *qm = message_alloc_and_encode(data, len); - qm->req_clock = sc->last_step_clock; - list_add_tail(&qm->node, sc->msg_queue); - return 0; -} - // Return history of queue_step commands int __visible stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 91d1bc11d..affc1e23c 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -33,7 +33,6 @@ int stepcompress_set_last_position(struct stepcompress *sc, uint64_t clock , int64_t last_position); int64_t stepcompress_find_past_position(struct stepcompress *sc , uint64_t clock); -int stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len); int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); diff --git a/klippy/stepper.py b/klippy/stepper.py index 3bcca565c..248e487d2 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -205,9 +205,7 @@ class MCU_stepper: if ret: raise error("Internal error in stepcompress") data = (self._reset_cmd_tag, self._oid, 0) - ret = ffi_lib.stepcompress_queue_msg(self._stepqueue, data, len(data)) - if ret: - raise error("Internal error in stepcompress") + ffi_lib.syncemitter_queue_msg(self._syncemitter, 0, data, len(data)) self._query_mcu_position() def _query_mcu_position(self): if self._mcu.is_fileoutput(): From 1da2e39b856eb4a334a59791936af9efab5b625f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 24 Sep 2025 15:30:46 -0400 Subject: [PATCH 059/117] docs: Update Code_Overview.md with recent motion generation changes Signed-off-by: Kevin O'Connor --- docs/Code_Overview.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index e8a61068c..56f98d600 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -129,7 +129,7 @@ There are several threads in the Klipper host code: the main Python thread. * A thread per stepper motor that calculates the timing of stepper motor step pulses and compresses those times. This thread resides in - the **klippy/chelper/stepcompress.c** C code and its operation is + the **klippy/chelper/steppersync.c** C code and its operation is generally not exposed to the Python code. ## Code flow of a move command @@ -151,9 +151,10 @@ provides further information on the mechanics of moves. * The ToolHead class (in toolhead.py) handles "look-ahead" and tracks the timing of printing actions. The main codepath for a move is: - `ToolHead.move() -> LookAheadQueue.add_move() -> - LookAheadQueue.flush() -> Move.set_junction() -> - ToolHead._process_moves() -> trapq_append()`. + `ToolHead.move() -> LookAheadQueue.add_move()`, then + `ToolHead.move() -> ToolHead._process_lookahead() -> + LookAheadQueue.flush() -> Move.set_junction()`, and then + `ToolHead._process_lookahead() -> trapq_append()`. * ToolHead.move() creates a Move() object with the parameters of the move (in cartesian space and in units of seconds and millimeters). * The kinematics class is given the opportunity to audit each move @@ -172,7 +173,7 @@ provides further information on the mechanics of moves. phase, followed by a constant deceleration phase. Every move contains these three phases in this order, but some phases may be of zero duration. - * When ToolHead._process_moves() is called, everything about the + * When ToolHead._process_lookahead() resumes, everything about the move is known - its start location, its end location, its acceleration, its start/cruising/end velocity, and distance traveled during acceleration/cruising/deceleration. All the information is @@ -184,9 +185,9 @@ provides further information on the mechanics of moves. C code. * Note that the extruder is handled in its own kinematic class: - `ToolHead._process_moves() -> PrinterExtruder.process_move()`. Since - the Move() class specifies the exact movement time and since step - pulses are sent to the micro-controller with specific timing, + `ToolHead._process_lookahead() -> PrinterExtruder.process_move()`. + Since the Move() class specifies the exact movement time and since + step pulses are sent to the micro-controller with specific timing, stepper movements produced by the extruder class will be in sync with head movement even though the code is kept separate. From 3c01f71d9e2b760cbbe567da90684af486731f95 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 24 Sep 2025 18:52:10 -0400 Subject: [PATCH 060/117] itersolve: Don't call trapq_check_sentinels() from itersolve_generate_steps() Commit a89694ac changed the code to run itersolve_generate_steps() from multiple threads simultaneously. However, trapq_check_sentinels() can modify the shared trapq object. So, calling it from multiple threads could introduce a race condition. Move the call to trapq_check_sentinels() to steppersyncmgr_gen_steps() to avoid the issue. Signed-off-by: Kevin O'Connor --- klippy/chelper/itersolve.c | 1 - klippy/chelper/steppersync.c | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/klippy/chelper/itersolve.c b/klippy/chelper/itersolve.c index 9b1206249..5b1683606 100644 --- a/klippy/chelper/itersolve.c +++ b/klippy/chelper/itersolve.c @@ -151,7 +151,6 @@ itersolve_generate_steps(struct stepper_kinematics *sk, struct stepcompress *sc sk->last_flush_time = flush_time; if (!sk->tq) return 0; - trapq_check_sentinels(sk->tq); struct move *m = list_first_entry(&sk->tq->moves, struct move, node); while (last_flush_time >= m->print_time + m->move_t) m = list_next_entry(m, node); diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 4dede38fb..6b571f4b6 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -21,6 +21,7 @@ #include "serialqueue.h" // struct queue_message #include "stepcompress.h" // stepcompress_flush #include "steppersync.h" // steppersync_alloc +#include "trapq.h" // trapq_check_sentinels /**************************************************************** @@ -397,6 +398,17 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time , double gen_steps_time, double clear_history_time) { struct steppersync *ss; + // Prepare trapqs for step generation + list_for_each_entry(ss, &ssm->ss_list, ssm_node) { + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + if (!se->sc || !se->sk) + continue; + struct trapq *tq = itersolve_get_trapq(se->sk); + if (tq) + trapq_check_sentinels(tq); + } + } // Start step generation threads list_for_each_entry(ss, &ssm->ss_list, ssm_node) { uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); From 8c7693c0482ce7da0cd2334017664b4bc44996fc Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 19 Sep 2025 22:29:57 +0200 Subject: [PATCH 061/117] uc1701: allow non blocking i2c writes Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/bus.py | 3 +++ klippy/extras/display/uc1701.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 9fb466390..b04fbe764 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -212,6 +212,9 @@ class MCU_I2C: "i2c_read oid=%c reg=%*s read_len=%u", "i2c_read_response oid=%c response=%*s", oid=self.oid, cq=self.cmd_queue) + def i2c_write_noack(self, data, minclock=0, reqclock=0): + self.i2c_write_cmd.send([self.oid, data], + minclock=minclock, reqclock=reqclock) def i2c_write(self, data, minclock=0, reqclock=0): if self.i2c_write_cmd is None: self._to_write.append(data) diff --git a/klippy/extras/display/uc1701.py b/klippy/extras/display/uc1701.py index 8e877cf2e..85b74decd 100644 --- a/klippy/extras/display/uc1701.py +++ b/klippy/extras/display/uc1701.py @@ -138,7 +138,7 @@ class I2C: hdr = 0x00 cmds = bytearray(cmds) cmds.insert(0, hdr) - self.i2c.i2c_write(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK) + self.i2c.i2c_write_noack(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK) # Helper code for toggling a reset pin on startup class ResetHelper: From 30a1f22e1d8feac8c7d3dd541105cdd4af255c5d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 26 Sep 2025 11:59:07 -0400 Subject: [PATCH 062/117] motion_queuing: Improve run to run stability of flushing when in debug mode Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 3491545a8..93f5594cd 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -224,8 +224,8 @@ class PrinterMotionQueuing: flush_count = 0 while self.last_step_gen_time < faux_time: target = self.last_step_gen_time + batch_time - if flush_count > 100.: - target = faux_time + if flush_count > 100. and faux_time > target: + target += int((faux_time-target) / batch_time) * batch_time self._advance_flush_time(0., target) flush_count += 1 if flush_count: From 870c0437e92e1de152078ba49d53420ec72d2f11 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 24 Sep 2025 14:22:24 -0400 Subject: [PATCH 063/117] stm32: Verify pin is valid in gpio_peripheral() Convert direct lookup of digital_regs[] to a new gpio_pin_to_regs() function that first validates the pin. This should help prevent invalid memory accesses if an invalid pin is provided. Signed-off-by: Kevin O'Connor --- src/stm32/gpio.c | 20 +++++++++----------- src/stm32/gpioperiph.c | 2 +- src/stm32/internal.h | 2 +- src/stm32/stm32f1.c | 2 +- src/stm32/stm32h7_gpio.c | 21 +++++++++------------ 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/stm32/gpio.c b/src/stm32/gpio.c index d19437cbe..0ef11822e 100644 --- a/src/stm32/gpio.c +++ b/src/stm32/gpio.c @@ -33,7 +33,7 @@ DECL_ENUMERATION_RANGE("pin", "PH0", GPIO('H', 0), 16); DECL_ENUMERATION_RANGE("pin", "PI0", GPIO('I', 0), 16); #endif -GPIO_TypeDef * const digital_regs[] = { +static GPIO_TypeDef * const digital_regs[] = { ['A' - 'A'] = GPIOA, GPIOB, GPIOC, #ifdef GPIOD ['D' - 'A'] = GPIOD, @@ -66,20 +66,20 @@ regs_to_pin(GPIO_TypeDef *regs, uint32_t bit) return 0; } -// Verify that a gpio is a valid pin -static int -gpio_valid(uint32_t pin) +// Verify that a gpio is a valid pin and return its hardware register +GPIO_TypeDef * +gpio_pin_to_regs(uint32_t pin) { uint32_t port = GPIO2PORT(pin); - return port < ARRAY_SIZE(digital_regs) && digital_regs[port]; + if (port >= ARRAY_SIZE(digital_regs) || !digital_regs[port]) + shutdown("Not a valid pin"); + return digital_regs[port]; } struct gpio_out gpio_out_setup(uint32_t pin, uint32_t val) { - if (!gpio_valid(pin)) - shutdown("Not an output pin"); - GPIO_TypeDef *regs = digital_regs[GPIO2PORT(pin)]; + GPIO_TypeDef *regs = gpio_pin_to_regs(pin); gpio_clock_enable(regs); struct gpio_out g = { .regs=regs, .bit=GPIO2BIT(pin) }; gpio_out_reset(g, val); @@ -129,9 +129,7 @@ gpio_out_write(struct gpio_out g, uint32_t val) struct gpio_in gpio_in_setup(uint32_t pin, int32_t pull_up) { - if (!gpio_valid(pin)) - shutdown("Not a valid input pin"); - GPIO_TypeDef *regs = digital_regs[GPIO2PORT(pin)]; + GPIO_TypeDef *regs = gpio_pin_to_regs(pin); struct gpio_in g = { .regs=regs, .bit=GPIO2BIT(pin) }; gpio_in_reset(g, pull_up); return g; diff --git a/src/stm32/gpioperiph.c b/src/stm32/gpioperiph.c index 10d03738e..84fbebf5c 100644 --- a/src/stm32/gpioperiph.c +++ b/src/stm32/gpioperiph.c @@ -10,7 +10,7 @@ void gpio_peripheral(uint32_t gpio, uint32_t mode, int pullup) { - GPIO_TypeDef *regs = digital_regs[GPIO2PORT(gpio)]; + GPIO_TypeDef *regs = gpio_pin_to_regs(gpio); // Enable GPIO clock gpio_clock_enable(regs); diff --git a/src/stm32/internal.h b/src/stm32/internal.h index 1d1e0a96b..a30bb6219 100644 --- a/src/stm32/internal.h +++ b/src/stm32/internal.h @@ -25,7 +25,7 @@ #endif // gpio.c -extern GPIO_TypeDef * const digital_regs[]; +GPIO_TypeDef *gpio_pin_to_regs(uint32_t pin); #define GPIO(PORT, NUM) (((PORT)-'A') * 16 + (NUM)) #define GPIO2PORT(PIN) ((PIN) / 16) #define GPIO2BIT(PIN) (1<<((PIN) % 16)) diff --git a/src/stm32/stm32f1.c b/src/stm32/stm32f1.c index 0e4cb7821..94cfe1390 100644 --- a/src/stm32/stm32f1.c +++ b/src/stm32/stm32f1.c @@ -115,7 +115,7 @@ stm32f1_alternative_remap(uint32_t mapr_mask, uint32_t mapr_value) void gpio_peripheral(uint32_t gpio, uint32_t mode, int pullup) { - GPIO_TypeDef *regs = digital_regs[GPIO2PORT(gpio)]; + GPIO_TypeDef *regs = gpio_pin_to_regs(gpio); // Enable GPIO clock gpio_clock_enable(regs); diff --git a/src/stm32/stm32h7_gpio.c b/src/stm32/stm32h7_gpio.c index f653c5708..78a1b4a45 100644 --- a/src/stm32/stm32h7_gpio.c +++ b/src/stm32/stm32h7_gpio.c @@ -33,7 +33,7 @@ DECL_ENUMERATION_RANGE("pin", "PH0", GPIO('H', 0), 16); DECL_ENUMERATION_RANGE("pin", "PI0", GPIO('I', 0), 16); #endif -GPIO_TypeDef * const digital_regs[] = { +static GPIO_TypeDef * const digital_regs[] = { ['A' - 'A'] = GPIOA, GPIOB, GPIOC, #ifdef GPIOD ['D' - 'A'] = GPIOD, @@ -66,12 +66,14 @@ regs_to_pin(GPIO_TypeDef *regs, uint32_t bit) return 0; } -// Verify that a gpio is a valid pin -static int -gpio_valid(uint32_t pin) +// Verify that a gpio is a valid pin and return its hardware register +GPIO_TypeDef * +gpio_pin_to_regs(uint32_t pin) { uint32_t port = GPIO2PORT(pin); - return port < ARRAY_SIZE(digital_regs) && digital_regs[port]; + if (port >= ARRAY_SIZE(digital_regs) || !digital_regs[port]) + shutdown("Not a valid pin"); + return digital_regs[port]; } // The stm32h7 has very slow read access to the gpio registers. Cache @@ -83,10 +85,7 @@ static struct odr_cache { struct gpio_out gpio_out_setup(uint32_t pin, uint32_t val) { - if (!gpio_valid(pin)) - shutdown("Not an output pin"); - uint32_t port = GPIO2PORT(pin); - GPIO_TypeDef *regs = digital_regs[port]; + GPIO_TypeDef *regs = gpio_pin_to_regs(pin); gpio_clock_enable(regs); struct gpio_out g = { .regs=regs, .oc=&ODR_CACHE[pin] }; @@ -141,9 +140,7 @@ gpio_out_write(struct gpio_out g, uint32_t val) struct gpio_in gpio_in_setup(uint32_t pin, int32_t pull_up) { - if (!gpio_valid(pin)) - shutdown("Not a valid input pin"); - GPIO_TypeDef *regs = digital_regs[GPIO2PORT(pin)]; + GPIO_TypeDef *regs = gpio_pin_to_regs(pin); struct gpio_in g = { .regs=regs, .bit=GPIO2BIT(pin) }; gpio_in_reset(g, pull_up); return g; From 6e73181c473b6738f78bb0e2de7a2f7579493d1d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 12:26:34 -0400 Subject: [PATCH 064/117] stm32: No need to special case GPIOI in stm32h7_spi.c Signed-off-by: Kevin O'Connor --- src/stm32/stm32h7_spi.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/stm32/stm32h7_spi.c b/src/stm32/stm32h7_spi.c index 1d8c2afdf..82a5ea0eb 100644 --- a/src/stm32/stm32h7_spi.c +++ b/src/stm32/stm32h7_spi.c @@ -34,10 +34,8 @@ DECL_ENUMERATION("spi_bus", "spi5a", 7); DECL_CONSTANT_STR("BUS_PINS_spi5a", "PH7,PF11,PH6"); DECL_ENUMERATION("spi_bus", "spi6", 8); DECL_CONSTANT_STR("BUS_PINS_spi6", "PG12,PG14,PG13"); -#ifdef GPIOI DECL_ENUMERATION("spi_bus", "spi2b", 9); DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); -#endif static const struct spi_info spi_bus[] = { { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION(5) }, @@ -49,9 +47,7 @@ static const struct spi_info spi_bus[] = { { SPI5, GPIO('F', 8), GPIO('F', 9), GPIO('F', 7), GPIO_FUNCTION(5) }, { SPI5, GPIO('H', 7), GPIO('F', 11), GPIO('H', 6), GPIO_FUNCTION(5) }, { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), GPIO_FUNCTION(5)}, -#ifdef GPIOI { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION(5) }, -#endif }; struct spi_config From af17c8c238ba8cdeaaabb87b25a3ca2375b47c11 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 12:31:27 -0400 Subject: [PATCH 065/117] stm32: No need to special case GPIOI in spi.c Signed-off-by: Kevin O'Connor --- src/stm32/spi.c | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/stm32/spi.c b/src/stm32/spi.c index 5e3e428be..62c5eed18 100644 --- a/src/stm32/spi.c +++ b/src/stm32/spi.c @@ -86,19 +86,14 @@ struct spi_info { DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 5); DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10"); - #ifdef GPIOI - DECL_ENUMERATION("spi_bus", "spi2_PI2_PI3_PI1", 6); - DECL_CONSTANT_STR("BUS_PINS_spi2_PI2_PI3_PI1", "PI2,PI3,PI1"); - #define SPI4_INDEX (1 + 6) - #else - #define SPI4_INDEX (0 + 6) - #endif + DECL_ENUMERATION("spi_bus", "spi2_PI2_PI3_PI1", 6); + DECL_CONSTANT_STR("BUS_PINS_spi2_PI2_PI3_PI1", "PI2,PI3,PI1"); #ifdef SPI4 - DECL_ENUMERATION("spi_bus", "spi4_PE13_PE14_PE12", SPI4_INDEX); + DECL_ENUMERATION("spi_bus", "spi4_PE13_PE14_PE12", 7); DECL_CONSTANT_STR("BUS_PINS_spi4_PE13_PE14_PE12", "PE13,PE14,PE12"); - #define SPI6_INDEX (1 + SPI4_INDEX) + #define SPI6_INDEX (1 + 7) #else - #define SPI6_INDEX (0 + SPI4_INDEX) + #define SPI6_INDEX (0 + 7) #endif #ifdef SPI6 DECL_ENUMERATION("spi_bus", "spi6_PG12_PG14_PG13", SPI6_INDEX); @@ -117,12 +112,10 @@ struct spi_info { DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); DECL_ENUMERATION("spi_bus", "spi3a", 5); DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10"); - #ifdef GPIOI - DECL_ENUMERATION("spi_bus", "spi2b", 6); - DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); - #endif + DECL_ENUMERATION("spi_bus", "spi2b", 6); + DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); #ifdef SPI4 - DECL_ENUMERATION("spi_bus", "spi4", SPI4_INDEX); + DECL_ENUMERATION("spi_bus", "spi4", 7); DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12"); #endif #elif CONFIG_MACH_STM32F7 @@ -254,9 +247,7 @@ static const struct spi_info spi_bus[] = { { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) }, { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) }, { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(6, 6, 6) }, - #ifdef GPIOI { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), SPI_FUNCTION(5, 5, 5) }, - #endif #ifdef SPI4 { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), SPI_FUNCTION(5, 5, 5) }, #endif From 366fb423c5efbee418c109724b9f53bb99cf448a Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Sat, 27 Sep 2025 16:32:29 +0800 Subject: [PATCH 066/117] stm32: Add spi2_PB6_PB7_PB8 and spi3_PC11_PC12_PC10 for stm32g0 Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- src/stm32/spi.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/stm32/spi.c b/src/stm32/spi.c index 62c5eed18..358d21a26 100644 --- a/src/stm32/spi.c +++ b/src/stm32/spi.c @@ -151,9 +151,13 @@ struct spi_info { DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); DECL_ENUMERATION("spi_bus", "spi2_PB2_PB11_PB10", 4); DECL_CONSTANT_STR("BUS_PINS_spi2_PB2_PB11_PB10", "PB2,PB11,PB10"); + DECL_ENUMERATION("spi_bus", "spi2_PB6_PB7_PB8", 5); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB6_PB7_PB8", "PB6,PB7,PB8"); #ifdef SPI3 - DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 5); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 6); DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 7); + DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10"); #endif // Deprecated "spi1" style mappings DECL_ENUMERATION("spi_bus", "spi2", 0); @@ -165,7 +169,7 @@ struct spi_info { DECL_ENUMERATION("spi_bus", "spi2a", 3); DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); #ifdef SPI3 - DECL_ENUMERATION("spi_bus", "spi3", 5); + DECL_ENUMERATION("spi_bus", "spi3", 6); DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); #endif #elif CONFIG_MACH_STM32G4 @@ -266,8 +270,10 @@ static const struct spi_info spi_bus[] = { { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) }, { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(1, 1, 5) }, { SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), SPI_FUNCTION(1, 0, 5) }, + { SPI2, GPIO('B', 6), GPIO('B', 7), GPIO('B', 8), SPI_FUNCTION(4, 1, 1) }, #ifdef SPI3 { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(9, 9, 9) }, + { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(4, 4, 4) }, #endif #elif CONFIG_MACH_STM32G4 { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, From 9e6430aa600b2532926285c15c4a80135d2ce37a Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Sat, 27 Sep 2025 16:49:16 +0800 Subject: [PATCH 067/117] config: Modify software SPI to hardware SPI for BIGTREETECH boards Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- config/generic-bigtreetech-manta-e3ez.cfg | 24 +++++-------------- config/sample-bigtreetech-ebb-canbus-v1.1.cfg | 4 +--- config/sample-bigtreetech-ebb-canbus-v1.2.cfg | 4 +--- .../sample-bigtreetech-ebb-sb-canbus-v1.0.cfg | 4 +--- 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/config/generic-bigtreetech-manta-e3ez.cfg b/config/generic-bigtreetech-manta-e3ez.cfg index 199eae708..39f12fd2a 100644 --- a/config/generic-bigtreetech-manta-e3ez.cfg +++ b/config/generic-bigtreetech-manta-e3ez.cfg @@ -133,44 +133,34 @@ max_z_accel: 100 #[tmc2130 stepper_x] #cs_pin: PB8 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 ##diag1_pin: PF3 #run_current: 0.800 #stealthchop_threshold: 999999 #[tmc2130 stepper_y] #cs_pin: PC9 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 ##diag1_pin: PF4 #run_current: 0.800 #stealthchop_threshold: 999999 #[tmc2130 stepper_z] #cs_pin: PD0 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 ##diag1_pin: PF5 #run_current: 0.650 #stealthchop_threshold: 999999 #[tmc2130 extruder] #cs_pin: PD1 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 #run_current: 0.800 #stealthchop_threshold: 999999 #[tmc2130 extruder1] #cs_pin: PB5 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 #run_current: 0.800 #stealthchop_threshold: 999999 @@ -195,6 +185,4 @@ aliases: #[adxl345] #cs_pin: PC15 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 diff --git a/config/sample-bigtreetech-ebb-canbus-v1.1.cfg b/config/sample-bigtreetech-ebb-canbus-v1.1.cfg index c84abf173..8da7cccd4 100644 --- a/config/sample-bigtreetech-ebb-canbus-v1.1.cfg +++ b/config/sample-bigtreetech-ebb-canbus-v1.1.cfg @@ -11,9 +11,7 @@ serial: /dev/serial/by-id/usb-Klipper_Klipper_firmware_12345-if00 [adxl345] cs_pin: EBBCan:PB12 -spi_software_sclk_pin: EBBCan:PB10 -spi_software_mosi_pin: EBBCan:PB11 -spi_software_miso_pin: EBBCan:PB2 +spi_bus: spi2_PB2_PB11_PB10 axes_map: x,y,z [extruder] diff --git a/config/sample-bigtreetech-ebb-canbus-v1.2.cfg b/config/sample-bigtreetech-ebb-canbus-v1.2.cfg index 053b783c3..7b55a91cf 100644 --- a/config/sample-bigtreetech-ebb-canbus-v1.2.cfg +++ b/config/sample-bigtreetech-ebb-canbus-v1.2.cfg @@ -11,9 +11,7 @@ serial: /dev/serial/by-id/usb-Klipper_Klipper_firmware_12345-if00 [adxl345] cs_pin: EBBCan:PB12 -spi_software_sclk_pin: EBBCan:PB10 -spi_software_mosi_pin: EBBCan:PB11 -spi_software_miso_pin: EBBCan:PB2 +spi_bus: spi2_PB2_PB11_PB10 axes_map: x,y,z [extruder] diff --git a/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg b/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg index 192d385e5..1e60467dd 100644 --- a/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg +++ b/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg @@ -15,9 +15,7 @@ sensor_pin: EBBCan:PA2 [adxl345] cs_pin: EBBCan:PB12 -spi_software_sclk_pin: EBBCan:PB10 -spi_software_mosi_pin: EBBCan:PB11 -spi_software_miso_pin: EBBCan:PB2 +spi_bus: spi2_PB2_PB11_PB10 axes_map: x,y,z [extruder] From 184ba4080ce8e7bbbd7f055d55622c92e22f6e3d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 29 Sep 2025 11:31:31 -0400 Subject: [PATCH 068/117] toolhead: Flush lookahead on dwell - fix flushing bug after long delays Commit 7ea5f5d2 changed how the lookahead queue is flushed. Previously, the main flush timer would always run while the toolhead was considered in an active state (print_time). After that commit, the flush timer could sleep if there were no steps generated (no call to note_mcu_movequeue_activity() ). This could lead to a situation where a G4 command (or series of commands) could cause the toolhead to be considered in an active state while the flush timer was disabled. The result was that a future command may not be properly flushed (the toolhead would fail to transition to a "Priming" state). Fix by ensuring that all dwell() requests fully flush the lookahead queue. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 1 + 1 file changed, 1 insertion(+) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index f54743b0a..a17750d74 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -398,6 +398,7 @@ class ToolHead: self.move(curpos, speed) self.printer.send_event("toolhead:manual_move") def dwell(self, delay): + self._flush_lookahead() next_print_time = self.get_last_move_time() + max(0., delay) self._advance_move_time(next_print_time) self._check_pause() From a683ef3503f462b1131932349df1dbb0acebbb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sezgin=20A=C3=87IKG=C3=96Z?= Date: Tue, 30 Sep 2025 03:24:13 +0300 Subject: [PATCH 069/117] spi_flash: add timestamp to firmware filenames on sdcard upload (#7063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some Creality bootloaders skip flashing if the firmware filename is unchanged. By appending a timestamp to the firmware filename during sdcard upload, each update generates a unique name, ensuring that the bootloader always accepts and flashes the new firmware. Signed-off-by: Sezgin AÇIKGÖZ --- scripts/spi_flash/board_defs.py | 3 ++- scripts/spi_flash/spi_flash.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index 4fdba64cc..0a403d7da 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -123,7 +123,8 @@ BOARD_DEFS = { 'spi_bus': "swspi", 'spi_pins': "PC8,PD2,PC12", 'cs_pin': "PC11", - 'skip_verify': True + 'skip_verify': True, + 'requires_unique_fw_name': True }, 'monster8': { 'mcu': "stm32f407xx", diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index 729dd2bbc..e9394dbe5 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -1380,7 +1380,32 @@ class MCUConnection: input_sha = hashlib.sha1() sd_sha = hashlib.sha1() klipper_bin_path = self.board_config['klipper_bin_path'] + add_ts = self.board_config.get('requires_unique_fw_name', False) fw_path = self.board_config.get('firmware_path', "firmware.bin") + if add_ts: + fw_dir = os.path.dirname(fw_path) + fw_name, fw_ext = os.path.splitext(os.path.basename(fw_path)) + ts = time.strftime("%Y%m%d%H%M%S") + fw_name_ts = f"{ts}{fw_name}{fw_ext}" + if fw_dir: + fw_path = os.path.join(fw_dir, fw_name_ts) + else: + fw_path = fw_name_ts + list_dir = fw_dir if fw_dir else "" + try: + output_line("\nSD Card FW Directory Contents:") + for f in self.fatfs.list_sd_directory(list_dir): + fname = f['name'].decode('utf-8') + if fname.endswith(fw_ext): + self.fatfs.remove_item( + os.path.join(list_dir, fname) + ) + output_line( + "Old firmware file %s found and deleted" + % (fname,) + ) + except Exception: + logging.exception("Error cleaning old firmware files") try: with open(klipper_bin_path, 'rb') as local_f: with self.fatfs.open_file(fw_path, "wb") as sd_f: From ff8c8eab5516242a654feee0f6d88700ae7ae4cc Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 26 Sep 2025 01:22:57 -0400 Subject: [PATCH 070/117] toolhead: Clarify internal minimum_cruise_ratio variable names Avoid using "smoothed" and "accel_to_decel" for variables associated with minimum_cruise_ratio. Instead introduce the prefix "mcr" for use with these variables. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index a17750d74..a65d37f18 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -45,9 +45,10 @@ class Move: self.max_start_v2 = 0. self.max_cruise_v2 = velocity**2 self.delta_v2 = 2.0 * move_d * self.accel - self.max_smoothed_v2 = 0. - self.smooth_delta_v2 = 2.0 * move_d * toolhead.max_accel_to_decel self.next_junction_v2 = 999999999.9 + # Setup for minimum_cruise_ratio checks + self.max_mcr_start_v2 = 0. + self.mcr_delta_v2 = 2.0 * move_d * toolhead.mcr_pseudo_accel def limit_speed(self, speed, accel): speed2 = speed**2 if speed2 < self.max_cruise_v2: @@ -55,7 +56,7 @@ class Move: self.min_move_t = self.move_d / speed self.accel = min(self.accel, accel) self.delta_v2 = 2.0 * self.move_d * self.accel - self.smooth_delta_v2 = min(self.smooth_delta_v2, self.delta_v2) + self.mcr_delta_v2 = min(self.mcr_delta_v2, self.delta_v2) def limit_next_junction_speed(self, speed): self.next_junction_v2 = min(self.next_junction_v2, speed**2) def move_error(self, msg="Move out of range"): @@ -94,8 +95,8 @@ class Move: move_centripetal_v2, pmove_centripetal_v2) # Apply limits self.max_start_v2 = max_start_v2 - self.max_smoothed_v2 = min( - max_start_v2, prev_move.max_smoothed_v2 + prev_move.smooth_delta_v2) + self.max_mcr_start_v2 = min( + max_start_v2, prev_move.max_mcr_start_v2 + prev_move.mcr_delta_v2) def set_junction(self, start_v2, cruise_v2, end_v2): # Determine accel, cruise, and decel portions of the move distance half_inv_accel = .5 / self.accel @@ -138,16 +139,16 @@ class LookAheadQueue: # junction speed assuming the robot comes to a complete stop # after the last move. delayed = [] - next_end_v2 = next_smoothed_v2 = peak_cruise_v2 = 0. + next_start_v2 = next_mcr_start_v2 = peak_cruise_v2 = 0. for i in range(flush_count-1, -1, -1): move = queue[i] - reachable_start_v2 = next_end_v2 + move.delta_v2 + reachable_start_v2 = next_start_v2 + move.delta_v2 start_v2 = min(move.max_start_v2, reachable_start_v2) - reachable_smoothed_v2 = next_smoothed_v2 + move.smooth_delta_v2 - smoothed_v2 = min(move.max_smoothed_v2, reachable_smoothed_v2) - if smoothed_v2 < reachable_smoothed_v2: + reach_mcr_start_v2 = next_mcr_start_v2 + move.mcr_delta_v2 + mcr_start_v2 = min(move.max_mcr_start_v2, reach_mcr_start_v2) + if mcr_start_v2 < reach_mcr_start_v2: # It's possible for this move to accelerate - if (smoothed_v2 + move.smooth_delta_v2 > next_smoothed_v2 + if (mcr_start_v2 + move.mcr_delta_v2 > next_mcr_start_v2 or delayed): # This move can decelerate or this is a full accel # move after a full decel move @@ -155,7 +156,7 @@ class LookAheadQueue: flush_count = i update_flush_count = False peak_cruise_v2 = min(move.max_cruise_v2, ( - smoothed_v2 + reachable_smoothed_v2) * .5) + mcr_start_v2 + reach_mcr_start_v2) * .5) if delayed: # Propagate peak_cruise_v2 to any delayed moves if not update_flush_count and i < flush_count: @@ -169,12 +170,12 @@ class LookAheadQueue: cruise_v2 = min((start_v2 + reachable_start_v2) * .5 , move.max_cruise_v2, peak_cruise_v2) move.set_junction(min(start_v2, cruise_v2), cruise_v2 - , min(next_end_v2, cruise_v2)) + , min(next_start_v2, cruise_v2)) else: # Delay calculating this move until peak_cruise_v2 is known - delayed.append((move, start_v2, next_end_v2)) - next_end_v2 = start_v2 - next_smoothed_v2 = smoothed_v2 + delayed.append((move, start_v2, next_start_v2)) + next_start_v2 = start_v2 + next_mcr_start_v2 = mcr_start_v2 if update_flush_count or not flush_count: return [] # Remove processed moves from the queue @@ -209,7 +210,7 @@ class ToolHead: 0.5, below=1., minval=0.) self.square_corner_velocity = config.getfloat( 'square_corner_velocity', 5., minval=0.) - self.junction_deviation = self.max_accel_to_decel = 0. + self.junction_deviation = self.mcr_pseudo_accel = 0. self._calc_junction_deviation() # Input stall detection self.check_stall_time = 0. @@ -510,7 +511,7 @@ class ToolHead: def _calc_junction_deviation(self): scv2 = self.square_corner_velocity**2 self.junction_deviation = scv2 * (math.sqrt(2.) - 1.) / self.max_accel - self.max_accel_to_decel = self.max_accel * (1. - self.min_cruise_ratio) + self.mcr_pseudo_accel = self.max_accel * (1. - self.min_cruise_ratio) def set_max_velocities(self, max_velocity, max_accel, square_corner_velocity, min_cruise_ratio): if max_velocity is not None: From 41901ec382ab3b87ee7a5ca6411a6e4bc2c0fe3f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 25 Sep 2025 14:32:05 -0400 Subject: [PATCH 071/117] toolhead: Simplify LookAheadQueue.flush() code Replace "delayed" storage with a full pass through the queue. This simplifies the lookahead processing logic. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index a65d37f18..6ebacd0a9 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -138,46 +138,45 @@ class LookAheadQueue: # Traverse queue from last to first move and determine maximum # junction speed assuming the robot comes to a complete stop # after the last move. - delayed = [] + junction_info = [None] * flush_count next_start_v2 = next_mcr_start_v2 = peak_cruise_v2 = 0. + pending_cv2_assign = 0 for i in range(flush_count-1, -1, -1): move = queue[i] reachable_start_v2 = next_start_v2 + move.delta_v2 start_v2 = min(move.max_start_v2, reachable_start_v2) + cruise_v2 = None + pending_cv2_assign += 1 reach_mcr_start_v2 = next_mcr_start_v2 + move.mcr_delta_v2 mcr_start_v2 = min(move.max_mcr_start_v2, reach_mcr_start_v2) if mcr_start_v2 < reach_mcr_start_v2: # It's possible for this move to accelerate if (mcr_start_v2 + move.mcr_delta_v2 > next_mcr_start_v2 - or delayed): - # This move can decelerate or this is a full accel - # move after a full decel move + or pending_cv2_assign > 1): + # This move can both accel and decel, or this is a + # full accel move followed by a full decel move if update_flush_count and peak_cruise_v2: flush_count = i update_flush_count = False - peak_cruise_v2 = min(move.max_cruise_v2, ( - mcr_start_v2 + reach_mcr_start_v2) * .5) - if delayed: - # Propagate peak_cruise_v2 to any delayed moves - if not update_flush_count and i < flush_count: - mc_v2 = peak_cruise_v2 - for m, ms_v2, me_v2 in reversed(delayed): - mc_v2 = min(mc_v2, ms_v2) - m.set_junction(min(ms_v2, mc_v2), mc_v2 - , min(me_v2, mc_v2)) - del delayed[:] - if not update_flush_count and i < flush_count: - cruise_v2 = min((start_v2 + reachable_start_v2) * .5 - , move.max_cruise_v2, peak_cruise_v2) - move.set_junction(min(start_v2, cruise_v2), cruise_v2 - , min(next_start_v2, cruise_v2)) - else: - # Delay calculating this move until peak_cruise_v2 is known - delayed.append((move, start_v2, next_start_v2)) + peak_cruise_v2 = (mcr_start_v2 + reach_mcr_start_v2) * .5 + cruise_v2 = min((start_v2 + reachable_start_v2) * .5 + , move.max_cruise_v2, peak_cruise_v2) + pending_cv2_assign = 0 + junction_info[i] = (move, start_v2, cruise_v2, next_start_v2) next_start_v2 = start_v2 next_mcr_start_v2 = mcr_start_v2 if update_flush_count or not flush_count: return [] + # Traverse queue in forward direction to propagate cruise_v2 + prev_cruise_v2 = 0. + for i in range(flush_count): + move, start_v2, cruise_v2, next_start_v2 = junction_info[i] + if cruise_v2 is None: + # This move can't accelerate - propagate cruise_v2 from previous + cruise_v2 = min(prev_cruise_v2, start_v2) + move.set_junction(min(start_v2, cruise_v2), cruise_v2 + , min(next_start_v2, cruise_v2)) + prev_cruise_v2 = cruise_v2 # Remove processed moves from the queue res = queue[:flush_count] del queue[:flush_count] From 6118525c19fdc7403817782591e584278866939f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 26 Sep 2025 11:43:19 -0400 Subject: [PATCH 072/117] toolhead: Allow more entries to flush from "lazy" lookahead flush Previously the code would always keep at least 2 items on the lookahead queue after a "lazy" flush. In most cases it's okay to leave only a single item. Update the code to better handle flushing of items that are fully ready. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 6ebacd0a9..6326e607f 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -156,7 +156,7 @@ class LookAheadQueue: # This move can both accel and decel, or this is a # full accel move followed by a full decel move if update_flush_count and peak_cruise_v2: - flush_count = i + flush_count = i + pending_cv2_assign update_flush_count = False peak_cruise_v2 = (mcr_start_v2 + reach_mcr_start_v2) * .5 cruise_v2 = min((start_v2 + reachable_start_v2) * .5 From fe09e2e6bfe677959315446086bfb9c4de9536b2 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 16 Sep 2025 00:23:01 +0200 Subject: [PATCH 073/117] klippy: track the device model Signed-off-by: Timofey Titovets --- klippy/klippy.py | 2 ++ klippy/util.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/klippy/klippy.py b/klippy/klippy.py index 316343cbd..df86f49fa 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -327,12 +327,14 @@ def main(): extra_git_desc += "\nTracked URL: %s" % (git_info["url"]) start_args['software_version'] = git_vers start_args['cpu_info'] = util.get_cpu_info() + start_args['device'] = util.get_device_info() if bglogger is not None: versions = "\n".join([ "Args: %s" % (sys.argv,), "Git version: %s%s" % (repr(start_args['software_version']), extra_git_desc), "CPU: %s" % (start_args['cpu_info'],), + "Device: %s" % (start_args['device']), "Python: %s" % (repr(sys.version),)]) logging.info(versions) elif not options.debugoutput: diff --git a/klippy/util.py b/klippy/util.py index 6a8baee7f..9b64ed8f2 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -125,6 +125,17 @@ def get_cpu_info(): model_name = dict(lines).get("model name", "?") return "%d core %s" % (core_count, model_name) +def get_device_info(): + try: + f = open('/proc/device-tree/model', 'r') + data = f.read() + f.close() + except (IOError, OSError) as e: + logging.debug("Exception on read /proc/device-tree/model: %s", + traceback.format_exc()) + return "?" + return data.rstrip(' \0') + def get_version_from_file(klippy_src): try: with open(os.path.join(klippy_src, '.version')) as h: From ac6f059cb9235eb1a51ae9768edaaf3a922538c3 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 25 Sep 2025 00:32:50 +0200 Subject: [PATCH 074/117] util: use dmi data on x86 instead of device-tree Signed-off-by: Timofey Titovets --- klippy/util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/klippy/util.py b/klippy/util.py index 9b64ed8f2..d4de87788 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -127,7 +127,12 @@ def get_cpu_info(): def get_device_info(): try: - f = open('/proc/device-tree/model', 'r') + path = '/proc/device-tree/model' + if not os.access(path, os.F_OK): + path = "/sys/class/dmi/id/product_name" + if not os.access(path, os.F_OK): + return "?" + f = open(path, 'r') data = f.read() f.close() except (IOError, OSError) as e: From 07c5973142533d13393a7a16b390a3553bc859f5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 21:21:00 -0400 Subject: [PATCH 075/117] util: Introduce _try_read_file() helper Introduce a helper function to read the contents of a file and use that helper throughout util.py . Signed-off-by: Kevin O'Connor --- klippy/util.py | 58 ++++++++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/klippy/util.py b/klippy/util.py index d4de87788..68a999bcd 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -51,6 +51,15 @@ def create_pty(ptyname): # Helper code for extracting mcu build info ###################################################################### +def _try_read_file(filename, maxsize=32*1024): + try: + with open(filename, 'r') as f: + return f.read(maxsize) + except (IOError, OSError) as e: + logging.debug("Exception on read %s: %s", filename, + traceback.format_exc()) + return None + def dump_file_stats(build_dir, filename): fname = os.path.join(build_dir, filename) try: @@ -66,20 +75,14 @@ def dump_mcu_build(): build_dir = os.path.join(os.path.dirname(__file__), '..') # Try to log last mcu config dump_file_stats(build_dir, '.config') - try: - f = open(os.path.join(build_dir, '.config'), 'r') - data = f.read(32*1024) - f.close() + data = _try_read_file(os.path.join(build_dir, '.config')) + if data is not None: logging.info("========= Last MCU build config =========\n%s" "=======================", data) - except: - pass # Try to log last mcu build version dump_file_stats(build_dir, 'out/klipper.dict') try: - f = open(os.path.join(build_dir, 'out/klipper.dict'), 'r') - data = f.read(32*1024) - f.close() + data = _try_read_file(os.path.join(build_dir, 'out/klipper.dict')) data = json.loads(data) logging.info("Last MCU build version: %s", data.get('version', '')) logging.info("Last MCU build tools: %s", data.get('build_versions', '')) @@ -111,13 +114,8 @@ setup_python2_wrappers() ###################################################################### def get_cpu_info(): - try: - f = open('/proc/cpuinfo', 'r') - data = f.read() - f.close() - except (IOError, OSError) as e: - logging.debug("Exception on read /proc/cpuinfo: %s", - traceback.format_exc()) + data = _try_read_file('/proc/cpuinfo', maxsize=1024*1024) + if data is None: return "?" lines = [l.split(':', 1) for l in data.split('\n')] lines = [(l[0].strip(), l[1].strip()) for l in lines if len(l) == 2] @@ -126,28 +124,18 @@ def get_cpu_info(): return "%d core %s" % (core_count, model_name) def get_device_info(): - try: - path = '/proc/device-tree/model' - if not os.access(path, os.F_OK): - path = "/sys/class/dmi/id/product_name" - if not os.access(path, os.F_OK): - return "?" - f = open(path, 'r') - data = f.read() - f.close() - except (IOError, OSError) as e: - logging.debug("Exception on read /proc/device-tree/model: %s", - traceback.format_exc()) - return "?" + data = _try_read_file('/proc/device-tree/model') + if data is None: + data = _try_read_file("/sys/class/dmi/id/product_name") + if data is None: + return "?" return data.rstrip(' \0') def get_version_from_file(klippy_src): - try: - with open(os.path.join(klippy_src, '.version')) as h: - return h.read().rstrip() - except IOError: - pass - return "?" + data = _try_read_file(os.path.join(klippy_src, '.version')) + if data is None: + return "?" + return data.rstrip() def _get_repo_info(gitdir): repo_info = {"branch": "?", "remote": "?", "url": "?"} From 35aeb78088f2a5c76c1945266e3458e097dc0c69 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 21:22:53 -0400 Subject: [PATCH 076/117] util: Strip all leading/trailing whitespace in get_device_info() Signed-off-by: Kevin O'Connor --- klippy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/util.py b/klippy/util.py index 68a999bcd..8e08e0438 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -129,7 +129,7 @@ def get_device_info(): data = _try_read_file("/sys/class/dmi/id/product_name") if data is None: return "?" - return data.rstrip(' \0') + return data.rstrip(' \0').strip() def get_version_from_file(klippy_src): data = _try_read_file(os.path.join(klippy_src, '.version')) From ea9c88526b390032c8f43a71332ab7c3ee8ee403 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 21:27:36 -0400 Subject: [PATCH 077/117] klippy: Report Linux version in log Signed-off-by: Kevin O'Connor --- klippy/klippy.py | 2 ++ klippy/util.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/klippy/klippy.py b/klippy/klippy.py index df86f49fa..1d3ffbf06 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -328,6 +328,7 @@ def main(): start_args['software_version'] = git_vers start_args['cpu_info'] = util.get_cpu_info() start_args['device'] = util.get_device_info() + start_args['linux_version'] = util.get_linux_version() if bglogger is not None: versions = "\n".join([ "Args: %s" % (sys.argv,), @@ -335,6 +336,7 @@ def main(): extra_git_desc), "CPU: %s" % (start_args['cpu_info'],), "Device: %s" % (start_args['device']), + "Linux: %s" % (start_args['linux_version']), "Python: %s" % (repr(sys.version),)]) logging.info(versions) elif not options.debugoutput: diff --git a/klippy/util.py b/klippy/util.py index 8e08e0438..9abb94df6 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -131,6 +131,12 @@ def get_device_info(): return "?" return data.rstrip(' \0').strip() +def get_linux_version(): + data = _try_read_file('/proc/version') + if data is None: + return "?" + return data.strip() + def get_version_from_file(klippy_src): data = _try_read_file(os.path.join(klippy_src, '.version')) if data is None: From 07466411acc398e6132887f544d7415ab628a40f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 13:57:34 -0400 Subject: [PATCH 078/117] mcu: Remove max_stepper_error config parameter Use a regular code constant - MAX_STEPCOMPRESS_ERROR in stepper.py. Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 3 +++ klippy/mcu.py | 5 ----- klippy/stepper.py | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 296343b12..3e7f0daf7 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,9 @@ All dates in this document are approximate. ## Changes +20251003: Support for the undocumented `max_stepper_error` option in +the `[printer]` config section has been removed. + 20250916: The definitions of EI, 2HUMP_EI, and 3HUMP_EI input shapers were updated. For best performance it is recommended to recalibrate input shapers, especially if some of these shapers are currently used. diff --git a/klippy/mcu.py b/klippy/mcu.py index 74e2164e7..6a7e84b66 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -603,9 +603,6 @@ class MCU: self._restart_cmds = [] self._init_cmds = [] self._mcu_freq = 0. - # Move command queuing - self._max_stepper_error = config.getfloat('max_stepper_error', 0.000025, - minval=0.) self._reserved_move_slots = 0 # Stats self._get_status_info = {} @@ -871,8 +868,6 @@ class MCU: return self.print_time_to_clock(t) + slot def seconds_to_clock(self, time): return int(time * self._mcu_freq) - def get_max_stepper_error(self): - return self._max_stepper_error def min_schedule_time(self): return MIN_SCHEDULE_TIME def max_nominal_duration(self): diff --git a/klippy/stepper.py b/klippy/stepper.py index 248e487d2..8d93d6c16 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -16,6 +16,7 @@ class error(Exception): MIN_BOTH_EDGE_DURATION = 0.000000500 MIN_OPTIMIZED_BOTH_EDGE_DURATION = 0.000000150 +MAX_STEPCOMPRESS_ERROR = 0.000025 # Interface to low-level mcu and chelper code class MCU_stepper: @@ -122,8 +123,7 @@ class MCU_stepper: self._get_position_cmd = self._mcu.lookup_query_command( "stepper_get_position oid=%c", "stepper_position oid=%c pos=%i", oid=self._oid) - max_error = self._mcu.get_max_stepper_error() - max_error_ticks = self._mcu.seconds_to_clock(max_error) + max_error_ticks = self._mcu.seconds_to_clock(MAX_STEPCOMPRESS_ERROR) ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.stepcompress_fill(self._stepqueue, self._oid, max_error_ticks, step_cmd_tag, dir_cmd_tag) From ce55d4116624177e8f6ced9a5320953ebfd024d5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 20:34:51 -0400 Subject: [PATCH 079/117] mcu: Split _mcu_identify() into separate methods Split up the _mcu_identify() into several internal methods separated by functionality. Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 83 +++++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 6a7e84b66..3903e8b48 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -659,7 +659,7 @@ class MCU: self._printer.request_exit('firmware_restart') self._reactor.pause(self._reactor.monotonic() + 2.000) raise error("Attempt MCU '%s' restart failed" % (self._name,)) - def _connect_file(self, pace=False): + def _attach_file(self, pace=False): # In a debugging mode. Open debug output file and read data dictionary start_args = self._printer.get_start_args() if self._name == 'mcu': @@ -775,40 +775,32 @@ class MCU: logging.info(move_msg) log_info = self._log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) - def _mcu_identify(self): - if self.is_fileoutput(): - self._connect_file() - else: - resmeth = self._restart_method - if resmeth == 'rpi_usb' and not os.path.exists(self._serialport): - # Try toggling usb power - self._check_restart("enable power") - try: - if self._canbus_iface is not None: - cbid = self._printer.lookup_object('canbus_ids') - nodeid = cbid.get_nodeid(self._serialport) - self._serial.connect_canbus(self._serialport, nodeid, - self._canbus_iface) - elif self._baud: - # Cheetah boards require RTS to be deasserted - # else a reset will trigger the built-in bootloader. - rts = (resmeth != "cheetah") - self._serial.connect_uart(self._serialport, self._baud, rts) - else: - self._serial.connect_pipe(self._serialport) - self._clocksync.connect(self._serial) - except serialhdl.error as e: - raise error(str(e)) - logging.info(self._log_info()) - ppins = self._printer.lookup_object('pins') - pin_resolver = ppins.get_pin_resolver(self._name) - for cname, value in self.get_constants().items(): - if cname.startswith("RESERVE_PINS_"): - for pin in value.split(','): - pin_resolver.reserve_pin(pin, cname[13:]) - self._mcu_freq = self.get_constant_float('CLOCK_FREQ') - self._stats_sumsq_base = self.get_constant_float('STATS_SUMSQ_BASE') + def _attach(self): + resmeth = self._restart_method + if resmeth == 'rpi_usb' and not os.path.exists(self._serialport): + # Try toggling usb power + self._check_restart("enable power") + try: + if self._canbus_iface is not None: + cbid = self._printer.lookup_object('canbus_ids') + nodeid = cbid.get_nodeid(self._serialport) + self._serial.connect_canbus(self._serialport, nodeid, + self._canbus_iface) + elif self._baud: + # Cheetah boards require RTS to be deasserted + # else a reset will trigger the built-in bootloader. + rts = (resmeth != "cheetah") + self._serial.connect_uart(self._serialport, self._baud, rts) + else: + self._serial.connect_pipe(self._serialport) + self._clocksync.connect(self._serial) + except serialhdl.error as e: + raise error(str(e)) + def _post_attach_setup_shutdown(self): self._emergency_stop_cmd = self.lookup_command("emergency_stop") + self.register_response(self._handle_shutdown, 'shutdown') + self.register_response(self._handle_shutdown, 'is_shutdown') + def _post_attach_setup_restart(self): self._reset_cmd = self.try_lookup_command("reset") self._config_reset_cmd = self.try_lookup_command("config_reset") ext_only = self._reset_cmd is None and self._config_reset_cmd is None @@ -820,13 +812,32 @@ class MCU: self._is_mcu_bridge = True self._printer.register_event_handler("klippy:firmware_restart", self._firmware_restart_bridge) + def _post_attach_setup_stats(self): + self._stats_sumsq_base = self.get_constant_float('STATS_SUMSQ_BASE') + msgparser = self._serial.get_msgparser() version, build_versions = msgparser.get_version_info() self._get_status_info['mcu_version'] = version self._get_status_info['mcu_build_versions'] = build_versions self._get_status_info['mcu_constants'] = msgparser.get_constants() - self.register_response(self._handle_shutdown, 'shutdown') - self.register_response(self._handle_shutdown, 'is_shutdown') self.register_response(self._handle_mcu_stats, 'stats') + def _post_attach_setup_for_config(self): + self._mcu_freq = self.get_constant_float('CLOCK_FREQ') + ppins = self._printer.lookup_object('pins') + pin_resolver = ppins.get_pin_resolver(self._name) + for cname, value in self.get_constants().items(): + if cname.startswith("RESERVE_PINS_"): + for pin in value.split(','): + pin_resolver.reserve_pin(pin, cname[13:]) + def _mcu_identify(self): + if self.is_fileoutput(): + self._attach_file() + else: + self._attach() + logging.info(self._log_info()) + self._post_attach_setup_shutdown() + self._post_attach_setup_restart() + self._post_attach_setup_for_config() + self._post_attach_setup_stats() def _ready(self): if self.is_fileoutput(): return From fcd9cefb3fb4b1325515acc35de150dd1aba1333 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 20:46:22 -0400 Subject: [PATCH 080/117] mcu: Add _check_restart_on_xxx() helper methods Breakout each of the possible restart cases into its own internal method. Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 3903e8b48..6af8fe430 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -659,6 +659,21 @@ class MCU: self._printer.request_exit('firmware_restart') self._reactor.pause(self._reactor.monotonic() + 2.000) raise error("Attempt MCU '%s' restart failed" % (self._name,)) + def _check_restart_on_crc_mismatch(self): + self._check_restart("CRC mismatch") + def _check_restart_on_send_config(self): + if self._restart_method == 'rpi_usb': + # Only configure mcu after usb power reset + self._check_restart("full reset before config") + def _check_restart_on_attach(self): + resmeth = self._restart_method + if resmeth == 'rpi_usb' and not os.path.exists(self._serialport): + # Try toggling usb power + self._check_restart("enable power") + def _lookup_attach_uart_rts(self): + # Cheetah boards require RTS to be deasserted + # else a reset will trigger the built-in bootloader. + return (self._restart_method != "cheetah") def _attach_file(self, pace=False): # In a debugging mode. Open debug output file and read data dictionary start_args = self._printer.get_start_args() @@ -696,7 +711,7 @@ class MCU: config_crc = zlib.crc32(encoded_config) & 0xffffffff self.add_config_cmd("finalize_config crc=%d" % (config_crc,)) if prev_crc is not None and config_crc != prev_crc: - self._check_restart("CRC mismatch") + self._check_restart_on_crc_mismatch() raise error("MCU '%s' CRC does not match config" % (self._name,)) # Transmit config messages (if needed) self.register_response(self._handle_starting, 'starting') @@ -747,9 +762,7 @@ class MCU: def _connect(self): config_params = self._send_get_config() if not config_params['is_config']: - if self._restart_method == 'rpi_usb': - # Only configure mcu after usb power reset - self._check_restart("full reset before config") + self._check_restart_on_send_config() # Not configured - send config and issue get_config again self._send_config(None) config_params = self._send_get_config() @@ -776,10 +789,7 @@ class MCU: log_info = self._log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) def _attach(self): - resmeth = self._restart_method - if resmeth == 'rpi_usb' and not os.path.exists(self._serialport): - # Try toggling usb power - self._check_restart("enable power") + self._check_restart_on_attach() try: if self._canbus_iface is not None: cbid = self._printer.lookup_object('canbus_ids') @@ -787,9 +797,7 @@ class MCU: self._serial.connect_canbus(self._serialport, nodeid, self._canbus_iface) elif self._baud: - # Cheetah boards require RTS to be deasserted - # else a reset will trigger the built-in bootloader. - rts = (resmeth != "cheetah") + rts = self._lookup_attach_uart_rts() self._serial.connect_uart(self._serialport, self._baud, rts) else: self._serial.connect_pipe(self._serialport) From 1e18f32914d8559f5f932287df5bd9dac619452b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 20:47:40 -0400 Subject: [PATCH 081/117] mcu: Move registration of "starting" message to _post_attach_setup_shutdown() Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 6af8fe430..9bfdc978d 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -714,7 +714,6 @@ class MCU: self._check_restart_on_crc_mismatch() raise error("MCU '%s' CRC does not match config" % (self._name,)) # Transmit config messages (if needed) - self.register_response(self._handle_starting, 'starting') try: if prev_crc is None: logging.info("Sending MCU '%s' printer configuration...", @@ -808,6 +807,7 @@ class MCU: self._emergency_stop_cmd = self.lookup_command("emergency_stop") self.register_response(self._handle_shutdown, 'shutdown') self.register_response(self._handle_shutdown, 'is_shutdown') + self.register_response(self._handle_starting, 'starting') def _post_attach_setup_restart(self): self._reset_cmd = self.try_lookup_command("reset") self._config_reset_cmd = self.try_lookup_command("config_reset") From b086349a9f1c313b0f7f8b5f45380fd59f1eb3c3 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 21:22:39 -0400 Subject: [PATCH 082/117] mcu: Setup debugging estimated_print_time() in constructor Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 9bfdc978d..9e269fd73 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -610,6 +610,11 @@ class MCU: self._mcu_tick_avg = 0. self._mcu_tick_stddev = 0. self._mcu_tick_awake = 0. + # Alter time reporting when debugging + if self.is_fileoutput(): + def dummy_estimated_print_time(eventtime): + return 0. + self.estimated_print_time = dummy_estimated_print_time # Register handlers printer.load_object(config, "error_mcu") printer.register_event_handler("klippy:firmware_restart", @@ -674,7 +679,7 @@ class MCU: # Cheetah boards require RTS to be deasserted # else a reset will trigger the built-in bootloader. return (self._restart_method != "cheetah") - def _attach_file(self, pace=False): + def _attach_file(self): # In a debugging mode. Open debug output file and read data dictionary start_args = self._printer.get_start_args() if self._name == 'mcu': @@ -688,12 +693,7 @@ class MCU: dict_data = dfile.read() dfile.close() self._serial.connect_file(outfile, dict_data) - self._clocksync.connect_file(self._serial, pace) - # Handle pacing - if not pace: - def dummy_estimated_print_time(eventtime): - return 0. - self.estimated_print_time = dummy_estimated_print_time + self._clocksync.connect_file(self._serial) def _send_config(self, prev_crc): # Build config commands for cb in self._config_callbacks: From 1668d6d7c65e05601d7ecc5e2c9733e35746e55b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 21:18:46 -0400 Subject: [PATCH 083/117] mcu: Separate low-level connection handling to new MCUConnectHelper class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 364 +++++++++++++++++++++++++++----------------------- 1 file changed, 194 insertions(+), 170 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 9e269fd73..f97d4954c 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -549,7 +549,7 @@ class MCU_adc: ###################################################################### -# Main MCU class +# Main MCU class (and its helper classes) ###################################################################### # Minimum time host needs to get scheduled events queued into mcu @@ -557,18 +557,16 @@ MIN_SCHEDULE_TIME = 0.100 # Maximum time all MCUs can internally schedule into the future MAX_NOMINAL_DURATION = 3.0 -class MCU: - error = error - def __init__(self, config, clocksync): - self._printer = printer = config.get_printer() +# Low-level mcu connection management helper +class MCUConnectHelper: + def __init__(self, config, mcu, clocksync): + self._mcu = mcu self._clocksync = clocksync + self._printer = printer = config.get_printer() self._reactor = printer.get_reactor() - self._name = config.get_name() - if self._name.startswith('mcu '): - self._name = self._name[4:] + self._name = name = mcu.get_name() # Serial port - name = self._name - self._serial = serialhdl.SerialReader(self._reactor, mcu_name = name) + self._serial = serialhdl.SerialReader(self._reactor, mcu_name=name) self._baud = 0 self._canbus_iface = None canbus_uuid = config.get('canbus_uuid', None) @@ -577,7 +575,7 @@ class MCU: self._canbus_iface = config.get('canbus_interface', 'can0') cbid = self._printer.load_object(config, 'canbus_ids') cbid.add_uuid(config, canbus_uuid, self._canbus_iface) - self._printer.load_object(config, 'canbus_stats %s' % (self._name,)) + self._printer.load_object(config, 'canbus_stats %s' % (name,)) else: self._serialport = config.get('serial') if not (self._serialport.startswith("/dev/rpmsg_") @@ -595,53 +593,20 @@ class MCU: self._is_shutdown = self._is_timeout = False self._shutdown_clock = 0 self._shutdown_msg = "" - # Config building - printer.lookup_object('pins').register_chip(self._name, self) - self._oid_count = 0 - self._config_callbacks = [] - self._config_cmds = [] - self._restart_cmds = [] - self._init_cmds = [] - self._mcu_freq = 0. - self._reserved_move_slots = 0 - # Stats - self._get_status_info = {} - self._stats_sumsq_base = 0. - self._mcu_tick_avg = 0. - self._mcu_tick_stddev = 0. - self._mcu_tick_awake = 0. - # Alter time reporting when debugging - if self.is_fileoutput(): - def dummy_estimated_print_time(eventtime): - return 0. - self.estimated_print_time = dummy_estimated_print_time # Register handlers - printer.load_object(config, "error_mcu") printer.register_event_handler("klippy:firmware_restart", self._firmware_restart) - printer.register_event_handler("klippy:mcu_identify", - self._mcu_identify) - printer.register_event_handler("klippy:connect", self._connect) printer.register_event_handler("klippy:shutdown", self._shutdown) printer.register_event_handler("klippy:disconnect", self._disconnect) - printer.register_event_handler("klippy:ready", self._ready) - # Serial callbacks - def _handle_mcu_stats(self, params): - count = params['count'] - tick_sum = params['sum'] - c = 1.0 / (count * self._mcu_freq) - self._mcu_tick_avg = tick_sum * c - tick_sumsq = params['sumsq'] * self._stats_sumsq_base - diff = count*tick_sumsq - tick_sum**2 - self._mcu_tick_stddev = c * math.sqrt(max(0., diff)) - self._mcu_tick_awake = tick_sum / self._mcu_freq + def get_serial(self): + return self._serial def _handle_shutdown(self, params): if self._is_shutdown: return self._is_shutdown = True clock = params.get("clock") if clock is not None: - self._shutdown_clock = self.clock32_to_clock64(clock) + self._shutdown_clock = self._mcu.clock32_to_clock64(clock) self._shutdown_msg = msg = params['static_string_id'] event_type = params['#name'] self._printer.invoke_async_shutdown( @@ -664,9 +629,9 @@ class MCU: self._printer.request_exit('firmware_restart') self._reactor.pause(self._reactor.monotonic() + 2.000) raise error("Attempt MCU '%s' restart failed" % (self._name,)) - def _check_restart_on_crc_mismatch(self): + def check_restart_on_crc_mismatch(self): self._check_restart("CRC mismatch") - def _check_restart_on_send_config(self): + def check_restart_on_send_config(self): if self._restart_method == 'rpi_usb': # Only configure mcu after usb power reset self._check_restart("full reset before config") @@ -679,6 +644,17 @@ class MCU: # Cheetah boards require RTS to be deasserted # else a reset will trigger the built-in bootloader. return (self._restart_method != "cheetah") + def log_info(self): + msgparser = self._serial.get_msgparser() + message_count = len(msgparser.get_messages()) + version, build_versions = msgparser.get_version_info() + log_info = [ + "Loaded MCU '%s' %d commands (%s / %s)" + % (self._name, message_count, version, build_versions), + "MCU '%s' config: %s" % (self._name, " ".join( + ["%s=%s" % (k, v) + for k, v in msgparser.get_constants().items()]))] + return "\n".join(log_info) def _attach_file(self): # In a debugging mode. Open debug output file and read data dictionary start_args = self._printer.get_start_args() @@ -694,6 +670,166 @@ class MCU: dfile.close() self._serial.connect_file(outfile, dict_data) self._clocksync.connect_file(self._serial) + def _attach(self): + self._check_restart_on_attach() + try: + if self._canbus_iface is not None: + cbid = self._printer.lookup_object('canbus_ids') + nodeid = cbid.get_nodeid(self._serialport) + self._serial.connect_canbus(self._serialport, nodeid, + self._canbus_iface) + elif self._baud: + rts = self._lookup_attach_uart_rts() + self._serial.connect_uart(self._serialport, self._baud, rts) + else: + self._serial.connect_pipe(self._serialport) + self._clocksync.connect(self._serial) + except serialhdl.error as e: + raise error(str(e)) + def _post_attach_setup_shutdown(self): + self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop") + self._mcu.register_response(self._handle_shutdown, 'shutdown') + self._mcu.register_response(self._handle_shutdown, 'is_shutdown') + self._mcu.register_response(self._handle_starting, 'starting') + def _post_attach_setup_restart(self): + self._reset_cmd = self._mcu.try_lookup_command("reset") + self._config_reset_cmd = self._mcu.try_lookup_command("config_reset") + ext_only = self._reset_cmd is None and self._config_reset_cmd is None + msgparser = self._serial.get_msgparser() + mbaud = msgparser.get_constant('SERIAL_BAUD', None) + if self._restart_method is None and mbaud is None and not ext_only: + self._restart_method = 'command' + if msgparser.get_constant('CANBUS_BRIDGE', 0): + self._is_mcu_bridge = True + self._printer.register_event_handler("klippy:firmware_restart", + self._firmware_restart_bridge) + def start_attach(self): + if self._mcu.is_fileoutput(): + self._attach_file() + else: + self._attach() + logging.info(self.log_info()) + self._post_attach_setup_shutdown() + self._post_attach_setup_restart() + # Restarts + def _disconnect(self): + self._serial.disconnect() + def _shutdown(self, force=False): + if (self._emergency_stop_cmd is None + or (self._is_shutdown and not force)): + return + self._emergency_stop_cmd.send() + def _restart_arduino(self): + logging.info("Attempting MCU '%s' reset", self._name) + self._disconnect() + serialhdl.arduino_reset(self._serialport, self._reactor) + def _restart_cheetah(self): + logging.info("Attempting MCU '%s' Cheetah-style reset", self._name) + self._disconnect() + serialhdl.cheetah_reset(self._serialport, self._reactor) + def _restart_via_command(self): + if ((self._reset_cmd is None and self._config_reset_cmd is None) + or not self._clocksync.is_active()): + logging.info("Unable to issue reset command on MCU '%s'", + self._name) + return + if self._reset_cmd is None: + # Attempt reset via config_reset command + logging.info("Attempting MCU '%s' config_reset command", self._name) + self._is_shutdown = True + self._shutdown(force=True) + self._reactor.pause(self._reactor.monotonic() + 0.015) + self._config_reset_cmd.send() + else: + # Attempt reset via reset command + logging.info("Attempting MCU '%s' reset command", self._name) + self._reset_cmd.send() + self._reactor.pause(self._reactor.monotonic() + 0.015) + self._disconnect() + def _restart_rpi_usb(self): + logging.info("Attempting MCU '%s' reset via rpi usb power", self._name) + self._disconnect() + chelper.run_hub_ctrl(0) + self._reactor.pause(self._reactor.monotonic() + 2.) + chelper.run_hub_ctrl(1) + def _firmware_restart(self, force=False): + if self._is_mcu_bridge and not force: + return + if self._restart_method == 'rpi_usb': + self._restart_rpi_usb() + elif self._restart_method == 'command': + self._restart_via_command() + elif self._restart_method == 'cheetah': + self._restart_cheetah() + else: + self._restart_arduino() + def _firmware_restart_bridge(self): + self._firmware_restart(True) + def check_timeout(self, eventtime): + if (self._clocksync.is_active() or self._mcu.is_fileoutput() + or self._is_timeout): + return + self._is_timeout = True + logging.info("Timeout with MCU '%s' (eventtime=%f)", + self._name, eventtime) + self._printer.invoke_shutdown("Lost communication with MCU '%s'" % ( + self._name,)) + def is_shutdown(self): + return self._is_shutdown + def get_shutdown_clock(self): + return self._shutdown_clock + def get_shutdown_msg(self): + return self._shutdown_msg + +# Main MCU class +class MCU: + error = error + def __init__(self, config, clocksync): + self._printer = printer = config.get_printer() + self._clocksync = clocksync + self._reactor = printer.get_reactor() + self._name = config.get_name() + if self._name.startswith('mcu '): + self._name = self._name[4:] + # Low-level connection + self._conn_helper = MCUConnectHelper(config, self, clocksync) + self._serial = self._conn_helper.get_serial() + # Config building + printer.lookup_object('pins').register_chip(self._name, self) + self._oid_count = 0 + self._config_callbacks = [] + self._config_cmds = [] + self._restart_cmds = [] + self._init_cmds = [] + self._mcu_freq = 0. + self._reserved_move_slots = 0 + # Stats + self._get_status_info = {} + self._stats_sumsq_base = 0. + self._mcu_tick_avg = 0. + self._mcu_tick_stddev = 0. + self._mcu_tick_awake = 0. + # Alter time reporting when debugging + if self.is_fileoutput(): + def dummy_estimated_print_time(eventtime): + return 0. + self.estimated_print_time = dummy_estimated_print_time + # Register handlers + printer.load_object(config, "error_mcu") + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) + printer.register_event_handler("klippy:connect", self._connect) + printer.register_event_handler("klippy:ready", self._ready) + # Serial callbacks + def _handle_mcu_stats(self, params): + count = params['count'] + tick_sum = params['sum'] + c = 1.0 / (count * self._mcu_freq) + self._mcu_tick_avg = tick_sum * c + tick_sumsq = params['sumsq'] * self._stats_sumsq_base + diff = count*tick_sumsq - tick_sum**2 + self._mcu_tick_stddev = c * math.sqrt(max(0., diff)) + self._mcu_tick_awake = tick_sum / self._mcu_freq def _send_config(self, prev_crc): # Build config commands for cb in self._config_callbacks: @@ -711,7 +847,7 @@ class MCU: config_crc = zlib.crc32(encoded_config) & 0xffffffff self.add_config_cmd("finalize_config crc=%d" % (config_crc,)) if prev_crc is not None and config_crc != prev_crc: - self._check_restart_on_crc_mismatch() + self._conn_helper.check_restart_on_crc_mismatch() raise error("MCU '%s' CRC does not match config" % (self._name,)) # Transmit config messages (if needed) try: @@ -741,27 +877,17 @@ class MCU: if self.is_fileoutput(): return { 'is_config': 0, 'move_count': 500, 'crc': 0 } config_params = get_config_cmd.send() - if self._is_shutdown: + if self._conn_helper.is_shutdown(): raise error("MCU '%s' error during config: %s" % ( - self._name, self._shutdown_msg)) + self._name, self._conn_helper.get_shutdown_msg())) if config_params['is_shutdown']: raise error("Can not update MCU '%s' config as it is shutdown" % ( self._name,)) return config_params - def _log_info(self): - msgparser = self._serial.get_msgparser() - message_count = len(msgparser.get_messages()) - version, build_versions = msgparser.get_version_info() - log_info = [ - "Loaded MCU '%s' %d commands (%s / %s)" - % (self._name, message_count, version, build_versions), - "MCU '%s' config: %s" % (self._name, " ".join( - ["%s=%s" % (k, v) for k, v in self.get_constants().items()]))] - return "\n".join(log_info) def _connect(self): config_params = self._send_get_config() if not config_params['is_config']: - self._check_restart_on_send_config() + self._conn_helper.check_restart_on_send_config() # Not configured - send config and issue get_config again self._send_config(None) config_params = self._send_get_config() @@ -785,41 +911,8 @@ class MCU: # Log config information move_msg = "Configured MCU '%s' (%d moves)" % (self._name, move_count) logging.info(move_msg) - log_info = self._log_info() + "\n" + move_msg + log_info = self._conn_helper.log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) - def _attach(self): - self._check_restart_on_attach() - try: - if self._canbus_iface is not None: - cbid = self._printer.lookup_object('canbus_ids') - nodeid = cbid.get_nodeid(self._serialport) - self._serial.connect_canbus(self._serialport, nodeid, - self._canbus_iface) - elif self._baud: - rts = self._lookup_attach_uart_rts() - self._serial.connect_uart(self._serialport, self._baud, rts) - else: - self._serial.connect_pipe(self._serialport) - self._clocksync.connect(self._serial) - except serialhdl.error as e: - raise error(str(e)) - def _post_attach_setup_shutdown(self): - self._emergency_stop_cmd = self.lookup_command("emergency_stop") - self.register_response(self._handle_shutdown, 'shutdown') - self.register_response(self._handle_shutdown, 'is_shutdown') - self.register_response(self._handle_starting, 'starting') - def _post_attach_setup_restart(self): - self._reset_cmd = self.try_lookup_command("reset") - self._config_reset_cmd = self.try_lookup_command("config_reset") - ext_only = self._reset_cmd is None and self._config_reset_cmd is None - msgparser = self._serial.get_msgparser() - mbaud = msgparser.get_constant('SERIAL_BAUD', None) - if self._restart_method is None and mbaud is None and not ext_only: - self._restart_method = 'command' - if msgparser.get_constant('CANBUS_BRIDGE', 0): - self._is_mcu_bridge = True - self._printer.register_event_handler("klippy:firmware_restart", - self._firmware_restart_bridge) def _post_attach_setup_stats(self): self._stats_sumsq_base = self.get_constant_float('STATS_SUMSQ_BASE') msgparser = self._serial.get_msgparser() @@ -837,13 +930,7 @@ class MCU: for pin in value.split(','): pin_resolver.reserve_pin(pin, cname[13:]) def _mcu_identify(self): - if self.is_fileoutput(): - self._attach_file() - else: - self._attach() - logging.info(self._log_info()) - self._post_attach_setup_shutdown() - self._post_attach_setup_restart() + self._conn_helper.start_attach() self._post_attach_setup_for_config() self._post_attach_setup_stats() def _ready(self): @@ -926,83 +1013,20 @@ class MCU: return self._clocksync.estimated_print_time(eventtime) def clock32_to_clock64(self, clock32): return self._clocksync.clock32_to_clock64(clock32) - # Restarts - def _disconnect(self): - self._serial.disconnect() - def _shutdown(self, force=False): - if (self._emergency_stop_cmd is None - or (self._is_shutdown and not force)): - return - self._emergency_stop_cmd.send() - def _restart_arduino(self): - logging.info("Attempting MCU '%s' reset", self._name) - self._disconnect() - serialhdl.arduino_reset(self._serialport, self._reactor) - def _restart_cheetah(self): - logging.info("Attempting MCU '%s' Cheetah-style reset", self._name) - self._disconnect() - serialhdl.cheetah_reset(self._serialport, self._reactor) - def _restart_via_command(self): - if ((self._reset_cmd is None and self._config_reset_cmd is None) - or not self._clocksync.is_active()): - logging.info("Unable to issue reset command on MCU '%s'", - self._name) - return - if self._reset_cmd is None: - # Attempt reset via config_reset command - logging.info("Attempting MCU '%s' config_reset command", self._name) - self._is_shutdown = True - self._shutdown(force=True) - self._reactor.pause(self._reactor.monotonic() + 0.015) - self._config_reset_cmd.send() - else: - # Attempt reset via reset command - logging.info("Attempting MCU '%s' reset command", self._name) - self._reset_cmd.send() - self._reactor.pause(self._reactor.monotonic() + 0.015) - self._disconnect() - def _restart_rpi_usb(self): - logging.info("Attempting MCU '%s' reset via rpi usb power", self._name) - self._disconnect() - chelper.run_hub_ctrl(0) - self._reactor.pause(self._reactor.monotonic() + 2.) - chelper.run_hub_ctrl(1) - def _firmware_restart(self, force=False): - if self._is_mcu_bridge and not force: - return - if self._restart_method == 'rpi_usb': - self._restart_rpi_usb() - elif self._restart_method == 'command': - self._restart_via_command() - elif self._restart_method == 'cheetah': - self._restart_cheetah() - else: - self._restart_arduino() - def _firmware_restart_bridge(self): - self._firmware_restart(True) # Move queue tracking def request_move_queue_slot(self): self._reserved_move_slots += 1 - def _check_timeout(self, eventtime): - if (self._clocksync.is_active() or self.is_fileoutput() - or self._is_timeout): - return - self._is_timeout = True - logging.info("Timeout with MCU '%s' (eventtime=%f)", - self._name, eventtime) - self._printer.invoke_shutdown("Lost communication with MCU '%s'" % ( - self._name,)) def calibrate_clock(self, print_time, eventtime): offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) - self._check_timeout(eventtime) + self._conn_helper.check_timeout(eventtime) return offset, freq # Misc external commands def is_fileoutput(self): return self._printer.get_start_args().get('debugoutput') is not None def is_shutdown(self): - return self._is_shutdown + return self._conn_helper.is_shutdown() def get_shutdown_clock(self): - return self._shutdown_clock + return self._conn_helper.get_shutdown_clock() def get_status(self, eventtime=None): return dict(self._get_status_info) def stats(self, eventtime): From 1bb674d0b8127f07db19c5e37b8db63ae6d9fc32 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 22:23:15 -0400 Subject: [PATCH 084/117] mcu: Add new MCUStatsHelper() helper class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 121 +++++++++++++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 50 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index f97d4954c..cb749b082 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -598,8 +598,12 @@ class MCUConnectHelper: self._firmware_restart) printer.register_event_handler("klippy:shutdown", self._shutdown) printer.register_event_handler("klippy:disconnect", self._disconnect) + def get_mcu(self): + return self._mcu def get_serial(self): return self._serial + def get_clocksync(self): + return self._clocksync def _handle_shutdown(self, params): if self._is_shutdown: return @@ -781,6 +785,69 @@ class MCUConnectHelper: def get_shutdown_msg(self): return self._shutdown_msg +# Handle statistics reporting +class MCUStatsHelper: + def __init__(self, config, conn_helper): + self._printer = printer = config.get_printer() + self._mcu = mcu = conn_helper.get_mcu() + self._serial = conn_helper.get_serial() + self._clocksync = conn_helper.get_clocksync() + self._reactor = printer.get_reactor() + self._name = mcu.get_name() + self._mcu_freq = 0. + self._get_status_info = {} + self._stats_sumsq_base = 0. + self._mcu_tick_avg = 0. + self._mcu_tick_stddev = 0. + self._mcu_tick_awake = 0. + printer.register_event_handler("klippy:ready", self._ready) + def _handle_mcu_stats(self, params): + count = params['count'] + tick_sum = params['sum'] + c = 1.0 / (count * self._mcu_freq) + self._mcu_tick_avg = tick_sum * c + tick_sumsq = params['sumsq'] * self._stats_sumsq_base + diff = count*tick_sumsq - tick_sum**2 + self._mcu_tick_stddev = c * math.sqrt(max(0., diff)) + self._mcu_tick_awake = tick_sum / self._mcu_freq + def post_attach_setup_stats(self): + self._mcu_freq = self._mcu.get_constant_float('CLOCK_FREQ') + self._stats_sumsq_base = self._mcu.get_constant_float( + 'STATS_SUMSQ_BASE') + msgparser = self._serial.get_msgparser() + version, build_versions = msgparser.get_version_info() + self._get_status_info['mcu_version'] = version + self._get_status_info['mcu_build_versions'] = build_versions + self._get_status_info['mcu_constants'] = msgparser.get_constants() + self._mcu.register_response(self._handle_mcu_stats, 'stats') + def _ready(self): + if self._mcu.is_fileoutput(): + return + # Check that reported mcu frequency is in range + mcu_freq = self._mcu_freq + systime = self._reactor.monotonic() + get_clock = self._clocksync.get_clock + calc_freq = get_clock(systime + 1) - get_clock(systime) + freq_diff = abs(mcu_freq - calc_freq) + mcu_freq_mhz = int(mcu_freq / 1000000. + 0.5) + calc_freq_mhz = int(calc_freq / 1000000. + 0.5) + if freq_diff > mcu_freq*0.01 and mcu_freq_mhz != calc_freq_mhz: + pconfig = self._printer.lookup_object('configfile') + msg = ("MCU '%s' configured for %dMhz but running at %dMhz!" + % (self._name, mcu_freq_mhz, calc_freq_mhz)) + pconfig.runtime_warning(msg) + def get_status(self, eventtime=None): + return dict(self._get_status_info) + def stats(self, eventtime): + load = "mcu_awake=%.03f mcu_task_avg=%.06f mcu_task_stddev=%.06f" % ( + self._mcu_tick_awake, self._mcu_tick_avg, self._mcu_tick_stddev) + stats = ' '.join([load, self._serial.stats(eventtime), + self._clocksync.stats(eventtime)]) + parts = [s.split('=', 1) for s in stats.split()] + last_stats = {k:(float(v) if '.' in v else int(v)) for k, v in parts} + self._get_status_info['last_stats'] = last_stats + return False, '%s: %s' % (self._name, stats) + # Main MCU class class MCU: error = error @@ -803,33 +870,18 @@ class MCU: self._init_cmds = [] self._mcu_freq = 0. self._reserved_move_slots = 0 - # Stats - self._get_status_info = {} - self._stats_sumsq_base = 0. - self._mcu_tick_avg = 0. - self._mcu_tick_stddev = 0. - self._mcu_tick_awake = 0. # Alter time reporting when debugging if self.is_fileoutput(): def dummy_estimated_print_time(eventtime): return 0. self.estimated_print_time = dummy_estimated_print_time # Register handlers + self._stats_helper = MCUStatsHelper(self, self._conn_helper) printer.load_object(config, "error_mcu") printer.register_event_handler("klippy:mcu_identify", self._mcu_identify) printer.register_event_handler("klippy:connect", self._connect) - printer.register_event_handler("klippy:ready", self._ready) # Serial callbacks - def _handle_mcu_stats(self, params): - count = params['count'] - tick_sum = params['sum'] - c = 1.0 / (count * self._mcu_freq) - self._mcu_tick_avg = tick_sum * c - tick_sumsq = params['sumsq'] * self._stats_sumsq_base - diff = count*tick_sumsq - tick_sum**2 - self._mcu_tick_stddev = c * math.sqrt(max(0., diff)) - self._mcu_tick_awake = tick_sum / self._mcu_freq def _send_config(self, prev_crc): # Build config commands for cb in self._config_callbacks: @@ -913,14 +965,6 @@ class MCU: logging.info(move_msg) log_info = self._conn_helper.log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) - def _post_attach_setup_stats(self): - self._stats_sumsq_base = self.get_constant_float('STATS_SUMSQ_BASE') - msgparser = self._serial.get_msgparser() - version, build_versions = msgparser.get_version_info() - self._get_status_info['mcu_version'] = version - self._get_status_info['mcu_build_versions'] = build_versions - self._get_status_info['mcu_constants'] = msgparser.get_constants() - self.register_response(self._handle_mcu_stats, 'stats') def _post_attach_setup_for_config(self): self._mcu_freq = self.get_constant_float('CLOCK_FREQ') ppins = self._printer.lookup_object('pins') @@ -932,23 +976,7 @@ class MCU: def _mcu_identify(self): self._conn_helper.start_attach() self._post_attach_setup_for_config() - self._post_attach_setup_stats() - def _ready(self): - if self.is_fileoutput(): - return - # Check that reported mcu frequency is in range - mcu_freq = self._mcu_freq - systime = self._reactor.monotonic() - get_clock = self._clocksync.get_clock - calc_freq = get_clock(systime + 1) - get_clock(systime) - freq_diff = abs(mcu_freq - calc_freq) - mcu_freq_mhz = int(mcu_freq / 1000000. + 0.5) - calc_freq_mhz = int(calc_freq / 1000000. + 0.5) - if freq_diff > mcu_freq*0.01 and mcu_freq_mhz != calc_freq_mhz: - pconfig = self._printer.lookup_object('configfile') - msg = ("MCU '%s' configured for %dMhz but running at %dMhz!" - % (self._name, mcu_freq_mhz, calc_freq_mhz)) - pconfig.runtime_warning(msg) + self._stats_helper.post_attach_setup_stats() # Config creation helpers def setup_pin(self, pin_type, pin_params): pcs = {'endstop': MCU_endstop, @@ -1028,16 +1056,9 @@ class MCU: def get_shutdown_clock(self): return self._conn_helper.get_shutdown_clock() def get_status(self, eventtime=None): - return dict(self._get_status_info) + return self._stats_helper.get_status(eventtime) def stats(self, eventtime): - load = "mcu_awake=%.03f mcu_task_avg=%.06f mcu_task_stddev=%.06f" % ( - self._mcu_tick_awake, self._mcu_tick_avg, self._mcu_tick_stddev) - stats = ' '.join([load, self._serial.stats(eventtime), - self._clocksync.stats(eventtime)]) - parts = [s.split('=', 1) for s in stats.split()] - last_stats = {k:(float(v) if '.' in v else int(v)) for k, v in parts} - self._get_status_info['last_stats'] = last_stats - return False, '%s: %s' % (self._name, stats) + return self._stats_helper.stats(eventtime) def add_printer_objects(config): printer = config.get_printer() From 64c155121e01bcfcb5bcace0f050eea9af94b5d7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 22:23:40 -0400 Subject: [PATCH 085/117] mcu: Add new MCUConfigHelper helper class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 104 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 41 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index cb749b082..fa1ac6eb0 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -848,21 +848,17 @@ class MCUStatsHelper: self._get_status_info['last_stats'] = last_stats return False, '%s: %s' % (self._name, stats) -# Main MCU class -class MCU: - error = error - def __init__(self, config, clocksync): +# Handle process of configuring an mcu +class MCUConfigHelper: + def __init__(self, config, conn_helper): self._printer = printer = config.get_printer() - self._clocksync = clocksync + self._conn_helper = conn_helper + self._mcu = mcu = conn_helper.get_mcu() + self._serial = conn_helper.get_serial() + self._clocksync = conn_helper.get_clocksync() self._reactor = printer.get_reactor() - self._name = config.get_name() - if self._name.startswith('mcu '): - self._name = self._name[4:] - # Low-level connection - self._conn_helper = MCUConnectHelper(config, self, clocksync) - self._serial = self._conn_helper.get_serial() - # Config building - printer.lookup_object('pins').register_chip(self._name, self) + self._name = mcu.get_name() + printer.lookup_object('pins').register_chip(self._name, mcu) self._oid_count = 0 self._config_callbacks = [] self._config_cmds = [] @@ -870,18 +866,7 @@ class MCU: self._init_cmds = [] self._mcu_freq = 0. self._reserved_move_slots = 0 - # Alter time reporting when debugging - if self.is_fileoutput(): - def dummy_estimated_print_time(eventtime): - return 0. - self.estimated_print_time = dummy_estimated_print_time - # Register handlers - self._stats_helper = MCUStatsHelper(self, self._conn_helper) - printer.load_object(config, "error_mcu") - printer.register_event_handler("klippy:mcu_identify", - self._mcu_identify) printer.register_event_handler("klippy:connect", self._connect) - # Serial callbacks def _send_config(self, prev_crc): # Build config commands for cb in self._config_callbacks: @@ -923,10 +908,10 @@ class MCU: % (enum_value, self._name)) raise def _send_get_config(self): - get_config_cmd = self.lookup_query_command( + get_config_cmd = self._mcu.lookup_query_command( "get_config", "config is_config=%c crc=%u is_shutdown=%c move_count=%hu") - if self.is_fileoutput(): + if self._mcu.is_fileoutput(): return { 'is_config': 0, 'move_count': 500, 'crc': 0 } config_params = get_config_cmd.send() if self._conn_helper.is_shutdown(): @@ -943,7 +928,7 @@ class MCU: # Not configured - send config and issue get_config again self._send_config(None) config_params = self._send_get_config() - if not config_params['is_config'] and not self.is_fileoutput(): + if not config_params['is_config'] and not self._mcu.is_fileoutput(): raise error("Unable to configure MCU '%s'" % (self._name,)) else: start_reason = self._printer.get_start_args().get("start_reason") @@ -959,31 +944,27 @@ class MCU: ss_move_count = move_count - self._reserved_move_slots motion_queuing = self._printer.lookup_object('motion_queuing') motion_queuing.setup_mcu_movequeue( - self, self._serial.get_serialqueue(), ss_move_count) + self._mcu, self._serial.get_serialqueue(), ss_move_count) # Log config information move_msg = "Configured MCU '%s' (%d moves)" % (self._name, move_count) logging.info(move_msg) log_info = self._conn_helper.log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) - def _post_attach_setup_for_config(self): - self._mcu_freq = self.get_constant_float('CLOCK_FREQ') + def post_attach_setup_for_config(self): + self._mcu_freq = self._mcu.get_constant_float('CLOCK_FREQ') ppins = self._printer.lookup_object('pins') pin_resolver = ppins.get_pin_resolver(self._name) - for cname, value in self.get_constants().items(): + for cname, value in self._mcu.get_constants().items(): if cname.startswith("RESERVE_PINS_"): for pin in value.split(','): pin_resolver.reserve_pin(pin, cname[13:]) - def _mcu_identify(self): - self._conn_helper.start_attach() - self._post_attach_setup_for_config() - self._stats_helper.post_attach_setup_stats() # Config creation helpers def setup_pin(self, pin_type, pin_params): pcs = {'endstop': MCU_endstop, 'digital_out': MCU_digital_out, 'pwm': MCU_pwm, 'adc': MCU_adc} if pin_type not in pcs: raise pins.error("pin type %s not supported on mcu" % (pin_type,)) - return pcs[pin_type](self, pin_params) + return pcs[pin_type](self._mcu, pin_params) def create_oid(self): self._oid_count += 1 return self._oid_count - 1 @@ -998,10 +979,54 @@ class MCU: self._config_cmds.append(cmd) def get_query_slot(self, oid): slot = self.seconds_to_clock(oid * .01) - t = int(self.estimated_print_time(self._reactor.monotonic()) + 1.5) - return self.print_time_to_clock(t) + slot + t = int(self._mcu.estimated_print_time(self._reactor.monotonic()) + 1.5) + return self._mcu.print_time_to_clock(t) + slot def seconds_to_clock(self, time): return int(time * self._mcu_freq) + def request_move_queue_slot(self): + self._reserved_move_slots += 1 + +# Main MCU class +class MCU: + error = error + def __init__(self, config, clocksync): + self._printer = printer = config.get_printer() + self._clocksync = clocksync + self._name = config.get_name() + if self._name.startswith('mcu '): + self._name = self._name[4:] + # Low-level connection + self._conn_helper = MCUConnectHelper(config, self, clocksync) + self._serial = self._conn_helper.get_serial() + self._config_helper = MCUConfigHelper(self, self._conn_helper) + # Alter time reporting when debugging + if self.is_fileoutput(): + def dummy_estimated_print_time(eventtime): + return 0. + self.estimated_print_time = dummy_estimated_print_time + # Register handlers + self._stats_helper = MCUStatsHelper(self, self._conn_helper) + printer.load_object(config, "error_mcu") + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) + def _mcu_identify(self): + self._conn_helper.start_attach() + self._config_helper.post_attach_setup_for_config() + self._stats_helper.post_attach_setup_stats() + def setup_pin(self, pin_type, pin_params): + return self._config_helper.setup_pin(pin_type, pin_params) + def create_oid(self): + return self._config_helper.create_oid() + def register_config_callback(self, cb): + self._config_helper.register_config_callback(cb) + def add_config_cmd(self, cmd, is_init=False, on_restart=False): + self._config_helper.add_config_cmd(cmd, is_init, on_restart) + def request_move_queue_slot(self): + self._config_helper.request_move_queue_slot() + def get_query_slot(self, oid): + return self._config_helper.get_query_slot(oid) + def seconds_to_clock(self, time): + return self._config_helper.seconds_to_clock(time) def min_schedule_time(self): return MIN_SCHEDULE_TIME def max_nominal_duration(self): @@ -1041,9 +1066,6 @@ class MCU: return self._clocksync.estimated_print_time(eventtime) def clock32_to_clock64(self, clock32): return self._clocksync.clock32_to_clock64(clock32) - # Move queue tracking - def request_move_queue_slot(self): - self._reserved_move_slots += 1 def calibrate_clock(self, print_time, eventtime): offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) self._conn_helper.check_timeout(eventtime) From de73e41d0f1b1af35a9faa9c01523a8a79912929 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 22 Sep 2025 15:47:57 -0400 Subject: [PATCH 086/117] mcu: Add new MCURestartHelper helper class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 272 +++++++++++++++++++++++++++----------------------- 1 file changed, 147 insertions(+), 125 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index fa1ac6eb0..9375f20e1 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -557,72 +557,28 @@ MIN_SCHEDULE_TIME = 0.100 # Maximum time all MCUs can internally schedule into the future MAX_NOMINAL_DURATION = 3.0 -# Low-level mcu connection management helper -class MCUConnectHelper: - def __init__(self, config, mcu, clocksync): - self._mcu = mcu - self._clocksync = clocksync +# Support for restarting a micro-controller +class MCURestartHelper: + def __init__(self, config, conn_helper): self._printer = printer = config.get_printer() + self._conn_helper = conn_helper + self._mcu = mcu = conn_helper.get_mcu() + self._serial = conn_helper.get_serial() + self._clocksync = conn_helper.get_clocksync() self._reactor = printer.get_reactor() - self._name = name = mcu.get_name() - # Serial port - self._serial = serialhdl.SerialReader(self._reactor, mcu_name=name) - self._baud = 0 - self._canbus_iface = None - canbus_uuid = config.get('canbus_uuid', None) - if canbus_uuid is not None: - self._serialport = canbus_uuid - self._canbus_iface = config.get('canbus_interface', 'can0') - cbid = self._printer.load_object(config, 'canbus_ids') - cbid.add_uuid(config, canbus_uuid, self._canbus_iface) - self._printer.load_object(config, 'canbus_stats %s' % (name,)) - else: - self._serialport = config.get('serial') - if not (self._serialport.startswith("/dev/rpmsg_") - or self._serialport.startswith("/tmp/klipper_host_")): - self._baud = config.getint('baud', 250000, minval=2400) - # Restarts + self._name = mcu.get_name() restart_methods = [None, 'arduino', 'cheetah', 'command', 'rpi_usb'] self._restart_method = 'command' - if self._baud: + serialport, baud = conn_helper.get_serialport() + if baud: self._restart_method = config.getchoice('restart_method', restart_methods, None) self._reset_cmd = self._config_reset_cmd = None self._is_mcu_bridge = False - self._emergency_stop_cmd = None - self._is_shutdown = self._is_timeout = False - self._shutdown_clock = 0 - self._shutdown_msg = "" # Register handlers printer.register_event_handler("klippy:firmware_restart", self._firmware_restart) - printer.register_event_handler("klippy:shutdown", self._shutdown) printer.register_event_handler("klippy:disconnect", self._disconnect) - def get_mcu(self): - return self._mcu - def get_serial(self): - return self._serial - def get_clocksync(self): - return self._clocksync - def _handle_shutdown(self, params): - if self._is_shutdown: - return - self._is_shutdown = True - clock = params.get("clock") - if clock is not None: - self._shutdown_clock = self._mcu.clock32_to_clock64(clock) - self._shutdown_msg = msg = params['static_string_id'] - event_type = params['#name'] - self._printer.invoke_async_shutdown( - "MCU shutdown", {"reason": msg, "mcu": self._name, - "event_type": event_type}) - logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, event_type, - self._shutdown_msg, self._clocksync.dump_debug(), - self._serial.dump_debug()) - def _handle_starting(self, params): - if not self._is_shutdown: - self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart" - % (self._name,)) # Connection phase def _check_restart(self, reason): start_reason = self._printer.get_start_args().get("start_reason") @@ -639,63 +595,17 @@ class MCUConnectHelper: if self._restart_method == 'rpi_usb': # Only configure mcu after usb power reset self._check_restart("full reset before config") - def _check_restart_on_attach(self): + def check_restart_on_attach(self): resmeth = self._restart_method - if resmeth == 'rpi_usb' and not os.path.exists(self._serialport): + serialport, baud = self._conn_helper.get_serialport() + if resmeth == 'rpi_usb' and not os.path.exists(serialport): # Try toggling usb power self._check_restart("enable power") - def _lookup_attach_uart_rts(self): + def lookup_attach_uart_rts(self): # Cheetah boards require RTS to be deasserted # else a reset will trigger the built-in bootloader. return (self._restart_method != "cheetah") - def log_info(self): - msgparser = self._serial.get_msgparser() - message_count = len(msgparser.get_messages()) - version, build_versions = msgparser.get_version_info() - log_info = [ - "Loaded MCU '%s' %d commands (%s / %s)" - % (self._name, message_count, version, build_versions), - "MCU '%s' config: %s" % (self._name, " ".join( - ["%s=%s" % (k, v) - for k, v in msgparser.get_constants().items()]))] - return "\n".join(log_info) - def _attach_file(self): - # In a debugging mode. Open debug output file and read data dictionary - start_args = self._printer.get_start_args() - if self._name == 'mcu': - out_fname = start_args.get('debugoutput') - dict_fname = start_args.get('dictionary') - else: - out_fname = start_args.get('debugoutput') + "-" + self._name - dict_fname = start_args.get('dictionary_' + self._name) - outfile = open(out_fname, 'wb') - dfile = open(dict_fname, 'rb') - dict_data = dfile.read() - dfile.close() - self._serial.connect_file(outfile, dict_data) - self._clocksync.connect_file(self._serial) - def _attach(self): - self._check_restart_on_attach() - try: - if self._canbus_iface is not None: - cbid = self._printer.lookup_object('canbus_ids') - nodeid = cbid.get_nodeid(self._serialport) - self._serial.connect_canbus(self._serialport, nodeid, - self._canbus_iface) - elif self._baud: - rts = self._lookup_attach_uart_rts() - self._serial.connect_uart(self._serialport, self._baud, rts) - else: - self._serial.connect_pipe(self._serialport) - self._clocksync.connect(self._serial) - except serialhdl.error as e: - raise error(str(e)) - def _post_attach_setup_shutdown(self): - self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop") - self._mcu.register_response(self._handle_shutdown, 'shutdown') - self._mcu.register_response(self._handle_shutdown, 'is_shutdown') - self._mcu.register_response(self._handle_starting, 'starting') - def _post_attach_setup_restart(self): + def post_attach_setup_restart(self): self._reset_cmd = self._mcu.try_lookup_command("reset") self._config_reset_cmd = self._mcu.try_lookup_command("config_reset") ext_only = self._reset_cmd is None and self._config_reset_cmd is None @@ -707,30 +617,18 @@ class MCUConnectHelper: self._is_mcu_bridge = True self._printer.register_event_handler("klippy:firmware_restart", self._firmware_restart_bridge) - def start_attach(self): - if self._mcu.is_fileoutput(): - self._attach_file() - else: - self._attach() - logging.info(self.log_info()) - self._post_attach_setup_shutdown() - self._post_attach_setup_restart() - # Restarts def _disconnect(self): self._serial.disconnect() - def _shutdown(self, force=False): - if (self._emergency_stop_cmd is None - or (self._is_shutdown and not force)): - return - self._emergency_stop_cmd.send() def _restart_arduino(self): logging.info("Attempting MCU '%s' reset", self._name) self._disconnect() - serialhdl.arduino_reset(self._serialport, self._reactor) + serialport, baud = self._conn_helper.get_serialport() + serialhdl.arduino_reset(serialport, self._reactor) def _restart_cheetah(self): logging.info("Attempting MCU '%s' Cheetah-style reset", self._name) self._disconnect() - serialhdl.cheetah_reset(self._serialport, self._reactor) + serialport, baud = self._conn_helper.get_serialport() + serialhdl.cheetah_reset(serialport, self._reactor) def _restart_via_command(self): if ((self._reset_cmd is None and self._config_reset_cmd is None) or not self._clocksync.is_active()): @@ -740,8 +638,7 @@ class MCUConnectHelper: if self._reset_cmd is None: # Attempt reset via config_reset command logging.info("Attempting MCU '%s' config_reset command", self._name) - self._is_shutdown = True - self._shutdown(force=True) + self._conn_helper.force_local_shutdown() self._reactor.pause(self._reactor.monotonic() + 0.015) self._config_reset_cmd.send() else: @@ -769,6 +666,129 @@ class MCUConnectHelper: self._restart_arduino() def _firmware_restart_bridge(self): self._firmware_restart(True) + +# Low-level mcu connection management helper +class MCUConnectHelper: + def __init__(self, config, mcu, clocksync): + self._mcu = mcu + self._clocksync = clocksync + self._printer = printer = config.get_printer() + self._reactor = printer.get_reactor() + self._name = name = mcu.get_name() + # Serial port + self._serial = serialhdl.SerialReader(self._reactor, mcu_name=name) + self._baud = 0 + self._canbus_iface = None + canbus_uuid = config.get('canbus_uuid', None) + if canbus_uuid is not None: + self._serialport = canbus_uuid + self._canbus_iface = config.get('canbus_interface', 'can0') + cbid = self._printer.load_object(config, 'canbus_ids') + cbid.add_uuid(config, canbus_uuid, self._canbus_iface) + self._printer.load_object(config, 'canbus_stats %s' % (name,)) + else: + self._serialport = config.get('serial') + if not (self._serialport.startswith("/dev/rpmsg_") + or self._serialport.startswith("/tmp/klipper_host_")): + self._baud = config.getint('baud', 250000, minval=2400) + self._emergency_stop_cmd = None + self._is_shutdown = self._is_timeout = False + self._shutdown_clock = 0 + self._shutdown_msg = "" + self._restart_helper = MCURestartHelper(config, self) + printer.register_event_handler("klippy:shutdown", self._shutdown) + def get_mcu(self): + return self._mcu + def get_serial(self): + return self._serial + def get_clocksync(self): + return self._clocksync + def get_serialport(self): + return self._serialport, self._baud + def get_restart_helper(self): + return self._restart_helper + def _handle_shutdown(self, params): + if self._is_shutdown: + return + self._is_shutdown = True + clock = params.get("clock") + if clock is not None: + self._shutdown_clock = self._mcu.clock32_to_clock64(clock) + self._shutdown_msg = msg = params['static_string_id'] + event_type = params['#name'] + self._printer.invoke_async_shutdown( + "MCU shutdown", {"reason": msg, "mcu": self._name, + "event_type": event_type}) + logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, event_type, + self._shutdown_msg, self._clocksync.dump_debug(), + self._serial.dump_debug()) + def _handle_starting(self, params): + if not self._is_shutdown: + self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart" + % (self._name,)) + def log_info(self): + msgparser = self._serial.get_msgparser() + message_count = len(msgparser.get_messages()) + version, build_versions = msgparser.get_version_info() + log_info = [ + "Loaded MCU '%s' %d commands (%s / %s)" + % (self._name, message_count, version, build_versions), + "MCU '%s' config: %s" % (self._name, " ".join( + ["%s=%s" % (k, v) + for k, v in msgparser.get_constants().items()]))] + return "\n".join(log_info) + def _attach_file(self): + # In a debugging mode. Open debug output file and read data dictionary + start_args = self._printer.get_start_args() + if self._name == 'mcu': + out_fname = start_args.get('debugoutput') + dict_fname = start_args.get('dictionary') + else: + out_fname = start_args.get('debugoutput') + "-" + self._name + dict_fname = start_args.get('dictionary_' + self._name) + outfile = open(out_fname, 'wb') + dfile = open(dict_fname, 'rb') + dict_data = dfile.read() + dfile.close() + self._serial.connect_file(outfile, dict_data) + self._clocksync.connect_file(self._serial) + def _attach(self): + self._restart_helper.check_restart_on_attach() + try: + if self._canbus_iface is not None: + cbid = self._printer.lookup_object('canbus_ids') + nodeid = cbid.get_nodeid(self._serialport) + self._serial.connect_canbus(self._serialport, nodeid, + self._canbus_iface) + elif self._baud: + rts = self._restart_helper.lookup_attach_uart_rts() + self._serial.connect_uart(self._serialport, self._baud, rts) + else: + self._serial.connect_pipe(self._serialport) + self._clocksync.connect(self._serial) + except serialhdl.error as e: + raise error(str(e)) + def _post_attach_setup_shutdown(self): + self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop") + self._mcu.register_response(self._handle_shutdown, 'shutdown') + self._mcu.register_response(self._handle_shutdown, 'is_shutdown') + self._mcu.register_response(self._handle_starting, 'starting') + def start_attach(self): + if self._mcu.is_fileoutput(): + self._attach_file() + else: + self._attach() + logging.info(self.log_info()) + self._post_attach_setup_shutdown() + self._restart_helper.post_attach_setup_restart() + def _shutdown(self, force=False): + if (self._emergency_stop_cmd is None + or (self._is_shutdown and not force)): + return + self._emergency_stop_cmd.send() + def force_local_shutdown(self): + self._is_shutdown = True + self._shutdown(force=True) def check_timeout(self, eventtime): if (self._clocksync.is_active() or self._mcu.is_fileoutput() or self._is_timeout): @@ -884,7 +904,8 @@ class MCUConfigHelper: config_crc = zlib.crc32(encoded_config) & 0xffffffff self.add_config_cmd("finalize_config crc=%d" % (config_crc,)) if prev_crc is not None and config_crc != prev_crc: - self._conn_helper.check_restart_on_crc_mismatch() + restart_helper = self._conn_helper.get_restart_helper() + restart_helper.check_restart_on_crc_mismatch() raise error("MCU '%s' CRC does not match config" % (self._name,)) # Transmit config messages (if needed) try: @@ -924,7 +945,8 @@ class MCUConfigHelper: def _connect(self): config_params = self._send_get_config() if not config_params['is_config']: - self._conn_helper.check_restart_on_send_config() + restart_helper = self._conn_helper.get_restart_helper() + restart_helper.check_restart_on_send_config() # Not configured - send config and issue get_config again self._send_config(None) config_params = self._send_get_config() From 95f263fa5951e719b2a644a31dd7fd7c2734615d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 22 Sep 2025 16:33:21 -0400 Subject: [PATCH 087/117] mcu: Directly register "mcu_identify" handler in each helper class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 9375f20e1..c4e44e18e 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -567,6 +567,7 @@ class MCURestartHelper: self._clocksync = conn_helper.get_clocksync() self._reactor = printer.get_reactor() self._name = mcu.get_name() + # Restart tracking restart_methods = [None, 'arduino', 'cheetah', 'command', 'rpi_usb'] self._restart_method = 'command' serialport, baud = conn_helper.get_serialport() @@ -579,6 +580,8 @@ class MCURestartHelper: printer.register_event_handler("klippy:firmware_restart", self._firmware_restart) printer.register_event_handler("klippy:disconnect", self._disconnect) + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) # Connection phase def _check_restart(self, reason): start_reason = self._printer.get_start_args().get("start_reason") @@ -605,7 +608,7 @@ class MCURestartHelper: # Cheetah boards require RTS to be deasserted # else a reset will trigger the built-in bootloader. return (self._restart_method != "cheetah") - def post_attach_setup_restart(self): + def _mcu_identify(self): self._reset_cmd = self._mcu.try_lookup_command("reset") self._config_reset_cmd = self._mcu.try_lookup_command("config_reset") ext_only = self._reset_cmd is None and self._config_reset_cmd is None @@ -691,10 +694,14 @@ class MCUConnectHelper: if not (self._serialport.startswith("/dev/rpmsg_") or self._serialport.startswith("/tmp/klipper_host_")): self._baud = config.getint('baud', 250000, minval=2400) + # Shutdown tracking self._emergency_stop_cmd = None self._is_shutdown = self._is_timeout = False self._shutdown_clock = 0 self._shutdown_msg = "" + # Register handlers + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) self._restart_helper = MCURestartHelper(config, self) printer.register_event_handler("klippy:shutdown", self._shutdown) def get_mcu(self): @@ -768,19 +775,17 @@ class MCUConnectHelper: self._clocksync.connect(self._serial) except serialhdl.error as e: raise error(str(e)) - def _post_attach_setup_shutdown(self): - self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop") - self._mcu.register_response(self._handle_shutdown, 'shutdown') - self._mcu.register_response(self._handle_shutdown, 'is_shutdown') - self._mcu.register_response(self._handle_starting, 'starting') - def start_attach(self): + def _mcu_identify(self): if self._mcu.is_fileoutput(): self._attach_file() else: self._attach() logging.info(self.log_info()) - self._post_attach_setup_shutdown() - self._restart_helper.post_attach_setup_restart() + # Setup shutdown handling + self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop") + self._mcu.register_response(self._handle_shutdown, 'shutdown') + self._mcu.register_response(self._handle_shutdown, 'is_shutdown') + self._mcu.register_response(self._handle_starting, 'starting') def _shutdown(self, force=False): if (self._emergency_stop_cmd is None or (self._is_shutdown and not force)): @@ -814,13 +819,17 @@ class MCUStatsHelper: self._clocksync = conn_helper.get_clocksync() self._reactor = printer.get_reactor() self._name = mcu.get_name() + # Statistics tracking self._mcu_freq = 0. self._get_status_info = {} self._stats_sumsq_base = 0. self._mcu_tick_avg = 0. self._mcu_tick_stddev = 0. self._mcu_tick_awake = 0. + # Register handlers printer.register_event_handler("klippy:ready", self._ready) + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) def _handle_mcu_stats(self, params): count = params['count'] tick_sum = params['sum'] @@ -830,7 +839,7 @@ class MCUStatsHelper: diff = count*tick_sumsq - tick_sum**2 self._mcu_tick_stddev = c * math.sqrt(max(0., diff)) self._mcu_tick_awake = tick_sum / self._mcu_freq - def post_attach_setup_stats(self): + def _mcu_identify(self): self._mcu_freq = self._mcu.get_constant_float('CLOCK_FREQ') self._stats_sumsq_base = self._mcu.get_constant_float( 'STATS_SUMSQ_BASE') @@ -878,7 +887,7 @@ class MCUConfigHelper: self._clocksync = conn_helper.get_clocksync() self._reactor = printer.get_reactor() self._name = mcu.get_name() - printer.lookup_object('pins').register_chip(self._name, mcu) + # Configuration tracking self._oid_count = 0 self._config_callbacks = [] self._config_cmds = [] @@ -886,6 +895,10 @@ class MCUConfigHelper: self._init_cmds = [] self._mcu_freq = 0. self._reserved_move_slots = 0 + # Register handlers + printer.lookup_object('pins').register_chip(self._name, mcu) + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) printer.register_event_handler("klippy:connect", self._connect) def _send_config(self, prev_crc): # Build config commands @@ -972,7 +985,7 @@ class MCUConfigHelper: logging.info(move_msg) log_info = self._conn_helper.log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) - def post_attach_setup_for_config(self): + def _mcu_identify(self): self._mcu_freq = self._mcu.get_constant_float('CLOCK_FREQ') ppins = self._printer.lookup_object('pins') pin_resolver = ppins.get_pin_resolver(self._name) @@ -1017,24 +1030,17 @@ class MCU: self._name = config.get_name() if self._name.startswith('mcu '): self._name = self._name[4:] - # Low-level connection + # Low-level connection and helpers self._conn_helper = MCUConnectHelper(config, self, clocksync) self._serial = self._conn_helper.get_serial() self._config_helper = MCUConfigHelper(self, self._conn_helper) + self._stats_helper = MCUStatsHelper(self, self._conn_helper) + printer.load_object(config, "error_mcu") # Alter time reporting when debugging if self.is_fileoutput(): def dummy_estimated_print_time(eventtime): return 0. self.estimated_print_time = dummy_estimated_print_time - # Register handlers - self._stats_helper = MCUStatsHelper(self, self._conn_helper) - printer.load_object(config, "error_mcu") - printer.register_event_handler("klippy:mcu_identify", - self._mcu_identify) - def _mcu_identify(self): - self._conn_helper.start_attach() - self._config_helper.post_attach_setup_for_config() - self._stats_helper.post_attach_setup_stats() def setup_pin(self, pin_type, pin_params): return self._config_helper.setup_pin(pin_type, pin_params) def create_oid(self): From a43846c277058c931d9ceb6b328d6d8fefdd5df7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 22 Sep 2025 16:40:36 -0400 Subject: [PATCH 088/117] mcu: Reorganize wrapper methods in main mcu class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index c4e44e18e..eeec7c0b4 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -1041,6 +1041,13 @@ class MCU: def dummy_estimated_print_time(eventtime): return 0. self.estimated_print_time = dummy_estimated_print_time + def get_name(self): + return self._name + def get_printer(self): + return self._printer + def is_fileoutput(self): + return self._printer.get_start_args().get('debugoutput') is not None + # MCU Configuration wrappers def setup_pin(self, pin_type, pin_params): return self._config_helper.setup_pin(pin_type, pin_params) def create_oid(self): @@ -1055,19 +1062,11 @@ class MCU: return self._config_helper.get_query_slot(oid) def seconds_to_clock(self, time): return self._config_helper.seconds_to_clock(time) + # Command Handler helpers def min_schedule_time(self): return MIN_SCHEDULE_TIME def max_nominal_duration(self): return MAX_NOMINAL_DURATION - # Wrapper functions - def get_printer(self): - return self._printer - def get_name(self): - return self._name - def register_response(self, cb, msg, oid=None): - self._serial.register_response(cb, msg, oid) - def alloc_command_queue(self): - return self._serial.alloc_command_queue() def lookup_command(self, msgformat, cq=None): return CommandWrapper(self._serial, msgformat, cq, debugoutput=self.is_fileoutput()) @@ -1080,12 +1079,19 @@ class MCU: return self.lookup_command(msgformat) except self._serial.get_msgparser().error as e: return None + # SerialHdl wrappers + def register_response(self, cb, msg, oid=None): + self._serial.register_response(cb, msg, oid) + def alloc_command_queue(self): + return self._serial.alloc_command_queue() + # MsgParser wrappers def get_enumerations(self): return self._serial.get_msgparser().get_enumerations() def get_constants(self): return self._serial.get_msgparser().get_constants() def get_constant_float(self, name): return self._serial.get_msgparser().get_constant_float(name) + # ClockSync wrappers def print_time_to_clock(self, print_time): return self._clocksync.print_time_to_clock(print_time) def clock_to_print_time(self, clock): @@ -1098,13 +1104,12 @@ class MCU: offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) self._conn_helper.check_timeout(eventtime) return offset, freq - # Misc external commands - def is_fileoutput(self): - return self._printer.get_start_args().get('debugoutput') is not None + # Low-level connection wrappers def is_shutdown(self): return self._conn_helper.is_shutdown() def get_shutdown_clock(self): return self._conn_helper.get_shutdown_clock() + # Statistics wrappers def get_status(self, eventtime=None): return self._stats_helper.get_status(eventtime) def stats(self, eventtime): From 4cd786fe087580d6370f9b8bccebd6c39de84bca Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 20:16:27 -0400 Subject: [PATCH 089/117] toolhead: Avoid pausing an infinitesimal amount of time Due to differences in mcu clock vs system clock it's possible to repeatedly underestimate a system delay needed to bring about a sufficient mcu time - which just wastes cpu cycles retrying a pause. It's okay to sleep a slightly longer time to avoid the issue. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 6326e607f..cf545d127 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -337,7 +337,8 @@ class ToolHead: if not self.can_pause: self.need_check_pause = self.reactor.NEVER return - eventtime = self.reactor.pause(eventtime + min(1., pause_time)) + eventtime = self.reactor.pause( + eventtime + max(.005, min(1., pause_time))) est_print_time = self.mcu.estimated_print_time(eventtime) buffer_time = self.print_time - est_print_time if not self.special_queuing_state: From 0c86b388a905b56cd511986eb22842152801f696 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 20:21:09 -0400 Subject: [PATCH 090/117] toolhead: Remove extra batching time added in _check_pause() The code currently adds an additional 100ms to BUFFER_TIME_HIGH in _check_pause() to reduce the number of calls to _check_pause(). However, LOOKAHEAD_FLUSH_TIME should already provide sufficient batching so adding more is not necessary. This change should hopefully make configuring BUFFER_TIME_HIGH a little more transparent. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index cf545d127..386f0620e 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -343,7 +343,7 @@ class ToolHead: buffer_time = self.print_time - est_print_time if not self.special_queuing_state: # In main state - defer pause checking until needed - self.need_check_pause = est_print_time + BUFFER_TIME_HIGH + 0.100 + self.need_check_pause = est_print_time + BUFFER_TIME_HIGH def _priming_handler(self, eventtime): self.reactor.unregister_timer(self.priming_timer) self.priming_timer = None From c803249467a84fe1e37e01bc02f88abb29384bab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 Oct 2025 14:09:24 -0400 Subject: [PATCH 091/117] docs: Minor wording change in Code_Overview.md thread description Signed-off-by: Kevin O'Connor --- docs/Code_Overview.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index 56f98d600..50666da04 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -115,13 +115,13 @@ There are several threads in the Klipper host code: (**klippy/reactor.py**) and most high-level actions originate from IO and timer event callbacks from that reactor. * A thread for writing messages to the log so that the other threads - do not block on log writes. This thread resides entirely in the - **klippy/queuelogger.py** code and its operation is generally not + do not block on log writes. This thread resides in the + **klippy/queuelogger.py** code and its multi-threaded nature is not exposed to the main Python thread. * A thread per micro-controller that performs the low-level reading and writing of messages to that micro-controller. It resides in the - **klippy/chelper/serialqueue.c** C code and its operation is - generally not exposed to the Python code. + **klippy/chelper/serialqueue.c** C code and its multi-threaded + nature is not exposed to the Python code. * A thread per micro-controller for processing messages received from that micro-controller in the Python code. This thread is created in **klippy/serialhdl.py**. Care must be taken in Python callbacks @@ -129,8 +129,8 @@ There are several threads in the Klipper host code: the main Python thread. * A thread per stepper motor that calculates the timing of stepper motor step pulses and compresses those times. This thread resides in - the **klippy/chelper/steppersync.c** C code and its operation is - generally not exposed to the Python code. + the **klippy/chelper/steppersync.c** C code and its multi-threaded + nature is not exposed to the Python code. ## Code flow of a move command From d825d43108288d702f9eefcde0cb3041e24948a9 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Wed, 1 Oct 2025 00:27:52 +0200 Subject: [PATCH 092/117] scripts: Updated graph_shaper.py script The change removes the shapers defined there in favor of the standard ones and makes the script a lot more configurable via command-line arguments. Signed-off-by: Dmitry Butyugin --- scripts/graph_shaper.py | 193 ++++++++++++++++------------------------ 1 file changed, 77 insertions(+), 116 deletions(-) diff --git a/scripts/graph_shaper.py b/scripts/graph_shaper.py index b9a6627c8..e97361693 100755 --- a/scripts/graph_shaper.py +++ b/scripts/graph_shaper.py @@ -5,20 +5,15 @@ # Copyright (C) 2020 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. -import optparse, math +import importlib, math, optparse, os, sys import matplotlib +sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), + '..', 'klippy')) +shaper_defs = importlib.import_module('.shaper_defs', 'extras') + # A set of damping ratios to calculate shaper response for -DAMPING_RATIOS=[0.05, 0.1, 0.2] - -# Parameters of the input shaper -SHAPER_FREQ=50.0 -SHAPER_DAMPING_RATIO=0.1 - -# Simulate input shaping of step function for these true resonance frequency -# and damping ratio -STEP_SIMULATION_RESONANCE_FREQ=60. -STEP_SIMULATION_DAMPING_RATIO=0.15 +DEFAULT_DAMPING_RATIOS=[0.075, 0.1, 0.15] # If set, defines which range of frequencies to plot shaper frequency response PLOT_FREQ_RANGE = [] # If empty, will be automatically determined @@ -30,86 +25,8 @@ PLOT_FREQ_STEP = .01 # Input shapers ###################################################################### -def get_zv_shaper(): - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - A = [1., K] - T = [0., .5*t_d] - return (A, T, "ZV") - -def get_zvd_shaper(): - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - A = [1., 2.*K, K**2] - T = [0., .5*t_d, t_d] - return (A, T, "ZVD") - -def get_mzv_shaper(): - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-.75 * SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - - a1 = 1. - 1. / math.sqrt(2.) - a2 = (math.sqrt(2.) - 1.) * K - a3 = a1 * K * K - - A = [a1, a2, a3] - T = [0., .375*t_d, .75*t_d] - return (A, T, "MZV") - -def get_ei_shaper(): - v_tol = 0.05 # vibration tolerance - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - - a1 = .25 * (1. + v_tol) - a2 = .5 * (1. - v_tol) * K - a3 = a1 * K * K - - A = [a1, a2, a3] - T = [0., .5*t_d, t_d] - return (A, T, "EI") - -def get_2hump_ei_shaper(): - v_tol = 0.05 # vibration tolerance - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - - V2 = v_tol**2 - X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.) - a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X) - a2 = (.5 - a1) * K - a3 = a2 * K - a4 = a1 * K * K * K - - A = [a1, a2, a3, a4] - T = [0., .5*t_d, t_d, 1.5*t_d] - return (A, T, "2-hump EI") - -def get_3hump_ei_shaper(): - v_tol = 0.05 # vibration tolerance - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - - K2 = K*K - a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol)) - a2 = 0.25 * (1. - v_tol) * K - a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2 - a4 = a2 * K2 - a5 = a1 * K2 * K2 - - A = [a1, a2, a3, a4, a5] - T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d] - return (A, T, "3-hump EI") - - def estimate_shaper(shaper, freq, damping_ratio): - A, T, _ = shaper + A, T = shaper n = len(T) inv_D = 1. / sum(A) omega = 2. * math.pi * freq @@ -123,14 +40,18 @@ def estimate_shaper(shaper, freq, damping_ratio): return math.sqrt(S*S + C*C) * inv_D def shift_pulses(shaper): - A, T, name = shaper + A, T = shaper n = len(T) ts = sum([A[i] * T[i] for i in range(n)]) / sum(A) for i in range(n): T[i] -= ts # Shaper selection -get_shaper = get_ei_shaper +def get_shaper(shaper_name, shaper_freq, damping_ratio): + for s in shaper_defs.INPUT_SHAPERS: + if shaper_name.lower() == s.name: + return s.init_func(shaper_freq, damping_ratio) + return shaper_defs.get_none_shaper() ###################################################################### @@ -148,44 +69,46 @@ def bisect(func, left, right): right = mid return .5 * (left + right) -def find_shaper_plot_range(shaper, vib_tol): +def find_shaper_plot_range(shaper, shaper_freq, test_damping_ratios, vib_tol): def eval_shaper(freq): - return estimate_shaper(shaper, freq, DAMPING_RATIOS[0]) - vib_tol + return estimate_shaper(shaper, freq, test_damping_ratios[0]) - vib_tol if not PLOT_FREQ_RANGE: - left = bisect(eval_shaper, 0., SHAPER_FREQ) - right = bisect(eval_shaper, SHAPER_FREQ, 2.4 * SHAPER_FREQ) + left = bisect(eval_shaper, 0., shaper_freq) + right = bisect(eval_shaper, shaper_freq, 2.4 * shaper_freq) else: left, right = PLOT_FREQ_RANGE return (left, right) -def gen_shaper_response(shaper): +def gen_shaper_response(shaper, shaper_freq, test_damping_ratios): # Calculate shaper vibration response on a range of frequencies response = [] freqs = [] - freq, freq_end = find_shaper_plot_range(shaper, vib_tol=0.25) + freq, freq_end = find_shaper_plot_range(shaper, shaper_freq, + test_damping_ratios, vib_tol=0.25) while freq <= freq_end: vals = [] - for damping_ratio in DAMPING_RATIOS: + for damping_ratio in test_damping_ratios: vals.append(estimate_shaper(shaper, freq, damping_ratio)) response.append(vals) freqs.append(freq) freq += PLOT_FREQ_STEP - legend = ['damping ratio = %.3f' % d_r for d_r in DAMPING_RATIOS] + legend = ['damping ratio = %.3f' % d_r for d_r in test_damping_ratios] return freqs, response, legend -def gen_shaped_step_function(shaper): +def gen_shaped_step_function(shaper, shaper_freq, + system_freq, system_damping_ratio): # Calculate shaping of a step function - A, T, _ = shaper + A, T = shaper inv_D = 1. / sum(A) n = len(T) - omega = 2. * math.pi * STEP_SIMULATION_RESONANCE_FREQ - damping = STEP_SIMULATION_DAMPING_RATIO * omega - omega_d = omega * math.sqrt(1. - STEP_SIMULATION_DAMPING_RATIO**2) - phase = math.acos(STEP_SIMULATION_DAMPING_RATIO) + omega = 2. * math.pi * system_freq + damping = system_damping_ratio * omega + omega_d = omega * math.sqrt(1. - system_damping_ratio**2) + phase = math.acos(system_damping_ratio) - t_start = T[0] - .5 / SHAPER_FREQ - t_end = T[-1] + 1.5 / STEP_SIMULATION_RESONANCE_FREQ + t_start = T[0] - .5 / shaper_freq + t_end = T[-1] + 1.5 / system_freq result = [] time = [] t = t_start @@ -214,20 +137,24 @@ def gen_shaped_step_function(shaper): result.append(val) time.append(t) - t += .01 / SHAPER_FREQ + t += .01 / shaper_freq legend = ['step', 'shaper commanded', 'system response'] return time, result, legend -def plot_shaper(shaper): +def plot_shaper(shaper_name, shaper_freq, damping_ratio, test_damping_ratios, + system_freq, system_damping_ratio): + shaper = get_shaper(shaper_name, shaper_freq, damping_ratio) shift_pulses(shaper) - freqs, response, response_legend = gen_shaper_response(shaper) - time, step_vals, step_legend = gen_shaped_step_function(shaper) + freqs, response, response_legend = gen_shaper_response( + shaper, shaper_freq, test_damping_ratios) + time, step_vals, step_legend = gen_shaped_step_function( + shaper, shaper_freq, system_freq, system_damping_ratio) fig, (ax1, ax2) = matplotlib.pyplot.subplots(nrows=2, figsize=(10,9)) ax1.set_title("Vibration response simulation for shaper '%s',\n" "shaper_freq=%.1f Hz, damping_ratio=%.3f" - % (shaper[-1], SHAPER_FREQ, SHAPER_DAMPING_RATIO)) + % (shaper_name, shaper_freq, damping_ratio)) ax1.plot(freqs, response) ax1.set_ylim(bottom=0.) fontP = matplotlib.font_manager.FontProperties() @@ -241,8 +168,7 @@ def plot_shaper(shaper): ax1.grid(which='minor', color='lightgrey') ax2.set_title("Unit step input, resonance frequency=%.1f Hz, " - "damping ratio=%.3f" % (STEP_SIMULATION_RESONANCE_FREQ, - STEP_SIMULATION_DAMPING_RATIO)) + "damping ratio=%.3f" % (system_freq, system_damping_ratio)) ax2.plot(time, step_vals) ax2.legend(step_legend, loc='best', prop=fontP) ax2.set_xlabel('Time, sec') @@ -264,13 +190,48 @@ def main(): opts = optparse.OptionParser(usage) opts.add_option("-o", "--output", type="string", dest="output", default=None, help="filename of output graph") + opts.add_option("--shaper", type="string", dest="shaper", default="mzv", + help="a shaper to plot") + opts.add_option("--shaper_freq", type="float", dest="shaper_freq", + default=50.0, help="shaper frequency") + opts.add_option("--damping_ratio", type="float", dest="damping_ratio", + default=shaper_defs.DEFAULT_DAMPING_RATIO, + help="shaper damping_ratio parameter") + opts.add_option("--test_damping_ratios", type="string", + dest="test_damping_ratios", + default=",".join(["%.3f" % dr + for dr in DEFAULT_DAMPING_RATIOS]), + help="a comma-separated list of damping ratios to test " + + "input shaper for") + opts.add_option("--system_freq", type="float", dest="system_freq", + default=60.0, + help="natural frequency of a system for step simulation") + opts.add_option("--system_damping_ratio", type="float", + dest="system_damping_ratio", default=0.15, + help="damping_ratio of a system for step simulation") options, args = opts.parse_args() if len(args) != 0: opts.error("Incorrect number of arguments") + if options.shaper.lower() not in [ + s.name for s in shaper_defs.INPUT_SHAPERS]: + opts.error("Invalid --shaper=%s specified" % options.shaper) + + if options.test_damping_ratios: + try: + test_damping_ratios = [float(s) for s in + options.test_damping_ratios.split(',')] + except ValueError: + opts.error("invalid floating point value in " + + "--test_damping_ratios param") + else: + test_damping_ratios = None + # Draw graph setup_matplotlib(options.output is not None) - fig = plot_shaper(get_shaper()) + fig = plot_shaper(options.shaper, options.shaper_freq, + options.damping_ratio, test_damping_ratios, + options.system_freq, options.system_damping_ratio) # Show graph if options.output is None: From c98527ff0089a7c850d1f994ae83c765f4644b6b Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Wed, 1 Oct 2025 00:30:36 +0200 Subject: [PATCH 093/117] docs: Updated the docs to the latest shaper changes and fixed typos Signed-off-by: Dmitry Butyugin --- docs/Config_Reference.md | 18 ++++++++---------- docs/Resonance_Compensation.md | 10 +++++----- scripts/calibrate_shaper.py | 2 +- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index ca21bcf21..6f76c5567 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1955,11 +1955,10 @@ section of the measuring resonances guide for more information on # are reachable by the toolhead. #accel_chip: # A name of the accelerometer chip to use for measurements. If -# adxl345 chip was defined without an explicit name, this parameter -# can simply reference it as "accel_chip: adxl345", otherwise an -# explicit name must be supplied as well, e.g. "accel_chip: adxl345 -# my_chip_name". Either this, or the next two parameters must be -# set. +# an accelerometer was defined without an explicit name, this parameter +# can simply reference it by type, e.g. "accel_chip: adxl345", otherwise +# a full name must be supplied, e.g. "accel_chip: adxl345 my_chip_name". +# Either this, or the next two parameters must be set. #accel_chip_x: #accel_chip_y: # Names of the accelerometer chips to use for measurements for each @@ -1978,16 +1977,15 @@ section of the measuring resonances guide for more information on # during the calibration. The default is 50. #min_freq: 5 # Minimum frequency to test for resonances. The default is 5 Hz. -#max_freq: 133.33 -# Maximum frequency to test for resonances. The default is 133.33 Hz. +#max_freq: 135 +# Maximum frequency to test for resonances. The default is 135 Hz. #accel_per_hz: 60 # This parameter is used to determine which acceleration to use to # test a specific frequency: accel = accel_per_hz * freq. Higher the # value, the higher is the energy of the oscillations. Can be set to # a lower than the default value if the resonances get too strong on -# the printer. However, lower values make measurements of -# high-frequency resonances less precise. The default value is 75 -# (mm/sec). +# the printer. However, lower values make measurements of high-frequency +# resonances less precise. The default value is 60 (mm/sec). #hz_per_sec: 1 # Determines the speed of the test. When testing all frequencies in # range [min_freq, max_freq], each second the frequency increases by diff --git a/docs/Resonance_Compensation.md b/docs/Resonance_Compensation.md index 8f2d2b643..436c762b4 100644 --- a/docs/Resonance_Compensation.md +++ b/docs/Resonance_Compensation.md @@ -469,8 +469,8 @@ parameters of each shaper. | MZV | 0.75 / shaper_freq | ± 4% shaper_freq | -10%...+15% shaper_freq | | ZVD | 1 / shaper_freq | ± 15% shaper_freq | ± 22% shaper_freq | | EI | 1 / shaper_freq | ± 20% shaper_freq | ± 25% shaper_freq | -| 2HUMP_EI | 1.5 / shaper_freq | ± 35% shaper_freq | ± 40 shaper_freq | -| 3HUMP_EI | 2 / shaper_freq | -45...+50% shaper_freq | -50%...+55% shaper_freq | +| 2HUMP_EI | 1.5 / shaper_freq | -40...+45% shaper_freq | -45..+50% shaper_freq | +| 3HUMP_EI | 2 / shaper_freq | -50...+60% shaper_freq | -55%...+65% shaper_freq | A note on vibration reduction: the values in the table above are approximate. If the damping ratio of the printer is known for each axis, the shaper can be @@ -502,11 +502,11 @@ so the values for 10% vibration tolerance are provided only for the reference. resonances at 35 Hz and 60 Hz on the same axis: a) EI shaper needs to have shaper_freq = 35 / (1 - 0.2) = 43.75 Hz, and it will reduce resonances until 43.75 * (1 + 0.2) = 52.5 Hz, so it is not sufficient; b) 2HUMP_EI - shaper needs to have shaper_freq = 35 / (1 - 0.35) = 53.85 Hz and will - reduce vibrations until 53.85 * (1 + 0.35) = 72.7 Hz - so this is an + shaper needs to have shaper_freq = 35 / (1 - 0.4) = 58.3 Hz and will + reduce vibrations until 58.3 * (1 + 0.45) = 84.5 Hz - so this is an acceptable configuration. Always try to use as high shaper_freq as possible for a given shaper (perhaps with some safety margin, so in this example - shaper_freq ≈ 50-52 Hz would work best), and try to use a shaper with as + shaper_freq ≈ 55 Hz would work best), and try to use a shaper with as small shaper duration as possible. * If one needs to reduce vibrations at several very different frequencies (say, 30 Hz and 100 Hz), they may see that the table above does not provide diff --git a/scripts/calibrate_shaper.py b/scripts/calibrate_shaper.py index b56ce5daa..cda0276f3 100755 --- a/scripts/calibrate_shaper.py +++ b/scripts/calibrate_shaper.py @@ -166,7 +166,7 @@ def main(): default=None, help="shaper damping_ratio parameter") opts.add_option("--test_damping_ratios", type="string", dest="test_damping_ratios", default=None, - help="a comma-separated liat of damping ratios to test " + + help="a comma-separated list of damping ratios to test " + "input shaper for") options, args = opts.parse_args() if len(args) < 1: From c570f4e095a424c61e0ee7974f4d8788841d3058 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Thu, 2 Oct 2025 01:28:43 +0200 Subject: [PATCH 094/117] docs: Added description of MZV input shaper Signed-off-by: Dmitry Butyugin --- docs/Resonance_Compensation.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/Resonance_Compensation.md b/docs/Resonance_Compensation.md index 436c762b4..43ceac8f0 100644 --- a/docs/Resonance_Compensation.md +++ b/docs/Resonance_Compensation.md @@ -457,11 +457,24 @@ parameter described in [this section](#selecting-max_accel)). ### Input shapers -Input shapers used in Klipper are rather standard, and one can find more -in-depth overview in the articles describing the corresponding shapers. This section contains a brief overview of some technical aspects of the -supported input shapers. The table below shows some (usually approximate) -parameters of each shaper. +supported input shapers. Input shapers used in Klipper are rather standard, +with the exception of MZV, and one can find more in-depth overview in +the articles describing the corresponding shapers. + +MZV stands for a Modified-ZV input shaper. The classic definition of ZV shaper +assumes the duration Ts equal to 1/2 of the damped period of oscillations Td and +has two pulses. However, ZV input shaper has a generalized form for an arbitrary +duration in the range (0, Td] with three pulses (Specified-Duration ZV, see also +SNA-ZV), with a negative middle pulse if Ts < Td and a positive one if Ts > Td. +The MZV shaper was designed as an intermediate shaper between ZV and ZVD, +offering better vibrations suppression than ZV when the determined (measured) +shaper parameters deviate from the ones actually required by the printer, +and smaller smoothing than ZVD. Effectively, it is a SD-ZV shaper with the +specific duration Ts = 3/4 Td, exactly between ZV (Ts = 1/2 Td) and +ZVD (Ts = Td), and it happens to work well for many real-life 3D printers. + +The table below shows some (usually approximate) parameters of each shaper. | Input
shaper | Shaper
duration | Vibration reduction 20x
(5% vibration tolerance) | Vibration reduction 10x
(10% vibration tolerance) | |:--:|:--:|:--:|:--:| From 768b19e8be6458f43615423f48b74cf1e3ad1a9d Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sat, 4 Oct 2025 00:18:21 +0200 Subject: [PATCH 095/117] input_shaper: Limit maximum damping_ratio of the shapers Numerically optimized versions of *EI shapers have been tuned for specific ranges of damping ratios and will show poor stability outside of these designated ranges. Signed-off-by: Dmitry Butyugin --- klippy/extras/input_shaper.py | 25 +++++++++++++++---------- klippy/extras/shaper_defs.py | 21 ++++++++++++++------- test/klippy/input_shaper.cfg | 3 ++- test/klippy/input_shaper.test | 2 +- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py index c79cdcf2c..ae8ffe815 100644 --- a/klippy/extras/input_shaper.py +++ b/klippy/extras/input_shaper.py @@ -11,34 +11,39 @@ from . import shaper_defs class InputShaperParams: def __init__(self, axis, config): self.axis = axis - self.shapers = {s.name : s.init_func for s in shaper_defs.INPUT_SHAPERS} + self.shapers = {s.name : s for s in shaper_defs.INPUT_SHAPERS} shaper_type = config.get('shaper_type', 'mzv') self.shaper_type = config.get('shaper_type_' + axis, shaper_type) if self.shaper_type not in self.shapers: raise config.error( 'Unsupported shaper type: %s' % (self.shaper_type,)) - self.damping_ratio = config.getfloat('damping_ratio_' + axis, - shaper_defs.DEFAULT_DAMPING_RATIO, - minval=0., maxval=1.) + self.damping_ratio = config.getfloat( + 'damping_ratio_' + axis, + shaper_defs.DEFAULT_DAMPING_RATIO, minval=0., + maxval=self.shapers[self.shaper_type].max_damping_ratio) self.shaper_freq = config.getfloat('shaper_freq_' + axis, 0., minval=0.) def update(self, gcmd): axis = self.axis.upper() - self.damping_ratio = gcmd.get_float('DAMPING_RATIO_' + axis, - self.damping_ratio, - minval=0., maxval=1.) - self.shaper_freq = gcmd.get_float('SHAPER_FREQ_' + axis, - self.shaper_freq, minval=0.) shaper_type = gcmd.get('SHAPER_TYPE', None) if shaper_type is None: shaper_type = gcmd.get('SHAPER_TYPE_' + axis, self.shaper_type) if shaper_type.lower() not in self.shapers: raise gcmd.error('Unsupported shaper type: %s' % (shaper_type,)) + damping_ratio = gcmd.get_float('DAMPING_RATIO_' + axis, + self.damping_ratio, minval=0.) + if damping_ratio > self.shapers[shaper_type.lower()].max_damping_ratio: + raise gcmd.error( + 'Too high value of damping_ratio=%.3f for shaper %s' + ' on axis %c' % (damping_ratio, shaper_type, axis)) + self.shaper_freq = gcmd.get_float('SHAPER_FREQ_' + axis, + self.shaper_freq, minval=0.) + self.damping_ratio = damping_ratio self.shaper_type = shaper_type.lower() def get_shaper(self): if not self.shaper_freq: A, T = shaper_defs.get_none_shaper() else: - A, T = self.shapers[self.shaper_type]( + A, T = self.shapers[self.shaper_type].init_func( self.shaper_freq, self.damping_ratio) return len(A), A, T def get_status(self): diff --git a/klippy/extras/shaper_defs.py b/klippy/extras/shaper_defs.py index 8a0d93962..60fb1aa66 100644 --- a/klippy/extras/shaper_defs.py +++ b/klippy/extras/shaper_defs.py @@ -9,7 +9,8 @@ SHAPER_VIBRATION_REDUCTION=20. DEFAULT_DAMPING_RATIO = 0.1 InputShaperCfg = collections.namedtuple( - 'InputShaperCfg', ('name', 'init_func', 'min_freq')) + 'InputShaperCfg', + ('name', 'init_func', 'min_freq', 'max_damping_ratio')) def get_none_shaper(): return ([], []) @@ -105,10 +106,16 @@ def get_3hump_ei_shaper(shaper_freq, damping_ratio): # min_freq for each shaper is chosen to have projected max_accel ~= 1500 INPUT_SHAPERS = [ - InputShaperCfg('zv', get_zv_shaper, min_freq=21.), - InputShaperCfg('mzv', get_mzv_shaper, min_freq=23.), - InputShaperCfg('zvd', get_zvd_shaper, min_freq=29.), - InputShaperCfg('ei', get_ei_shaper, min_freq=29.), - InputShaperCfg('2hump_ei', get_2hump_ei_shaper, min_freq=39.), - InputShaperCfg('3hump_ei', get_3hump_ei_shaper, min_freq=48.), + InputShaperCfg(name='zv', init_func=get_zv_shaper, + min_freq=21., max_damping_ratio=0.99), + InputShaperCfg(name='mzv', init_func=get_mzv_shaper, + min_freq=23., max_damping_ratio=0.99), + InputShaperCfg(name='zvd', init_func=get_zvd_shaper, + min_freq=29., max_damping_ratio=0.99), + InputShaperCfg(name='ei', init_func=get_ei_shaper, + min_freq=29., max_damping_ratio=0.4), + InputShaperCfg(name='2hump_ei', init_func=get_2hump_ei_shaper, + min_freq=39., max_damping_ratio=0.3), + InputShaperCfg(name='3hump_ei', init_func=get_3hump_ei_shaper, + min_freq=48., max_damping_ratio=0.2), ] diff --git a/test/klippy/input_shaper.cfg b/test/klippy/input_shaper.cfg index bca94eb77..4491f1e5f 100644 --- a/test/klippy/input_shaper.cfg +++ b/test/klippy/input_shaper.cfg @@ -70,8 +70,9 @@ max_z_accel: 100 [input_shaper] shaper_type_x: mzv shaper_freq_x: 33.2 -shaper_type_x: ei +shaper_type_y: ei shaper_freq_x: 39.3 +damping_ratio_y: 0.4 [adxl345] cs_pin: PK7 diff --git a/test/klippy/input_shaper.test b/test/klippy/input_shaper.test index 8714ec5e2..bc993ce27 100644 --- a/test/klippy/input_shaper.test +++ b/test/klippy/input_shaper.test @@ -4,4 +4,4 @@ DICTIONARY atmega2560.dict # Simple command test SET_INPUT_SHAPER SHAPER_FREQ_X=22.2 DAMPING_RATIO_X=.1 SHAPER_TYPE_X=zv -SET_INPUT_SHAPER SHAPER_FREQ_Y=33.3 DAMPING_RATIO_X=.11 SHAPER_TYPE_X=2hump_ei +SET_INPUT_SHAPER SHAPER_FREQ_Y=33.3 DAMPING_RATIO_Y=.11 SHAPER_TYPE_Y=2hump_ei From c49dbb5a879df16ebf3014ef0901eb9dd61e6225 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 4 Oct 2025 19:35:41 -0400 Subject: [PATCH 096/117] trapq: Fix bug that broke numerical stability workaround for initial movement Commit b60804bb changed the trapq head sentinel to store print_time=-1. However, it failed to update trapq_add_move() that relied on that value to detect the head sentinel. As a result, numerical stability issues could lead to stepcompress errors. Fix by changing trapq_add_move() to detect the head sentinel even with a negative print_time. Signed-off-by: Kevin O'Connor --- klippy/chelper/trapq.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/chelper/trapq.c b/klippy/chelper/trapq.c index a21969414..c238a3818 100644 --- a/klippy/chelper/trapq.c +++ b/klippy/chelper/trapq.c @@ -104,7 +104,7 @@ trapq_add_move(struct trapq *tq, struct move *m) // Add a null move to fill time gap struct move *null_move = move_alloc(); null_move->start_pos = m->start_pos; - if (!prev->print_time && m->print_time > MAX_NULL_MOVE) + if (prev->print_time <= 0. && m->print_time > MAX_NULL_MOVE) // Limit the first null move to improve numerical stability null_move->print_time = m->print_time - MAX_NULL_MOVE; else From caf7accf2d7b4d3c810bbb16f4ff7d4be80b043c Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Fri, 19 Sep 2025 21:46:56 +0200 Subject: [PATCH 097/117] input_shaper: Z-axis input shaper Signed-off-by: Dmitry Butyugin --- klippy/chelper/kin_shaper.c | 96 ++++++++++++++++++++++++----------- klippy/extras/input_shaper.py | 10 ++-- test/klippy/input_shaper.cfg | 3 +- 3 files changed, 73 insertions(+), 36 deletions(-) diff --git a/klippy/chelper/kin_shaper.c b/klippy/chelper/kin_shaper.c index d5138ff04..1c4724360 100644 --- a/klippy/chelper/kin_shaper.c +++ b/klippy/chelper/kin_shaper.c @@ -1,7 +1,7 @@ // Kinematic input shapers to minimize motion vibrations in XY plane // // Copyright (C) 2019-2020 Kevin O'Connor -// Copyright (C) 2020 Dmitry Butyugin +// Copyright (C) 2020-2025 Dmitry Butyugin // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -18,6 +18,8 @@ * Shaper initialization ****************************************************************/ +static const int KIN_FLAGS[3] = { AF_X, AF_Y, AF_Z }; + struct shaper_pulses { int num_pulses; struct { @@ -113,7 +115,7 @@ struct input_shaper { struct stepper_kinematics sk; struct stepper_kinematics *orig_sk; struct move m; - struct shaper_pulses sx, sy; + struct shaper_pulses sp[3]; }; // Optimized calc_position when only x axis is needed @@ -122,9 +124,10 @@ shaper_x_calc_position(struct stepper_kinematics *sk, struct move *m , double move_time) { struct input_shaper *is = container_of(sk, struct input_shaper, sk); - if (!is->sx.num_pulses) + struct shaper_pulses *sx = &is->sp[0]; + if (!sx->num_pulses) return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time); - is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx); + is->m.start_pos.x = calc_position(m, 'x', move_time, sx); return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T); } @@ -134,25 +137,41 @@ shaper_y_calc_position(struct stepper_kinematics *sk, struct move *m , double move_time) { struct input_shaper *is = container_of(sk, struct input_shaper, sk); - if (!is->sy.num_pulses) + struct shaper_pulses *sy = &is->sp[1]; + if (!sy->num_pulses) return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time); - is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy); + is->m.start_pos.y = calc_position(m, 'y', move_time, sy); return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T); } -// General calc_position for both x and y axes +// Optimized calc_position when only z axis is needed static double -shaper_xy_calc_position(struct stepper_kinematics *sk, struct move *m - , double move_time) +shaper_z_calc_position(struct stepper_kinematics *sk, struct move *m + , double move_time) { struct input_shaper *is = container_of(sk, struct input_shaper, sk); - if (!is->sx.num_pulses && !is->sy.num_pulses) + struct shaper_pulses *sz = &is->sp[2]; + if (!sz->num_pulses) + return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time); + is->m.start_pos.z = calc_position(m, 'z', move_time, sz); + return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T); +} + +// General calc_position for all x, y, and z axes +static double +shaper_xyz_calc_position(struct stepper_kinematics *sk, struct move *m + , double move_time) +{ + struct input_shaper *is = container_of(sk, struct input_shaper, sk); + if (!is->sp[0].num_pulses && !is->sp[1].num_pulses && !is->sp[2].num_pulses) return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time); is->m.start_pos = move_get_coord(m, move_time); - if (is->sx.num_pulses) - is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx); - if (is->sy.num_pulses) - is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy); + if (is->sp[0].num_pulses) + is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sp[0]); + if (is->sp[1].num_pulses) + is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sp[1]); + if (is->sp[2].num_pulses) + is->m.start_pos.z = calc_position(m, 'z', move_time, &is->sp[2]); return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T); } @@ -170,15 +189,24 @@ static void shaper_note_generation_time(struct input_shaper *is) { double pre_active = 0., post_active = 0.; - if ((is->sk.active_flags & AF_X) && is->sx.num_pulses) { - pre_active = is->sx.pulses[is->sx.num_pulses-1].t; - post_active = -is->sx.pulses[0].t; + struct shaper_pulses *sx = &is->sp[0]; + if ((is->sk.active_flags & AF_X) && sx->num_pulses) { + pre_active = sx->pulses[sx->num_pulses-1].t; + post_active = -sx->pulses[0].t; } - if ((is->sk.active_flags & AF_Y) && is->sy.num_pulses) { - pre_active = is->sy.pulses[is->sy.num_pulses-1].t > pre_active - ? is->sy.pulses[is->sy.num_pulses-1].t : pre_active; - post_active = -is->sy.pulses[0].t > post_active - ? -is->sy.pulses[0].t : post_active; + struct shaper_pulses *sy = &is->sp[1]; + if ((is->sk.active_flags & AF_Y) && sy->num_pulses) { + pre_active = sy->pulses[sy->num_pulses-1].t > pre_active + ? sy->pulses[sy->num_pulses-1].t : pre_active; + post_active = -sy->pulses[0].t > post_active + ? -sy->pulses[0].t : post_active; + } + struct shaper_pulses *sz = &is->sp[2]; + if ((is->sk.active_flags & AF_Z) && sz->num_pulses) { + pre_active = sz->pulses[sz->num_pulses-1].t > pre_active + ? sz->pulses[sz->num_pulses-1].t : pre_active; + post_active = -sz->pulses[0].t > post_active + ? -sz->pulses[0].t : post_active; } is->sk.gen_steps_pre_active = pre_active; is->sk.gen_steps_post_active = post_active; @@ -188,12 +216,15 @@ void __visible input_shaper_update_sk(struct stepper_kinematics *sk) { struct input_shaper *is = container_of(sk, struct input_shaper, sk); - if ((is->orig_sk->active_flags & (AF_X | AF_Y)) == (AF_X | AF_Y)) - is->sk.calc_position_cb = shaper_xy_calc_position; - else if (is->orig_sk->active_flags & AF_X) + int kin_flags = is->orig_sk->active_flags & (AF_X | AF_Y | AF_Z); + if ((kin_flags & AF_X) == AF_X) is->sk.calc_position_cb = shaper_x_calc_position; - else if (is->orig_sk->active_flags & AF_Y) + else if ((kin_flags & AF_Y) == AF_Y) is->sk.calc_position_cb = shaper_y_calc_position; + else if ((kin_flags & AF_Z) == AF_Z) + is->sk.calc_position_cb = shaper_z_calc_position; + else + is->sk.calc_position_cb = shaper_xyz_calc_position; is->sk.active_flags = is->orig_sk->active_flags; shaper_note_generation_time(is); } @@ -207,8 +238,10 @@ input_shaper_set_sk(struct stepper_kinematics *sk is->sk.calc_position_cb = shaper_x_calc_position; else if (orig_sk->active_flags == AF_Y) is->sk.calc_position_cb = shaper_y_calc_position; - else if (orig_sk->active_flags & (AF_X | AF_Y)) - is->sk.calc_position_cb = shaper_xy_calc_position; + else if (orig_sk->active_flags == AF_Z) + is->sk.calc_position_cb = shaper_z_calc_position; + else if (orig_sk->active_flags & (AF_X | AF_Y | AF_Z)) + is->sk.calc_position_cb = shaper_xyz_calc_position; else return -1; is->sk.active_flags = orig_sk->active_flags; @@ -226,13 +259,14 @@ int __visible input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis , int n, double a[], double t[]) { - if (axis != 'x' && axis != 'y') + int axis_ind = axis-'x'; + if (axis_ind < 0 || axis_ind >= ARRAY_SIZE(KIN_FLAGS)) return -1; struct input_shaper *is = container_of(sk, struct input_shaper, sk); - struct shaper_pulses *sp = axis == 'x' ? &is->sx : &is->sy; + struct shaper_pulses *sp = &is->sp[axis_ind]; int status = 0; // Ignore input shaper update if the axis is not active - if (is->orig_sk->active_flags & (axis == 'x' ? AF_X : AF_Y)) { + if (is->orig_sk->active_flags & KIN_FLAGS[axis_ind]) { status = init_shaper(n, a, t, sp); shaper_note_generation_time(is); } diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py index ae8ffe815..39b3f5a85 100644 --- a/klippy/extras/input_shaper.py +++ b/klippy/extras/input_shaper.py @@ -1,7 +1,7 @@ # Kinematic input shaper to minimize motion vibrations in XY plane # # Copyright (C) 2019-2020 Kevin O'Connor -# Copyright (C) 2020 Dmitry Butyugin +# Copyright (C) 2020-2025 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. import collections @@ -100,7 +100,8 @@ class InputShaper: self._update_kinematics) self.toolhead = None self.shapers = [AxisInputShaper('x', config), - AxisInputShaper('y', config)] + AxisInputShaper('y', config), + AxisInputShaper('z', config)] self.input_shaper_stepper_kinematics = [] self.orig_stepper_kinematics = [] # Register gcode commands @@ -191,8 +192,9 @@ class InputShaper: for shaper in self.shapers: shaper.update(gcmd) self._update_input_shaping() - for shaper in self.shapers: - shaper.report(gcmd) + for ind, shaper in enumerate(self.shapers): + if ind < 2 or shaper.is_enabled(): + shaper.report(gcmd) def load_config(config): return InputShaper(config) diff --git a/test/klippy/input_shaper.cfg b/test/klippy/input_shaper.cfg index 4491f1e5f..c6c47c1ad 100644 --- a/test/klippy/input_shaper.cfg +++ b/test/klippy/input_shaper.cfg @@ -71,8 +71,9 @@ max_z_accel: 100 shaper_type_x: mzv shaper_freq_x: 33.2 shaper_type_y: ei -shaper_freq_x: 39.3 +shaper_freq_y: 39.3 damping_ratio_y: 0.4 +shaper_freq_z: 42 [adxl345] cs_pin: PK7 From ec82cee7fc6653304b766b15d4d31dbd59698e21 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Fri, 19 Sep 2025 21:48:36 +0200 Subject: [PATCH 098/117] resonance_tester: Support testing resonances over Z axis Signed-off-by: Dmitry Butyugin --- klippy/extras/resonance_tester.py | 149 ++++++++++++++++++++++-------- scripts/calibrate_shaper.py | 3 + test/klippy/input_shaper.cfg | 1 + 3 files changed, 117 insertions(+), 36 deletions(-) diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index 4f4ef7bb4..b92a163bc 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -1,19 +1,23 @@ # A utility class to test resonances of the printer # -# Copyright (C) 2020-2024 Dmitry Butyugin +# Copyright (C) 2020-2025 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. -import logging, math, os, time +import itertools, logging, math, os, time from . import shaper_calibrate class TestAxis: def __init__(self, axis=None, vib_dir=None): if axis is None: - self._name = "axis=%.3f,%.3f" % (vib_dir[0], vib_dir[1]) + self._name = "axis=%.3f,%.3f,%.3f" % ( + vib_dir[0], vib_dir[1], + (vib_dir[2] if len(vib_dir) == 3 else 0.)) else: self._name = axis if vib_dir is None: - self._vib_dir = (1., 0.) if axis == 'x' else (0., 1.) + self._vib_dir = [(1., 0., 0.), + (0., 1., 0.), + (0., 0., 1.)][ord(axis)-ord('x')] else: s = math.sqrt(sum([d*d for d in vib_dir])) self._vib_dir = [d / s for d in vib_dir] @@ -22,43 +26,54 @@ class TestAxis: return True if self._vib_dir[1] and 'y' in chip_axis: return True + if self._vib_dir[2] and 'z' in chip_axis: + return True return False + def get_dir(self): + return self._vib_dir def get_name(self): return self._name def get_point(self, l): - return (self._vib_dir[0] * l, self._vib_dir[1] * l) + return tuple(d * l for d in self._vib_dir) def _parse_axis(gcmd, raw_axis): if raw_axis is None: return None raw_axis = raw_axis.lower() - if raw_axis in ['x', 'y']: + if raw_axis in ['x', 'y', 'z']: return TestAxis(axis=raw_axis) dirs = raw_axis.split(',') - if len(dirs) != 2: + if len(dirs) not in (2, 3): raise gcmd.error("Invalid format of axis '%s'" % (raw_axis,)) try: dir_x = float(dirs[0].strip()) dir_y = float(dirs[1].strip()) + dir_z = float(dirs[2].strip()) if len(dirs) == 3 else 0. except: raise gcmd.error( "Unable to parse axis direction '%s'" % (raw_axis,)) - return TestAxis(vib_dir=(dir_x, dir_y)) + return TestAxis(vib_dir=(dir_x, dir_y, dir_z)) class VibrationPulseTestGenerator: def __init__(self, config): self.min_freq = config.getfloat('min_freq', 5., minval=1.) self.max_freq = config.getfloat('max_freq', 135., minval=self.min_freq, maxval=300.) + self.max_freq_z = config.getfloat('max_freq_z', 100., + minval=self.min_freq, maxval=300.) self.accel_per_hz = config.getfloat('accel_per_hz', 60., above=0.) + self.accel_per_hz_z = config.getfloat('accel_per_hz_z', 15., above=0.) self.hz_per_sec = config.getfloat('hz_per_sec', 1., minval=0.1, maxval=2.) - def prepare_test(self, gcmd): + def prepare_test(self, gcmd, is_z): self.freq_start = gcmd.get_float("FREQ_START", self.min_freq, minval=1.) - self.freq_end = gcmd.get_float("FREQ_END", self.max_freq, + self.freq_end = gcmd.get_float("FREQ_END", (self.max_freq_z if is_z + else self.max_freq), minval=self.freq_start, maxval=300.) self.test_accel_per_hz = gcmd.get_float("ACCEL_PER_HZ", - self.accel_per_hz, above=0.) + (self.accel_per_hz_z if is_z + else self.accel_per_hz), + above=0.) self.test_hz_per_sec = gcmd.get_float("HZ_PER_SEC", self.hz_per_sec, above=0., maxval=2.) def gen_test(self): @@ -83,12 +98,15 @@ class SweepingVibrationsTestGenerator: def __init__(self, config): self.vibration_generator = VibrationPulseTestGenerator(config) self.sweeping_accel = config.getfloat('sweeping_accel', 400., above=0.) + self.sweeping_accel_z = config.getfloat('sweeping_accel_z', 50., + above=0.) self.sweeping_period = config.getfloat('sweeping_period', 1.2, minval=0.) - def prepare_test(self, gcmd): - self.vibration_generator.prepare_test(gcmd) + def prepare_test(self, gcmd, is_z): + self.vibration_generator.prepare_test(gcmd, is_z) self.test_sweeping_accel = gcmd.get_float( - "SWEEPING_ACCEL", self.sweeping_accel, above=0.) + "SWEEPING_ACCEL", (self.sweeping_accel_z if is_z + else self.sweeping_accel), above=0.) self.test_sweeping_period = gcmd.get_float( "SWEEPING_PERIOD", self.sweeping_period, minval=0.) def gen_test(self): @@ -120,25 +138,62 @@ class SweepingVibrationsTestGenerator: def get_max_freq(self): return self.vibration_generator.get_max_freq() +# Helper to lookup Z kinematics limits +def lookup_z_limits(configfile): + sconfig = configfile.get_status(None)['settings'] + printer_config = sconfig.get('printer') + max_z_velocity = printer_config.get('max_z_velocity') + if max_z_velocity is None: + max_z_velocity = printer_config.get('max_velocity') + max_z_accel = printer_config.get('max_z_accel') + if max_z_accel is None: + max_z_accel = printer_config.get('max_accel') + return max_z_velocity, max_z_accel + class ResonanceTestExecutor: def __init__(self, config): self.printer = config.get_printer() self.gcode = self.printer.lookup_object('gcode') def run_test(self, test_seq, axis, gcmd): reactor = self.printer.get_reactor() + configfile = self.printer.lookup_object('configfile') toolhead = self.printer.lookup_object('toolhead') tpos = toolhead.get_position() - X, Y = tpos[:2] + X, Y, Z = tpos[:3] # Override maximum acceleration and acceleration to # deceleration based on the maximum test frequency systime = reactor.monotonic() toolhead_info = toolhead.get_status(systime) + old_max_velocity = toolhead_info['max_velocity'] old_max_accel = toolhead_info['max_accel'] old_minimum_cruise_ratio = toolhead_info['minimum_cruise_ratio'] max_accel = max([abs(a) for _, a, _ in test_seq]) + max_velocity = 0. + last_v = last_t = 0. + for next_t, accel, freq in test_seq: + v = last_v + accel * (next_t - last_t) + max_velocity = max(max_velocity, abs(v)) + last_t, last_v = next_t, v + if axis.get_dir()[2]: + max_z_velocity, max_z_accel = lookup_z_limits(configfile) + error_msg = "" + if max_velocity > max_z_velocity: + error_msg = ( + "Insufficient maximum Z velocity for these" + " test parameters, increase at least to %.f mm/s" + " for the resonance test." % (max_velocity+0.5)) + if max_accel > max_z_accel: + if error_msg: + error_msg += "\n" + error_msg += ( + "Insufficient maximum Z acceleration for these" + " test parameters, increase at least to %.f mm/s^2" + " for the resonance test." % (max_accel+0.5)) + if error_msg: + raise gcmd.error(error_msg) self.gcode.run_script_from_command( - "SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=0" - % (max_accel,)) + "SET_VELOCITY_LIMIT VELOCITY=%.f ACCEL=%.f MINIMUM_CRUISE_RATIO=0" + % (max_velocity+0.5, max_accel+0.5,)) input_shaper = self.printer.lookup_object('input_shaper', None) if input_shaper is not None and not gcmd.get_int('INPUT_SHAPING', 0): input_shaper.disable_shaping() @@ -166,34 +221,38 @@ class ResonanceTestExecutor: v = abs_v = 0. half_inv_accel = .5 / accel d = (v * v - last_v2) * half_inv_accel - dX, dY = axis.get_point(d) + dX, dY, dZ = axis.get_point(d) nX = X + dX nY = Y + dY + nZ = Z + dZ toolhead.limit_next_junction_speed(abs_last_v) if v * last_v < 0: # The move first goes to a complete stop, then changes direction d_decel = -last_v2 * half_inv_accel - decel_X, decel_Y = axis.get_point(d_decel) - toolhead.move([X + decel_X, Y + decel_Y] + tpos[2:], abs_last_v) - toolhead.move([nX, nY] + tpos[2:], abs_v) + decel_X, decel_Y, decel_Z = axis.get_point(d_decel) + toolhead.move([X + decel_X, Y + decel_Y, Z + decel_Z] + + tpos[3:], abs_last_v) + toolhead.move([nX, nY, nZ] + tpos[3:], abs_v) else: - toolhead.move([nX, nY] + tpos[2:], max(abs_v, abs_last_v)) + toolhead.move([nX, nY, nZ] + tpos[3:], max(abs_v, abs_last_v)) if math.floor(freq) > math.floor(last_freq): gcmd.respond_info("Testing frequency %.0f Hz" % (freq,)) reactor.pause(reactor.monotonic() + 0.01) - X, Y = nX, nY + X, Y, Z = nX, nY, nZ last_t = next_t last_v = v last_freq = freq if last_v: d_decel = -.5 * last_v2 / old_max_accel - decel_X, decel_Y = axis.get_point(d_decel) + decel_X, decel_Y, decel_Z = axis.get_point(d_decel) toolhead.set_max_velocities(None, old_max_accel, None, None) - toolhead.move([X + decel_X, Y + decel_Y] + tpos[2:], abs(last_v)) + toolhead.move([X + decel_X, Y + decel_Y, Z + decel_Z] + tpos[3:], + abs(last_v)) # Restore the original acceleration values self.gcode.run_script_from_command( - "SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=%.3f" - % (old_max_accel, old_minimum_cruise_ratio)) + ("SET_VELOCITY_LIMIT VELOCITY=%.3f ACCEL=%.3f" + + " MINIMUM_CRUISE_RATIO=%.3f") % (old_max_velocity, old_max_accel, + old_minimum_cruise_ratio)) # Restore input shaper if it was disabled for resonance testing if input_shaper is not None: input_shaper.enable_shaping() @@ -206,13 +265,21 @@ class ResonanceTester: self.generator = SweepingVibrationsTestGenerator(config) self.executor = ResonanceTestExecutor(config) if not config.get('accel_chip_x', None): - self.accel_chip_names = [('xy', config.get('accel_chip').strip())] + accel_chip_names = [ + ('xy', config.get('accel_chip').strip()), + ('z', config.get('accel_chip_z', '').strip())] else: - self.accel_chip_names = [ - ('x', config.get('accel_chip_x').strip()), - ('y', config.get('accel_chip_y').strip())] - if self.accel_chip_names[0][1] == self.accel_chip_names[1][1]: - self.accel_chip_names = [('xy', self.accel_chip_names[0][1])] + accel_chip_names = [ + ('x', config.get('accel_chip_x').strip()), + ('y', config.get('accel_chip_y').strip()), + ('z', config.get('accel_chip_z', '').strip())] + get_chip_name = lambda t: t[1] + # Group chips by their axes + self.accel_chip_names = [ + (''.join(sorted(axis for axis, _ in vals)), chip_name) + for chip_name, vals in itertools.groupby( + sorted(accel_chip_names, key=get_chip_name), + key=get_chip_name)] self.max_smoothing = config.getfloat('max_smoothing', None, minval=0.05) self.probe_points = config.getlists('probe_points', seps=(',', '\n'), parser=float, count=3) @@ -232,6 +299,8 @@ class ResonanceTester: def connect(self): self.accel_chips = [] for chip_axis, chip_name in self.accel_chip_names: + if not chip_name: + continue chip = self.printer.lookup_object(chip_name) if not hasattr(chip, 'start_internal_client'): raise self.printer.config_error( @@ -243,7 +312,10 @@ class ResonanceTester: toolhead = self.printer.lookup_object('toolhead') calibration_data = {axis: None for axis in axes} - self.generator.prepare_test(gcmd) + has_z = [axis.get_dir()[2] for axis in axes] + if all(has_z) != any(has_z): + raise gcmd.error("Cannot test Z axis together with other axes") + self.generator.prepare_test(gcmd, is_z=all(has_z)) test_points = [test_point] if test_point else self.probe_points @@ -268,6 +340,10 @@ class ResonanceTester: for chip in accel_chips: aclient = chip.start_internal_client() raw_values.append((axis, aclient, chip.name)) + if not raw_values: + raise gcmd.error( + "No accelerometers specified that can measure" + " resonances over axis '%s'" % axis.get_name()) # Generate moves test_seq = self.generator.gen_test() @@ -278,7 +354,8 @@ class ResonanceTester: raw_name = self.get_filename( 'raw_data', raw_name_suffix, axis, point if len(test_points) > 1 else None, - chip_name if accel_chips is not None else None,) + chip_name if (accel_chips is not None + or len(raw_values) > 1) else None) aclient.write_to_file(raw_name) gcmd.respond_info( "Writing raw accelerometer data to " @@ -366,7 +443,7 @@ class ResonanceTester: axis = gcmd.get("AXIS", None) if not axis: calibrate_axes = [TestAxis('x'), TestAxis('y')] - elif axis.lower() not in 'xy': + elif axis.lower() not in 'xyz': raise gcmd.error("Unsupported axis '%s'" % (axis,)) else: calibrate_axes = [TestAxis(axis.lower())] diff --git a/scripts/calibrate_shaper.py b/scripts/calibrate_shaper.py index cda0276f3..ebf5c8aa1 100755 --- a/scripts/calibrate_shaper.py +++ b/scripts/calibrate_shaper.py @@ -77,6 +77,9 @@ def calibrate_shaper(datas, csv_output, *, shapers, damping_ratio, scv, def plot_freq_response(lognames, calibration_data, shapers, selected_shaper, max_freq): + max_freq_bin = calibration_data.freq_bins.max() + if max_freq > max_freq_bin: + max_freq = max_freq_bin freqs = calibration_data.freq_bins psd = calibration_data.psd_sum[freqs <= max_freq] px = calibration_data.psd_x[freqs <= max_freq] diff --git a/test/klippy/input_shaper.cfg b/test/klippy/input_shaper.cfg index c6c47c1ad..33ec4e213 100644 --- a/test/klippy/input_shaper.cfg +++ b/test/klippy/input_shaper.cfg @@ -85,3 +85,4 @@ axes_map: -x,-y,z probe_points: 20,20,20 accel_chip_x: adxl345 accel_chip_y: mpu9250 my_mpu +accel_chip_z: adxl345 From 470803853e4d6b547e933a979ff3736cb95c7c2a Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Tue, 23 Sep 2025 22:23:33 +0200 Subject: [PATCH 099/117] docs: Documentation for Z axis input shaper and resonance measurements Signed-off-by: Dmitry Butyugin --- docs/Config_Reference.md | 22 ++++++-- docs/G-Codes.md | 29 ++++++----- docs/Measuring_Resonances.md | 89 +++++++++++++++++++++++++++++++++ docs/Resonance_Compensation.md | 21 ++++++-- docs/img/calibrate-z.png | Bin 0 -> 161737 bytes 5 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 docs/img/calibrate-z.png diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 6f76c5567..94d3a9730 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1780,17 +1780,22 @@ the [command reference](G-Codes.md#input_shaper). # input shapers, this parameter can be set from different # considerations. The default value is 0, which disables input # shaping for Y axis. +#shaper_freq_z: 0 +# A frequency (in Hz) of the input shaper for Z axis. The default +# value is 0, which disables input shaping for Z axis. #shaper_type: mzv -# A type of the input shaper to use for both X and Y axes. Supported +# A type of the input shaper to use for all axes. Supported # shapers are zv, mzv, zvd, ei, 2hump_ei, and 3hump_ei. The default # is mzv input shaper. #shaper_type_x: #shaper_type_y: -# If shaper_type is not set, these two parameters can be used to -# configure different input shapers for X and Y axes. The same +#shaper_type_z: +# If shaper_type is not set, these parameters can be used to +# configure different input shapers for X, Y, and Z axes. The same # values are supported as for shaper_type parameter. #damping_ratio_x: 0.1 #damping_ratio_y: 0.1 +#damping_ratio_z: 0.1 # Damping ratios of vibrations of X and Y axes used by input shapers # to improve vibration suppression. Default value is 0.1 which is a # good all-round value for most printers. In most circumstances this @@ -1967,6 +1972,10 @@ section of the measuring resonances guide for more information on # and on the toolhead (for X axis). These parameters have the same # format as 'accel_chip' parameter. Only 'accel_chip' or these two # parameters must be provided. +#accel_chip_z: +# A name of the accelerometer chip to use for measurements of Z axis. +# This parameter has the same format as 'accel_chip'. The default is +# not to configure an accelerometer for Z axis. #max_smoothing: # Maximum input shaper smoothing to allow for each axis during shaper # auto-calibration (with 'SHAPER_CALIBRATE' command). By default no @@ -1979,6 +1988,8 @@ section of the measuring resonances guide for more information on # Minimum frequency to test for resonances. The default is 5 Hz. #max_freq: 135 # Maximum frequency to test for resonances. The default is 135 Hz. +#max_freq_z: 100 +# Maximum frequency to test Z axis for resonances. The default is 100 Hz. #accel_per_hz: 60 # This parameter is used to determine which acceleration to use to # test a specific frequency: accel = accel_per_hz * freq. Higher the @@ -1986,6 +1997,9 @@ section of the measuring resonances guide for more information on # a lower than the default value if the resonances get too strong on # the printer. However, lower values make measurements of high-frequency # resonances less precise. The default value is 60 (mm/sec). +#accel_per_hz_z: 15 +# This parameter has the same meaning as accel_per_hz, but applies to +# Z axis specifically. The default is 15 (mm/sec). #hz_per_sec: 1 # Determines the speed of the test. When testing all frequencies in # range [min_freq, max_freq], each second the frequency increases by @@ -1994,6 +2008,8 @@ section of the measuring resonances guide for more information on # (Hz/sec == sec^-2). #sweeping_accel: 400 # An acceleration of slow sweeping moves. The default is 400 mm/sec^2. +#sweeping_accel_z: 50 +# Same as sweeping_accel above, but for Z axis. The default is 50 mm/sec^2. #sweeping_period: 1.2 # A period of slow sweeping moves. Setting this parameter to 0 # disables slow sweeping moves. Avoid setting it to a too small diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 7a60aa8a0..caad39bec 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -828,15 +828,17 @@ been enabled (also see the #### SET_INPUT_SHAPER `SET_INPUT_SHAPER [SHAPER_FREQ_X=] -[SHAPER_FREQ_Y=] [DAMPING_RATIO_X=] -[DAMPING_RATIO_Y=] [SHAPER_TYPE=] -[SHAPER_TYPE_X=] [SHAPER_TYPE_Y=]`: +[SHAPER_FREQ_Y=] [SHAPER_FREQ_Y=] +[DAMPING_RATIO_X=] [DAMPING_RATIO_Y=] +[DAMPING_RATIO_Z=] [SHAPER_TYPE=] +[SHAPER_TYPE_X=] [SHAPER_TYPE_Y=] +[SHAPER_TYPE_Z=]`: Modify input shaper parameters. Note that SHAPER_TYPE parameter resets -input shaper for both X and Y axes even if different shaper types have +input shaper for all axes even if different shaper types have been configured in [input_shaper] section. SHAPER_TYPE cannot be used -together with either of SHAPER_TYPE_X and SHAPER_TYPE_Y parameters. -See [config reference](Config_Reference.md#input_shaper) for more -details on each of these parameters. +together with any of SHAPER_TYPE_X, SHAPER_TYPE_Y, and SHAPER_TYPE_Z +parameters. See [config reference](Config_Reference.md#input_shaper) +for more details on each of these parameters. ### [led] @@ -1284,13 +1286,14 @@ all enabled accelerometer chips. [POINT=x,y,z] [INPUT_SHAPING=<0:1>]`: Runs the resonance test in all configured probe points for the requested "axis" and measures the acceleration using the accelerometer chips configured for -the respective axis. "axis" can either be X or Y, or specify an -arbitrary direction as `AXIS=dx,dy`, where dx and dy are floating +the respective axis. "axis" can either be X, Y or Z, or specify an +arbitrary direction as `AXIS=dx,dy[,dz]`, where dx, dy, dz are floating point numbers defining a direction vector (e.g. `AXIS=X`, `AXIS=Y`, or -`AXIS=1,-1` to define a diagonal direction). Note that `AXIS=dx,dy` -and `AXIS=-dx,-dy` is equivalent. `chip_name` can be one or -more configured accel chips, delimited with comma, for example -`CHIPS="adxl345, adxl345 rpi"`. If POINT is specified it will override the point(s) +`AXIS=1,-1` to define a diagonal direction in XY plane, or `AXIS=0,1,1` +to define a direction in YZ plane). Note that `AXIS=dx,dy` and `AXIS=-dx,-dy` +is equivalent. `chip_name` can be one or more configured accel chips, +delimited with comma, for example `CHIPS="adxl345, adxl345 rpi"`. +If POINT is specified it will override the point(s) configured in `[resonance_tester]`. If `INPUT_SHAPING=0` or not set(default), disables input shaping for the resonance testing, because it is not valid to run the resonance testing with the input shaper diff --git a/docs/Measuring_Resonances.md b/docs/Measuring_Resonances.md index 49c050e0c..fe140d93d 100644 --- a/docs/Measuring_Resonances.md +++ b/docs/Measuring_Resonances.md @@ -697,6 +697,95 @@ If you are doing a shaper re-calibration and the reported smoothing for the suggested shaper configuration is almost the same as what you got during the previous calibration, this step can be skipped. +### Measuring the resonances of Z axis + +Measuring the resonances of Z axis is similar in many aspects to measuring +resonances of X and Y axes, with some subtle differences. Similarly to other +axes measurements, you will need to have an accelerometer mounted on the +moving parts of Z axis - either the bed itself (if the bed moves over Z axis), +or the toolhead (if the toolhead/gantry moves over Z). You will need to +add the appropriate chip configuration to `printer.cfg` and also add it to +`[resonance_tester]` section, e.g. +``` +[resonance_tester] +accel_chip_z: +``` +Also make sure that `probe_points` configured in `[resonance_tester]` allow +sufficient clearance for Z axis movements (20 mm above bed surface should +provide enough clearance with the default test parameters). + +The next consideration is that Z axis can typically reach lower maximum +speeds and accelerations that X and Y axes. Default parameters of the test +take that into consideration and are much less agressive, but it may still +be necessary to increase `max_z_accel` and `max_z_velocity`. If you have +them configured in `[printer]` section, make sure to set them to at least +``` +[printer] +max_z_velocity: 20 +max_z_accel: 1550 +``` +but only for the duration of the test, afterwards you can revert them back +to their original values if necessary. And if you use custom test parameters +for Z axis, `TEST_RESONANCES` and `SHAPER_CALIBRATE` will provide the minimum +required limits if necessary for your specific case. + +After all changes to `printer.cfg` have been made, restart Klipper and run +either +``` +TEST_RESONANCES AXIS=Z +``` +or +``` +SHAPER_CALIBRATE AXIS=Z +``` +and proceed from there accordingly how you would for other axes. +For example, after `TEST_RESONANCES` command you can run +`calibrate_shaper.py` script and get shaper recommendations and +the chart of resonance response: + +![Resonances](img/calibrate-z.png) + +After the calibration, the shaper parameters can be stored in the +`printer.cfg`, e.g. from the example above: +``` +[input_shaper] +... +shaper_type_z: mzv +shaper_freq_z: 42.6 +``` + +Also, given the movements of Z axis are slow, you can easily consider +more aggressive input shapers, e.g. +``` +[input_shaper] +... +shaper_type_z: 2hump_ei +shaper_freq_z: 63.0 +``` + +If the test produces bogus results, you may try to increase +`accel_per_hz_z` parameter in `[resonance_tester]` from its +default value 15 to a larger value in the range of 20-30, e.g. +``` +[resonance_tester] +accel_per_hz_z: 25 +``` +and repeat the test. Increasing this value will likely require +increasing `max_z_accel` and `max_z_velocity` parameters as well. +You can run `TEST_RESONANCES AXIS=Z` command to get the required +minimum values. + +However, if you are unable to measure the resonances of Z axis, +you can consider just using +``` +[input_shaper] +... +shaper_type_z: 3hump_ei +shaper_freq_z: 65 +``` +as an acceptable all-round choice, given that the smoothing of +Z axis movements is not of particular concerns. + ### Unreliable measurements of resonance frequencies Sometimes the resonance measurements can produce bogus results, leading to diff --git a/docs/Resonance_Compensation.md b/docs/Resonance_Compensation.md index 43ceac8f0..dcfb4ed78 100644 --- a/docs/Resonance_Compensation.md +++ b/docs/Resonance_Compensation.md @@ -439,9 +439,12 @@ gcode: SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= ``` Note that `SHAPER_TYPE_Y` and `SHAPER_FREQ_Y` should be the same in both -commands. It is also possible to put a similar snippet into the start g-code -in the slicer, however then the shaper will not be enabled until any print -is started. +commands. If you need to configure an input shaper for Z axis, include +its parameters in both `SET_INPUT_SHAPER` commands. + +Besides `delayed_gcode`, it is also possible to put a similar snippet into +the start g-code in the slicer, however then the shaper will not be enabled +until any print is started. Note that the input shaper only needs to be configured once. Subsequent changes of the carriages or their modes via `SET_DUAL_CARRIAGE` command will preserve @@ -453,6 +456,18 @@ No, `input_shaper` feature has pretty much no impact on the print times by itself. However, the value of `max_accel` certainly does (tuning of this parameter described in [this section](#selecting-max_accel)). +### Should I enable and tune input shaper for Z axis? + +Most of the users are not likely to see improvements in the quality of +the prints directly, much unlike X and Y shapers. However, users of +delta printers, printers with flying gantry, or printers with heavy +moving beds may be able to increase the `max_z_accel` and `max_z_velocity` +kinematics limits and thus get faster Z movements. This can be especially +useful e.g. for toolchangers, but also when Z-hops are enabled in slicer. +And in general, after enabling Z input shaper many users will hear that +Z axis operates more smoothly, which may increase the comfort of printer +operation, and may somewhat extend lifespan of Z axis parts. + ## Technical details ### Input shapers diff --git a/docs/img/calibrate-z.png b/docs/img/calibrate-z.png new file mode 100644 index 0000000000000000000000000000000000000000..b75a65c33902d7c304c3b8f0117b8be1182bc270 GIT binary patch literal 161737 zcmdSBhdY<=|39p)p_CRXL{=mtLPnICEm@I~l~HD7v?OJZ>{Ld13!z9>ii|||3?VyY zWb=ER@6UJK_wW7-ZpZOHKA)rVdR^Cfo#*rUSkKE>Raus58{IYv3JNNDxpV3i6dM>R zDAsFJZpNQPJKw#I{}FaPukEN|XXfZ)WPg=H$;k1#wVk82r7@%PReJ|ZJ6rw}f+zU7 z7%dzfuRDnF@Ywv%KR97$Z_ZOYTQrLg*?L`0$AN-kml64I-7ATgmK5tKDCEzb)w~%y z7U!a^S>AHHJ1Je0W7|>Zb6f)3TSfnNoyz>O{}i3SZ()mtG1Ze-!L7G6$5yR<3twoz zFqZwe{?Xe_&Mrj~^Y4v*Y&x6C(lzHgzCm=ZI?ueOP*l`mu-Z?Gk|`+oe}5Jz4tiK` zF#X>jafbe#5B>heSa?P=H@)n zUQMA+q%McY;BoTWfo}v60bP z{9+WZ$!3bwOBpiHRj-dUaum9UvZ;J&Yz%w&@ZklID1)xY2YBk!FN=axDRMf+QhpFe+!6wwJ-P%B29 zDEsl_u!Dm`RT#(T2UPSk)*swvIkD`eH8nev)P3<`#cQiedP^ro|6a{~{eW`&_5}0V zr(s+=d$w=izHL93-1NY=tyEMV*J8Y>tD{dnK6?JqGfhqo!=f{{=Egk|xQDH~3TVB& zyoRGV3<^#cy3N*yDk>@AFShJDl6dv&llLD!ywK#l;oy*9SnPSmYgJ$N#EQ-b$KgFT z?QcuIeA%q0r&qK*rTA3rRATfCbz!Um*QqQK=dp*`+1UncvT0h`j>GD@xwdg>Z{7*o zwD$}Qd>Qx_qmyO2m11Rawy(pkI0^g3y1OXu{Nw#Mez%|cM=Pxg+k!$)PL7e0@iv>v zQ|f~UD@yKdi4w9q;=TO<>xmP}gLI-U z;n!NyGq04a&#~!{iQ>CDRF|+N+w5Ca=C15(hR#*dw7YljK7IQ1Nr!<8e}8w#Ub(`^ z%E~&wv_wCqYBpG;5-s4nn^}gMn))ODc42WeW4PgE;8~9h2kcqgn%tk>vbHxy8}eNy z%$Dc>xOK;mwc8KY9R5c$@J%A8J3-Sy@rx{)Lhjg~K|_{+GhKgJ;w=G|GaRWj8WfHNQ>?a zcuj!r=kMR#%gf7&0@$-ZdwY+DgoGr%y_8|F_13Lh!I@PbTqd4UlvafwpB>v+dVecT zVl5TD;NJA~^rM1;TC%p^pPiLT(R>scd64OT(3o9)qHjPacFaab;RiJ(q9B&1k$--YU7gJY6d? z9C$=*4-WQjiRH?5*3FJ#MwpVF@pJl@o8b4Jc%Uu|`=AM2*==iuOYVOkktJoNp!x|_3&=bv70 zRnG7iPNU5t_H4?r)VE(T6?NOYUrujz)8v$t_U$@yhWd7EriqUu z&TN&o8Pk33eCITZ(&$sy?o&(Ql&MBWMn1GFT9A=Ffjd7fD*AZmVG%jYANei^ZqNVD z?RGTHUi$Y5byYAkq81CAq0YvNx+o7;oGwqW>Bte8!hU99WqmOIBY)H{+Txuq-6^Zm zJcmKD+nJc|I1JX*B`eW~DI|q;x3@>*Ar^xdIPSd1*w9PdVd~!}QB-l58i(bO#s^1BPOaCk9841>@2nSz zZ;-y){_dJjN=i!2pT0neqxVq)9vt8?c-CZf!`ZnxKl%CFG3vvI{dLpKetzkhk$ z2r0_g*w~pWhs7Do6t0|{92|)q3=9l1fk%|uvaa5^al`1-{jF|4g>@E0MMQKLmp*V$ z){dAD+Yc91`m|;8fht|Mr+j&~|oz?m4*H4<)#QyBebGUiymZEci>A(zM(eG(g zeI?YD6Q`ywU%t$7DgDrBOGa%bw{F^#%~W)6Y&rxz=YMX&ZY^!gHczT~`+Ta&~4!)${6$UxssDte>94Bbr^dF*3U2@9(dytNc_HD_48KRfi7$t3NoawoX=7mSX=2 zeYsy%;Zy@R?CnFesvkXiR4=;brn^WfZ*QNayf{i_Bq}QE=kGt<{_gmOEjtBn{x&Xq zbXAScW9rw}EkSAW@1!5>Fv89nYt6jpvGg}s$j+cps7}YIA_4%Zcy&?HD$I<Lq` zHhMv8Ny?J&RR*nG(DkYmH6~fbVG+J93qoj4IaT!#gF!LFUlN75hjhAg^i6o5wq&Zvw#!qx^LMp z{-ar$!;RYb3s<^v)26z@TP_@89wl3(&z(Q7_U6jj;cVSBRy-okON(L`;Vk_g?GnX-XSVAq&@c8B`&u=(2UrFNg z7ygtp42tBI{d*BJ(=%b8-|yA6jrO4Q}V*|TT$3vQ|nEW8_dTP!h&R6QMk()yq3?NJ9odPe*s@;WMmX{d)xQTp7h zauq6->+(D=ii2?Tr9AtqVG764T${FviMUR&pkh{NZDApR%c|+0^IZk5ZN**^SkEf8 zEi5Zhw+h{X$i4~G;;5ylKEFHTiqrk7$;xs=8L*kLvGJ!kQI{F1)UF3YVlD&}^Vn}W zCnGaZXma&x*(dMq!(D}Ft(jju{@ zMwRMbocDeFn3;>quczkwq|pdC16@7a z`L`|r`?uFR*>B#x`=%zGWn$?3HJbKY^eJX`cFJA5b`=y9yubc)J(kGRWJuqP)N}Xw z>&eP-3KLz0mguhNy2ibwzHQq|=p;PP05~_MUfOl((j|i;_e!iW@2OKsT4}n%S!JI; z-`jIc;iP@P+1lNlDp*r%Eo`&dx5y;nG_RHCC0U)seg= z?{CeW@TWWV35Xrt?mCtp0GBGAE+RPg%)d{Wm*Lvqe1O^Fz?qA0YT`uAC*2pfiN^@p z3Eo^q`+Z^Ako>8u%OEm3`qAA@+aCdNj8sl^=4sq?b=}I(&#(6Wx)Cp+o8_^1ztS&X z9(5Jo+RnztW@Kr}0Z4jcyt_CfciR(c%TzN|8O6O^+N$J}+!v;N(QY2$C>7PMR%>;( zXPXBC3p{!A_U+#N`}e;=m744Z|Hz@V#FM3=H}Y9za4g;y1Nav zb#)(|ah-aI)p>}==1YJJ)BX9SiQ@CXa_4>b96R^)jO+chw6w|TX&M|r5jP6n>pha0 z7IjC}UYy%%WM##9=FFK=tln%oEgc;vxoB$}8}F#7sL8*7o2TWSJt8kJWoa~5-voG> zA4$zPheGC!pH~KjALfC+&!y?UbJTiWRq4@U_0IT4x3Z z*y5%*PzCM_>J$3l`$_(F;-|0&1Erobva_?7yJi>`*OdCw2bKZ04KWAa+ovjEyVnGz0zjRi`{3|PFETQ`iS zX#Q?TuHEQx>g9aFZQfE77LBihpIT#Ifrh=XsCy9^6VvzeX9+4OReK(4hP#J{8fW-v z5fOaX0YJ7yW*S=BN*uKm^`vb90Rh19*g%^+S7t8>(D?^g%gIrK`z2lf`3Ws|3z!0- zHbB8y_L*l1{L3&XoWzCJ*N5PI)?*<~=)Zr)9MQ1EZ`XCE{jH@m^a@q>y-}XIckkZa zv8exCb?Eo9I1~b0NM>%XY>K9O!Oa_se}8|yw`G@OKlVptin6!V^X%H{my#{%mo1zA zX(|_#mKn&FG}xplGr&xH2u5@RxwYU(r|>W?Kx8<)K&Z24cpElLiTNof+Cqpe{;F zthh&X*43{gt!lWj=Jd;{p??>%*IUx808V@h;{f7K*#kkvydsDR6pzcNHi;Mac<(S6P~yN43jL@Tn-4VYnAg{eiUKzAkrOOv?!7(? zp47O?y>$Kqg+pjZ`Ru^{Px$QGB`1|wSXlbyJaPQjQ_qe6xRpBU!Nu1RbW#**iw;F^ zMKE)o?Iqh6TB&8IwzKsOzYPoB-i7@IynC+6dA_^I16*1uh861Dd*|QLVP+Pe^UDJn zwrgl;{As#mIGot>&qc*BZoR+$IgnB=qIdIpEIP)`hGh6{u)g!{3 zE8@<$s#6$4^HA5laKTSbUcN5B`Qx1pn;4IXyU)wyFR!ee$GUg1N9>kf1%RMYR#tw2 zk5<=J9uK6JdTGF+mt&nAbF9cRv`#3~y!Obthx?9enR2*HbcsDzWiJKyTF`27?H0J! zd;u81-J0Ljbanb)0goX#ENm~4RDhx!Wh6pEL!-{P@+B!o$fRiY0`h_epLU%}WP$P~ zhrVAKFCl?vchk*{?^-ig;r9d51xN0N2Ph;Nx4tm~Lj@B*0)SG64{$Vfn;E(j9=`AS z^XCE<-ziYsf&aFAL|eCP%Q^v$qIm6yUP9%iavZSttG&97q|aOrgNE9kYqyKsW+LB9 zAN#B2c5Tm5cB3BTUx+=;t0w|xG23->17P*Kq5E7ABzJ`?oX56yGQIV8%$NEu{zTCd z-L9o!xVQIiYHF&%K}}~gFHtGVdo3!c0oW}R`h{-ct*zRHN{p$RDR&DC#Xuj!>!CZI zJawumU(0Rb^XJdq77+;v%&e@!+fSbCXE=WRI3&e{zWCTqhMs%={|tPM*wy3zYZa`Y zTIxHbAnV~3K!9PKMC=6=U=5%Y94}UnIT{)oT5WKd z>-Oyp4PqeO0R4_d=4h$E&{7#(#ycyYc;MLB`5d}?jCp#uu*1NPy1F{BXIUbPpbbWX zEIqa>UU`DA!O;-*SQOaBAfAY)O#yc3bK>#`)z{j*_}KUE3w$VSeR!75TWKn{qJbr& zW^ox7#bOaqedW-%@}Nt*i*H^!4>MLNT-M$njslX&Z$q8s|O16iv>N#+1(h_>rj2kSLZ>K>-i%1qD$9 z!=^D8-UT`5P4f%<9O{95O&?T=`nz>zSf<6zgX42#8~)8SV(xVX6BSCq)MF`eix zRzceuYD}T|`}Z$kcGk^5#zeRTOt=9g4e)p&UOcWYL6)45g{fcbpe3KnS$T38>{fPd zb;YTE{e}&ME+?;kJK?j7Ve|K`;Y1{+c>46INY16ixQ< zstYz^?(-)hF}}~wAB}6UdI#Odpx|Z+Dh=q8SNa$i13mp$EPnRBou)&p1ynfKeZZWk zM(X*_Hh|R^Akprq`lrbXh+qNRYynvGhKmboo?z5Sv(-DW)t>_cGJu6$7GJ?DQVa@3 z(ASAP8gfi=51KtzT3%kh1lR&vLZ;iS6*$48j0~QwTep(KfC@-KbZHsCgJ)G$sZrs> z9iblLp-4jzcXIa00MwA8M1lS+)a@R6ITx6!KP*s`0^HYi8r^uCBuKKnWvW)%p4-|v z);c+}06M|Z(VXaD$8cuPql`M%s8VVLA3c8#B|fQkm`NK~dO$Z#OQG0ntq3P?U*WB} z=DIBpyFHh<6=VLtqFgHVYbYSY_kFo^vd&Q$V@r$J4;P7E@3|eenGy`(&q&j84sPg% z2We@2Y5C1eE$w9X^BBA~&!)C~E>^&_Q87*H@%;Qv!W0H-<9U+Ig*F~PUi>Qz%eWQr zgT{D9@V<`^Aro+1&c_8kMT4LOx4q}u`QyiVi1B<)QMe_mmUM1NVl$tj0R)(VH&_6Q zxUfUHff-mpj?Uo$I;ArJU@L@kUR5myvfoR$|L|dM*j+PHg`Yls+Q>+Oy&asA22gN7 zMn;Cnm78nlMq59Lb#2Mj4uK5hX2bN%AWm&gdcNZAa)PzIWH?)vpN zr=7V7$K24}EdIDJ7S| zvxTeH=DNKegTy=rZQPDUQbV!f1;)E~qnvNe=D`lyqiDyfsZHv-zzW{#Deir-K`?ko zf87p98TWvKQ8`|myYg4!hN%V>iQ&sHu~lAxuZt{F?53wrWn1d6q^Zj=l=wL^!ij}* zw2l^bNM?x$-6;nf<9Ziyw9Xp7n;zsGVi%`!FD2e*}-={?*mhda#V7gr_JGXSXyx&lKrmZRz5T$6YV{MTOVc7cPLexr08bcQy?RXd zt;KVf6Q@r7D>fgJ@YE;5FH9|UR2=iRUlxM9hYe8 z#-$H;6OeV3msd@N_4H|N3S$&XbzK;E-tcjhf5Zf2WMqJSF4EhCfooyL_Cw#@5zcNn*GD?l^EDg3BbA zLEK#q1p6kWB|y4CpxRzEt&N*EE4Xz(Xpz>bZ>axFTc7#n&3C=?im6TY&(3ZD5&E)A zV)-MatkbvtazOz1WQ%!`kN~|FhT>4-U~N1D4vSdFjksBOq%Z4r=fa-m9Gkp# zT5!vjEs)*O>)c#TjE&#q=C&vvSYuc#Hv2{gmSX`aH^+Jb645%A;zYx>*`-~UPL2u+ zk7{P2Diigb_T&``$gT5>i-5;xQM`DCgoNfhA^G{u~%_T$&W{ zMT%A$9jfWTEYg|{N2_|54WX91U;$2;Zm)l{B=27_ogsL}eDFK378y}7cES;p@Q z9ykC7VSFgGN!bMseP8@JRG(2m(hhS(8w`cuG2B=}8-n)RdExP4?T5f6 z!fY+3%$2WSzup7ijgrYReF@${qt!d*WoO%;Jv|@MPG{>4=Txu@K?seZxP#-VE2t} znwooolUrg$gSr-Aq8>Z0^*TsopD z>lwEh$^!=u96EgX&*bDY9DTRYmpqq0oWnP9s3%fx*?H(=bF*NF;3A}B)5<;LU4_S$ zYHCh^+!F!|7_4Sa`?IC8W~GwqK6P7k#rVj`qsNc8;|TKI^n~V>n3!mKYbpBy9C+n8Cx{#S*CGiRh_-sx`5~(g<%1197igw+^Nr zkpzjIgmQ>;J%BngVOon1u>A4il*i%>lc3-e6o14fP`4WCr`>MeRE6#-6L83vdOb?Z z-Fx?hZ%#gYYP}dU4WdT0!wX7E92fuD+uYpjmi6O<6Z6TFL0B$_wN(!Y=)$lAiOd2q zr!m8DEw0L%=&rmjzU*1|aBvQws>5o>lLB;~Eht=W_<_5j)7nc9EcBYhFL%~e$ zAJC=a_y+6(toak>;zD0gr0Y;ZiS9*Iv1}H=s8RaQI*e)e8s8X6x}Y5MqbZC&Ck`5XR2rZ-obYfS#Tnp8s`w`(CUs zB$Ox==c$#s?m>$gqW+N7f`C=yLn5quizT_Nx3~9Vw22x*W85$a41J&4x-DzdZR|36 z=2tkdFM<59{}COMM(IP4Lh8BdNo;WWZk(TkB981R_X*Id0lN`esX`yWasB!gq@dic zih{MGx0*mn*)08(f;a@=d%})GK{I?OJdT}4DuL{am>7CenE(_~3iblFL13|LeoYJ7 z2qo-B-xn&a3Iz(l+=UB`I#H@npT#DF2c9mQ z_q>xILabuXZ6M=DVFJrh!yTs};HVPm>NRQ!F$b0*CNi|TMXB6@7ypn|DS9aJ*s){2 zfBMQFgF9^ljzj-o!~;97{PYY*<;E>rw!`rtx)Vxy87e4G8eiy*7Y@p-&%qFNA?a!N ze~nOq1n?LiIF$DBzr=CNH(V_lh6iwjKDD>&;Cs$T`RW#K*Yq;&Nd7ey;DKOKAT{a0L=_Lc$&ZSIC*Y{;t=_=pkKH9pE>& z3YU%sb%Gjv^;2o-x&Ci4mtf*6+VOuJQ$-8l%7{@`o={nQvD=RGEB-B!VVH?;0VIaP zxf@Da(tFqGf15stW5qQ+213V`XAc!Ak^+#daz0LdRSNSR2)XDd9 zpRuve6Mzdxf5v@ws7~5#Kei$VLIUg%g6`+#<*^$SyvO!_Rq~AvfaTIUq7@$0KPOGe z1Sx?H?VJDdbx9=eTvRE8nYyG{%h5Gf=lgbXG+sd~a{p6yfb6pgrNHp;i^-uAgiHg> zK`#6{Lls=@6^c|)eWr=*kux_Xv6Uopgi*%%;*TNP1!RcK*e8?@aq_2YmH^y&exU1! z7XbJ64h$qan7*6xp}vbuME`2fpe!~z(U=j#n{Q1fZijmg1oH=AyG6*m1=>9FtM)qL zy0b!$nt>rBTk0FYA?4PsC0$)dAg+)w2C1dB5^Lm30BQ!&IiM*L3<@>-aY_mYsv5Sm z12k!DLP8H`wT$7~AR-95PfHqy{jcLuolG7rf2+Y-7Fj0F+P0?ES!B6aLhRXmmwoLz>oC;(iU-K}9IoB5p;|S{ycWbD<@0rnLhS3W*A-!A~SkS!`U~d0>(< z9B^cCtXtol4dXF1Al?+bQZ*<|M9GC@IosLQM{~z&td49BN>Cax z&5~X0eDHdwG`)Ql7G^mjc#tGb-35V@t|Q67brr$I#C>Dp`|Q7tfB&uu$(3Ks&c;Rw zGE-NT4DIK0=gv`stS9+JL>z$T$xv~W7*m0P|Ce>-?slh!Ji-6^S<~r{vTHk+rsXhP ze+(|Nc;(iA{`>Fv;!hx%0qY3ZWT}lu{Eusd_!eUROGp&|o9X;dF1ETj668-E*AZy* zMfUvoZBrU(6BGdq63UQK?3FTP5}3HSb_faO>R(=+>P{8{md1JTK6`KTuMyK~uNVf? zwN=sCs#T9-x2tPKhJ{&P-5CaL#S@n%iYtr32BD(L+f>!k=4}X3s{fomHCU^1XZ@!C zd@wK<*na=wftBt=Az@)!@E<%3ke1q0g=>Nm6{{;QBjGDcp8wa!cX@?+ouZ?rudU8WReaJ3SW9J zr{+VDY~rWHIas}0^v~aG{y-+-=jzH5JTQTlZUJ|$LO^%nr~2WsvDIigv?7ic>BS}Q zoksVMkB{e#`*?-^d#@Q|-9n=t5pH}~9g>IyvKVxGSmu?s>OpV%->cH4(p3DQU5b1$ zx31>+ZPUAg@<1P=-#CpPraULBmN2d-bbB$(+Q*RGBZoc%8qrk2nq_y zq-l-L3kfZB{`Y#Ar3>Yb$fLm>WoBN}nC*X=cwjbu&g3e?e1NLRp8MtBbL%+<+{2o zjIxeaoUJXw6jTCc{)iwYSeHhFzM2B zRfLc4r-OzsRs`{RnySbF8of zp7bbyn5|CYfI2jL_Iv_9It{1+x1G40uw3U<`N$#dWIA~g6^~)~x`V@Kh@YQ*b`g$u zxDARuXtx6=(^p1`muSBI;hxP{eNy48c@^erz+!uaD+ywT3 z6P9jJ_zxRDvYd#@9re&k(ZsHqv3X<{1To`ZH@C%2k7(GJGLC3zX@!hC;@;#T;<6)| zg<2^PxdxgNcKp&#RC<6St|S+z%Am_6w91hN)3CG1V=2zYGoC2`0!;Tn(Hwkf-2)ZTmP$9+vo1@GYT^JuwCCxB!?o<4CF@!qOx(mU!y6*Wxf6tfVhE z{m1g)V>|^6Cjn`F{X>v9n;K5JPMKCk@YLdbf5Tp|cQWZ1Q-_8-Tz1h}t~;OcIN;ww z1YS^1o8@;=QC&snM=&ZFLN~9|h$cLbnWycxTKxAM zZZ1Rp*rCv%prEwD4MbW3b{$CyWRQ3Y9)>+Eg%%!($QA`E__0gr`b;b=_n`xygsn$` z|xHh3S&Ucyxp>R z9hxIFAtXt0>R@7y4!0w+*6lt`6|`T^*2X3-fe+ROt)ZbIws8dj zrygPTDq#SDv4)0u!ox>&K?V%=m=}B!pfbbn?{D;37m5k~C+q}5BFnY{$wxC?pB8W* zMVLeD^+AM%zX!v&l%zZhZI4$_aNsdd5##C82jME?Xsf2KxoRa}i28)j>KbFOcfSlU zg47)eSp^C9Hl$wS8EMH-K^$m)x>jPL_c0GZu{mg7Tm2I2ed<`TTUvBRqsT}iGT#{5 zft;72`=3n|V80En`(^#<%AnX}oto>xHbgb`!8y=CM1xR}xEL71fMcL!nF!ZYK+_|0 z4bWl{Tcf{?%|}B`E$O{&ABlwsvB+i^M5g{~fRotp6VMUD%Oz$c(_i`7<+fE};(DnXQrlw{1kH7z24qT z9h(=yrDV)eytV20>pz7G)#4hOpq1*x8RH0}gurv<_73HT0_UuZIr(4Pdx@96npca| zF)K`;y|ZBpR69l_@E~GlISeX)31kS=O8tehF+V>~@f`_gON)F^KoEln6h#F$QgW%v ze7wEo7f3i5WO&_aNCb#+;6(FAC<0zg=c$^xx4$a!&YZpRMnUiJK#Ro0WWyaj$A2^* zM%fgyv~AN^l~w=eL7G@q&(vBleX6q9m8$g7w=*}4JlPDzRvus?e`~3MRzHkc1EgA@NMml{AyTZ!ZIH+l z*!1v_>!03DI8*M^--OWPHls3w&f8u0j)j~nKYQa#K>Pfkz9?}I5vZJqV+eh%ISWQ- z+5R@A@yf29JI!FoqN1Od9mlCAE)0|^xt1!2kI;(B?U*rr|VSJ6K(>t z`QJT9w2fF4FhPKnkws#&=qktyxBy)NWG+lR9}3pccP0=kX`nwsBZvcsqE%8`yGvHK z-1?LoYCD|`%$*`wf$>>aF$gzci7(2x6xwPNhMiBQ3Kr|i? zwvH)|ALdB5qFuwQC_x*ZP0w>0)q8O{il%1RQ)>u+jTO#LPQO6DCEKTyo2-(Q z$73o&*><64mLd=jpn*FoM@uG&N{GdS$T&ff+Ko^GdKS?l@P{vv@GdParLbi7M$vo; z&lcVM4m5CL6~~PtADVTePlm`a2(yA08UdQtPsfC-KtTykS&>+sXT|_c4-z(IB_((G z&1>Wa2WsNJLJ9(n?FUKP=oo*%Tk6iedmD&LaP+9u;4g;{D%~8F zn(r53B&v8_j1w9e?lh?uXOqb8Gh=NUvdYg@54zLO+6_XaLgGCV9kw2i{DeV)9HOFl zLc$3CgJ{wpn%W3LRjG`oxp7Yhjjt5KyBpBfi5Oe<(>xHt(8$Smk})RGhma%h zbiaLz5h594Xne_b-9-Qnkw_b8O+=e~@e3X?30Fws7doG&)sUjW=D9fhl56k*

TI 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 +TBt?At86s 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%0D4T=cn+HU2kxv+cHsplE~>OA(BBB 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 S-e|%CUvdy7>4N7*0sjTvE(aX| literal 0 HcmV?d00001 From b7602ff969bf4e1534ed30bfa51caa6613ca0e8d Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Mon, 6 Oct 2025 18:40:57 +0200 Subject: [PATCH 100/117] input_shaper: Fixed initialization for dual_carriage Signed-off-by: Dmitry Butyugin --- klippy/chelper/kin_shaper.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/klippy/chelper/kin_shaper.c b/klippy/chelper/kin_shaper.c index 1c4724360..682d7fa49 100644 --- a/klippy/chelper/kin_shaper.c +++ b/klippy/chelper/kin_shaper.c @@ -217,11 +217,11 @@ input_shaper_update_sk(struct stepper_kinematics *sk) { struct input_shaper *is = container_of(sk, struct input_shaper, sk); int kin_flags = is->orig_sk->active_flags & (AF_X | AF_Y | AF_Z); - if ((kin_flags & AF_X) == AF_X) + if (kin_flags == AF_X) is->sk.calc_position_cb = shaper_x_calc_position; - else if ((kin_flags & AF_Y) == AF_Y) + else if (kin_flags == AF_Y) is->sk.calc_position_cb = shaper_y_calc_position; - else if ((kin_flags & AF_Z) == AF_Z) + else if (kin_flags == AF_Z) is->sk.calc_position_cb = shaper_z_calc_position; else is->sk.calc_position_cb = shaper_xyz_calc_position; From d55baaf2654e989aac570d47910ca9434ec5814a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 28 Sep 2025 14:05:09 -0400 Subject: [PATCH 101/117] bus: Additional devices require i2c_write_noack() Currently, the LEDHelper() and GCodeRequestQueue() helper classes require that their callbacks do not block. As a result, the pca9533, pca9632, and sx1509 devices need to use non-blocking i2c write calls. Signed-off-by: Kevin O'Connor --- klippy/extras/bus.py | 3 +++ klippy/extras/pca9533.py | 4 ++-- klippy/extras/pca9632.py | 4 ++-- klippy/extras/sx1509.py | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index b04fbe764..676ff3b93 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -213,6 +213,9 @@ class MCU_I2C: "i2c_read_response oid=%c response=%*s", oid=self.oid, cq=self.cmd_queue) def i2c_write_noack(self, data, minclock=0, reqclock=0): + if self.i2c_write_cmd is None: + self._to_write.append(data) + return self.i2c_write_cmd.send([self.oid, data], minclock=minclock, reqclock=reqclock) def i2c_write(self, data, minclock=0, reqclock=0): diff --git a/klippy/extras/pca9533.py b/klippy/extras/pca9533.py index a94e1334e..20f1a6ca9 100644 --- a/klippy/extras/pca9533.py +++ b/klippy/extras/pca9533.py @@ -27,8 +27,8 @@ class PCA9533: minclock = 0 if print_time is not None: minclock = self.i2c.get_mcu().print_time_to_clock(print_time) - self.i2c.i2c_write([PCA9533_PLS0, ls0], minclock=minclock, - reqclock=BACKGROUND_PRIORITY_CLOCK) + self.i2c.i2c_write_noack([PCA9533_PLS0, ls0], minclock=minclock, + reqclock=BACKGROUND_PRIORITY_CLOCK) def get_status(self, eventtime): return self.led_helper.get_status(eventtime) diff --git a/klippy/extras/pca9632.py b/klippy/extras/pca9632.py index b8a813c33..099676e0f 100644 --- a/klippy/extras/pca9632.py +++ b/klippy/extras/pca9632.py @@ -37,8 +37,8 @@ class PCA9632: if self.prev_regs.get(reg) == val: return self.prev_regs[reg] = val - self.i2c.i2c_write([reg, val], minclock=minclock, - reqclock=BACKGROUND_PRIORITY_CLOCK) + self.i2c.i2c_write_noack([reg, val], minclock=minclock, + reqclock=BACKGROUND_PRIORITY_CLOCK) def handle_connect(self): #Configure MODE1 self.reg_write(PCA9632_MODE1, 0x00) diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index fd36c7fe1..99df55df3 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -92,7 +92,8 @@ class SX1509(object): # Byte data += [self.reg_i_on_dict[reg] & 0xFF] clock = self._mcu.print_time_to_clock(print_time) - self._i2c.i2c_write(data, minclock=self._last_clock, reqclock=clock) + self._i2c.i2c_write_noack(data, minclock=self._last_clock, + reqclock=clock) self._last_clock = clock class SX1509_digital_out(object): From e87de2ae493e694e6f2bdca5ba2410b3894a07d2 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 20:10:24 -0400 Subject: [PATCH 102/117] motion_queuing: Don't disable step+dir+step filter in drip_update_time() Allow the step compress code to perform regular step+dir+step filtering even during probing and homing actions. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 93f5594cd..20473503c 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -250,7 +250,7 @@ class PrinterMotionQueuing: # Disable background flushing from timer self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) self.do_kick_flush_timer = False - self._advance_flush_time(start_time) + self._advance_flush_time(start_time - SDS_CHECK_TIME, start_time) # Flush in segments until drip_completion signal flush_time = start_time while flush_time < end_time: @@ -265,7 +265,7 @@ class PrinterMotionQueuing: continue flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) self.note_mcu_movequeue_activity(flush_time) - self._advance_flush_time(flush_time) + self._advance_flush_time(flush_time - SDS_CHECK_TIME, flush_time) # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) self._advance_flush_time(flush_time + self.kin_flush_delay) From 8fd263ca691cb4f91b5a721901687b2f3cc8340d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 Oct 2025 17:29:30 -0400 Subject: [PATCH 103/117] toolhead: Add a lookahead.is_empty() helper function Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 386f0620e..f56f0f114 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -126,6 +126,8 @@ class LookAheadQueue: self.junction_flush = LOOKAHEAD_FLUSH_TIME def set_flush_time(self, flush_time): self.junction_flush = flush_time + def is_empty(self): + return not self.queue def get_last(self): if self.queue: return self.queue[-1] @@ -476,8 +478,7 @@ class ToolHead: self.print_time, max(buffer_time, 0.), self.print_stall) def check_busy(self, eventtime): est_print_time = self.mcu.estimated_print_time(eventtime) - lookahead_empty = not self.lookahead.queue - return self.print_time, est_print_time, lookahead_empty + return self.print_time, est_print_time, self.lookahead.is_empty() def get_status(self, eventtime): print_time = self.print_time estimated_print_time = self.mcu.estimated_print_time(eventtime) From 3f9733d04dc4eb3245352139995f485cafe776e1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 Oct 2025 17:40:23 -0400 Subject: [PATCH 104/117] toolhead: Move priming logic from _check_pause() to new _check_priming_state() Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index f56f0f114..aa31dbc8e 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -313,36 +313,39 @@ class ToolHead: else: self._process_lookahead() return self.print_time + def _check_priming_state(self, eventtime): + est_print_time = self.mcu.estimated_print_time(eventtime) + if self.check_stall_time: + # Was in "NeedPrime" state and got there from idle input + if est_print_time < self.check_stall_time: + self.print_stall += 1 + self.check_stall_time = 0. + # Transition from "NeedPrime"/"Priming" state to "Priming" state + self.special_queuing_state = "Priming" + self.need_check_pause = -1. + if self.priming_timer is None: + self.priming_timer = self.reactor.register_timer( + self._priming_handler) + buffer_time = self.print_time - est_print_time + wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_HIGH) + self.reactor.update_timer(self.priming_timer, wtime) def _check_pause(self): eventtime = self.reactor.monotonic() - est_print_time = self.mcu.estimated_print_time(eventtime) - buffer_time = self.print_time - est_print_time if self.special_queuing_state: - if self.check_stall_time: - # Was in "NeedPrime" state and got there from idle input - if est_print_time < self.check_stall_time: - self.print_stall += 1 - self.check_stall_time = 0. - # Transition from "NeedPrime"/"Priming" state to "Priming" state - self.special_queuing_state = "Priming" - self.need_check_pause = -1. - if self.priming_timer is None: - self.priming_timer = self.reactor.register_timer( - self._priming_handler) - wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_HIGH) - self.reactor.update_timer(self.priming_timer, wtime) + # In "NeedPrime"/"Priming" state - update priming expiration timer + self._check_priming_state(eventtime) # Check if there are lots of queued moves and pause if so while 1: + est_print_time = self.mcu.estimated_print_time(eventtime) + buffer_time = self.print_time - est_print_time pause_time = buffer_time - BUFFER_TIME_HIGH if pause_time <= 0.: break if not self.can_pause: self.need_check_pause = self.reactor.NEVER return - eventtime = self.reactor.pause( - eventtime + max(.005, min(1., pause_time))) - est_print_time = self.mcu.estimated_print_time(eventtime) - buffer_time = self.print_time - est_print_time + pause_time = max(.005, min(1., pause_time)) + eventtime = self.reactor.pause(eventtime + pause_time) if not self.special_queuing_state: # In main state - defer pause checking until needed self.need_check_pause = est_print_time + BUFFER_TIME_HIGH From 7f177aad1a1a8687737d98c0eb647528e0bdbf49 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 Oct 2025 17:49:06 -0400 Subject: [PATCH 105/117] toolhead: Minor code movement Move flushing and priming code together. No code changes - only code movement. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index aa31dbc8e..192793463 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -303,6 +303,13 @@ class ToolHead: self.check_stall_time = 0. if is_runout and prev_print_time != self.print_time: self.check_stall_time = self.print_time + def _handle_step_flush(self, flush_time, step_gen_time): + if self.special_queuing_state: + return + # In "main" state - flush lookahead if buffer runs low + kin_flush_delay = self.motion_queuing.get_kin_flush_delay() + if step_gen_time >= self.print_time - kin_flush_delay - 0.001: + self._flush_lookahead(is_runout=True) def flush_step_generation(self): self._flush_lookahead() self.motion_queuing.flush_all_steps() @@ -313,6 +320,16 @@ class ToolHead: else: self._process_lookahead() return self.print_time + def _priming_handler(self, eventtime): + self.reactor.unregister_timer(self.priming_timer) + self.priming_timer = None + try: + if self.special_queuing_state == "Priming": + self._flush_lookahead(is_runout=True) + except: + logging.exception("Exception in priming_handler") + self.printer.invoke_shutdown("Exception in priming_handler") + return self.reactor.NEVER def _check_priming_state(self, eventtime): est_print_time = self.mcu.estimated_print_time(eventtime) if self.check_stall_time: @@ -349,23 +366,6 @@ class ToolHead: if not self.special_queuing_state: # In main state - defer pause checking until needed self.need_check_pause = est_print_time + BUFFER_TIME_HIGH - def _priming_handler(self, eventtime): - self.reactor.unregister_timer(self.priming_timer) - self.priming_timer = None - try: - if self.special_queuing_state == "Priming": - self._flush_lookahead(is_runout=True) - except: - logging.exception("Exception in priming_handler") - self.printer.invoke_shutdown("Exception in priming_handler") - return self.reactor.NEVER - def _handle_step_flush(self, flush_time, step_gen_time): - if self.special_queuing_state: - return - # In "main" state - flush lookahead if buffer runs low - kin_flush_delay = self.motion_queuing.get_kin_flush_delay() - if step_gen_time >= self.print_time - kin_flush_delay - 0.001: - self._flush_lookahead(is_runout=True) # Movement commands def get_position(self): return list(self.commanded_pos) From b85b92fdfb8496123671eccc696c856e3ca18b39 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 Oct 2025 17:53:09 -0400 Subject: [PATCH 106/117] toolhead: Don't enter "Priming" state on a dwell() After a toolhead dwell, there is no reason to enter the priming state and to create the priming exiration timer. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 192793463..996c34a37 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -331,6 +331,9 @@ class ToolHead: self.printer.invoke_shutdown("Exception in priming_handler") return self.reactor.NEVER def _check_priming_state(self, eventtime): + if self.lookahead.is_empty(): + # In "NeedPrime" state and can remain there + return est_print_time = self.mcu.estimated_print_time(eventtime) if self.check_stall_time: # Was in "NeedPrime" state and got there from idle input From cba7a285e4693fbca736f76df9ff407892676cdf Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 15:45:00 -0400 Subject: [PATCH 107/117] trapq: Extend trapq_extract_old() to also check active moves Support extracting moves from both the "live" trapq->moves as well as the "history" from trapq->history storage. Now that moves are flushed separately from the lookahead queue, there is a good chance that the current move being processed on the mcus will still be in the active trapq list. Signed-off-by: Kevin O'Connor --- klippy/chelper/list.h | 5 +++++ klippy/chelper/trapq.c | 36 ++++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/klippy/chelper/list.h b/klippy/chelper/list.h index 12fe2b038..f38fca6cc 100644 --- a/klippy/chelper/list.h +++ b/klippy/chelper/list.h @@ -116,6 +116,11 @@ list_join_tail(struct list_head *add, struct list_head *h) ; &pos->member != &(head)->root \ ; pos = list_next_entry(pos, member)) +#define list_for_each_entry_reverse(pos, head, member) \ + for (pos = list_last_entry((head), typeof(*pos), member) \ + ; &pos->member != &(head)->root \ + ; pos = list_prev_entry(pos, member)) + #define list_for_each_entry_safe(pos, n, head, member) \ for (pos = list_first_entry((head), typeof(*pos), member) \ , n = list_next_entry(pos, member) \ diff --git a/klippy/chelper/trapq.c b/klippy/chelper/trapq.c index c238a3818..a2a5f37f3 100644 --- a/klippy/chelper/trapq.c +++ b/klippy/chelper/trapq.c @@ -228,6 +228,22 @@ trapq_set_position(struct trapq *tq, double print_time list_add_head(&m->node, &tq->history); } +// Copy the info in a 'struct move' to a 'struct pull_move' +static void +copy_pull_move(struct pull_move *p, struct move *m) +{ + p->print_time = m->print_time; + p->move_t = m->move_t; + p->start_v = m->start_v; + p->accel = 2. * m->half_accel; + p->start_x = m->start_pos.x; + p->start_y = m->start_pos.y; + p->start_z = m->start_pos.z; + p->x_r = m->axes_r.x; + p->y_r = m->axes_r.y; + p->z_r = m->axes_r.z; +} + // Return history of movement queue int __visible trapq_extract_old(struct trapq *tq, struct pull_move *p, int max @@ -235,21 +251,21 @@ trapq_extract_old(struct trapq *tq, struct pull_move *p, int max { int res = 0; struct move *m; + list_for_each_entry_reverse(m, &tq->moves, node) { + if (start_time >= m->print_time + m->move_t || res >= max) + break; + if (end_time <= m->print_time || (!m->start_v && !m->half_accel)) + continue; + copy_pull_move(p, m); + p++; + res++; + } list_for_each_entry(m, &tq->history, node) { if (start_time >= m->print_time + m->move_t || res >= max) break; if (end_time <= m->print_time) continue; - p->print_time = m->print_time; - p->move_t = m->move_t; - p->start_v = m->start_v; - p->accel = 2. * m->half_accel; - p->start_x = m->start_pos.x; - p->start_y = m->start_pos.y; - p->start_z = m->start_pos.z; - p->x_r = m->axes_r.x; - p->y_r = m->axes_r.y; - p->z_r = m->axes_r.z; + copy_pull_move(p, m); p++; res++; } From 6269dda56bb7c08263153b18715d08e2d964bcab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 6 Oct 2025 13:32:01 -0400 Subject: [PATCH 108/117] sensor_lis2dw: Fix fifo_empty check on lis2dw chips Fix inverted check for fifo empty. The fifo is empty when the number of entries in the fifo is zero. Signed-off-by: Kevin O'Connor --- src/sensor_lis2dw.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sensor_lis2dw.c b/src/sensor_lis2dw.c index bf4beba1d..26452a0ad 100644 --- a/src/sensor_lis2dw.c +++ b/src/sensor_lis2dw.c @@ -141,7 +141,7 @@ lis2dw_query(struct lis2dw *ax, uint8_t oid) if (ax->model == LIS3DH) fifo_empty = fifo[1] & 0x20; else - fifo_empty = fifo[1] & 0x3F; + fifo_empty = ((fifo[1] & 0x3F) == 0); fifo_ovrn = fifo[1] & 0x40; @@ -167,7 +167,7 @@ lis2dw_query(struct lis2dw *ax, uint8_t oid) if (ax->model == LIS3DH) fifo_empty = fifo[0] & 0x20; else - fifo_empty = fifo[0] & 0x3F; + fifo_empty = ((fifo[0] & 0x3F) == 0); fifo_ovrn = fifo[0] & 0x40; From 4415b988c1b719ef3c56bb338d696310f849a121 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 9 Oct 2025 01:14:53 -0400 Subject: [PATCH 109/117] toolhead: Clarify priming timer scheduling Make sure each command gets an additional 100ms before flushing via the priming timer. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 996c34a37..895014cdb 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -194,6 +194,7 @@ class LookAheadQueue: BUFFER_TIME_HIGH = 2.0 BUFFER_TIME_START = 0.250 +PRIMING_CMD_TIME = 0.100 # Main code to track events (and their timing) on the printer toolhead class ToolHead: @@ -346,8 +347,8 @@ class ToolHead: if self.priming_timer is None: self.priming_timer = self.reactor.register_timer( self._priming_handler) - buffer_time = self.print_time - est_print_time - wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_HIGH) + will_pause_time = self.print_time - est_print_time - BUFFER_TIME_HIGH + wtime = eventtime + max(0., will_pause_time) + PRIMING_CMD_TIME self.reactor.update_timer(self.priming_timer, wtime) def _check_pause(self): eventtime = self.reactor.monotonic() @@ -357,8 +358,7 @@ class ToolHead: # Check if there are lots of queued moves and pause if so while 1: est_print_time = self.mcu.estimated_print_time(eventtime) - buffer_time = self.print_time - est_print_time - pause_time = buffer_time - BUFFER_TIME_HIGH + pause_time = self.print_time - est_print_time - BUFFER_TIME_HIGH if pause_time <= 0.: break if not self.can_pause: From 50cb362234f277a4923f1d59d21473d1e0317f62 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 23:15:09 -0400 Subject: [PATCH 110/117] toolhead: Make sure to periodically yield to other tasks when buffering moves Normally the toolhead code will flush the lookahead buffer every ~250ms and will briefly pause to avoid buffering too much data. That pause allows other tasks to run. Make sure to periodically yield to other tasks on each lookahead buffer flush even if a delay isn't needed. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 895014cdb..9163e82cc 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -356,6 +356,7 @@ class ToolHead: # In "NeedPrime"/"Priming" state - update priming expiration timer self._check_priming_state(eventtime) # Check if there are lots of queued moves and pause if so + did_pause = False while 1: est_print_time = self.mcu.estimated_print_time(eventtime) pause_time = self.print_time - est_print_time - BUFFER_TIME_HIGH @@ -366,9 +367,13 @@ class ToolHead: return pause_time = max(.005, min(1., pause_time)) eventtime = self.reactor.pause(eventtime + pause_time) + did_pause = True if not self.special_queuing_state: - # In main state - defer pause checking until needed - self.need_check_pause = est_print_time + BUFFER_TIME_HIGH + # In main state - defer pause checking + self.need_check_pause = self.print_time + if not did_pause: + # May be falling behind - yield to avoid starving other tasks + self.reactor.pause(self.reactor.NOW) # Movement commands def get_position(self): return list(self.commanded_pos) From 16fc46fe5ff0dbbc5188ee6a7829eee5976c1eb9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 19:32:49 -0400 Subject: [PATCH 111/117] toolhead: Reduce LOOKAHEAD_FLUSH_TIME to 0.150 seconds The current code is likely to perform a lazy flush of the lookahead queue around 4 times a second. Increase that to around 6-7 times a second. This change may slightly improve the responsiveness to user requests mid-print (eg, changing extrusion ratio) and may make a "print stall" less likely in some corner cases. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 9163e82cc..da297e966 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -113,7 +113,7 @@ class Move: self.cruise_t = cruise_d / cruise_v self.decel_t = decel_d / ((end_v + cruise_v) * 0.5) -LOOKAHEAD_FLUSH_TIME = 0.250 +LOOKAHEAD_FLUSH_TIME = 0.150 # Class to track a list of pending move requests and to facilitate # "look-ahead" across moves to reduce acceleration between moves. From 8de426d2446345ad38e3a19ad04e5572525707ff Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 7 Sep 2025 00:08:24 -0400 Subject: [PATCH 112/117] toolhead: Reduce target buffer time to 1 second from 2 seconds During normal printing the host software would attempt to stay ahead of the micro-controller by 2 full seconds. Change that time to 1 second. This should make the software more responsive to user requests (such as pause requests). Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 4 ++++ klippy/toolhead.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 3e7f0daf7..cb43d9564 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,10 @@ All dates in this document are approximate. ## Changes +20251010: During normal printing the command processing will now +attempt to stay one second ahead of printer movement (reduced from two +seconds previously). + 20251003: Support for the undocumented `max_stepper_error` option in the `[printer]` config section has been removed. diff --git a/klippy/toolhead.py b/klippy/toolhead.py index da297e966..d7ce66b5c 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -192,7 +192,7 @@ class LookAheadQueue: # Check if enough moves have been queued to reach the target flush time. return self.junction_flush <= 0. -BUFFER_TIME_HIGH = 2.0 +BUFFER_TIME_HIGH = 1.0 BUFFER_TIME_START = 0.250 PRIMING_CMD_TIME = 0.100 From 84e9a281416216bbef6cc200c77eea4ace5baf71 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 22:02:08 -0400 Subject: [PATCH 113/117] virtual_sdcard: Reduce pause time on busy detection If there are other users of the gcode mutex then pause for 50ms (instead of 100ms). Signed-off-by: Kevin O'Connor --- klippy/extras/virtual_sdcard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/virtual_sdcard.py b/klippy/extras/virtual_sdcard.py index 6dc49e2f5..b17004562 100644 --- a/klippy/extras/virtual_sdcard.py +++ b/klippy/extras/virtual_sdcard.py @@ -259,7 +259,7 @@ class VirtualSD: continue # Pause if any other request is pending in the gcode class if gcode_mutex.test(): - self.reactor.pause(self.reactor.monotonic() + 0.100) + self.reactor.pause(self.reactor.monotonic() + 0.050) continue # Dispatch command self.cmd_from_sd = True From 9117c090376533848f820ee5b76f7fcd07cfb0ab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 9 Oct 2025 17:56:25 -0400 Subject: [PATCH 114/117] graphstats: Set MAXBUFFER=1 Signed-off-by: Kevin O'Connor --- scripts/graphstats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/graphstats.py b/scripts/graphstats.py index 5cd2ad34f..c49223705 100755 --- a/scripts/graphstats.py +++ b/scripts/graphstats.py @@ -1,14 +1,14 @@ #!/usr/bin/env python # Script to parse a logging file, extract the stats, and graph them # -# Copyright (C) 2016-2021 Kevin O'Connor +# Copyright (C) 2016-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import optparse, datetime import matplotlib MAXBANDWIDTH=25000. -MAXBUFFER=2. +MAXBUFFER=1. STATS_INTERVAL=5. TASK_MAX=0.0025 From a5c764bbe93f5ffcffa8067d706dec4304afbdf7 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Sun, 12 Oct 2025 18:02:34 +0100 Subject: [PATCH 115/117] docs: Command parameter fix Signed-off-by: Pedro Lamas --- docs/G-Codes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/G-Codes.md b/docs/G-Codes.md index caad39bec..eb38e0134 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -493,7 +493,7 @@ enabled. `SET_FAN_SPEED FAN=config_name SPEED=` This command sets the speed of a fan. "speed" must be between 0.0 and 1.0. -`SET_FAN_SPEED PIN=config_name TEMPLATE= +`SET_FAN_SPEED FAN=config_name TEMPLATE= [=]`: If `TEMPLATE` is specified then it assigns a [display_template](Config_Reference.md#display_template) to the given fan. For example, if one defined a `[display_template From e3909fb2053657eb44a300d86e5fa7d541cab676 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 11 Oct 2025 01:33:38 -0400 Subject: [PATCH 116/117] manual_stepper: Remove some unused code Signed-off-by: Kevin O'Connor --- klippy/extras/manual_stepper.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 9c775567f..8a8911e18 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -4,7 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import logging -import stepper, chelper +import stepper from . import force_move class ManualStepper: @@ -203,7 +203,6 @@ class ManualStepper: self.motion_queuing.drip_update_time(start_time, end_time, drip_completion) # Clear trapq of any remaining parts of movement - reactor = self.printer.get_reactor() self.motion_queuing.wipe_trapq(self.trapq) self.rail.set_position([self.commanded_pos, 0., 0.]) self.sync_print_time() From b1dd6a73f78c8cdae2b835c7c2af2db297813097 Mon Sep 17 00:00:00 2001 From: JamesH1978 <87171443+JamesH1978@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:53:13 +0100 Subject: [PATCH 117/117] docs: Update FAQ.md - Typo (#7089) Kliper is not Klipper! Signed-off-by: James Hartley --- docs/FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 7c8214b32..dbea920f0 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -106,7 +106,7 @@ Klipper will run on a Raspberry Pi 1, 2 and on the Raspberry Pi Zero1, but these boards don't have enough processing power to run Klipper well. It is common for print stalls to occur on these slower machines when printing (The printer may move faster than Klipper can send -movement commands.) It is not reccomended to run Kliper on these older +movement commands.) It is not reccomended to run Klipper on these older machines. For running on the Beaglebone, see the