From 91b5e8e9425b5063c8a5bab9358d4c45c0e3430c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 20 Aug 2025 16:20:12 -0400 Subject: [PATCH 01/40] manual_stepper: Internally track commanded_pos Commit 9399e738 changed the manual_stepper class to no longer explicitly flush all steps after each move. As a result, calls to self.rail.get_commanded_position() may no longer reflect the last requested position. This discrepancy could result in "internal stepcompress" errors. Change the manual_stepper code to internally track the last requested position and use that when scheduling moves. This allows the manual_stepper code to utilize the standard "lazy" step flushing mechanism. Signed-off-by: Kevin O'Connor --- klippy/extras/manual_stepper.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 05899f58..3d3e614a 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -22,6 +22,7 @@ class ManualStepper: self.velocity = config.getfloat('velocity', 5., above=0.) self.accel = self.homing_accel = config.getfloat('accel', 0., minval=0.) self.next_cmd_time = 0. + self.commanded_pos = 0. self.pos_min = config.getfloat('position_min', None) self.pos_max = config.getfloat('position_max', None) # Setup iterative solver @@ -60,9 +61,12 @@ class ManualStepper: se.motor_disable(self.next_cmd_time) self.sync_print_time() def do_set_position(self, setpos): - self.rail.set_position([setpos, 0., 0.]) + toolhead = self.printer.lookup_object('toolhead') + toolhead.flush_step_generation() + self.commanded_pos = setpos + self.rail.set_position([self.commanded_pos, 0., 0.]) def _submit_move(self, movetime, movepos, speed, accel): - cp = self.rail.get_commanded_position() + cp = self.commanded_pos dist = movepos - cp axis_r, accel_t, cruise_t, cruise_v = force_move.calc_move_time( dist, speed, accel) @@ -70,6 +74,7 @@ class ManualStepper: accel_t, cruise_t, accel_t, cp, 0., 0., axis_r, 0., 0., 0., cruise_v, accel) + self.commanded_pos = movepos return movetime + accel_t + cruise_t + accel_t def do_move(self, movepos, speed, accel, sync=True): self.sync_print_time() @@ -149,7 +154,7 @@ class ManualStepper: self.instant_corner_v = instant_corner_v self.gaxis_limit_velocity = limit_velocity self.gaxis_limit_accel = limit_accel - toolhead.add_extra_axis(self, self.get_position()[0]) + toolhead.add_extra_axis(self, self.commanded_pos) def process_move(self, print_time, move, ea_index): axis_r = move.axes_r[ea_index] start_pos = move.start_pos[ea_index] @@ -161,6 +166,7 @@ class ManualStepper: start_pos, 0., 0., 1., 0., 0., start_v, cruise_v, accel) + self.commanded_pos = move.end_pos[ea_index] def check_move(self, move, ea_index): # Check move is in bounds movepos = move.end_pos[ea_index] @@ -185,9 +191,10 @@ class ManualStepper: return self.trapq # Toolhead wrappers to support homing def flush_step_generation(self): - self.sync_print_time() + toolhead = self.printer.lookup_object('toolhead') + toolhead.flush_step_generation() def get_position(self): - return [self.rail.get_commanded_position(), 0., 0., 0.] + return [self.commanded_pos, 0., 0., 0.] def set_position(self, newpos, homing_axes=""): self.do_set_position(newpos[0]) def get_last_move_time(self): @@ -206,7 +213,7 @@ class ManualStepper: # Clear trapq of any remaining parts of movement reactor = self.printer.get_reactor() self.motion_queuing.wipe_trapq(self.trapq) - self.rail.set_position([newpos[0], 0., 0.]) + self.rail.set_position([self.commanded_pos, 0., 0.]) self.sync_print_time() def get_kinematics(self): return self From 371647109f11d46d67155666ea6c65ad0099642a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 20 Aug 2025 16:34:49 -0400 Subject: [PATCH 02/40] test: Add a long move test to manual_stepper.test Signed-off-by: Kevin O'Connor --- test/klippy/manual_stepper.test | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/klippy/manual_stepper.test b/test/klippy/manual_stepper.test index d19c39c5..6ab45c2d 100644 --- a/test/klippy/manual_stepper.test +++ b/test/klippy/manual_stepper.test @@ -10,6 +10,10 @@ MANUAL_STEPPER STEPPER=basic_stepper MOVE=5 MANUAL_STEPPER STEPPER=basic_stepper MOVE=12 SPEED=12 ACCEL=9000.2 MANUAL_STEPPER STEPPER=basic_stepper ENABLE=0 +# Test long move +MANUAL_STEPPER STEPPER=basic_stepper MOVE=300 SPEED=10 ACCEL=2000 +MANUAL_STEPPER STEPPER=basic_stepper MOVE=100 SPEED=10 ACCEL=2000 + # Test homing move MANUAL_STEPPER STEPPER=homing_stepper ENABLE=1 MANUAL_STEPPER STEPPER=homing_stepper SET_POSITION=0 From 2ddfa32dd84a24802b459410313afc2d66c8d49a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 09:33:52 -0400 Subject: [PATCH 03/40] heaters: Reduce next_pwm_time window Commit 0f94f6c8 decreased the MAX_HEAT_TIME from 5 seconds to 3 seconds. However, that also decreased the amount of tolerance for lost temperature updates from 1.25 seconds to 0.75 seconds. With the default temperature update every 300ms, only 2 consecutive missing temperature updates could lead to a fault. Tweak the internal "next_pwm_time" setting so that it is more tolerant of two consecutive lost temperature updates. Signed-off-by: Kevin O'Connor --- klippy/extras/heaters.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/klippy/extras/heaters.py b/klippy/extras/heaters.py index fce3c49a..fcec44fc 100644 --- a/klippy/extras/heaters.py +++ b/klippy/extras/heaters.py @@ -75,7 +75,8 @@ class Heater: # No significant change in value - can suppress update return pwm_time = read_time + self.pwm_delay - self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME + self.next_pwm_time = (pwm_time + MAX_HEAT_TIME + - (3. * self.pwm_delay + 0.001)) self.last_pwm_value = value self.mcu_pwm.set_pwm(pwm_time, value) #logging.debug("%s: pwm=%.3f@%.3f (from %.3f@%.3f [%.3f])", From 3b68769ea514976243032cc408c1115efbf9e6f1 Mon Sep 17 00:00:00 2001 From: Hendrik Poernama Date: Thu, 21 Aug 2025 09:33:31 -0400 Subject: [PATCH 04/40] tmc2240: Add OTW_OV_VTH to FieldFormatters (#6987) This register is readable and contains the overvoltage and overtemp threshold settings. Signed-off-by: Hendrik Poernama --- klippy/extras/tmc2240.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klippy/extras/tmc2240.py b/klippy/extras/tmc2240.py index 35d2ce83..d57a93b8 100644 --- a/klippy/extras/tmc2240.py +++ b/klippy/extras/tmc2240.py @@ -262,6 +262,9 @@ FieldFormatters.update({ "adc_temp": (lambda v: "0x%04x(%.1fC)" % (v, ((v - 2038) / 7.7))), "adc_vsupply": (lambda v: "0x%04x(%.3fV)" % (v, v * 0.009732)), "adc_ain": (lambda v: "0x%04x(%.3fmV)" % (v, v * 0.3052)), + "overvoltage_vth": (lambda v: "0x%04x(%.3fV)" % (v, v * 0.009732)), + "overtempprewarning_vth": (lambda v: + "0x%04x(%.1fC)" % (v, ((v - 2038) / 7.7))), }) From 7ed7791723c9344dd756074fe10c05479e68372a Mon Sep 17 00:00:00 2001 From: C0co <6054234+Phil1988@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:32:33 +0200 Subject: [PATCH 05/40] spi_flash: Update board_defs.py (#7006) Added X-Smart3, X-Plus3 and X-Max3 mainboards Signed-off-by: Phil Groenewold --- scripts/spi_flash/board_defs.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index 0e82d27b..4fdba64c 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -170,6 +170,14 @@ BOARD_DEFS = { "firmware_path": "ZNP_ROBIN_NANO.bin", "current_firmware_path": "ZNP_ROBIN_NANO.CUR" }, + 'qidi-x6': { + 'mcu': "stm32f401xc", + 'spi_bus': "spi2", + 'cs_pin': "PB12", + 'skip_verify': False, + 'firmware_path': 'X_4.bin', + 'current_firmware_path': 'X_4.CUR' + }, 'qidi-x7': { 'mcu': "stm32f401xc", 'spi_bus': "spi2", @@ -228,6 +236,9 @@ BOARD_ALIASES = { 'robin_v3': BOARD_DEFS['monster8'], 'btt-skrat-v1.0': BOARD_DEFS['btt-skrat'], 'chitu-v6': BOARD_DEFS['chitu-v6'], + 'qidi-x-smart3': BOARD_DEFS['qidi-x6'], + 'qidi-x-plus3': BOARD_DEFS['qidi-x6'], + 'qidi-x-max3': BOARD_DEFS['qidi-x6'], 'qidi-q1-pro': BOARD_DEFS['qidi-x7'], 'qidi-plus4': BOARD_DEFS['qidi-x7'] } From 3a11645afeaf5573a0f244a65c8533a513799395 Mon Sep 17 00:00:00 2001 From: minicx <39405619+loss-and-quick@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:41:07 +0300 Subject: [PATCH 06/40] stm32: Fix N32G45x ADC pin mapping (#7016) Fixes PA0 (GPIO 0) incorrectly mapping to ADC1_IN0 due to collision with placeholder zeros. Signed-off-by: Lev Voronov Co-authored-by: Alexander Simonov --- src/stm32/n32g45x_adc.c | 56 +++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/stm32/n32g45x_adc.c b/src/stm32/n32g45x_adc.c index d27e70c9..52cee2f5 100644 --- a/src/stm32/n32g45x_adc.c +++ b/src/stm32/n32g45x_adc.c @@ -14,6 +14,8 @@ #include "sched.h" // sched_shutdown #include "n32g45x_adc.h" // ADC +#define ADC_INVALID_PIN 0xFF + DECL_CONSTANT("ADC_MAX", 4095); #define ADC_TEMPERATURE_PIN 0xfe @@ -21,42 +23,42 @@ DECL_ENUMERATION("pin", "ADC_TEMPERATURE", ADC_TEMPERATURE_PIN); static const uint8_t adc_pins[] = { // ADC1 - 0, GPIO('A', 0), GPIO('A', 1), GPIO('A', 6), - GPIO('A', 3), GPIO('F', 4), 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - ADC_TEMPERATURE_PIN, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, + ADC_INVALID_PIN, GPIO('A', 0), GPIO('A', 1), GPIO('A', 6), + GPIO('A', 3), GPIO('F', 4), ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_TEMPERATURE_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, // ADC2 - 0, GPIO('A', 4), GPIO('A', 5), GPIO('B', 1), + ADC_INVALID_PIN, GPIO('A', 4), GPIO('A', 5), GPIO('B', 1), GPIO('A', 7), GPIO('C', 4), GPIO('C', 0), GPIO('C', 1), GPIO('C', 2), GPIO('C', 3), GPIO('F', 2), GPIO('A', 2), - GPIO('C', 5), GPIO('B', 2), 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, + GPIO('C', 5), GPIO('B', 2), ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, #if CONFIG_MACH_N32G455 // ADC3/4 for G455 only // ADC3 - 0, GPIO('B', 11), GPIO('E', 9), GPIO('E', 13), + ADC_INVALID_PIN, GPIO('B', 11), GPIO('E', 9), GPIO('E', 13), GPIO('E', 12), GPIO('B', 13), GPIO('E', 8), GPIO('D', 10), GPIO('D', 11), GPIO('D', 12), GPIO('D', 13), GPIO('D', 14), GPIO('B', 0), GPIO('E', 7), GPIO('E', 10), GPIO('E', 11), - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, // ADC4 - 0, GPIO('E', 14), GPIO('E', 15), GPIO('B', 12), - GPIO('B', 14), GPIO('B', 15), 0, 0, - 0, 0, 0, 0, - GPIO('D', 8), GPIO('D', 9), 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, + ADC_INVALID_PIN, GPIO('E', 14), GPIO('E', 15), GPIO('B', 12), + GPIO('B', 14), GPIO('B', 15), ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + GPIO('D', 8), GPIO('D', 9), ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, + ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, ADC_INVALID_PIN, #endif }; From b817848567f3e0fefd3ae3a7db0ed3b2a86cf716 Mon Sep 17 00:00:00 2001 From: minicx Date: Wed, 25 Jun 2025 04:58:52 +0300 Subject: [PATCH 07/40] stm32: enable 64KiB bootloader for n32g45x, clarify Makefile output - Allow selection of 64KiB bootloader offset for MACH_N32G45x in Kconfig Signed-off-by: Lev Voronov Co-authored-by: Alexander Simonov --- src/stm32/Kconfig | 2 +- src/stm32/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index 8a7afc3d..1e0df93d 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -302,7 +302,7 @@ choice config STM32_FLASH_START_C000 bool "48KiB bootloader" if MACH_STM32F4x5 || MACH_STM32F401 config STM32_FLASH_START_10000 - bool "64KiB bootloader" if MACH_STM32F103 || MACH_STM32F4 + bool "64KiB bootloader" if MACH_STM32F103 || MACH_STM32F4 || MACH_N32G45x config STM32_FLASH_START_800 bool "2KiB bootloader" if MACH_STM32F103 diff --git a/src/stm32/Makefile b/src/stm32/Makefile index 0ee71dcf..3c158d51 100644 --- a/src/stm32/Makefile +++ b/src/stm32/Makefile @@ -94,7 +94,7 @@ src-$(CONFIG_HAVE_GPIO_SDIO) += stm32/sdio.c target-y += $(OUT)klipper.bin $(OUT)klipper.bin: $(OUT)klipper.elf - @echo " Creating hex file $@" + @echo " Creating bin file $@" $(Q)$(OBJCOPY) -O binary $< $@ # Flash rules From 9a1ac45d195a08c785b3f931806a117207cea82e Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 13 Aug 2025 18:55:54 +0200 Subject: [PATCH 08/40] sx1509: migrate i2c write to connect phase Signed-off-by: Timofey Titovets --- klippy/extras/sx1509.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index 070b9133..82b41d81 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -29,7 +29,6 @@ class SX1509(object): self._ppins = self._printer.lookup_object("pins") self._ppins.register_chip("sx1509_" + self._name, self) self._mcu = self._i2c.get_mcu() - self._mcu.register_config_callback(self._build_config) self._oid = self._i2c.get_oid() self._last_clock = 0 # Set up registers default values @@ -37,22 +36,22 @@ class SX1509(object): REG_PULLUP : 0, REG_PULLDOWN : 0, REG_INPUT_DISABLE : 0, REG_ANALOG_DRIVER_ENABLE : 0} self.reg_i_on_dict = {reg : 0 for reg in REG_I_ON} - def _build_config(self): + config.get_printer().register_event_handler("klippy:connect", + self.handle_connect) + def handle_connect(self): # Reset the chip, Default RegClock/RegMisc 0x0 - self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % ( - self._oid, REG_RESET, 0x12)) - self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % ( - self._oid, REG_RESET, 0x34)) + self._i2c.i2c_write([REG_RESET, 0x12]) + self._i2c.i2c_write([REG_RESET, 0x34]) # Enable Oscillator - self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % ( - self._oid, REG_CLOCK, (1 << 6))) + self._i2c.i2c_write([REG_CLOCK, (1 << 6)]) # Setup Clock Divider - self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % ( - self._oid, REG_MISC, (1 << 4))) + self._i2c.i2c_write([REG_MISC, (1 << 4)]) # Transfer all regs with their initial cached state - for _reg, _data in self.reg_dict.items(): - self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%04x" % ( - self._oid, _reg, _data), is_init=True) + reactor = self._mcu.get_printer().get_reactor() + for _reg in self.reg_dict: + curtime = reactor.monotonic() + printtime = self._mcu.estimated_print_time(curtime) + self.send_register(_reg, printtime) def setup_pin(self, pin_type, pin_params): if pin_type == 'digital_out' and pin_params['pin'][0:4] == "PIN_": return SX1509_digital_out(self, pin_params) From 1965298ab08f51bad4b06a5336fc8ff8d074d3cd Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 13 Aug 2025 19:18:37 +0200 Subject: [PATCH 09/40] sx1509: init pwm pin on connect Signed-off-by: Timofey Titovets --- klippy/extras/sx1509.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index 82b41d81..fd36c7fe 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -52,6 +52,10 @@ class SX1509(object): curtime = reactor.monotonic() printtime = self._mcu.estimated_print_time(curtime) self.send_register(_reg, printtime) + for reg in self.reg_i_on_dict: + curtime = reactor.monotonic() + printtime = self._mcu.estimated_print_time(curtime) + self.send_register(reg, printtime) def setup_pin(self, pin_type, pin_params): if pin_type == 'digital_out' and pin_params['pin'][0:4] == "PIN_": return SX1509_digital_out(self, pin_params) @@ -158,15 +162,6 @@ class SX1509_pwm(object): raise pins.error("SX1509_pwm must have hardware_pwm enabled") if self._max_duration: raise pins.error("SX1509 pins are not suitable for heaters") - # Send initial value - self._sx1509.set_register(self._i_on_reg, - ~int(255 * self._start_value) & 0xFF) - self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % ( - self._sx1509.get_oid(), - self._i_on_reg, - self._sx1509.reg_i_on_dict[self._i_on_reg] - ), - is_init=True) def get_mcu(self): return self._mcu def setup_max_duration(self, max_duration): @@ -180,6 +175,8 @@ class SX1509_pwm(object): shutdown_value = 1. - shutdown_value self._start_value = max(0., min(1., start_value)) self._shutdown_value = max(0., min(1., shutdown_value)) + self._sx1509.set_register(self._i_on_reg, + ~int(255 * self._start_value) & 0xFF) def set_pwm(self, print_time, value): self._sx1509.set_register(self._i_on_reg, ~int(255 * value) if not self._invert From eec81683ebc859f3d2b2accdf3c03ddd1701d8c2 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 13 Aug 2025 18:59:45 +0200 Subject: [PATCH 10/40] bus: move early i2c writes to the connect phase Signed-off-by: Timofey Titovets --- klippy/extras/bus.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 4121c1c8..4490efe4 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -180,6 +180,13 @@ class MCU_I2C: self.cmd_queue = self.mcu.alloc_command_queue() self.mcu.register_config_callback(self.build_config) self.i2c_write_cmd = self.i2c_read_cmd = None + printer = self.mcu.get_printer() + printer.register_event_handler("klippy:connect", self._handle_connect) + # backward support i2c_write inside the init section + self._to_write = [] + def _handle_connect(self): + for data in self._to_write: + self.i2c_write(data) def get_oid(self): return self.oid def get_mcu(self): @@ -207,10 +214,7 @@ class MCU_I2C: cq=self.cmd_queue) def i2c_write(self, data, minclock=0, reqclock=0): if self.i2c_write_cmd is None: - # Send setup message via mcu initialization - data_msg = "".join(["%02x" % (x,) for x in data]) - self.mcu.add_config_cmd("i2c_write oid=%d data=%s" % ( - self.oid, data_msg), is_init=True) + self._to_write.append(data) return self.i2c_write_cmd.send([self.oid, data], minclock=minclock, reqclock=reqclock) From ae010215e7a37469e88b77fab5bd585b981567a4 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 16 Aug 2025 11:29:29 -0400 Subject: [PATCH 11/40] chelper: Build library first in temporary file and then rename Try to avoid cases where an incomplete library build causes confusing future failures. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 6ed6ed5b..60ba91e7 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -274,6 +274,28 @@ def do_build_code(cmd): logging.error(msg) raise Exception(msg) +# Build the main c_helper.so c code library +def check_build_c_library(): + srcdir = os.path.dirname(os.path.realpath(__file__)) + srcfiles = get_abs_files(srcdir, SOURCE_FILES) + ofiles = get_abs_files(srcdir, OTHER_FILES) + destlib = get_abs_files(srcdir, [DEST_LIB])[0] + if not check_build_code(srcfiles+ofiles+[__file__], destlib): + # Code already built + return destlib + # Select command line options + if check_gcc_option(SSE_FLAGS): + cmd = "%s %s %s" % (GCC_CMD, SSE_FLAGS, COMPILE_ARGS) + else: + cmd = "%s %s" % (GCC_CMD, COMPILE_ARGS) + # Invoke compiler + logging.info("Building C code module %s", DEST_LIB) + tempdestlib = get_abs_files(srcdir, ["_temp_" + DEST_LIB])[0] + do_build_code(cmd % (tempdestlib, ' '.join(srcfiles))) + # Rename from temporary file to final file name + os.rename(tempdestlib, destlib) + return destlib + FFI_main = None FFI_lib = None pyhelper_logging_callback = None @@ -286,17 +308,9 @@ def logging_callback(msg): def get_ffi(): global FFI_main, FFI_lib, pyhelper_logging_callback if FFI_lib is None: - srcdir = os.path.dirname(os.path.realpath(__file__)) - srcfiles = get_abs_files(srcdir, SOURCE_FILES) - ofiles = get_abs_files(srcdir, OTHER_FILES) - destlib = get_abs_files(srcdir, [DEST_LIB])[0] - if check_build_code(srcfiles+ofiles+[__file__], destlib): - if check_gcc_option(SSE_FLAGS): - cmd = "%s %s %s" % (GCC_CMD, SSE_FLAGS, COMPILE_ARGS) - else: - cmd = "%s %s" % (GCC_CMD, COMPILE_ARGS) - logging.info("Building C code module %s", DEST_LIB) - do_build_code(cmd % (destlib, ' '.join(srcfiles))) + # Check if library needs to be built, and build if so + destlib = check_build_c_library() + # Open library FFI_main = cffi.FFI() for d in defs_all: FFI_main.cdef(d) From fe44dd8baad8d41ac6ff39e91ae806dbeeaa7c9f Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 21 Aug 2025 19:36:56 +0200 Subject: [PATCH 12/40] bus: make i2c_write syncronous When we introduce the host-side status check, it will be synchronous. There would be no sense in having an asynchronous call. Preliminary migrate callers to synchronous call. Signed-off-by: Timofey Titovets --- klippy/extras/bus.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 4490efe4..66b8330e 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -182,6 +182,7 @@ class MCU_I2C: self.i2c_write_cmd = self.i2c_read_cmd = None printer = self.mcu.get_printer() printer.register_event_handler("klippy:connect", self._handle_connect) + self._debugoutput = printer.get_start_args().get('debugoutput') # backward support i2c_write inside the init section self._to_write = [] def _handle_connect(self): @@ -216,8 +217,12 @@ class MCU_I2C: 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) + if self._debugoutput is not None: + self.i2c_write_cmd.send([self.oid, data], + minclock=minclock, reqclock=reqclock) + return + self.i2c_write_cmd.send_wait_ack([self.oid, data], + minclock=minclock, reqclock=reqclock) def i2c_write_wait_ack(self, data, minclock=0, reqclock=0): self.i2c_write_cmd.send_wait_ack([self.oid, data], minclock=minclock, reqclock=reqclock) From eb7bdf18ade9fd826e69f7c8cc49e02b23879368 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 21 Aug 2025 19:42:29 +0200 Subject: [PATCH 13/40] bme280: drop obsolete i2c_write_wait_ack Signed-off-by: Timofey Titovets --- klippy/extras/bme280.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/klippy/extras/bme280.py b/klippy/extras/bme280.py index 1c26bbee..284380d2 100644 --- a/klippy/extras/bme280.py +++ b/klippy/extras/bme280.py @@ -284,7 +284,7 @@ class BME280: self.chip_type, self.i2c.i2c_address)) # Reset chip - self.write_register('RESET', [RESET_CHIP_VALUE], wait=True) + self.write_register('RESET', [RESET_CHIP_VALUE]) self.reactor.pause(self.reactor.monotonic() + .5) # Make sure non-volatile memory has been copied to registers @@ -394,7 +394,7 @@ class BME280: self.write_register('CTRL_HUM', self.os_hum) # Enter normal (periodic) mode meas = self.os_temp << 5 | self.os_pres << 2 | MODE_PERIODIC - self.write_register('CTRL_MEAS', meas, wait=True) + self.write_register('CTRL_MEAS', meas) if self.chip_type == 'BME680': self.write_register('CONFIG', self.iir_filter << 2) @@ -528,7 +528,7 @@ class BME280: # Enter forced mode meas = self.os_temp << 5 | self.os_pres << 2 | MODE - self.write_register('CTRL_MEAS', meas, wait=True) + self.write_register('CTRL_MEAS', meas) max_sample_time = self.max_sample_time if run_gas: max_sample_time += self.gas_heat_duration / 1000 @@ -776,15 +776,12 @@ class BME280: params = self.i2c.i2c_read(regs, read_len) return bytearray(params['response']) - def write_register(self, reg_name, data, wait = False): + def write_register(self, reg_name, data): if type(data) is not list: data = [data] reg = self.chip_registers[reg_name] data.insert(0, reg) - if not wait: - self.i2c.i2c_write(data) - else: - self.i2c.i2c_write_wait_ack(data) + self.i2c.i2c_write(data) def get_status(self, eventtime): data = { From 718be7c6a3ce2de11fb884fef4d113c45db94d02 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 21 Aug 2025 19:43:27 +0200 Subject: [PATCH 14/40] sht3x: drop obsolete i2c_write_wait_ack Signed-off-by: Timofey Titovets --- klippy/extras/sht3x.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/klippy/extras/sht3x.py b/klippy/extras/sht3x.py index c76ceb76..c4fb2296 100644 --- a/klippy/extras/sht3x.py +++ b/klippy/extras/sht3x.py @@ -80,10 +80,10 @@ class SHT3X: def _init_sht3x(self): # Device Soft Reset - self.i2c.i2c_write_wait_ack(SHT3X_CMD['OTHER']['BREAK']) + self.i2c.i2c_write(SHT3X_CMD['OTHER']['BREAK']) # Break takes ~ 1ms self.reactor.pause(self.reactor.monotonic() + .0015) - self.i2c.i2c_write_wait_ack(SHT3X_CMD['OTHER']['SOFTRESET']) + self.i2c.i2c_write(SHT3X_CMD['OTHER']['SOFTRESET']) # Wait <=1.5ms after reset self.reactor.pause(self.reactor.monotonic() + .0015) @@ -97,7 +97,7 @@ class SHT3X: logging.warning("sht3x: Reading status - checksum error!") # Enable periodic mode - self.i2c.i2c_write_wait_ack( + self.i2c.i2c_write( SHT3X_CMD['PERIODIC']['2HZ']['HIGH_REP'] ) # Wait <=15.5ms for first measurement From 159b71e51ed64d8ac6a64180ddbbfa48d32c9c30 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 21 Aug 2025 19:43:56 +0200 Subject: [PATCH 15/40] bus: drop obsolete i2c_write_wait_ack Signed-off-by: Timofey Titovets --- klippy/extras/bus.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 66b8330e..033e9341 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -223,9 +223,6 @@ class MCU_I2C: return self.i2c_write_cmd.send_wait_ack([self.oid, data], minclock=minclock, reqclock=reqclock) - def i2c_write_wait_ack(self, data, minclock=0, reqclock=0): - self.i2c_write_cmd.send_wait_ack([self.oid, data], - minclock=minclock, reqclock=reqclock) def i2c_read(self, write, read_len, retry=True): return self.i2c_read_cmd.send([self.oid, write, read_len], retry) From 3aadda6fb35514b952cdafb16c248f4ccbcfe235 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 22 Aug 2025 16:53:34 -0400 Subject: [PATCH 16/40] mcu: Disable waiting in send_wait_ack() if in debugging mode Signed-off-by: Kevin O'Connor --- klippy/extras/bus.py | 5 ----- klippy/mcu.py | 8 ++++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 033e9341..9fb46639 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -182,7 +182,6 @@ class MCU_I2C: self.i2c_write_cmd = self.i2c_read_cmd = None printer = self.mcu.get_printer() printer.register_event_handler("klippy:connect", self._handle_connect) - self._debugoutput = printer.get_start_args().get('debugoutput') # backward support i2c_write inside the init section self._to_write = [] def _handle_connect(self): @@ -217,10 +216,6 @@ class MCU_I2C: if self.i2c_write_cmd is None: self._to_write.append(data) return - if self._debugoutput is not None: - self.i2c_write_cmd.send([self.oid, data], - minclock=minclock, reqclock=reqclock) - return self.i2c_write_cmd.send_wait_ack([self.oid, data], minclock=minclock, reqclock=reqclock) def i2c_read(self, write, read_len, retry=True): diff --git a/klippy/mcu.py b/klippy/mcu.py index db02e2a4..146b5bac 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -84,7 +84,7 @@ class CommandQueryWrapper: # Wrapper around command sending class CommandWrapper: - def __init__(self, serial, msgformat, cmd_queue=None): + def __init__(self, serial, msgformat, cmd_queue=None, debugoutput=False): self._serial = serial msgparser = serial.get_msgparser() self._cmd = msgparser.lookup_command(msgformat) @@ -92,6 +92,9 @@ class CommandWrapper: cmd_queue = serial.get_default_command_queue() self._cmd_queue = cmd_queue self._msgtag = msgparser.lookup_msgid(msgformat) & 0xffffffff + if debugoutput: + # Can't use send_wait_ack when in debugging mode + self.send_wait_ack = self.send def send(self, data=(), minclock=0, reqclock=0): cmd = self._cmd.encode(data) self._serial.raw_send(cmd, minclock, reqclock, self._cmd_queue) @@ -888,7 +891,8 @@ class MCU: def alloc_command_queue(self): return self._serial.alloc_command_queue() def lookup_command(self, msgformat, cq=None): - return CommandWrapper(self._serial, msgformat, cq) + return CommandWrapper(self._serial, msgformat, cq, + debugoutput=self.is_fileoutput()) def lookup_query_command(self, msgformat, respformat, oid=None, cq=None, is_async=False): return CommandQueryWrapper(self._serial, msgformat, respformat, oid, From 179a56ce9206b5e6e37ea39306f8652829fd4e1a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 31 Aug 2025 12:17:37 -0400 Subject: [PATCH 17/40] 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 94a0ce42..655b8108 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 6ab45c2d..12726411 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 18/40] 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 417fb1d4..7c8214b3 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 19/40] 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 c480ddae..aebb1076 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 20/40] 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 21aeca3d..0b66e818 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 21/40] 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 60c6de9b..8d4e4ee8 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 22/40] 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 00f835f5..3947292b 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 3d3e614a..a2ce57da 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 2bad7555..cd3f4f73 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 9f5ea0b9..0316ee72 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 23/40] 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 a2ce57da..3c1b29b6 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 cd3f4f73..926e9592 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 24/40] 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 a06b556e..49fafd6b 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 0b66e818..f68d4972 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 25/40] 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 f68d4972..2f9909f5 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 26/40] 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 2f9909f5..167e43c0 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 27/40] 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 167e43c0..e9746b54 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 28/40] 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 e9746b54..dbafdb92 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 29/40] 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 dbafdb92..cf8ee583 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 30/40] 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 3c1b29b6..f57bbbbb 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 cf8ee583..62756dfd 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 31/40] 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 62756dfd..bd9f67da 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 32/40] 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 3947292b..277c68e3 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 f57bbbbb..9c775567 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 49fafd6b..226aa9f2 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 63862d97..a5129299 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 6d401c0b..d9e72c5e 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 bd9f67da..3eadbb3d 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 33/40] 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 60ba91e7..59971c1c 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 eba1deef..9b120624 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 e2e46ebe..50a30f7d 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 42d572d0..d5138ff0 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 2889570d..52dd4077 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 e21c4fd9..7ca0f2e4 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 67a287cd..cb9027d9 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 226aa9f2..0b0981f1 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 a89e3bdf..4e6f14e4 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 3eadbb3d..877e4f34 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 34/40] 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 0b0981f1..9d2ef513 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 877e4f34..cf648695 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 35/40] 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 4ac5289f..28293a3c 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 36/40] 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 d7123d08..7384617e 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 37/40] 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 83de9609..ca21bcf2 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 38/40] 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 412d53ed..f9bedcf3 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 39/40] 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 e9ba31d6..cc33bc15 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 40/40] 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 975da792..1c50695d 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: