From f54b7b9376ac86d5073e1df875c199184dfafb23 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 11 Jun 2025 17:59:02 -0400 Subject: [PATCH 001/411] sos_filter: Fix validate_section_index() check A section_idx equal to max_sections is also invalid. Signed-off-by: Kevin O'Connor --- src/sos_filter.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sos_filter.c b/src/sos_filter.c index 3ec5d6178..04eb366eb 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -108,7 +108,7 @@ sos_filter_oid_lookup(uint8_t oid) static void validate_section_index(struct sos_filter *sf, uint8_t section_idx) { - if (section_idx > sf->max_sections) + if (section_idx >= sf->max_sections) shutdown("Filter section index larger than max_sections"); } @@ -154,7 +154,8 @@ command_sos_filter_activate(uint32_t *args) { struct sos_filter *sf = sos_filter_oid_lookup(args[0]); uint8_t n_sections = args[1]; - validate_section_index(sf, n_sections); + if (n_sections > sf->max_sections) + shutdown("Filter section index larger than max_sections"); sf->n_sections = n_sections; const uint8_t coeff_int_bits = args[2]; sf->coeff_frac_bits = (31 - coeff_int_bits); From 0e52f03b5bdca0f9003ec1a1360ccbfbbb846a01 Mon Sep 17 00:00:00 2001 From: jimmyjon711 Date: Wed, 18 Jun 2025 08:05:17 -0700 Subject: [PATCH 002/411] stm32: Adding more hardware pwm capable pins for STM32Hx series chips (#6965) Signed-off-by: Jim Madill --- src/stm32/hard_pwm.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index d9be268ee..6ed27a305 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -230,6 +230,14 @@ static const struct gpio_pwm_info pwm_regs[] = { {TIM15, GPIO('F', 12), 1, GPIO_FUNCTION(0)}, {TIM15, GPIO('F', 13), 2, GPIO_FUNCTION(0)}, #elif CONFIG_MACH_STM32H7 + {TIM1, GPIO('A', 8), 1, GPIO_FUNCTION(1)}, + {TIM1, GPIO('E', 9), 1, GPIO_FUNCTION(1)}, + {TIM1, GPIO('A', 9), 2, GPIO_FUNCTION(1)}, + {TIM1, GPIO('E', 11), 2, GPIO_FUNCTION(1)}, + {TIM1, GPIO('A', 10), 3, GPIO_FUNCTION(1)}, + {TIM1, GPIO('E', 13), 3, GPIO_FUNCTION(1)}, + {TIM1, GPIO('A', 11), 4, GPIO_FUNCTION(1)}, + {TIM1, GPIO('E', 14), 4, GPIO_FUNCTION(1)}, {TIM2, GPIO('A', 0), 1, GPIO_FUNCTION(1)}, {TIM2, GPIO('A', 5), 1, GPIO_FUNCTION(1)}, {TIM2, GPIO('A', 15), 1, GPIO_FUNCTION(1)}, @@ -237,6 +245,8 @@ static const struct gpio_pwm_info pwm_regs[] = { {TIM2, GPIO('A', 1), 2, GPIO_FUNCTION(1)}, {TIM2, GPIO('B', 10), 3, GPIO_FUNCTION(1)}, {TIM2, GPIO('A', 2), 3, GPIO_FUNCTION(1)}, + {TIM2, GPIO('A', 3), 4, GPIO_FUNCTION(1)}, + {TIM2, GPIO('B', 11), 4, GPIO_FUNCTION(1)}, {TIM3, GPIO('C', 6), 1, GPIO_FUNCTION(2)}, {TIM3, GPIO('B', 4), 1, GPIO_FUNCTION(2)}, {TIM3, GPIO('A', 6), 1, GPIO_FUNCTION(2)}, @@ -245,18 +255,27 @@ static const struct gpio_pwm_info pwm_regs[] = { {TIM3, GPIO('A', 7), 2, GPIO_FUNCTION(2)}, {TIM3, GPIO('C', 8), 3, GPIO_FUNCTION(2)}, {TIM3, GPIO('B', 0), 3, GPIO_FUNCTION(2)}, + {TIM3, GPIO('B', 1), 4, GPIO_FUNCTION(2)}, + {TIM3, GPIO('C', 9), 4, GPIO_FUNCTION(2)}, {TIM4, GPIO('D', 12), 1, GPIO_FUNCTION(2)}, {TIM4, GPIO('B', 6), 1, GPIO_FUNCTION(2)}, {TIM4, GPIO('D', 13), 2, GPIO_FUNCTION(2)}, {TIM4, GPIO('B', 7), 2, GPIO_FUNCTION(2)}, {TIM4, GPIO('D', 14), 3, GPIO_FUNCTION(2)}, {TIM4, GPIO('B', 8), 3, GPIO_FUNCTION(2)}, + {TIM4, GPIO('B', 9), 4, GPIO_FUNCTION(2)}, + {TIM4, GPIO('D', 15), 4, GPIO_FUNCTION(2)}, {TIM5, GPIO('H', 10), 1, GPIO_FUNCTION(2)}, {TIM5, GPIO('A', 0), 1, GPIO_FUNCTION(2)}, {TIM5, GPIO('H', 11), 2, GPIO_FUNCTION(2)}, {TIM5, GPIO('A', 1), 2, GPIO_FUNCTION(2)}, {TIM5, GPIO('H', 12), 3, GPIO_FUNCTION(2)}, {TIM5, GPIO('A', 2), 3, GPIO_FUNCTION(2)}, + {TIM5, GPIO('A', 3), 4, GPIO_FUNCTION(2)}, + {TIM8, GPIO('C', 6), 1, GPIO_FUNCTION(3)}, + {TIM8, GPIO('C', 7), 2, GPIO_FUNCTION(3)}, + {TIM8, GPIO('C', 8), 3, GPIO_FUNCTION(3)}, + {TIM8, GPIO('C', 9), 4, GPIO_FUNCTION(3)}, {TIM12, GPIO('H', 6), 1, GPIO_FUNCTION(2)}, {TIM12, GPIO('B', 14), 1, GPIO_FUNCTION(2)}, {TIM12, GPIO('H', 9), 2, GPIO_FUNCTION(2)}, @@ -265,8 +284,11 @@ static const struct gpio_pwm_info pwm_regs[] = { {TIM13, GPIO('A', 6), 1, GPIO_FUNCTION(9)}, {TIM14, GPIO('F', 9), 1, GPIO_FUNCTION(9)}, {TIM14, GPIO('A', 7), 1, GPIO_FUNCTION(9)}, + {TIM15, GPIO('C', 12), 1, GPIO_FUNCTION(2)}, {TIM15, GPIO('E', 5), 1, GPIO_FUNCTION(4)}, {TIM15, GPIO('A', 2), 1, GPIO_FUNCTION(4)}, + {TIM15, GPIO('A', 3), 2, GPIO_FUNCTION(4)}, + {TIM15, GPIO('E', 6), 2, GPIO_FUNCTION(4)}, {TIM16, GPIO('F', 6), 1, GPIO_FUNCTION(1)}, {TIM16, GPIO('B', 8), 1, GPIO_FUNCTION(1)}, {TIM17, GPIO('F', 7), 1, GPIO_FUNCTION(1)}, From 9346ad1914dc50d12f1e5efe630448bf763d1469 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 18 Jun 2025 11:08:18 -0400 Subject: [PATCH 003/411] load_cell_probe: Fix warnings on avr builds On AVR, integers are 16bit, so be sure to promote math to 32bit where needed. Signed-off-by: Kevin O'Connor --- src/load_cell_probe.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/load_cell_probe.c b/src/load_cell_probe.c index 49dfbe52b..48da77937 100644 --- a/src/load_cell_probe.c +++ b/src/load_cell_probe.c @@ -32,7 +32,7 @@ typedef int32_t fixedQ16_t; typedef int64_t fixedQ48_t; #define FIXEDQ48_FRAC_BITS FIXEDQ16_FRAC_BITS -#define MAX_TRIGGER_GRAMS ((1 << FIXEDQ16) - 1) +#define MAX_TRIGGER_GRAMS ((1L << FIXEDQ16) - 1) #define ERROR_SAFETY_RANGE 0 #define ERROR_OVERFLOW 1 #define ERROR_WATCHDOG 2 @@ -204,7 +204,7 @@ set_endstop_range(struct load_cell_probe *lce shutdown("trigger_grams too large"); } // grams_per_count must be a positive fraction in Q2 format - const fixedQ2_t one = 1 << FIXEDQ2_FRAC_BITS; + const fixedQ2_t one = 1L << FIXEDQ2_FRAC_BITS; if (grams_per_count < 0 || grams_per_count >= one) { shutdown("grams_per_count is invalid"); } From 42fbf8256f37b2b7ab77304184e9d8909e5a9612 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 8 Jul 2025 18:49:12 -0400 Subject: [PATCH 004/411] ads1x1x: Revert incorrect removal of assignment to self.config Commit d120a313b incorrectly removed an assignment to self.config - in this instance the reference was to a local variable not related to the global configfile storage. Signed-off-by: Kevin O'Connor --- klippy/extras/ads1x1x.py | 1 + 1 file changed, 1 insertion(+) diff --git a/klippy/extras/ads1x1x.py b/klippy/extras/ads1x1x.py index 10cd01f9b..1d72996f3 100644 --- a/klippy/extras/ads1x1x.py +++ b/klippy/extras/ads1x1x.py @@ -321,6 +321,7 @@ class ADS1X1X_pin: def __init__(self, chip, config): self.mcu = chip.mcu self.chip = chip + self.config = config self.invalid_count = 0 From c01e6eee1d96617459934e83467dd5c62ece0bfc Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 8 Jul 2025 18:55:25 -0400 Subject: [PATCH 005/411] ads1x1x: Rename local 'config' variable to pcfg Avoid using the name "config" as a local register storage variable as it can be confused with the common "config" configfile reference. Signed-off-by: Kevin O'Connor --- klippy/extras/ads1x1x.py | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/klippy/extras/ads1x1x.py b/klippy/extras/ads1x1x.py index 1d72996f3..9290b3f14 100644 --- a/klippy/extras/ads1x1x.py +++ b/klippy/extras/ads1x1x.py @@ -210,28 +210,28 @@ class ADS1X1X_chip: raise pins.error('ADS1x1x pin %s is not valid' % \ pin_params['pin']) - config = 0 - config |= (ADS1X1X_OS['OS_SINGLE'] & \ - ADS1X1X_REG_CONFIG['OS_MASK']) - config |= (ADS1X1X_MUX[pin_params['pin']] & \ - ADS1X1X_REG_CONFIG['MULTIPLEXER_MASK']) - config |= (self.pga & ADS1X1X_REG_CONFIG['PGA_MASK']) + pcfg = 0 + pcfg |= (ADS1X1X_OS['OS_SINGLE'] & \ + ADS1X1X_REG_CONFIG['OS_MASK']) + pcfg |= (ADS1X1X_MUX[pin_params['pin']] & \ + ADS1X1X_REG_CONFIG['MULTIPLEXER_MASK']) + pcfg |= (self.pga & ADS1X1X_REG_CONFIG['PGA_MASK']) # Have to use single mode, because in continuous, it never reaches # idle state, which we use to determine if the sampling is done. - config |= (ADS1X1X_MODE['single'] & \ + pcfg |= (ADS1X1X_MODE['single'] & \ ADS1X1X_REG_CONFIG['MODE_MASK']) # lowest sample rate per default, until report time has been set in # setup_adc_sample - config |= (self.comp_mode \ - & ADS1X1X_REG_CONFIG['COMPARATOR_MODE_MASK']) - config |= (self.comp_polarity \ - & ADS1X1X_REG_CONFIG['COMPARATOR_POLARITY_MASK']) - config |= (self.comp_latching \ - & ADS1X1X_REG_CONFIG['COMPARATOR_LATCHING_MASK']) - config |= (self.comp_queue \ - & ADS1X1X_REG_CONFIG['COMPARATOR_QUEUE_MASK']) + pcfg |= (self.comp_mode \ + & ADS1X1X_REG_CONFIG['COMPARATOR_MODE_MASK']) + pcfg |= (self.comp_polarity \ + & ADS1X1X_REG_CONFIG['COMPARATOR_POLARITY_MASK']) + pcfg |= (self.comp_latching \ + & ADS1X1X_REG_CONFIG['COMPARATOR_LATCHING_MASK']) + pcfg |= (self.comp_queue \ + & ADS1X1X_REG_CONFIG['COMPARATOR_QUEUE_MASK']) - pin_obj = ADS1X1X_pin(self, config) + pin_obj = ADS1X1X_pin(self, pcfg) if pin in self._pins: raise pins.error( 'pin %s for chip %s is used multiple times' \ @@ -250,8 +250,8 @@ class ADS1X1X_chip: logging.exception("ADS1X1X: error while resetting device") def is_ready(self): - config = self._read_register(ADS1X1X_REG_POINTER['CONFIG']) - return bool((config & ADS1X1X_REG_CONFIG['OS_MASK']) == \ + cfg = self._read_register(ADS1X1X_REG_POINTER['CONFIG']) + return bool((cfg & ADS1X1X_REG_CONFIG['OS_MASK']) == \ ADS1X1X_OS['OS_IDLE']) def calculate_sample_rate(self): @@ -281,7 +281,7 @@ class ADS1X1X_chip: (sample_rate, sample_rate_bits) = self.calculate_sample_rate() for pin in self._pins.values(): - pin.config = (pin.config & ~ADS1X1X_REG_CONFIG['DATA_RATE_MASK']) \ + pin.pcfg = (pin.pcfg & ~ADS1X1X_REG_CONFIG['DATA_RATE_MASK']) \ | (sample_rate_bits & ADS1X1X_REG_CONFIG['DATA_RATE_MASK']) self.delay = 1 / float(sample_rate) @@ -289,7 +289,7 @@ class ADS1X1X_chip: def sample(self, pin): with self._mutex: try: - self._write_register(ADS1X1X_REG_POINTER['CONFIG'], pin.config) + self._write_register(ADS1X1X_REG_POINTER['CONFIG'], pin.pcfg) self._reactor.pause(self._reactor.monotonic() + self.delay) start_time = self._reactor.monotonic() while not self.is_ready(): @@ -318,10 +318,10 @@ class ADS1X1X_chip: self._i2c.i2c_write(data) class ADS1X1X_pin: - def __init__(self, chip, config): + def __init__(self, chip, pcfg): self.mcu = chip.mcu self.chip = chip - self.config = config + self.pcfg = pcfg self.invalid_count = 0 From 1931b11001cc7b53c00e82ac992c70d7bd50a310 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 3 Jul 2025 20:50:51 +0200 Subject: [PATCH 006/411] stm32: f0 make i2c distinguish I2C NACKs Some devices can return a read NACK on host retries. When the MCU receives the I2C CMD, reads out data, but fails to deliver a response to the host. The host retries, the device returns NACK, and the MCU goes into the shutdown state. Signed-off-by: Timofey Titovets --- src/stm32/stm32f0_i2c.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/stm32/stm32f0_i2c.c b/src/stm32/stm32f0_i2c.c index 9f4bbcc69..da632c358 100644 --- a/src/stm32/stm32f0_i2c.c +++ b/src/stm32/stm32f0_i2c.c @@ -195,6 +195,7 @@ i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) I2C_TypeDef *i2c = config.i2c; uint32_t timeout = timer_read_time() + timer_from_us(5000); int ret = I2C_BUS_SUCCESS; + uint8_t *write_orig = write; // Send start and address i2c->CR2 = (I2C_CR2_START | config.addr | (write_len << I2C_CR2_NBYTES_Pos) @@ -207,6 +208,8 @@ i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) } return i2c_wait(i2c, I2C_ISR_TXE, timeout); abrt: + if (write == write_orig && ret == I2C_BUS_NACK) + ret = I2C_BUS_START_NACK; i2c->CR2 |= I2C_CR2_STOP; return ret; } @@ -218,6 +221,8 @@ i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg I2C_TypeDef *i2c = config.i2c; uint32_t timeout = timer_read_time() + timer_from_us(5000); int ret = I2C_BUS_SUCCESS; + uint8_t *write_orig = reg; + uint8_t *read_orig = read; // Send start, address, reg i2c->CR2 = (I2C_CR2_START | config.addr | @@ -236,11 +241,16 @@ i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg while (read_len--) { ret = i2c_wait(i2c, I2C_ISR_RXNE, timeout); if (ret != I2C_BUS_SUCCESS) - goto abrt; + goto abrt_read; *read++ = i2c->RXDR; } return i2c_wait(i2c, I2C_ISR_STOPF, timeout); +abrt_read: + if (read == read_orig && ret == I2C_BUS_NACK) + ret = I2C_BUS_START_READ_NACK; abrt: + if (reg == write_orig && ret == I2C_BUS_NACK) + ret = I2C_BUS_START_NACK; i2c->CR2 |= I2C_CR2_STOP; return ret; } From 119d00705836eda8c150437af66c39f58732b492 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 8 Jul 2025 21:23:27 +0200 Subject: [PATCH 007/411] stm32: f0 do not send empty write on read Signed-off-by: Timofey Titovets --- src/stm32/stm32f0_i2c.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/stm32/stm32f0_i2c.c b/src/stm32/stm32f0_i2c.c index da632c358..381fe8b40 100644 --- a/src/stm32/stm32f0_i2c.c +++ b/src/stm32/stm32f0_i2c.c @@ -224,16 +224,18 @@ i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg uint8_t *write_orig = reg; uint8_t *read_orig = read; - // Send start, address, reg - i2c->CR2 = (I2C_CR2_START | config.addr | - (reg_len << I2C_CR2_NBYTES_Pos)); - while (reg_len--) { - ret = i2c_wait(i2c, I2C_ISR_TXIS, timeout); - if (ret != I2C_BUS_SUCCESS) - goto abrt; - i2c->TXDR = *reg++; + if (reg_len) { + // Send start, address, reg + i2c->CR2 = (I2C_CR2_START | config.addr | + (reg_len << I2C_CR2_NBYTES_Pos)); + while (reg_len--) { + ret = i2c_wait(i2c, I2C_ISR_TXIS, timeout); + if (ret != I2C_BUS_SUCCESS) + goto abrt; + i2c->TXDR = *reg++; + } + i2c_wait(i2c, I2C_ISR_TC, timeout); } - i2c_wait(i2c, I2C_ISR_TC, timeout); // send restart, read data i2c->CR2 = (I2C_CR2_START | I2C_CR2_RD_WRN | config.addr | From 37ddab223f54823d7252f87ea1b025a43031b878 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 3 Jul 2025 21:39:49 +0200 Subject: [PATCH 008/411] mcu: allow disable send retries Signed-off-by: Timofey Titovets --- klippy/mcu.py | 18 +++++++++++------- klippy/serialhdl.py | 5 ++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index ca5191989..702f981a5 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -31,11 +31,13 @@ class RetryAsyncCommand: if self.need_response and params['#sent_time'] >= self.min_query_time: self.need_response = False self.reactor.async_complete(self.completion, params) - def get_response(self, cmds, cmd_queue, minclock=0, reqclock=0): + def get_response(self, cmds, cmd_queue, minclock=0, reqclock=0, retry=True): cmd, = cmds self.serial.raw_send_wait_ack(cmd, minclock, reqclock, cmd_queue) self.min_query_time = 0. first_query_time = query_time = self.reactor.monotonic() + if not retry: + self.TIMEOUT_TIME=.0 while 1: params = self.completion.wait(query_time + self.RETRY_TIME) if params is not None: @@ -64,19 +66,21 @@ class CommandQueryWrapper: if cmd_queue is None: cmd_queue = serial.get_default_command_queue() self._cmd_queue = cmd_queue - def _do_send(self, cmds, minclock, reqclock): + def _do_send(self, cmds, minclock, reqclock, retry): xh = self._xmit_helper(self._serial, self._response, self._oid) reqclock = max(minclock, reqclock) try: - return xh.get_response(cmds, self._cmd_queue, minclock, reqclock) + return xh.get_response(cmds, self._cmd_queue, minclock, reqclock, + retry) except serialhdl.error as e: raise self._error(str(e)) - def send(self, data=(), minclock=0, reqclock=0): - return self._do_send([self._cmd.encode(data)], minclock, reqclock) + def send(self, data=(), minclock=0, reqclock=0, retry=True): + return self._do_send([self._cmd.encode(data)], minclock, reqclock, + retry) def send_with_preface(self, preface_cmd, preface_data=(), data=(), - minclock=0, reqclock=0): + minclock=0, reqclock=0, retry=True): cmds = [preface_cmd._cmd.encode(preface_data), self._cmd.encode(data)] - return self._do_send(cmds, minclock, reqclock) + return self._do_send(cmds, minclock, reqclock, retry) # Wrapper around command sending class CommandWrapper: diff --git a/klippy/serialhdl.py b/klippy/serialhdl.py index 30db61707..fc8846387 100644 --- a/klippy/serialhdl.py +++ b/klippy/serialhdl.py @@ -310,9 +310,12 @@ class SerialRetryCommand: self.serial.register_response(self.handle_callback, name, oid) def handle_callback(self, params): self.last_params = params - def get_response(self, cmds, cmd_queue, minclock=0, reqclock=0): + def get_response(self, cmds, cmd_queue, minclock=0, reqclock=0, + retry=True): retries = 5 retry_delay = .010 + if not retry: + retries = 0 while 1: for cmd in cmds[:-1]: self.serial.raw_send(cmd, minclock, reqclock, cmd_queue) From 2585accfeb0e8291a0d428e8d3dfc72ff8f0cec7 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 3 Jul 2025 21:50:35 +0200 Subject: [PATCH 009/411] sht3x: reads should be retried with at least 0.5s pause SHT3x would return a read NACK on host retries. When the MCU receives the I2C CMD, it reads out data. SHT3x clears the data buffer. The MCU fails to deliver a response to the host. The host retries, the device returns NACK, then the MCU goes into the shutdown state. Make sure there is at least 0.5s between retries. Signed-off-by: Timofey Titovets --- klippy/extras/bus.py | 4 ++-- klippy/extras/sht3x.py | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index c07ec8269..4121c1c86 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -217,8 +217,8 @@ class MCU_I2C: 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): - return self.i2c_read_cmd.send([self.oid, write, read_len]) + def i2c_read(self, write, read_len, retry=True): + return self.i2c_read_cmd.send([self.oid, write, read_len], retry) def MCU_I2C_from_config(config, default_addr=None, default_speed=100000): # Load bus parameters diff --git a/klippy/extras/sht3x.py b/klippy/extras/sht3x.py index 5a8785e8d..79cd7bfe3 100644 --- a/klippy/extras/sht3x.py +++ b/klippy/extras/sht3x.py @@ -56,6 +56,7 @@ class SHT3X: self.reactor = self.printer.get_reactor() self.i2c = bus.MCU_I2C_from_config( config, default_addr=SHT3X_I2C_ADDR, default_speed=100000) + self._error = self.i2c.get_mcu().error self.report_time = config.getint('sht3x_report_time', 1, minval=1) self.deviceId = config.get('sensor_type') self.temp = self.min_temp = self.max_temp = self.humidity = 0. @@ -105,7 +106,20 @@ class SHT3X: def _sample_sht3x(self, eventtime): try: # Read measurment - params = self.i2c.i2c_read(SHT3X_CMD['OTHER']['FETCH'], 6) + retries = 5 + params = None + error = None + while retries > 0 and params is None: + try: + params = self.i2c.i2c_read( + SHT3X_CMD['OTHER']['FETCH'], 6, retry=False + ) + except self._error as e: + error = e + self.reactor.pause(self.reactor.monotonic() + .5) + retries -= 1 + if params is None: + raise error response = bytearray(params['response']) rtemp = response[0] << 8 From 697c6e8d2870542f2114bb0edcf5c026ba310ffe Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 11 Jul 2025 10:52:27 -0400 Subject: [PATCH 010/411] mcu: Avoid altering self.TIMEOUT_TIME in RetryAsyncCommand Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 702f981a5..8b41e4e03 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -35,16 +35,16 @@ class RetryAsyncCommand: cmd, = cmds self.serial.raw_send_wait_ack(cmd, minclock, reqclock, cmd_queue) self.min_query_time = 0. - first_query_time = query_time = self.reactor.monotonic() - if not retry: - self.TIMEOUT_TIME=.0 + timeout_time = query_time = self.reactor.monotonic() + if retry: + timeout_time += self.TIMEOUT_TIME while 1: params = self.completion.wait(query_time + self.RETRY_TIME) if params is not None: self.serial.register_response(None, self.name, self.oid) return params query_time = self.reactor.monotonic() - if query_time > first_query_time + self.TIMEOUT_TIME: + if query_time > timeout_time: self.serial.register_response(None, self.name, self.oid) raise serialhdl.error("Timeout on wait for '%s' response" % (self.name,)) From 993cec0891c056bca3ab3c4436faffb878da2ae5 Mon Sep 17 00:00:00 2001 From: Findlay Feng Date: Fri, 11 Jul 2025 23:08:35 +0800 Subject: [PATCH 011/411] sos_filter: fix overflows_int32 (#6976) Modify the inline function overflows_int32 to static inline Inline functions cannot be debugged in -O mode https://gcc.gnu.org/bugzilla/show_bug.cgi?id=49653 Signed-off-by: Findlay Feng --- src/sos_filter.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sos_filter.c b/src/sos_filter.c index 04eb366eb..6e64bc0a2 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -29,7 +29,7 @@ struct sos_filter { struct sos_filter_section filter[0]; }; -inline uint8_t +static inline uint8_t overflows_int32(int64_t value) { return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; } From 33bd67f9b7a39a4f74fdacc786c1b3a5fa629a84 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Mon, 9 Jun 2025 18:10:25 +0200 Subject: [PATCH 012/411] tmc: add enriched SPI read Currently TMC spi just drop the data that could be useful. Export that data. Signed-off-by: Timofey Titovets --- klippy/extras/tmc2130.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/klippy/extras/tmc2130.py b/klippy/extras/tmc2130.py index e97313a2a..17e8e8c0f 100644 --- a/klippy/extras/tmc2130.py +++ b/klippy/extras/tmc2130.py @@ -200,12 +200,21 @@ class MCU_TMC_SPI_chain: cmd = self._build_cmd([reg, 0x00, 0x00, 0x00, 0x00], chain_pos) self.spi.spi_send(cmd) if self.printer.get_start_args().get('debugoutput') is not None: - return 0 + return { + "spi_status": 0, + "data": 0, + "#receive_time": .0, + } params = self.spi.spi_transfer(cmd) pr = bytearray(params['response']) pr = pr[(self.chain_len - chain_pos) * 5 : (self.chain_len - chain_pos + 1) * 5] - return (pr[1] << 24) | (pr[2] << 16) | (pr[3] << 8) | pr[4] + return { + "spi_status": pr[0], + "data": (pr[1] << 24) | (pr[2] << 16) | (pr[3] << 8) | pr[4], + "#receive_time": params["#receive_time"], + } + def reg_write(self, reg, val, chain_pos, print_time=None): minclock = 0 if print_time is not None: @@ -258,11 +267,13 @@ class MCU_TMC_SPI: self.tmc_frequency = tmc_frequency def get_fields(self): return self.fields - def get_register(self, reg_name): + def get_register_raw(self, reg_name): reg = self.name_to_reg[reg_name] with self.mutex: - read = self.tmc_spi.reg_read(reg, self.chain_pos) - return read + resp = self.tmc_spi.reg_read(reg, self.chain_pos) + return resp + def get_register(self, reg_name): + return self.get_register_raw(reg_name)["data"] def set_register(self, reg_name, val, print_time=None): reg = self.name_to_reg[reg_name] with self.mutex: From 8d67e1a4e969c1656cf72490d8af5fa38e32ef1a Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Mon, 9 Jun 2025 18:36:41 +0200 Subject: [PATCH 013/411] tmc2660: add enriched SPI read Signed-off-by: Timofey Titovets --- klippy/extras/tmc2660.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/klippy/extras/tmc2660.py b/klippy/extras/tmc2660.py index dcffac759..7fd015463 100644 --- a/klippy/extras/tmc2660.py +++ b/klippy/extras/tmc2660.py @@ -198,11 +198,14 @@ class MCU_TMC2660_SPI: self.fields = fields def get_fields(self): return self.fields - def get_register(self, reg_name): + def get_register_raw(self, reg_name): new_rdsel = ReadRegisters.index(reg_name) reg = self.name_to_reg["DRVCONF"] if self.printer.get_start_args().get('debugoutput') is not None: - return 0 + return { + 'data': 0, + '#receive_time': .0, + } with self.mutex: old_rdsel = self.fields.get_field("rdsel") val = self.fields.set_field("rdsel", new_rdsel) @@ -212,7 +215,12 @@ class MCU_TMC2660_SPI: self.spi.spi_send(msg) params = self.spi.spi_transfer(msg) pr = bytearray(params['response']) - return (pr[0] << 16) | (pr[1] << 8) | pr[2] + return { + 'data': (pr[0] << 16) | (pr[1] << 8) | pr[2], + '#receive_time': params['#receive_time'], + } + def get_register(self, reg_name): + return self.get_register_raw(reg_name)['data'] def set_register(self, reg_name, val, print_time=None): minclock = 0 if print_time is not None: From 5923a2e3a1501c700ea4e357fa8491324b7ce7f3 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Mon, 9 Jun 2025 18:42:19 +0200 Subject: [PATCH 014/411] tmc: add spi status decode Signed-off-by: Timofey Titovets --- klippy/extras/tmc2130.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/klippy/extras/tmc2130.py b/klippy/extras/tmc2130.py index 17e8e8c0f..b41eba025 100644 --- a/klippy/extras/tmc2130.py +++ b/klippy/extras/tmc2130.py @@ -272,6 +272,13 @@ class MCU_TMC_SPI: with self.mutex: resp = self.tmc_spi.reg_read(reg, self.chain_pos) return resp + def decode_spi_status(spi_status): + return { + "standstill": spi_status >> 3 & 0x1, + "sg2": spi_status >> 2 & 0x1, + "driver_error": spi_status >> 1 & 0x1, + "reset_flag": spi_status & 0x1 + } def get_register(self, reg_name): return self.get_register_raw(reg_name)["data"] def set_register(self, reg_name, val, print_time=None): From 9c0d0f6a72897dd8eea8a95a7fabcc3c28f3a12c Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Mon, 9 Jun 2025 18:33:21 +0200 Subject: [PATCH 015/411] tmc: add enriched UART read Signed-off-by: Timofey Titovets --- klippy/extras/tmc_uart.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/klippy/extras/tmc_uart.py b/klippy/extras/tmc_uart.py index 4d5ec1d5a..939350425 100644 --- a/klippy/extras/tmc_uart.py +++ b/klippy/extras/tmc_uart.py @@ -175,7 +175,10 @@ class MCU_TMC_uart_bitbang: self.analog_mux.activate(instance_id) msg = self._encode_read(0xf5, addr, reg) params = self.tmcuart_send_cmd.send([self.oid, msg, 10]) - return self._decode_read(reg, params['read']) + return { + 'data': self._decode_read(reg, params['read']), + '#receive_time': params['#receive_time'] + } def reg_write(self, instance_id, addr, reg, val, print_time=None): minclock = 0 if print_time is not None: @@ -225,16 +228,21 @@ class MCU_TMC_uart: def _do_get_register(self, reg_name): reg = self.name_to_reg[reg_name] if self.printer.get_start_args().get('debugoutput') is not None: - return 0 + return { + 'data': 0, + '#receive_time': 0. + } for retry in range(5): val = self.mcu_uart.reg_read(self.instance_id, self.addr, reg) - if val is not None: + if val['data'] is not None: return val raise self.printer.command_error( "Unable to read tmc uart '%s' register %s" % (self.name, reg_name)) - def get_register(self, reg_name): + def get_register_raw(self, reg_name): with self.mutex: return self._do_get_register(reg_name) + def get_register(self, reg_name): + return self.get_register_raw(reg_name)['data'] def set_register(self, reg_name, val, print_time=None): reg = self.name_to_reg[reg_name] if self.printer.get_start_args().get('debugoutput') is not None: @@ -243,10 +251,10 @@ class MCU_TMC_uart: for retry in range(5): ifcnt = self.ifcnt if ifcnt is None: - self.ifcnt = ifcnt = self._do_get_register("IFCNT") + self.ifcnt = ifcnt = self._do_get_register("IFCNT")['data'] self.mcu_uart.reg_write(self.instance_id, self.addr, reg, val, print_time) - self.ifcnt = self._do_get_register("IFCNT") + self.ifcnt = self._do_get_register("IFCNT")['data'] if self.ifcnt == (ifcnt + 1) & 0xff: return raise self.printer.command_error( From 317f8c94c8bf83af4551fe57f1f8d7395a14773d Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 15 May 2024 02:37:58 +0200 Subject: [PATCH 016/411] tmc.py: add track of stallguard Signed-off-by: Timofey Titovets --- klippy/extras/tmc.py | 92 +++++++++++++++++++++++++++++++++++++++ klippy/extras/tmc2130.py | 4 ++ klippy/extras/tmc2660.py | 2 + klippy/extras/tmc_uart.py | 4 ++ 4 files changed, 102 insertions(+) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 1d8599e2e..aad19cf4d 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -5,6 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging, collections import stepper +from . import bulk_sensor ###################################################################### @@ -220,6 +221,96 @@ class TMCErrorCheck: self.last_drv_fields = {n: v for n, v in fields.items() if v} return {'drv_status': self.last_drv_fields, 'temperature': temp} +###################################################################### +# Record driver status +###################################################################### + +class TMCStallguardDump: + def __init__(self, config, mcu_tmc): + self.printer = config.get_printer() + self.stepper_name = ' '.join(config.get_name().split()[1:]) + self.mcu_tmc = mcu_tmc + self.mcu = self.mcu_tmc.get_mcu() + self.fields = self.mcu_tmc.get_fields() + self.sg2_supp = False + self.sg4_reg_name = None + # It is possible to support TMC2660, just disable it for now + if not self.fields.all_fields.get("DRV_STATUS", None): + return + # Collect driver capabilities + if self.fields.all_fields["DRV_STATUS"].get("sg_result", None): + self.sg2_supp = True + # New drivers have separate register for SG4 result + if self.mcu_tmc.name_to_reg.get("SG_RESULT", 0): + self.sg4_reg_name = "SG_RESULT" + # 2240 supports both SG2 & SG4 + if self.sg4_reg_name is None: + if self.mcu_tmc.name_to_reg.get("SG4_RESULT", 0): + self.sg4_reg_name = "SG4_RESULT" + # TMC2208 + if self.sg2_supp is None and self.sg4_reg_name is None: + return + self.optimized_spi = False + # Bulk API + self.samples = [] + self.query_timer = None + self.error = None + self.batch_bulk = bulk_sensor.BatchBulkHelper( + self.printer, self._dump, self._start, self._stop) + api_resp = {'header': ('time', 'sg_result', 'cs_actual')} + self.batch_bulk.add_mux_endpoint("tmc/stallguard_dump", "name", + self.stepper_name, api_resp) + def _start(self): + self.error = None + status = self.mcu_tmc.get_register_raw("DRV_STATUS") + if status.get("spi_status"): + self.optimized_spi = True + reactor = self.printer.get_reactor() + self.query_timer = reactor.register_timer(self._query_tmc, + reactor.NOW) + def _stop(self): + self.printer.get_reactor().unregister_timer(self.query_timer) + self.query_timer = None + self.samples = [] + def _query_tmc(self, eventtime): + sg_result = -1 + cs_actual = -1 + recv_time = eventtime + try: + if self.optimized_spi or self.sg4_reg_name == "SG4_RESULT": + #TMC2130/TMC5160/TMC2240 + status = self.mcu_tmc.get_register_raw("DRV_STATUS") + reg_val = status["data"] + cs_actual = self.fields.get_field("cs_actual", reg_val) + sg_result = self.fields.get_field("sg_result", reg_val) + is_stealth = self.fields.get_field("stealth", reg_val) + recv_time = status["#receive_time"] + if is_stealth and self.sg4_reg_name == "SG4_RESULT": + sg4_ret = self.mcu_tmc.get_register_raw("SG4_RESULT") + sg_result = sg4_ret["data"] + recv_time = sg4_ret["#receive_time"] + else: + # TMC2209 + if self.sg4_reg_name == "SG_RESULT": + sg4_ret = self.mcu_tmc.get_register_raw("SG_RESULT") + sg_result = sg4_ret["data"] + recv_time = sg4_ret["#receive_time"] + except self.printer.command_error as e: + self.error = e + return self.printer.get_reactor().NEVER + print_time = self.mcu.estimated_print_time(recv_time) + self.samples.append((print_time, sg_result, cs_actual)) + if self.optimized_spi: + return eventtime + 0.001 + # UART queried as fast as possible + return eventtime + 0.005 + def _dump(self, eventtime): + if self.error: + raise self.error + samples = self.samples + self.samples = [] + return {"data": samples} + ###################################################################### # G-Code command helpers @@ -233,6 +324,7 @@ class TMCCommandHelper: self.mcu_tmc = mcu_tmc self.current_helper = current_helper self.echeck_helper = TMCErrorCheck(config, mcu_tmc) + self.record_helper = TMCStallguardDump(config, mcu_tmc) self.fields = mcu_tmc.get_fields() self.read_registers = self.read_translate = None self.toff = None diff --git a/klippy/extras/tmc2130.py b/klippy/extras/tmc2130.py index b41eba025..181fe07ae 100644 --- a/klippy/extras/tmc2130.py +++ b/klippy/extras/tmc2130.py @@ -232,6 +232,8 @@ class MCU_TMC_SPI_chain: pr = pr[(self.chain_len - chain_pos) * 5 : (self.chain_len - chain_pos + 1) * 5] return (pr[1] << 24) | (pr[2] << 16) | (pr[3] << 8) | pr[4] + def get_mcu(self): + return self.spi.get_mcu() # Helper to setup an spi daisy chain bus from settings in a config section def lookup_tmc_spi_chain(config): @@ -292,6 +294,8 @@ class MCU_TMC_SPI: "Unable to write tmc spi '%s' register %s" % (self.name, reg_name)) def get_tmc_frequency(self): return self.tmc_frequency + def get_mcu(self): + return self.tmc_spi.get_mcu() ###################################################################### diff --git a/klippy/extras/tmc2660.py b/klippy/extras/tmc2660.py index 7fd015463..c97f1effc 100644 --- a/klippy/extras/tmc2660.py +++ b/klippy/extras/tmc2660.py @@ -231,6 +231,8 @@ class MCU_TMC2660_SPI: self.spi.spi_send(msg, minclock) def get_tmc_frequency(self): return None + def get_mcu(self): + return self.spi.get_mcu() ###################################################################### diff --git a/klippy/extras/tmc_uart.py b/klippy/extras/tmc_uart.py index 939350425..fa0d62626 100644 --- a/klippy/extras/tmc_uart.py +++ b/klippy/extras/tmc_uart.py @@ -187,6 +187,8 @@ class MCU_TMC_uart_bitbang: self.analog_mux.activate(instance_id) msg = self._encode_write(0xf5, addr, reg | 0x80, val) self.tmcuart_send_cmd.send([self.oid, msg, 0], minclock=minclock) + def get_mcu(self): + return self.mcu # Lookup a (possibly shared) tmc uart def lookup_tmc_uart_bitbang(config, max_addr): @@ -261,3 +263,5 @@ class MCU_TMC_uart: "Unable to write tmc uart '%s' register %s" % (self.name, reg_name)) def get_tmc_frequency(self): return self.tmc_frequency + def get_mcu(self): + return self.mcu_uart.get_mcu() From b724b3a348039b8861f7f56fa9739f1d789bf06d Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 21 May 2024 22:33:00 +0200 Subject: [PATCH 017/411] data_logger.py: add tmc/stallguard_dump endpoint Signed-off-by: Timofey Titovets --- scripts/motan/data_logger.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/motan/data_logger.py b/scripts/motan/data_logger.py index e00c99560..fd4de7a5d 100755 --- a/scripts/motan/data_logger.py +++ b/scripts/motan/data_logger.py @@ -162,6 +162,10 @@ class DataLogger: lname = "%s:%s" % (st, aname) qcmd = "%s/dump_%s" % (st, st) self.send_subscribe(lname, qcmd, {"sensor": aname}) + if cfgname.startswith("tmc"): + driver = ' '.join(cfgname.split()[1:]) + self.send_subscribe("stallguard:" + driver, + "tmc/stallguard_dump", {"name": driver}) def handle_dump(self, msg, raw_msg): msg_id = msg["id"] if "result" not in msg: From 9323a5dfe28619a53c7f350c2e894d299c342bca Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 22 May 2024 01:28:46 +0200 Subject: [PATCH 018/411] readlog.py: add support for stallguard data Signed-off-by: Timofey Titovets --- scripts/motan/readlog.py | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/scripts/motan/readlog.py b/scripts/motan/readlog.py index 48284ec2b..4c078c9bc 100644 --- a/scripts/motan/readlog.py +++ b/scripts/motan/readlog.py @@ -234,6 +234,64 @@ class HandleStepQ: step_data.append((step_time, step_halfpos, step_pos)) LogHandlers["stepq"] = HandleStepQ +# Extract tmc currect and stallguard data from the log +class HandleStallguard: + SubscriptionIdParts = 2 + ParametersMin = 2 + ParametersMax = 2 + DataSets = [ + ('stallguard(,sg_result)', + 'Stallguard result of the given stepper driver'), + ('stallguard(,cs_actual)', + 'Current level result of the given stepper driver'), + ] + def __init__(self, lmanager, name, name_parts): + self.name = name + self.stepper_name = name_parts[1] + self.filter = name_parts[2] + self.jdispatch = lmanager.get_jdispatch() + self.data = [] + self.ret = None + self.driver_name = "" + for k in lmanager.get_initial_status()['configfile']['settings']: + if not k.startswith("tmc"): + continue + if k.endswith(self.stepper_name): + self.driver_name = k + break + # Current decode + self.status_tracker = lmanager.get_status_tracker() + self.next_status_time = 0. + self.irun = 0 + def get_label(self): + label = '%s %s %s' % (self.driver_name, self.stepper_name, + self.filter) + if self.filter == "sg_result": + return {'label': label, 'units': 'Stallguard'} + elif self.filter == "cs_actual": + return {'label': label, 'units': 'CS Actual'} + # Search datapoint in dataset extrapolate in between + def pull_data(self, req_time): + while 1: + if len(self.data) == 0: + jmsg = self.jdispatch.pull_msg(req_time, self.name) + if jmsg is None: + return + self.data = jmsg["data"] + if self.ret is None and len(self.data) > 0: + time, sg_result, cs_actual = self.data.pop(0) + self.ret = { + "time": time, + "sg_result": sg_result, + "cs_actual": cs_actual, + } + if self.ret: + time = self.ret["time"] + if req_time <= time: + return self.ret[self.filter] + self.ret = None +LogHandlers["stallguard"] = HandleStallguard + # Extract stepper motor phase position class HandleStepPhase: SubscriptionIdParts = 0 From 4e4a5c633652978c2ea6e1cabac2a2f5568e5366 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 11 Jul 2025 00:38:21 +0200 Subject: [PATCH 019/411] stm32: make i2c distinguish I2C NACKs Signed-off-by: Timofey Titovets --- src/stm32/i2c.c | 60 +++++++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/stm32/i2c.c b/src/stm32/i2c.c index ccdf4fdc2..7ecbcd6da 100644 --- a/src/stm32/i2c.c +++ b/src/stm32/i2c.c @@ -94,21 +94,21 @@ i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr) return (struct i2c_config){ .i2c=i2c, .addr=addr<<1 }; } -static uint32_t +static int i2c_wait(I2C_TypeDef *i2c, uint32_t set, uint32_t clear, uint32_t timeout) { for (;;) { uint32_t sr1 = i2c->SR1; if ((sr1 & set) == set && (sr1 & clear) == 0) - return sr1; + return I2C_BUS_SUCCESS; if (sr1 & I2C_SR1_AF) - shutdown("I2C NACK error encountered"); + return I2C_BUS_NACK; if (!timer_is_before(timer_read_time(), timeout)) - shutdown("i2c timeout"); + return I2C_BUS_TIMEOUT; } } -static void +static int i2c_start(I2C_TypeDef *i2c, uint8_t addr, uint8_t xfer_len, uint32_t timeout) { @@ -117,7 +117,7 @@ i2c_start(I2C_TypeDef *i2c, uint8_t addr, uint8_t xfer_len, i2c->DR = addr; if (addr & 0x01) i2c->CR1 |= I2C_CR1_ACK; - i2c_wait(i2c, I2C_SR1_ADDR, 0, timeout); + int ret = i2c_wait(i2c, I2C_SR1_ADDR, 0, timeout); irqstatus_t flag = irq_save(); uint32_t sr2 = i2c->SR2; if (addr & 0x01 && xfer_len == 1) @@ -125,13 +125,14 @@ i2c_start(I2C_TypeDef *i2c, uint8_t addr, uint8_t xfer_len, irq_restore(flag); if (!(sr2 & I2C_SR2_MSL)) shutdown("Failed to send i2c addr"); + return ret; } -static void +static int i2c_send_byte(I2C_TypeDef *i2c, uint8_t b, uint32_t timeout) { i2c->DR = b; - i2c_wait(i2c, I2C_SR1_TXE, 0, timeout); + return i2c_wait(i2c, I2C_SR1_TXE, 0, timeout); } static uint8_t @@ -146,11 +147,11 @@ i2c_read_byte(I2C_TypeDef *i2c, uint32_t timeout, uint8_t remaining) return b; } -static void +static int i2c_stop(I2C_TypeDef *i2c, uint32_t timeout) { i2c->CR1 = I2C_CR1_STOP | I2C_CR1_PE; - i2c_wait(i2c, 0, I2C_SR1_TXE, timeout); + return i2c_wait(i2c, 0, I2C_SR1_TXE, timeout); } int @@ -159,12 +160,16 @@ i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) I2C_TypeDef *i2c = config.i2c; uint32_t timeout = timer_read_time() + timer_from_us(5000); - i2c_start(i2c, config.addr, write_len, timeout); - while (write_len--) - i2c_send_byte(i2c, *write++, timeout); - i2c_stop(i2c, timeout); + int ret = i2c_start(i2c, config.addr, write_len, timeout); + if (ret == I2C_BUS_NACK) + ret = I2C_BUS_START_NACK; + while (write_len-- && ret == I2C_BUS_SUCCESS) + ret = i2c_send_byte(i2c, *write++, timeout); + int timeout_err = i2c_stop(i2c, timeout); - return I2C_BUS_SUCCESS; + if (ret == I2C_BUS_SUCCESS && timeout_err != I2C_BUS_SUCCESS) + ret = timeout_err; + return ret; } int @@ -174,20 +179,31 @@ i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg I2C_TypeDef *i2c = config.i2c; uint32_t timeout = timer_read_time() + timer_from_us(5000); uint8_t addr = config.addr | 0x01; + int ret; if (reg_len) { // write the register - i2c_start(i2c, config.addr, reg_len, timeout); - while(reg_len--) - i2c_send_byte(i2c, *reg++, timeout); + ret = i2c_start(i2c, config.addr, reg_len, timeout); + if (ret == I2C_BUS_NACK) + ret = I2C_BUS_START_NACK; + while(reg_len-- && ret == I2C_BUS_SUCCESS) + ret = i2c_send_byte(i2c, *reg++, timeout); + if (ret != I2C_BUS_SUCCESS) + goto abrt; } // start/re-start and read data - i2c_start(i2c, addr, read_len, timeout); + ret = i2c_start(i2c, addr, read_len, timeout); + if (ret == I2C_BUS_NACK) + ret = I2C_BUS_START_READ_NACK; + if (ret != I2C_BUS_SUCCESS) + goto abrt; while(read_len--) { *read = i2c_read_byte(i2c, timeout, read_len); read++; } - i2c_wait(i2c, 0, I2C_SR1_RXNE, timeout); - - return I2C_BUS_SUCCESS; + // covers read timeout + return i2c_wait(i2c, 0, I2C_SR1_RXNE, timeout); +abrt: + i2c_stop(i2c, timeout); + return ret; } From 46912431794c02e468d4bcb2ccd3b41ab6911ba7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 19 Jul 2025 11:24:59 -0400 Subject: [PATCH 020/411] heaters: Increase time before clearing the temperature of an inactive heater The get_temp() code will stop reporting the last temperature of the heater if there hasn't been any recent temperature updates. However, on a full mcu communication loss this can cause the verify_heater code to report a heating error prior to the mcu code reporting the communication failure. Increase the heater timeout from 5 to 7 seconds to make it more likely the mcu failure is reported first. Signed-off-by: Kevin O'Connor --- klippy/extras/heaters.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/klippy/extras/heaters.py b/klippy/extras/heaters.py index 5cbc27e48..fce3c49a9 100644 --- a/klippy/extras/heaters.py +++ b/klippy/extras/heaters.py @@ -15,6 +15,7 @@ MAX_HEAT_TIME = 3.0 AMBIENT_TEMP = 25. PID_PARAM_BASE = 255. MAX_MAINTHREAD_TIME = 5.0 +QUELL_STALE_TIME = 7.0 class Heater: def __init__(self, config, sensor): @@ -110,9 +111,10 @@ class Heater: with self.lock: self.target_temp = degrees def get_temp(self, eventtime): - print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) - 5. + est_print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) + quell_time = est_print_time - QUELL_STALE_TIME with self.lock: - if self.last_temp_time < print_time: + if self.last_temp_time < quell_time: return 0., self.target_temp return self.smoothed_temp, self.target_temp def check_busy(self, eventtime): From 354b1e666b92d1b51bf19b3d834848d6ce1639df Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 14 Jul 2025 12:58:38 -0400 Subject: [PATCH 021/411] pca9632: Remove custom software i2c - use normal mcu software i2c instead Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 4 ++++ docs/Config_Reference.md | 5 ----- klippy/extras/pca9632.py | 7 ++----- test/klippy/led.cfg | 4 ++-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 87bcc9716..001791929 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,10 @@ All dates in this document are approximate. ## Changes +20250721: The `[pca9632]` module no longer accepts the `scl_pin` and +`sda_pin` options. Use `i2c_software_scl_pin` and +`i2c_software_sda_pin` instead. + 20250428: The maximum `cycle_time` for pwm `[output_pin]`, `[pwm_cycle_time]`, `[pwm_tool]`, and similar config sections is now 3 seconds (reduced from 5 seconds). The `maximum_mcu_duration` in diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index d9125d47b..bc99bd94b 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3477,11 +3477,6 @@ PCA9632 LED support. The PCA9632 is used on the FlashForge Dreamer. #i2c_speed: # See the "common I2C settings" section for a description of the # above parameters. -#scl_pin: -#sda_pin: -# Alternatively, if the pca9632 is not connected to a hardware I2C -# bus, then one may specify the "clock" (scl_pin) and "data" -# (sda_pin) pins. The default is to use hardware I2C. #color_order: RGBW # Set the pixel order of the LED (using a string containing the # letters R, G, B, W). The default is RGBW. diff --git a/klippy/extras/pca9632.py b/klippy/extras/pca9632.py index 8a3551cf2..b8a813c33 100644 --- a/klippy/extras/pca9632.py +++ b/klippy/extras/pca9632.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Ricardo Alcantara # # This file may be distributed under the terms of the GNU GPLv3 license. -from . import bus, led, mcp4018 +from . import bus, led BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000 @@ -25,10 +25,7 @@ PCA9632_LED3 = 0x06 class PCA9632: def __init__(self, config): self.printer = printer = config.get_printer() - if config.get("scl_pin", None) is not None: - self.i2c = mcp4018.SoftwareI2C(config, 98) - else: - self.i2c = bus.MCU_I2C_from_config(config, default_addr=98) + self.i2c = bus.MCU_I2C_from_config(config, default_addr=98) color_order = config.get("color_order", "RGBW") if sorted(color_order) != sorted("RGBW"): raise config.error("Invalid color_order '%s'" % (color_order,)) diff --git a/test/klippy/led.cfg b/test/klippy/led.cfg index d134490fa..c7f685d76 100644 --- a/test/klippy/led.cfg +++ b/test/klippy/led.cfg @@ -25,8 +25,8 @@ initial_GREEN: 0.2 initial_BLUE: 0.3 [pca9632 p6led] -scl_pin: PB1 -sda_pin: PB2 +i2c_software_scl_pin: PB1 +i2c_software_sda_pin: PB2 initial_RED: 0.4 initial_GREEN: 0.5 initial_BLUE: 0.6 From a209d4db5b99d62c745179aee407330da7e67e81 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 14 Jul 2025 13:27:17 -0400 Subject: [PATCH 022/411] mcp4018: Remove support for manual i2c - use standard mcu software i2c instead Signed-off-by: Kevin O'Connor --- config/generic-mightyboard.cfg | 20 +++--- .../printer-flashforge-creator-pro-2018.cfg | 20 +++--- docs/Config_Changes.md | 4 +- docs/Config_Reference.md | 19 +++-- klippy/extras/mcp4018.py | 69 ++----------------- 5 files changed, 38 insertions(+), 94 deletions(-) diff --git a/config/generic-mightyboard.cfg b/config/generic-mightyboard.cfg index 85dbe54da..13a63f450 100644 --- a/config/generic-mightyboard.cfg +++ b/config/generic-mightyboard.cfg @@ -89,32 +89,32 @@ max_z_velocity: 5 max_z_accel: 100 [mcp4018 x_axis_pot] -scl_pin: PJ5 -sda_pin: PF3 +i2c_software_scl_pin: PJ5 +i2c_software_sda_pin: PF3 wiper: 0.50 scale: 0.773 [mcp4018 y_axis_pot] -scl_pin: PJ5 -sda_pin: PF7 +i2c_software_scl_pin: PJ5 +i2c_software_sda_pin: PF7 wiper: 0.50 scale: 0.773 [mcp4018 z_axis_pot] -scl_pin: PJ5 -sda_pin: PK3 +i2c_software_scl_pin: PJ5 +i2c_software_sda_pin: PK3 wiper: 0.50 scale: 0.773 [mcp4018 a_axis_pot] -scl_pin: PJ5 -sda_pin: PA5 +i2c_software_scl_pin: PJ5 +i2c_software_sda_pin: PA5 wiper: 0.50 scale: 0.773 [mcp4018 b_axis_pot] -scl_pin: PJ5 -sda_pin: PJ6 +i2c_software_scl_pin: PJ5 +i2c_software_sda_pin: PJ6 wiper: 0.50 scale: 0.773 diff --git a/config/printer-flashforge-creator-pro-2018.cfg b/config/printer-flashforge-creator-pro-2018.cfg index 072f75afb..1ed04313a 100644 --- a/config/printer-flashforge-creator-pro-2018.cfg +++ b/config/printer-flashforge-creator-pro-2018.cfg @@ -127,32 +127,32 @@ max_z_velocity: 5 max_z_accel: 100 [mcp4018 x_axis_pot] -scl_pin: PJ5 -sda_pin: PF3 +i2c_software_scl_pin: PJ5 +i2c_software_sda_pin: PF3 wiper: 118 scale: 127 [mcp4018 y_axis_pot] -scl_pin: PJ5 -sda_pin: PF7 +i2c_software_scl_pin: PJ5 +i2c_software_sda_pin: PF7 wiper: 118 scale: 127 [mcp4018 z_axis_pot] -scl_pin: PJ5 -sda_pin: PK3 +i2c_software_scl_pin: PJ5 +i2c_software_sda_pin: PK3 wiper: 40 scale: 127 [mcp4018 a_axis_pot] -scl_pin: PJ5 -sda_pin: PA5 +i2c_software_scl_pin: PJ5 +i2c_software_sda_pin: PA5 wiper: 118 scale: 127 [mcp4018 b_axis_pot] -scl_pin: PJ5 -sda_pin: PJ6 +i2c_software_scl_pin: PJ5 +i2c_software_sda_pin: PJ6 wiper: 118 scale: 127 diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 001791929..559c84f8b 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,8 +8,8 @@ All dates in this document are approximate. ## Changes -20250721: The `[pca9632]` module no longer accepts the `scl_pin` and -`sda_pin` options. Use `i2c_software_scl_pin` and +20250721: The `[pca9632]` and `[mcp4018]` modules no longer accept the +`scl_pin` and `sda_pin` options. Use `i2c_software_scl_pin` and `i2c_software_sda_pin` instead. 20250428: The maximum `cycle_time` for pwm `[output_pin]`, diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index bc99bd94b..3fd035a65 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -4392,16 +4392,21 @@ prefix). ### [mcp4018] -Statically configured MCP4018 digipot connected via two gpio "bit -banging" pins (one may define any number of sections with an "mcp4018" -prefix). +Statically configured MCP4018 digipot connected via i2c (one may +define any number of sections with an "mcp4018" prefix). ``` [mcp4018 my_digipot] -scl_pin: -# The SCL "clock" pin. This parameter must be provided. -sda_pin: -# The SDA "data" pin. This parameter must be provided. +#i2c_address: 47 +# The i2c address that the chip is using on the i2c bus. The default +# is 47. +#i2c_mcu: +#i2c_bus: +#i2c_software_scl_pin: +#i2c_software_sda_pin: +#i2c_speed: +# See the "common I2C settings" section for a description of the +# above parameters. wiper: # The value to statically set the given MCP4018 "wiper" to. This is # typically set to a number between 0.0 and 1.0 with 1.0 being the diff --git a/klippy/extras/mcp4018.py b/klippy/extras/mcp4018.py index c7d3d3123..2f0f39891 100644 --- a/klippy/extras/mcp4018.py +++ b/klippy/extras/mcp4018.py @@ -1,75 +1,14 @@ -# MCP4018 digipot support (via bit-banging) +# MCP4018 digipot support # -# Copyright (C) 2019 Kevin O'Connor +# Copyright (C) 2019-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. - -class SoftwareI2C: - def __init__(self, config, addr): - self.addr = addr << 1 - self.update_pin_cmd = None - # Lookup pins - ppins = config.get_printer().lookup_object('pins') - scl_pin = config.get('scl_pin') - scl_params = ppins.lookup_pin(scl_pin, share_type='sw_scl') - self.mcu = scl_params['chip'] - self.scl_pin = scl_params['pin'] - self.scl_main = scl_params.get('class') - if self.scl_main is None: - self.scl_main = scl_params['class'] = self - self.scl_oid = self.mcu.create_oid() - self.cmd_queue = self.mcu.alloc_command_queue() - self.mcu.register_config_callback(self.build_config) - else: - self.scl_oid = self.scl_main.scl_oid - self.cmd_queue = self.scl_main.cmd_queue - sda_params = ppins.lookup_pin(config.get('sda_pin')) - self.sda_oid = self.mcu.create_oid() - if sda_params['chip'] != self.mcu: - raise ppins.error("%s: scl_pin and sda_pin must be on same mcu" % ( - config.get_name(),)) - self.mcu.add_config_cmd("config_digital_out oid=%d pin=%s" - " value=%d default_value=%d max_duration=%d" % ( - self.sda_oid, sda_params['pin'], 1, 1, 0)) - def get_mcu(self): - return self.mcu - def build_config(self): - self.mcu.add_config_cmd("config_digital_out oid=%d pin=%s value=%d" - " default_value=%d max_duration=%d" % ( - self.scl_oid, self.scl_pin, 1, 1, 0)) - self.update_pin_cmd = self.mcu.lookup_command( - "update_digital_out oid=%c value=%c", cq=self.cmd_queue) - def i2c_write(self, msg, minclock=0, reqclock=0): - msg = [self.addr] + msg - send = self.scl_main.update_pin_cmd.send - # Send ack - send([self.sda_oid, 0], minclock=minclock, reqclock=reqclock) - send([self.scl_oid, 0], minclock=minclock, reqclock=reqclock) - # Send bytes - sda_last = 0 - for data in msg: - # Transmit 8 data bits - for i in range(8): - sda_next = not not (data & (0x80 >> i)) - if sda_last != sda_next: - sda_last = sda_next - send([self.sda_oid, sda_last], - minclock=minclock, reqclock=reqclock) - send([self.scl_oid, 1], minclock=minclock, reqclock=reqclock) - send([self.scl_oid, 0], minclock=minclock, reqclock=reqclock) - # Transmit clock for ack - send([self.scl_oid, 1], minclock=minclock, reqclock=reqclock) - send([self.scl_oid, 0], minclock=minclock, reqclock=reqclock) - # Send stop - if sda_last: - send([self.sda_oid, 0], minclock=minclock, reqclock=reqclock) - send([self.scl_oid, 1], minclock=minclock, reqclock=reqclock) - send([self.sda_oid, 1], minclock=minclock, reqclock=reqclock) +from . import bus class mcp4018: def __init__(self, config): self.printer = config.get_printer() - self.i2c = SoftwareI2C(config, 0x2f) + self.i2c = bus.MCU_I2C_from_config(config, default_addr=0x2f) self.scale = config.getfloat('scale', 1., above=0.) self.start_value = config.getfloat('wiper', minval=0., maxval=self.scale) From b761b8c65439262ec94572e09d38ae9024869f01 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 14 Jul 2025 14:00:09 -0400 Subject: [PATCH 023/411] i2c_software: Implement regular timing even on AVR chips Signed-off-by: Kevin O'Connor --- src/i2c_software.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/i2c_software.c b/src/i2c_software.c index 01fce4c6d..d2072a568 100644 --- a/src/i2c_software.c +++ b/src/i2c_software.c @@ -38,13 +38,6 @@ DECL_COMMAND(command_i2c_set_sw_bus, "i2c_set_sw_bus oid=%c scl_pin=%u sda_pin=%u" " pulse_ticks=%u address=%u"); -// The AVR micro-controllers require specialized timing -#if CONFIG_MACH_AVR - -#define i2c_delay(ticks) (void)(ticks) - -#else - static void i2c_delay(uint32_t ticks) { @@ -53,8 +46,6 @@ i2c_delay(uint32_t ticks) ; } -#endif - static void i2c_software_send_ack(struct i2c_software *is, const uint8_t ack) { From 3219712c1715f5d84f23b5b7fc406812bc2bc6db Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 14 Jul 2025 14:39:15 -0400 Subject: [PATCH 024/411] i2c_software: Place wires in high impedance state after setup Don't leave the wires in a high output state during setup - leave them in a high-impedance with pullup state. Signed-off-by: Kevin O'Connor --- src/i2c_software.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i2c_software.c b/src/i2c_software.c index d2072a568..ffbf754f0 100644 --- a/src/i2c_software.c +++ b/src/i2c_software.c @@ -28,10 +28,10 @@ command_i2c_set_sw_bus(uint32_t *args) struct i2c_software *is = alloc_chunk(sizeof(*is)); is->ticks = args[3]; is->addr = (args[4] & 0x7f) << 1; // address format shifted - is->scl_in = gpio_in_setup(args[1], 1); is->scl_out = gpio_out_setup(args[1], 1); - is->sda_in = gpio_in_setup(args[2], 1); + is->scl_in = gpio_in_setup(args[1], 1); is->sda_out = gpio_out_setup(args[2], 1); + is->sda_in = gpio_in_setup(args[2], 1); i2cdev_set_software_bus(i2c, is); } DECL_COMMAND(command_i2c_set_sw_bus, From 116b304541f75a7975e7cdfb0689e673a9782d09 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 14 Jul 2025 14:58:49 -0400 Subject: [PATCH 025/411] avr: Switch to input state prior to enabling pullup in gpio_in_reset() If switching a pin from output low to input with pullup, there is an intermediate state of either driven high or high impedance without a pullup. Similarly, when switching from output high to input without a pullup, there is an intermediate state of either driven low or high impedence with a pullup. In both cases it is preferable for the latter transition. Also, calculate the final setting prior to making any changes to reduce the time in that intermediate state. Signed-off-by: Kevin O'Connor --- src/avr/gpio.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/avr/gpio.c b/src/avr/gpio.c index d10fc2a19..f52251770 100644 --- a/src/avr/gpio.c +++ b/src/avr/gpio.c @@ -70,8 +70,10 @@ void gpio_out_reset(struct gpio_out g, uint8_t val) { irqstatus_t flag = irq_save(); - g.regs->out = val ? (g.regs->out | g.bit) : (g.regs->out & ~g.bit); - g.regs->mode |= g.bit; + uint8_t newmode = g.regs->mode | g.bit; + uint8_t newout = val ? (g.regs->out | g.bit) : (g.regs->out & ~g.bit); + g.regs->out = newout; + g.regs->mode = newmode; irq_restore(flag); } @@ -114,8 +116,10 @@ void gpio_in_reset(struct gpio_in g, int8_t pull_up) { irqstatus_t flag = irq_save(); - g.regs->out = pull_up > 0 ? (g.regs->out | g.bit) : (g.regs->out & ~g.bit); - g.regs->mode &= ~g.bit; + uint8_t newout = pull_up>0 ? (g.regs->out | g.bit) : (g.regs->out & ~g.bit); + uint8_t newmode = g.regs->mode & ~g.bit; + g.regs->mode = newmode; + g.regs->out = newout; irq_restore(flag); } From ef4c76fe9494d0e011d6da5610fa868d2e702fff Mon Sep 17 00:00:00 2001 From: Paul Arthur Date: Tue, 22 Jul 2025 15:34:00 +0000 Subject: [PATCH 026/411] safe_z_home: correct error call Signed-off-by: Paul Arthur --- klippy/extras/safe_z_home.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/safe_z_home.py b/klippy/extras/safe_z_home.py index 1693487a6..64b1ab006 100644 --- a/klippy/extras/safe_z_home.py +++ b/klippy/extras/safe_z_home.py @@ -14,7 +14,7 @@ class SafeZHoming: self.z_hop_speed = config.getfloat('z_hop_speed', 15., above=0.) zconfig = manual_probe.lookup_z_endstop_config(config) if zconfig is None: - raise gcmd.error('Missing Z endstop config for safe_z_homing') + raise config.error('Missing Z endstop config for safe_z_homing') self.max_z = zconfig.getfloat('position_max', note_valid=False) self.speed = config.getfloat('speed', 50.0, above=0.) self.move_to_previous = config.getboolean('move_to_previous', False) From 60879fd298fe3b4f3dda63561e6a1c9bdc5ee9af Mon Sep 17 00:00:00 2001 From: Thijs Triemstra Date: Fri, 25 Jul 2025 18:31:19 +0200 Subject: [PATCH 027/411] klippy: fix typos in python code (#6989) Signed-off-by: Thijs Triemstra --- klippy/chelper/__init__.py | 2 +- klippy/extras/angle.py | 2 +- klippy/extras/bed_mesh.py | 10 +++++----- klippy/extras/display/__init__.py | 2 +- klippy/extras/display/font8x14.py | 2 +- klippy/extras/firmware_retraction.py | 2 +- klippy/extras/htu21d.py | 2 +- klippy/extras/load_cell.py | 2 +- klippy/extras/palette2.py | 2 +- klippy/extras/quad_gantry_level.py | 2 +- klippy/extras/resonance_tester.py | 4 ++-- klippy/extras/sht3x.py | 4 ++-- klippy/extras/smart_effector.py | 2 +- klippy/extras/sos_filter.py | 2 +- klippy/extras/temperature_probe.py | 8 ++++---- klippy/extras/thermistor.py | 4 ++-- klippy/stepper.py | 2 +- klippy/webhooks.py | 2 +- scripts/filter_workbench.ipynb | 12 ++++++------ scripts/flash-sdcard.sh | 2 +- scripts/graph_shaper.py | 4 ++-- scripts/motan/readlog.py | 2 +- scripts/spi_flash/spi_flash.py | 8 ++++---- scripts/update_chitu.py | 2 +- 24 files changed, 43 insertions(+), 43 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 671a5d2bb..2aed107a4 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -274,7 +274,7 @@ FFI_main = None FFI_lib = None pyhelper_logging_callback = None -# Hepler invoked from C errorf() code to log errors +# Helper invoked from C errorf() code to log errors def logging_callback(msg): logging.error(FFI_main.string(msg)) diff --git a/klippy/extras/angle.py b/klippy/extras/angle.py index 3b1f323e9..73af67ca1 100644 --- a/klippy/extras/angle.py +++ b/klippy/extras/angle.py @@ -97,7 +97,7 @@ class AngleCalibration: return None return self.mcu_stepper.mcu_to_commanded_position(self.mcu_pos_offset) def load_calibration(self, angles): - # Calculate linear intepolation calibration buckets by solving + # Calculate linear interpolation calibration buckets by solving # linear equations angle_max = 1 << ANGLE_BITS calibration_count = 1 << CALIBRATION_BITS diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 3f0498ffe..a8e5764c0 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -34,7 +34,7 @@ def constrain(val, min_val, max_val): def lerp(t, v0, v1): return (1. - t) * v0 + t * v1 -# retreive commma separated pair from config +# retrieve comma separated pair from config def parse_config_pair(config, option, default, minval=None, maxval=None): pair = config.getintlist(option, (default, default)) if len(pair) != 2: @@ -54,7 +54,7 @@ def parse_config_pair(config, option, default, minval=None, maxval=None): % (option, str(maxval))) return pair -# retreive commma separated pair from a g-code command +# retrieve comma separated pair from a g-code command def parse_gcmd_pair(gcmd, name, minval=None, maxval=None): try: pair = [int(v.strip()) for v in gcmd.get(name).split(',')] @@ -74,7 +74,7 @@ def parse_gcmd_pair(gcmd, name, minval=None, maxval=None): % (name, maxval)) return pair -# retreive commma separated coordinate from a g-code command +# retrieve comma separated coordinate from a g-code command def parse_gcmd_coord(gcmd, name): try: v1, v2 = [float(v.strip()) for v in gcmd.get(name).split(',')] @@ -914,7 +914,7 @@ class ProbeManager: for i in range(y_cnt): for j in range(x_cnt): if not i % 2: - # move in positive directon + # move in positive direction pos_x = min_x + j * x_dist else: # move in negative direction @@ -1164,7 +1164,7 @@ class ProbeManager: def _gen_arc(self, origin, radius, start, step, count): end = start + step * count - # create a segent for every 3 degress of travel + # create a segent for every 3 degrees of travel for angle in range(start, end, step): rad = math.radians(angle % 360) opp = math.sin(rad) * radius diff --git a/klippy/extras/display/__init__.py b/klippy/extras/display/__init__.py index 1cea00e02..da0e861cf 100644 --- a/klippy/extras/display/__init__.py +++ b/klippy/extras/display/__init__.py @@ -12,7 +12,7 @@ def load_config_prefix(config): if not config.has_section('display'): raise config.error( "A primary [display] section must be defined in printer.cfg " - "to use auxilary displays") + "to use auxiliary displays") name = config.get_name().split()[-1] if name == "display": raise config.error( diff --git a/klippy/extras/display/font8x14.py b/klippy/extras/display/font8x14.py index 3d16ee25b..66592edb5 100644 --- a/klippy/extras/display/font8x14.py +++ b/klippy/extras/display/font8x14.py @@ -13,7 +13,7 @@ # ftp://ftp.simtel.net/pub/simtelnet/msdos/screen/fntcol16.zip # (c) Joseph Gil # -# Indivdual fonts are public domain +# Individual fonts are public domain ###################################################################### VGA_FONT = [ diff --git a/klippy/extras/firmware_retraction.py b/klippy/extras/firmware_retraction.py index bcecf8f26..da0d67aff 100644 --- a/klippy/extras/firmware_retraction.py +++ b/klippy/extras/firmware_retraction.py @@ -43,7 +43,7 @@ class FirmwareRetraction: self.unretract_length = (self.retract_length + self.unretract_extra_length) self.is_retracted = False - cmd_GET_RETRACTION_help = ("Report firmware retraction paramters") + cmd_GET_RETRACTION_help = ("Report firmware retraction parameters") def cmd_GET_RETRACTION(self, gcmd): gcmd.respond_info("RETRACT_LENGTH=%.5f RETRACT_SPEED=%.5f" " UNRETRACT_EXTRA_LENGTH=%.5f UNRETRACT_SPEED=%.5f" diff --git a/klippy/extras/htu21d.py b/klippy/extras/htu21d.py index 01b3750dd..688338d9a 100644 --- a/klippy/extras/htu21d.py +++ b/klippy/extras/htu21d.py @@ -158,7 +158,7 @@ class HTU21D: def _sample_htu21d(self, eventtime): try: - # Read Temeprature + # Read Temperature if self.hold_master_mode: params = self.i2c.i2c_write([HTU21D_COMMANDS['HTU21D_TEMP']]) else: diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py index 4117a92c0..5ef2c5b77 100644 --- a/klippy/extras/load_cell.py +++ b/klippy/extras/load_cell.py @@ -53,7 +53,7 @@ class ApiClientHelper(object): wh = self.printer.lookup_object('webhooks') wh.register_mux_endpoint(path, key, value, self._add_webhooks_client) -# Class for handling commands related ot load cells +# Class for handling commands related to load cells class LoadCellCommandHelper: def __init__(self, config, load_cell): self.printer = config.get_printer() diff --git a/klippy/extras/palette2.py b/klippy/extras/palette2.py index ec8631b0a..4fe720808 100644 --- a/klippy/extras/palette2.py +++ b/klippy/extras/palette2.py @@ -235,7 +235,7 @@ class Palette2: "Initialize the print, and check connection with the Palette 2") def cmd_O1(self, gcmd): - logging.info("Initializing print with Pallete 2") + logging.info("Initializing print with Palette 2") if not self._check_P2(gcmd): raise self.printer.command_error( "Cannot initialize print, palette 2 is not connected") diff --git a/klippy/extras/quad_gantry_level.py b/klippy/extras/quad_gantry_level.py index 7c6febcb1..98cd53c5a 100644 --- a/klippy/extras/quad_gantry_level.py +++ b/klippy/extras/quad_gantry_level.py @@ -1,4 +1,4 @@ -# Mechanicaly conforms a moving gantry to the bed with 4 Z steppers +# Mechanically conforms a moving gantry to the bed with 4 Z steppers # # Copyright (C) 2018 Maks Zolin # diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index a4d3876f8..ff32dcacf 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -295,7 +295,7 @@ class ResonanceTester: return parsed_chips def _get_max_calibration_freq(self): return 1.5 * self.generator.get_max_freq() - cmd_TEST_RESONANCES_help = ("Runs the resonance test for a specifed axis") + cmd_TEST_RESONANCES_help = ("Runs the resonance test for a specified axis") def cmd_TEST_RESONANCES(self, gcmd): # Parse parameters axis = _parse_axis(gcmd, gcmd.get("AXIS").lower()) @@ -345,7 +345,7 @@ class ResonanceTester: gcmd.respond_info( "Resonances data written to %s file" % (csv_name,)) cmd_SHAPER_CALIBRATE_help = ( - "Simular to TEST_RESONANCES but suggest input shaper config") + "Similar to TEST_RESONANCES but suggest input shaper config") def cmd_SHAPER_CALIBRATE(self, gcmd): # Parse parameters axis = gcmd.get("AXIS", None) diff --git a/klippy/extras/sht3x.py b/klippy/extras/sht3x.py index 79cd7bfe3..c76ceb767 100644 --- a/klippy/extras/sht3x.py +++ b/klippy/extras/sht3x.py @@ -100,12 +100,12 @@ class SHT3X: self.i2c.i2c_write_wait_ack( SHT3X_CMD['PERIODIC']['2HZ']['HIGH_REP'] ) - # Wait <=15.5ms for first measurment + # Wait <=15.5ms for first measurement self.reactor.pause(self.reactor.monotonic() + .0155) def _sample_sht3x(self, eventtime): try: - # Read measurment + # Read measurement retries = 5 params = None error = None diff --git a/klippy/extras/smart_effector.py b/klippy/extras/smart_effector.py index 4b90c309f..3caa4e6b7 100644 --- a/klippy/extras/smart_effector.py +++ b/klippy/extras/smart_effector.py @@ -129,7 +129,7 @@ class SmartEffectorProbe: start_time = toolhead.get_last_move_time() # Write generated bits to the control pin end_time = self.control_pin.write_bits(start_time, bit_stream) - # Dwell to make sure no subseqent actions are queued together + # Dwell to make sure no subsequent actions are queued together # with the SmartEffector programming toolhead.dwell(end_time - start_time) toolhead.wait_moves() diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index 8c405782d..293b2258a 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -116,7 +116,7 @@ class FixedPointSosFilter: if col != 3: # omit column 3 fixed_coeff = to_fixed_32(coeff, self._coeff_int_bits) fixed_section.append(fixed_coeff) - elif coeff != 1.0: # double check colum 3 is always 1.0 + elif coeff != 1.0: # double check column 3 is always 1.0 raise ValueError("Coefficient 3 is expected to be 1.0" " but was %f" % (coeff,)) sos_fixed.append(fixed_section) diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py index 05eac34ef..c480ddae8 100644 --- a/klippy/extras/temperature_probe.py +++ b/klippy/extras/temperature_probe.py @@ -408,7 +408,7 @@ class TemperatureProbe: except self.printer.command_error: self._finalize_drift_cal(False, "Error during initial move") raise - # Caputure start position and begin initial probe + # Capture start position and begin initial probe toolhead = self.printer.lookup_object("toolhead") self.start_pos = toolhead.get_position()[:2] manual_probe.ManualProbeHelper( @@ -637,7 +637,7 @@ class EddyDriftCompensation: gcode = self.printer.lookup_object("gcode") if len(cal_samples) < 3: raise gcode.error( - "calbration error, not enough samples" + "calibration error, not enough samples" ) min_temp, _ = cal_samples[0][0] max_temp, _ = cal_samples[-1][0] @@ -687,7 +687,7 @@ class EddyDriftCompensation: return self._calc_freq(freq, origin_temp, self.cal_temp) def unadjust_freq(self, freq, dest_temp=None): - # Given a frequency and its orignal sampled temp, find the + # Given a frequency and its original sampled temp, find the # offset frequency based on the current temp if not self.enabled or freq < self.min_freq: return freq @@ -703,7 +703,7 @@ class EddyDriftCompensation: low_freq = poly(origin_temp) if freq >= low_freq: if high_freq is None: - # Freqency above max calibration value + # Frequency above max calibration value err = poly(dest_temp) - low_freq return freq + err t = min(1., max(0., (freq - low_freq) / (high_freq - low_freq))) diff --git a/klippy/extras/thermistor.py b/klippy/extras/thermistor.py index 16d6ca61b..db26b50c8 100644 --- a/klippy/extras/thermistor.py +++ b/klippy/extras/thermistor.py @@ -15,7 +15,7 @@ class Thermistor: self.inline_resistor = inline_resistor self.c1 = self.c2 = self.c3 = 0. def setup_coefficients(self, t1, r1, t2, r2, t3, r3, name=""): - # Calculate Steinhart-Hart coefficents from temp measurements. + # Calculate Steinhart-Hart coefficients from temp measurements. # Arrange samples as 3 linear equations and solve for c1, c2, and c3. inv_t1 = 1. / (t1 - KELVIN_TO_CELSIUS) inv_t2 = 1. / (t2 - KELVIN_TO_CELSIUS) @@ -40,7 +40,7 @@ class Thermistor: self.c2 = (inv_t12 - self.c3 * ln3_r12) / ln_r12 self.c1 = inv_t1 - self.c2 * ln_r1 - self.c3 * ln3_r1 def setup_coefficients_beta(self, t1, r1, beta): - # Calculate equivalent Steinhart-Hart coefficents from beta + # Calculate equivalent Steinhart-Hart coefficients from beta inv_t1 = 1. / (t1 - KELVIN_TO_CELSIUS) ln_r1 = math.log(r1) self.c3 = 0. diff --git a/klippy/stepper.py b/klippy/stepper.py index 09f3dad9b..86a923581 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -86,7 +86,7 @@ class MCU_stepper: if self._step_pulse_duration > MIN_BOTH_EDGE_DURATION: # If user has requested a very large step pulse duration # then disable step on both edges (rise and fall times may - # not be symetric) + # not be symmetric) want_both_edges = False elif sbe and self._step_pulse_duration>MIN_OPTIMIZED_BOTH_EDGE_DURATION: # Older MCU and user has requested large pulse duration diff --git a/klippy/webhooks.py b/klippy/webhooks.py index ddba60d92..9e17177af 100644 --- a/klippy/webhooks.py +++ b/klippy/webhooks.py @@ -12,7 +12,7 @@ except ImportError: import json # Json decodes strings as unicode types in Python 2.x. This doesn't - # play well with some parts of Klipper (particuarly displays), so we + # play well with some parts of Klipper (particularly displays), so we # need to create an object hook. This solution borrowed from: # # https://stackoverflow.com/questions/956867/ diff --git a/scripts/filter_workbench.ipynb b/scripts/filter_workbench.ipynb index e912ee868..1e1adfe77 100644 --- a/scripts/filter_workbench.ipynb +++ b/scripts/filter_workbench.ipynb @@ -69,7 +69,7 @@ "metadata": {}, "source": [ "## Sensor Information\n", - "Fill out the sensor's sample frequency, in samples per second, below. This must match the frquency of the captured data in `probe_data`." + "Fill out the sensor's sample frequency, in samples per second, below. This must match the frequency of the captured data in `probe_data`." ] }, { @@ -104,7 +104,7 @@ "metadata": {}, "outputs": [], "source": [ - "# set filter settig here. If you dont want a filter to be used, assign `None` to it\n", + "# set filter setting here. If you dont want a filter to be used, assign `None` to it\n", "drift_filter_cutoff_frequency = 0.8\n", "buzz_filter_cutoff_frequency = 150.0\n", "notch_filter_frequencies = [50.0, 60.0]\n", @@ -182,7 +182,7 @@ "### Calculate Filter Initial Consitions (zi)\n", "This block automatically calculates the filters starting conditions using `signal.sosfilt_zi`.\n", "\n", - "On the MCU, the tare functionality zeros out the grams value so the inital zi matrix doesnt have to be calculated and there will be no filter settling time." + "On the MCU, the tare functionality zeros out the grams value so the initial zi matrix doesnt have to be calculated and there will be no filter settling time." ] }, { @@ -214,7 +214,7 @@ "metadata": {}, "source": [ "## Display Filter Frequency Response\n", - "This block plots the filter's response to sinals from 0 up to the [nyquist frequency]() which is 1/2 the `sensor_frequency`. " + "This block plots the filter's response to signals from 0 up to the [nyquist frequency]() which is 1/2 the `sensor_frequency`. " ] }, { @@ -260,7 +260,7 @@ "## Test Filter Performance\n", "Test `filter_design` on `probe_data`. This shows plots of the filters performance when filtering the force graph in `probe_data`.\n", "\n", - "To help decorate the graphs please prvide the desired trigger threashold of the probe:" + "To help decorate the graphs please provide the desired trigger threshold of the probe:" ] }, { @@ -339,7 +339,7 @@ "metadata": {}, "source": [ "#### `force` vs force filtered with `filter_design`\n", - "In this graph we are looking to see the 'Filtered Force' line remain inside the red lines denoting the trigger force. A Properly filtered force grpah will be horizontal." + "In this graph we are looking to see the 'Filtered Force' line remain inside the red lines denoting the trigger force. A Properly filtered force graph will be horizontal." ] }, { diff --git a/scripts/flash-sdcard.sh b/scripts/flash-sdcard.sh index 088864091..6a08eab14 100755 --- a/scripts/flash-sdcard.sh +++ b/scripts/flash-sdcard.sh @@ -1,5 +1,5 @@ #!/bin/bash -# This script launches flash_sdcard.py, a utitlity that enables +# This script launches flash_sdcard.py, a utility that enables # unattended firmware updates on boards with "SD Card" bootloaders # Non-standard installations may need to change this location diff --git a/scripts/graph_shaper.py b/scripts/graph_shaper.py index 0ea194589..b9a6627c8 100755 --- a/scripts/graph_shaper.py +++ b/scripts/graph_shaper.py @@ -20,7 +20,7 @@ SHAPER_DAMPING_RATIO=0.1 STEP_SIMULATION_RESONANCE_FREQ=60. STEP_SIMULATION_DAMPING_RATIO=0.15 -# If set, defines which range of frequencies to plot shaper frequency responce +# If set, defines which range of frequencies to plot shaper frequency response PLOT_FREQ_RANGE = [] # If empty, will be automatically determined #PLOT_FREQ_RANGE = [10., 100.] @@ -159,7 +159,7 @@ def find_shaper_plot_range(shaper, vib_tol): return (left, right) def gen_shaper_response(shaper): - # Calculate shaper vibration responce on a range of requencies + # Calculate shaper vibration response on a range of frequencies response = [] freqs = [] freq, freq_end = find_shaper_plot_range(shaper, vib_tol=0.25) diff --git a/scripts/motan/readlog.py b/scripts/motan/readlog.py index 4c078c9bc..43c01619b 100644 --- a/scripts/motan/readlog.py +++ b/scripts/motan/readlog.py @@ -234,7 +234,7 @@ class HandleStepQ: step_data.append((step_time, step_halfpos, step_pos)) LogHandlers["stepq"] = HandleStepQ -# Extract tmc currect and stallguard data from the log +# Extract tmc current and stallguard data from the log class HandleStallguard: SubscriptionIdParts = 2 ParametersMin = 2 diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index 2a4b48ade..e45e0b7c7 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -46,7 +46,7 @@ def calc_crc7(data, with_padding=True): crc ^= b & 0xFF for i in range(8): crc = (crc << 1) ^ poly if crc & 0x80 else crc << 1 - # The sdcard protocol likes the crc left justfied with a + # The sdcard protocol likes the crc left justified with a # padded bit if not with_padding: return crc @@ -566,7 +566,7 @@ class SDCardSPI: # At this time MMC is not supported if len(resp) == 5: if self.sd_version == 1 and resp[0] == 1: - # Check acceptable volatage range for V1 cards + # Check acceptable voltage range for V1 cards if resp[2] != 0xFF: raise OSError("flash_sdcard: card does not support" " 3.3v range") @@ -903,7 +903,7 @@ class SDCardSDIO: " out of IDLE after reset") if len(resp) == 4: if self.sd_version == 1: - # Check acceptable volatage range for V1 cards + # Check acceptable voltage range for V1 cards if resp[1] != 0xFF: raise OSError("flash_sdcard: card does not support" " 3.3v range") @@ -1643,7 +1643,7 @@ def main(): logging.basicConfig(level=log_level) flash_args = board_defs.lookup_board(args.board) if flash_args is None: - output_line("Unable to find defintion for board: %s" % (args.board,)) + output_line("Unable to find definition for board: %s" % (args.board,)) sys.exit(-1) flash_args['device'] = args.device flash_args['baud'] = args.baud diff --git a/scripts/update_chitu.py b/scripts/update_chitu.py index a1ef4d8bd..cf7fcfe9d 100755 --- a/scripts/update_chitu.py +++ b/scripts/update_chitu.py @@ -76,7 +76,7 @@ def encode_file(input, output_file, file_length): xor_crc = 0xef3d4323; - # the input file is exepcted to be in chunks of 0x800 + # the input file is expected to be in chunks of 0x800 # so round the size while len(input_file) % block_size != 0: input_file.extend(b'0x0') From 4a567c8d1050d95ea22019545877715b842fa1b3 Mon Sep 17 00:00:00 2001 From: Sasquatch Date: Sun, 27 Jul 2025 17:09:11 +0100 Subject: [PATCH 028/411] spi_flash: FATFS fix missing long filenames support needed by flash-sdcard.sh (#6981) enable long file support, needed for boards using swspi and long filenames for firmware like mks robin 1.1/1.2 added MKS robin nano 1.2 board with description what and why Signed-off-by: Leszek Zajac --- lib/fatfs/ffconf.h | 2 +- scripts/spi_flash/board_defs.py | 11 +++++++++++ scripts/spi_flash/fatfs_api.h | 3 ++- scripts/spi_flash/fatfs_lib.py | 3 ++- scripts/spi_flash/spi_flash.py | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/fatfs/ffconf.h b/lib/fatfs/ffconf.h index c47808bbc..34522e7eb 100644 --- a/lib/fatfs/ffconf.h +++ b/lib/fatfs/ffconf.h @@ -97,7 +97,7 @@ */ -#define FF_USE_LFN 0 +#define FF_USE_LFN 1 #define FF_MAX_LFN 255 /* The FF_USE_LFN switches the support for LFN (long file name). / diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index bc0c3d577..fe6ec9ab9 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -50,6 +50,17 @@ BOARD_DEFS = { "firmware_path": "Robin_e3.bin", "current_firmware_path": "Robin_e3.cur" }, + # twotrees sapphire 5 v1.1 using mks robin nano 1.2 board + 'mks-robin-v12': { + 'mcu': "stm32f103xe", + 'spi_bus': "swspi", + 'spi_pins': "PC8,PD2,PC12", + 'cs_pin': "PC11", + 'skip_verify': True, + "conversion_script": "scripts/update_mks_robin.py", + "firmware_path": "ROBIN_NANO35.BIN", + "current_firmware_path": "ROBIN_NANO35.BIN" + }, 'btt-octopus-f407-v1': { 'mcu': "stm32f407xx", 'spi_bus': "swspi", diff --git a/scripts/spi_flash/fatfs_api.h b/scripts/spi_flash/fatfs_api.h index 2400bc860..adbf75886 100644 --- a/scripts/spi_flash/fatfs_api.h +++ b/scripts/spi_flash/fatfs_api.h @@ -26,7 +26,8 @@ struct ff_file_info { uint16_t modified_date; uint16_t modified_time; uint8_t attrs; - char name[13]; + char name_sfn[13]; // unused, just for compatibility with fatfs lib + char name[256]; }; struct ff_disk_info { diff --git a/scripts/spi_flash/fatfs_lib.py b/scripts/spi_flash/fatfs_lib.py index 2f27c083a..ef92070ab 100644 --- a/scripts/spi_flash/fatfs_lib.py +++ b/scripts/spi_flash/fatfs_lib.py @@ -32,7 +32,8 @@ FATFS_CDEFS = """ uint16_t modified_date; uint16_t modified_time; uint8_t attrs; - char name[13]; + char name_sfn[13]; + char name[256]; }; struct ff_disk_info { diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index e45e0b7c7..729dd2bbc 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -380,7 +380,7 @@ class FatFS: (fdate >> 5) & 0xF, fdate & 0x1F, ((fdate >> 9) & 0x7F) + 1980, (ftime >> 11) & 0x1F, (ftime >> 5) & 0x3F, ftime & 0x1F) return { - 'name': self.ffi_main.string(finfo.name, 13), + 'name': self.ffi_main.string(finfo.name, 256), 'size': finfo.size, 'modified': dstr, 'is_dir': bool(finfo.attrs & 0x10), From 6773ab074b132d6dc417e91b379d5a7b8d986b8d Mon Sep 17 00:00:00 2001 From: Thijs Triemstra Date: Sun, 27 Jul 2025 18:12:48 +0200 Subject: [PATCH 029/411] docs: Fix typos in config and docs (#6991) * fix typos in configs * fix typos in docs Signed-off-by: Thijs Triemstra --- config/generic-bigtreetech-manta-m8p-v1.0.cfg | 2 +- config/generic-bigtreetech-manta-m8p-v1.1.cfg | 2 +- config/generic-bigtreetech-octopus-max-ez.cfg | 2 +- config/generic-bigtreetech-octopus-v1.1.cfg | 2 +- config/kit-voron2-250mm.cfg | 2 +- config/kit-zav3d-2019.cfg | 2 +- config/printer-anycubic-kossel-2016.cfg | 2 +- config/printer-creality-cr10-v3-2020.cfg | 2 +- config/printer-creality-ender5-s1-2023.cfg | 2 +- config/printer-lulzbot-mini1-2016.cfg | 2 +- config/printer-lulzbot-taz6-dual-v3-2017.cfg | 2 +- config/printer-robo3d-r2-2017.cfg | 2 +- config/printer-seemecnc-rostock-max-v2-2015.cfg | 2 +- config/sample-duet3-1lc.cfg | 2 +- config/sample-mmu2s-diy.cfg | 6 +++--- docs/Bed_Mesh.md | 2 +- docs/Bootloaders.md | 2 +- docs/CONTRIBUTING.md | 2 +- docs/Config_Changes.md | 6 +++--- docs/Config_Reference.md | 6 +++--- docs/G-Codes.md | 2 +- docs/Load_Cell.md | 4 ++-- docs/Measuring_Resonances.md | 2 +- docs/Skew_Correction.md | 2 +- docs/Slicers.md | 2 +- docs/Status_Reference.md | 2 +- 26 files changed, 33 insertions(+), 33 deletions(-) diff --git a/config/generic-bigtreetech-manta-m8p-v1.0.cfg b/config/generic-bigtreetech-manta-m8p-v1.0.cfg index 2a801cded..4459076fa 100644 --- a/config/generic-bigtreetech-manta-m8p-v1.0.cfg +++ b/config/generic-bigtreetech-manta-m8p-v1.0.cfg @@ -39,7 +39,7 @@ position_max: 270 # Motor4 # The M8P only has 4 heater outputs which leaves an extra stepper # This can be used for a second Z stepper, dual_carriage, extruder co-stepper, -# or other accesory such as an MMU +# or other accessory such as an MMU #[stepper_] #step_pin: PD3 #dir_pin: PD2 diff --git a/config/generic-bigtreetech-manta-m8p-v1.1.cfg b/config/generic-bigtreetech-manta-m8p-v1.1.cfg index 36ef710cd..aadb5c453 100644 --- a/config/generic-bigtreetech-manta-m8p-v1.1.cfg +++ b/config/generic-bigtreetech-manta-m8p-v1.1.cfg @@ -40,7 +40,7 @@ position_max: 270 # Motor4 # The M8P only has 4 heater outputs which leaves an extra stepper # This can be used for a second Z stepper, dual_carriage, extruder co-stepper, -# or other accesory such as an MMU +# or other accessory such as an MMU #[stepper_] #step_pin: PD3 #dir_pin: PD2 diff --git a/config/generic-bigtreetech-octopus-max-ez.cfg b/config/generic-bigtreetech-octopus-max-ez.cfg index 60822b046..3ccc2c750 100644 --- a/config/generic-bigtreetech-octopus-max-ez.cfg +++ b/config/generic-bigtreetech-octopus-max-ez.cfg @@ -43,7 +43,7 @@ position_max: 200 # Motor-4 # The Octopus only has 4 heater outputs which leaves an extra stepper # This can be used for a second Z stepper, dual_carriage, extruder co-stepper, -# or other accesory such as an MMU +# or other accessory such as an MMU #[stepper_] #step_pin: PB8 #dir_pin: PB9 diff --git a/config/generic-bigtreetech-octopus-v1.1.cfg b/config/generic-bigtreetech-octopus-v1.1.cfg index f3a2b87c3..dcd0e97c6 100644 --- a/config/generic-bigtreetech-octopus-v1.1.cfg +++ b/config/generic-bigtreetech-octopus-v1.1.cfg @@ -52,7 +52,7 @@ position_max: 200 # Driver3 # The Octopus only has 4 heater outputs which leaves an extra stepper # This can be used for a second Z stepper, dual_carriage, extruder co-stepper, -# or other accesory such as an MMU +# or other accessory such as an MMU #[stepper_] #step_pin: PG4 #dir_pin: PC1 diff --git a/config/kit-voron2-250mm.cfg b/config/kit-voron2-250mm.cfg index 343e9c674..e82ee28a1 100644 --- a/config/kit-voron2-250mm.cfg +++ b/config/kit-voron2-250mm.cfg @@ -19,7 +19,7 @@ # FSR switch (z endstop) location [homing_override] section # FSR switch (z endstop) offset for Z0 [stepper_z] section # Probe points [quad_gantry_level] section -# Min & Max gantry corner postions [quad_gantry_level] section +# Min & Max gantry corner positions [quad_gantry_level] section # PID tune [extruder] and [heater_bed] sections # Fine tune E steps [extruder] section diff --git a/config/kit-zav3d-2019.cfg b/config/kit-zav3d-2019.cfg index e3b57b8ed..e2385c941 100644 --- a/config/kit-zav3d-2019.cfg +++ b/config/kit-zav3d-2019.cfg @@ -20,7 +20,7 @@ # FSR switch (z endstop) location [homing_override] section # FSR switch (z endstop) offset for Z0 [stepper_z] section # Probe points [quad_gantry_level] section -# Min & Max gantry corner postions [quad_gantry_level] section +# Min & Max gantry corner positions [quad_gantry_level] section # PID tune [extruder] and [heater_bed] sections # Fine tune E steps [extruder] section diff --git a/config/printer-anycubic-kossel-2016.cfg b/config/printer-anycubic-kossel-2016.cfg index 7df93b41f..1f7819f2e 100644 --- a/config/printer-anycubic-kossel-2016.cfg +++ b/config/printer-anycubic-kossel-2016.cfg @@ -17,7 +17,7 @@ endstop_pin: ^PE4 homing_speed: 60 # The next parameter needs to be adjusted for # your printer. You may want to start with 280 -# and meassure the distance from nozzle to bed. +# and measure the distance from nozzle to bed. # This value then needs to be added. position_endstop: 273.0 arm_length: 229.4 diff --git a/config/printer-creality-cr10-v3-2020.cfg b/config/printer-creality-cr10-v3-2020.cfg index 9bf84df59..af9e923ca 100644 --- a/config/printer-creality-cr10-v3-2020.cfg +++ b/config/printer-creality-cr10-v3-2020.cfg @@ -43,7 +43,7 @@ position_max: 400 #Uncomment if you have a BL-Touch: #position_min: -4 #endstop_pin: probe:z_virtual_endstop -#and comment the follwing lines: +#and comment the following lines: position_endstop: 0.0 endstop_pin: ^PD3 #ar18 diff --git a/config/printer-creality-ender5-s1-2023.cfg b/config/printer-creality-ender5-s1-2023.cfg index 68a89fa5e..17bb2a460 100644 --- a/config/printer-creality-ender5-s1-2023.cfg +++ b/config/printer-creality-ender5-s1-2023.cfg @@ -81,7 +81,7 @@ pin: PA0 kick_start_time: 0.5 # Hotend fan -# set fan runnig when extruder temperature is over 60 +# set fan running when extruder temperature is over 60 [heater_fan heatbreak_fan] pin: PC0 heater:extruder diff --git a/config/printer-lulzbot-mini1-2016.cfg b/config/printer-lulzbot-mini1-2016.cfg index 52b8061ee..cff0eee45 100644 --- a/config/printer-lulzbot-mini1-2016.cfg +++ b/config/printer-lulzbot-mini1-2016.cfg @@ -195,7 +195,7 @@ samples_tolerance: 0.200 samples_tolerance_retries: 2 [bed_tilt] -# Enable bed tilt measurments using the probe we defined above +# Enable bed tilt measurements using the probe we defined above # Probe points using X0 Y0 offsets @ 0.01mm/step points: -2, -6 156, -6 diff --git a/config/printer-lulzbot-taz6-dual-v3-2017.cfg b/config/printer-lulzbot-taz6-dual-v3-2017.cfg index f94b06846..f4b23fdb8 100644 --- a/config/printer-lulzbot-taz6-dual-v3-2017.cfg +++ b/config/printer-lulzbot-taz6-dual-v3-2017.cfg @@ -183,7 +183,7 @@ samples: 2 samples_tolerance: 0.100 [bed_tilt] -#Enable bed tilt measurments using the probe we defined above +#Enable bed tilt measurements using the probe we defined above #Probe points using X0 Y0 offsets @ 0.01mm/step points: -3, -6 282, -6 diff --git a/config/printer-robo3d-r2-2017.cfg b/config/printer-robo3d-r2-2017.cfg index a87f4befc..89fe127bf 100644 --- a/config/printer-robo3d-r2-2017.cfg +++ b/config/printer-robo3d-r2-2017.cfg @@ -37,7 +37,7 @@ microsteps: 16 rotation_distance: 4 # Required if not using probe for the virtual endstop # endstop_pin: ^PD3 -# position_endstop: 250 # Will need ajustment +# position_endstop: 250 # Will need adjustment endstop_pin: probe:z_virtual_endstop homing_speed: 10.0 position_max: 250 diff --git a/config/printer-seemecnc-rostock-max-v2-2015.cfg b/config/printer-seemecnc-rostock-max-v2-2015.cfg index 70ebd876c..09ea7bdbd 100644 --- a/config/printer-seemecnc-rostock-max-v2-2015.cfg +++ b/config/printer-seemecnc-rostock-max-v2-2015.cfg @@ -1,4 +1,4 @@ -# This file constains the pin mappings for the SeeMeCNC Rostock Max +# This file contains the pin mappings for the SeeMeCNC Rostock Max # (version 2) delta printer from 2015. To use this config, the # firmware should be compiled for the AVR atmega2560. diff --git a/config/sample-duet3-1lc.cfg b/config/sample-duet3-1lc.cfg index ef7e40428..1e0eaf10d 100644 --- a/config/sample-duet3-1lc.cfg +++ b/config/sample-duet3-1lc.cfg @@ -6,7 +6,7 @@ # Communication interface of "CAN bus (on PA25/PA24)" # To flash the board use a debugger, or use a raspberry pi and follow -# the instructions at docs/Bootloaders.md fot the SAMC21. You may +# the instructions at docs/Bootloaders.md for the SAMC21. You may # supply power to the 1LC by connecting the 3.3v rail on the Pi to the # 5v input of the SWD header on the 1LC. diff --git a/config/sample-mmu2s-diy.cfg b/config/sample-mmu2s-diy.cfg index d1d7793e9..41fe669f9 100644 --- a/config/sample-mmu2s-diy.cfg +++ b/config/sample-mmu2s-diy.cfg @@ -96,7 +96,7 @@ switch_pin: !P1.28 # P1.28 for X-max # variable_pause_z : z lift when MMU2S need intervention and the printer is paused # variable_min_temp_extruder : minimal required heater temperature to load/unload filament from the extruder gear to the nozzle # variable_extruder_eject_temp : heater temperature used to eject filament during home if the filament is already loaded -# variable_enable_5in1 : pass from MMU2S standart (0) to MMU2S-5in1 mode with splitter +# variable_enable_5in1 : pass from MMU2S standard (0) to MMU2S-5in1 mode with splitter # ################################ [gcode_macro VAR_MMU2S] @@ -394,7 +394,7 @@ gcode: {% endif %} {% endif %} -# Retry unload, try correct misalignement of bondtech gear +# Retry unload, try correct misalignment of bondtech gear [gcode_macro RETRY_UNLOAD_FILAMENT_IN_EXTRUDER] gcode: {% if printer["filament_switch_sensor ir_sensor"].filament_detected == True %} @@ -444,7 +444,7 @@ gcode: {% endif %} {% endif %} -# Ramming process for standart PLA, code extracted from slic3r gcode +# Ramming process for standard PLA, code extracted from slic3r gcode [gcode_macro RAMMING_SLICER] gcode: G91 diff --git a/docs/Bed_Mesh.md b/docs/Bed_Mesh.md index 9956de9c4..5df081d15 100644 --- a/docs/Bed_Mesh.md +++ b/docs/Bed_Mesh.md @@ -267,7 +267,7 @@ by heat or interference. This can make calculating the probe's z-offset challenging, particularly at different bed temperatures. As such, some printers use an endstop for homing the Z axis and a probe for calibrating the mesh. In this configuration it is possible offset the mesh so that the (X, Y) -`reference position` applies zero adjustment. The `reference postion` should +`reference position` applies zero adjustment. The `reference position` should be the location on the bed where a [Z_ENDSTOP_CALIBRATE](./Manual_Level.md#calibrating-a-z-endstop) paper test is performed. The bed_mesh module provides the diff --git a/docs/Bootloaders.md b/docs/Bootloaders.md index 56a810955..ee125ae1c 100644 --- a/docs/Bootloaders.md +++ b/docs/Bootloaders.md @@ -194,7 +194,7 @@ Alternatively, one can use a When using OpenOCD with the SAMC21, extra steps must be taken to first put the chip into Cold Plugging mode if the board makes use of the -SWD pins for other purposes. If using OpenOCD on a Rasberry Pi, this +SWD pins for other purposes. If using OpenOCD on a Raspberry Pi, this can be done by running the following commands before invoking OpenOCD. ``` SWCLK=25 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index db664c86e..31267b999 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -323,7 +323,7 @@ a month without updates. Once the requirements are met, you need to: -1. update klipper-tranlations repository +1. update klipper-translations repository [active_translations](https://github.com/Klipper3d/klipper-translations/blob/translations/active_translations) 2. Optional: add a manual-index.md file in klipper-translations repository's `docs\locals\` folder to replace the language specific index.md (generated diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 559c84f8b..4ab7d33d3 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -62,7 +62,7 @@ object were issued faster than the minimum scheduling time (typically 100ms) then actual updates could be queued far into the future. Now if many updates are issued in rapid succession then it is possible that only the latest request will be applied. If the previous behavior is -requried then consider adding explicit `G4` delay commands between +required then consider adding explicit `G4` delay commands between updates. 20240912: Support for `maximum_mcu_duration` and `static_value` @@ -135,7 +135,7 @@ carriage are exported as `printer.dual_carriage.carriage_0` and `printer.dual_carriage.carriage_1`. 20230619: The `relative_reference_index` option has been deprecated -and superceded by the `zero_reference_position` option. Refer to the +and superseded by the `zero_reference_position` option. Refer to the [Bed Mesh Documentation](./Bed_Mesh.md#the-deprecated-relative_reference_index) for details on how to update the configuration. With this deprecation the `RELATIVE_REFERENCE_INDEX` is no longer available as a parameter @@ -369,7 +369,7 @@ endstop phases by running the ENDSTOP_PHASE_CALIBRATE command. `gear_ratio` for their rotary steppers, and they may no longer specify a `step_distance` parameter. See the [config reference](Config_Reference.md#stepper) for the format of the -new gear_ratio paramter. +new gear_ratio parameter. 20201213: It is not valid to specify a Z "position_endstop" when using "probe:z_virtual_endstop". An error will now be raised if a Z diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 3fd035a65..a9b6db6ce 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -5180,7 +5180,7 @@ sensor_type: # load cell will be igfiltered outnored. This option requires the SciPy # library. Default: None #buzz_filter_delay: 2 -# The delay, or 'order', of the buzz filter. This controle the number of +# The delay, or 'order', of the buzz filter. This controls the number of # samples required to make a trigger detection. Can be 1 or 2, the default # is 2. #notch_filter_frequencies: 50, 60 @@ -5314,7 +5314,7 @@ chip: ADS1115 # scales all values read from the ADC. Options are: 6.144V, 4.096V, 2.048V, # 1.024V, 0.512V, 0.256V #adc_voltage: 3.3 -# The suppy voltage for the device. This allows additional software scaling +# The supply voltage for the device. This allows additional software scaling # for all values read from the ADC. i2c_mcu: host i2c_bus: i2c.1 @@ -5333,7 +5333,7 @@ sensor_pin: my_ads1x1x:AIN0 # A combination of the name of the ads1x1x chip and the pin. Possible # pin values are AIN0, AIN1, AIN2 and AIN3 for single ended lines and # DIFF01, DIFF03, DIFF13 and DIFF23 for differential between their -# correspoding lines. For example +# corresponding lines. For example # DIFF03 measures the differential between line 0 and 3. Only specific # combinations for the differentials are allowed. ``` diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 184bc66af..7a60aa8a0 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -372,7 +372,7 @@ restored and "MOVE_SPEED" is specified, then the toolhead moves will be performed with the given speed (in mm/s); otherwise the toolhead move will use the rail homing speed. Note that the carriages restore their positions only over their own axis, which may be necessary to correctly restore COPY -and MIRROR mode of the dual carraige. +and MIRROR mode of the dual carriage. ### [endstop_phase] diff --git a/docs/Load_Cell.md b/docs/Load_Cell.md index 93ed36bbc..8345cae49 100644 --- a/docs/Load_Cell.md +++ b/docs/Load_Cell.md @@ -36,7 +36,7 @@ Things you can check with this data: * 'Unique values' should be a large percentage of the 'Samples Collected' value. If 'Unique values' is 1 it is very likely a wiring issue. * Tap or push on the sensor while `LOAD_CELL_DIAGNOSTIC` runs. If - things are working correctly ths should increase the 'Sample range'. + things are working correctly this should increase the 'Sample range'. ## Calibrating a Load Cell @@ -189,7 +189,7 @@ Multiple cycles of this will result in ever-increasing force on the toolhead. `force_safety_limit` stops this cycle from running out of control. Another way this run-away can happen is damage to a strain gauge. If the metal -part is permanently bent it wil change the `reference_tare_counts` of the +part is permanently bent it will change the `reference_tare_counts` of the device. This puts the starting tare value much closer to the limit making it more likely to be violated. You want to be notified if this is happening because your hardware has been permanently damaged. diff --git a/docs/Measuring_Resonances.md b/docs/Measuring_Resonances.md index e4f9b8e95..49c050e0c 100644 --- a/docs/Measuring_Resonances.md +++ b/docs/Measuring_Resonances.md @@ -152,7 +152,7 @@ Recommended connection scheme for I2C on the Raspberry Pi: | SDA | 03 | GPIO02 (SDA1) | | SCL | 05 | GPIO03 (SCL1) | -The RPi has buit-in 1.8K pull-ups on both SCL and SDA. +The RPi has built-in 1.8K pull-ups on both SCL and SDA. ![MPU-9250 connected to Pi](img/mpu9250-PI-fritzing.png) diff --git a/docs/Skew_Correction.md b/docs/Skew_Correction.md index 55bc22b0a..9191b7c44 100644 --- a/docs/Skew_Correction.md +++ b/docs/Skew_Correction.md @@ -31,7 +31,7 @@ AD do not include the flats on the corners that some test objects provide. ## Configure your skew Make sure `[skew_correction]` is in printer.cfg. You may now use the `SET_SKEW` -gcode to configure skew_correcton. For example, if your measured lengths +gcode to configure skew_correction. For example, if your measured lengths along XY are as follows: ``` diff --git a/docs/Slicers.md b/docs/Slicers.md index afffe7499..f6964ab90 100644 --- a/docs/Slicers.md +++ b/docs/Slicers.md @@ -121,5 +121,5 @@ M104 S0 before the macro call. Also note that SuperSlicer has a "custom gcode only" button option, which achieves the same outcome. -An example of a START_PRINT macro using these paramaters can +An example of a START_PRINT macro using these parameters can be found in config/sample-macros.cfg diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index c75f1d6f5..f6f649b0f 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -300,7 +300,7 @@ The following information is available for each `[led led_name]`, - `color_data`: A list of color lists containing the RGBW values for a led in the chain. Each value is represented as a float from 0.0 to 1.0. Each color list contains 4 items (red, green, blue, white) even - if the underyling LED supports fewer color channels. For example, + if the underlying LED supports fewer color channels. For example, the blue value (3rd item in color list) of the second neopixel in a chain could be accessed at `printer["neopixel "].color_data[1][2]`. From e1176e4dfb9018e712d4fa86daf41e9e762a1698 Mon Sep 17 00:00:00 2001 From: Burrito <37681398+burritosoftware@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:25:48 -0700 Subject: [PATCH 030/411] spi_flash: Add ZNP Robin Nano v2.2 to board defs (#6986) Adds support for the ZNP Robin Nano DW v2.2 board, used in the Neptune 3 Pro/Plus/Max. Signed-off-by: Zyjay Cruz --- scripts/spi_flash/board_defs.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index fe6ec9ab9..44eefa4f3 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -162,6 +162,13 @@ BOARD_DEFS = { "conversion_script": "scripts/update_chitu.py", "firmware_path": "update.cbd", 'skip_verify': True + }, + 'znp-robin-nano-dw-v2.2': { + 'mcu': "stm32f401xc", + 'spi_bus': "spi2", + "cs_pin": "PB12", + "firmware_path": "ZNP_ROBIN_NANO.bin", + "current_firmware_path": "ZNP_ROBIN_NANO.CUR" } } From 2cbb8959780f545a0dc410ae91c49452abd0d5e5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 1 Aug 2025 12:33:50 -0400 Subject: [PATCH 031/411] tmc2240: Add OTW_OV_VTH to list of ReadRegisters Reported by @poernahi. Signed-off-by: Kevin O'Connor --- klippy/extras/tmc2240.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/klippy/extras/tmc2240.py b/klippy/extras/tmc2240.py index 214896e78..35d2ce838 100644 --- a/klippy/extras/tmc2240.py +++ b/klippy/extras/tmc2240.py @@ -58,8 +58,9 @@ Registers = { ReadRegisters = [ "GCONF", "GSTAT", "IOIN", "DRV_CONF", "GLOBALSCALER", "IHOLD_IRUN", "TPOWERDOWN", "TSTEP", "TPWMTHRS", "TCOOLTHRS", "THIGH", "ADC_VSUPPLY_AIN", - "ADC_TEMP", "MSCNT", "MSCURACT", "CHOPCONF", "COOLCONF", "DRV_STATUS", - "PWMCONF", "PWM_SCALE", "PWM_AUTO", "SG4_THRS", "SG4_RESULT", "SG4_IND" + "ADC_TEMP", "OTW_OV_VTH", "MSCNT", "MSCURACT", "CHOPCONF", "COOLCONF", + "DRV_STATUS", "PWMCONF", "PWM_SCALE", "PWM_AUTO", "SG4_THRS", "SG4_RESULT", + "SG4_IND" ] Fields = {} From d5c031bc1326f38f7528ac83a6ed046b6d48bca5 Mon Sep 17 00:00:00 2001 From: Contomo <139701597+Contomo@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:37:47 +0200 Subject: [PATCH 032/411] idle_timeout: Add status field for current idle timeout (#6982) Signed-off-by: Eric Billmeyer --- docs/Status_Reference.md | 3 +++ klippy/extras/idle_timeout.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index f6f649b0f..77313682f 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -291,6 +291,9 @@ is always available): - `printing_time`: The amount of time (in seconds) the printer has been in the "Printing" state (as tracked by the idle_timeout module). +- `idle_timeout`: The current 'timeout' (in seconds) + to wait for the gcode to be triggered. + (as set by [SET_IDLE_TIMEOUT](G-Codes.md#set_idle_timeout)) ## led diff --git a/klippy/extras/idle_timeout.py b/klippy/extras/idle_timeout.py index 6ab2a34a5..cc31a97c9 100644 --- a/klippy/extras/idle_timeout.py +++ b/klippy/extras/idle_timeout.py @@ -35,7 +35,9 @@ class IdleTimeout: printing_time = 0. if self.state == "Printing": printing_time = eventtime - self.last_print_start_systime - return { "state": self.state, "printing_time": printing_time } + return {"state": self.state, + "printing_time": printing_time, + "idle_timeout": self.idle_timeout} def handle_ready(self): self.toolhead = self.printer.lookup_object('toolhead') self.timeout_timer = self.reactor.register_timer(self.timeout_handler) From c78dd6a00a7434e0a235d4b722f4cac3b41884da Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Mon, 28 Jul 2025 01:12:19 +0200 Subject: [PATCH 033/411] pyhelper: define set_thread_name() helper Signed-off-by: Timofey Titovets --- klippy/chelper/__init__.py | 1 + klippy/chelper/pyhelper.c | 9 +++++++++ klippy/chelper/pyhelper.h | 1 + 3 files changed, 11 insertions(+) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 2aed107a4..de8a496a5 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -219,6 +219,7 @@ defs_trdispatch = """ defs_pyhelper = """ void set_python_logging_callback(void (*func)(const char *)); double get_monotonic(void); + int set_thread_name(char name[16]); """ defs_std = """ diff --git a/klippy/chelper/pyhelper.c b/klippy/chelper/pyhelper.c index a0a42923d..60c6de9b3 100644 --- a/klippy/chelper/pyhelper.c +++ b/klippy/chelper/pyhelper.c @@ -10,6 +10,8 @@ #include // fprintf #include // strerror #include // struct timespec +#include // PR_SET_NAME +#include // prctl #include "compiler.h" // __visible #include "pyhelper.h" // get_monotonic @@ -92,3 +94,10 @@ dump_string(char *outbuf, int outbuf_size, char *inbuf, int inbuf_size) *o = '\0'; return outbuf; } + +// Set custom thread names +int __visible +set_thread_name(char name[16]) +{ + return prctl(PR_SET_NAME, name); +} diff --git a/klippy/chelper/pyhelper.h b/klippy/chelper/pyhelper.h index 1042214b4..a29992a26 100644 --- a/klippy/chelper/pyhelper.h +++ b/klippy/chelper/pyhelper.h @@ -7,5 +7,6 @@ void set_python_logging_callback(void (*func)(const char *)); void errorf(const char *fmt, ...) __attribute__ ((format (printf, 1, 2))); void report_errno(char *where, int rc); char *dump_string(char *outbuf, int outbuf_size, char *inbuf, int inbuf_size); +int set_thread_name(char name[16]); #endif // pyhelper.h From 73c6674306599000281064b599545aaaa24a5448 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 25 Jul 2025 19:13:20 +0200 Subject: [PATCH 034/411] queuelogger: set thread name Python 2.7 does not allow loading the cffi lib inside the thread, but function calls are allowed Signed-off-by: Timofey Titovets --- klippy/queuelogger.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/klippy/queuelogger.py b/klippy/queuelogger.py index c6447f8e5..7fa93acec 100644 --- a/klippy/queuelogger.py +++ b/klippy/queuelogger.py @@ -4,6 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import logging, logging.handlers, threading, queue, time +import chelper # Class to forward all messages through a queue to a background thread class QueueHandler(logging.Handler): @@ -25,11 +26,15 @@ class QueueListener(logging.handlers.TimedRotatingFileHandler): def __init__(self, filename): logging.handlers.TimedRotatingFileHandler.__init__( self, filename, when='midnight', backupCount=5) + _, ffi_lib = chelper.get_ffi() + self._set_thread_name = ffi_lib.set_thread_name self.bg_queue = queue.Queue() self.bg_thread = threading.Thread(target=self._bg_thread) self.bg_thread.start() self.rollover_info = {} def _bg_thread(self): + name_short = "queuelogger"[:15] + self._set_thread_name(name_short.encode('utf-8')) while 1: record = self.bg_queue.get(True) if record is None: From 39d01158ba41137f46b80eb9018561d834a2e860 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 25 Jul 2025 19:11:50 +0200 Subject: [PATCH 035/411] serialhdl: name the threads per mcu Signed-off-by: Timofey Titovets --- klippy/mcu.py | 4 ++-- klippy/serialhdl.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 8b41e4e03..d3b8ffd79 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -564,8 +564,8 @@ class MCU: if self._name.startswith('mcu '): self._name = self._name[4:] # Serial port - wp = "mcu '%s': " % (self._name) - self._serial = serialhdl.SerialReader(self._reactor, warn_prefix=wp) + name = self._name + self._serial = serialhdl.SerialReader(self._reactor, mcu_name = name) self._baud = 0 self._canbus_iface = None canbus_uuid = config.get('canbus_uuid', None) diff --git a/klippy/serialhdl.py b/klippy/serialhdl.py index fc8846387..ab26514cb 100644 --- a/klippy/serialhdl.py +++ b/klippy/serialhdl.py @@ -12,12 +12,15 @@ class error(Exception): pass class SerialReader: - def __init__(self, reactor, warn_prefix=""): + def __init__(self, reactor, mcu_name=""): self.reactor = reactor - self.warn_prefix = warn_prefix + self.warn_prefix = "" + self.mcu_name = mcu_name + if self.mcu_name: + self.warn_prefix = "mcu '%s': " % (self.mcu_name) # Serial port self.serial_dev = None - self.msgparser = msgproto.MessageParser(warn_prefix=warn_prefix) + self.msgparser = msgproto.MessageParser(warn_prefix=self.warn_prefix) # C interface self.ffi_main, self.ffi_lib = chelper.get_ffi() self.serialqueue = None @@ -34,6 +37,8 @@ class SerialReader: self.last_notify_id = 0 self.pending_notifications = {} def _bg_thread(self): + name_short = ("serialhdl %s" % (self.mcu_name))[:15] + self.ffi_lib.set_thread_name(name_short.encode('utf-8')) response = self.ffi_main.new('struct pull_queue_message *') while 1: self.ffi_lib.serialqueue_pull(self.serialqueue, response) From 17ce45d212821efed96a24801b10b157bde6ad35 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 25 Jul 2025 20:14:18 +0200 Subject: [PATCH 036/411] serialqueue: name the threads per mcu Signed-off-by: Timofey Titovets --- klippy/chelper/__init__.py | 2 +- klippy/chelper/serialqueue.c | 6 +++++- klippy/chelper/serialqueue.h | 2 +- klippy/serialhdl.py | 8 ++++++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index de8a496a5..622cc9a72 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -182,7 +182,7 @@ defs_serialqueue = """ }; struct serialqueue *serialqueue_alloc(int serial_fd, char serial_fd_type - , int client_id); + , int client_id, char name[16]); void serialqueue_exit(struct serialqueue *sq); void serialqueue_free(struct serialqueue *sq); struct command_queue *serialqueue_alloc_commandqueue(void); diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index c207495cd..1af00b067 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -47,6 +47,7 @@ struct serialqueue { pthread_mutex_t lock; // protects variables below pthread_cond_t cond; int receive_waiting; + char name[16]; // Baud / clock tracking int receive_window; double bittime_adjust, idle_time; @@ -612,6 +613,7 @@ static void * background_thread(void *data) { struct serialqueue *sq = data; + set_thread_name(sq->name); pollreactor_run(sq->pr); pthread_mutex_lock(&sq->lock); @@ -623,13 +625,15 @@ background_thread(void *data) // Create a new 'struct serialqueue' object struct serialqueue * __visible -serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id) +serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id + , char name[16]) { struct serialqueue *sq = malloc(sizeof(*sq)); memset(sq, 0, sizeof(*sq)); sq->serial_fd = serial_fd; sq->serial_fd_type = serial_fd_type; sq->client_id = client_id; + strncpy(sq->name, name, sizeof(sq->name)); int ret = pipe(sq->pipe_fds); if (ret) diff --git a/klippy/chelper/serialqueue.h b/klippy/chelper/serialqueue.h index 4d447f2fb..9da0ad083 100644 --- a/klippy/chelper/serialqueue.h +++ b/klippy/chelper/serialqueue.h @@ -27,7 +27,7 @@ struct pull_queue_message { struct serialqueue; struct serialqueue *serialqueue_alloc(int serial_fd, char serial_fd_type - , int client_id); + , int client_id, char name[16]); void serialqueue_exit(struct serialqueue *sq); void serialqueue_free(struct serialqueue *sq); struct command_queue *serialqueue_alloc_commandqueue(void); diff --git a/klippy/serialhdl.py b/klippy/serialhdl.py index ab26514cb..2a401f899 100644 --- a/klippy/serialhdl.py +++ b/klippy/serialhdl.py @@ -18,6 +18,8 @@ class SerialReader: self.mcu_name = mcu_name if self.mcu_name: self.warn_prefix = "mcu '%s': " % (self.mcu_name) + sq_name = ("serialq %s" % (self.mcu_name))[:15] + self.sq_name = sq_name.encode("utf-8") # Serial port self.serial_dev = None self.msgparser = msgproto.MessageParser(warn_prefix=self.warn_prefix) @@ -85,7 +87,8 @@ class SerialReader: self.serial_dev = serial_dev self.serialqueue = self.ffi_main.gc( self.ffi_lib.serialqueue_alloc(serial_dev.fileno(), - serial_fd_type, client_id), + serial_fd_type, client_id, + self.sq_name), self.ffi_lib.serialqueue_free) self.background_thread = threading.Thread(target=self._bg_thread) self.background_thread.start() @@ -205,7 +208,8 @@ class SerialReader: self.serial_dev = debugoutput self.msgparser.process_identify(dictionary, decompress=False) self.serialqueue = self.ffi_main.gc( - self.ffi_lib.serialqueue_alloc(self.serial_dev.fileno(), b'f', 0), + self.ffi_lib.serialqueue_alloc(self.serial_dev.fileno(), b'f', 0, + self.sq_name), self.ffi_lib.serialqueue_free) def set_clock_est(self, freq, conv_time, conv_clock, last_clock): self.ffi_lib.serialqueue_set_clock_est( From 0df40b43e8c3b3fb4db2d6af38de5a0944ff64e9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 1 Aug 2025 12:44:59 -0400 Subject: [PATCH 037/411] serialqueue: Be sure sq->name is null terminated Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 1af00b067..ed1215df9 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -43,11 +43,11 @@ struct serialqueue { uint8_t need_sync; int input_pos; // Threading + char name[16]; pthread_t tid; pthread_mutex_t lock; // protects variables below pthread_cond_t cond; int receive_waiting; - char name[16]; // Baud / clock tracking int receive_window; double bittime_adjust, idle_time; @@ -634,6 +634,7 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id sq->serial_fd_type = serial_fd_type; sq->client_id = client_id; strncpy(sq->name, name, sizeof(sq->name)); + sq->name[sizeof(sq->name)-1] = '\0'; int ret = pipe(sq->pipe_fds); if (ret) From e1ba7c17cec8f22ce2bec67699a7902577aadd8d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 1 Aug 2025 13:07:44 -0400 Subject: [PATCH 038/411] Revert "queuelogger: set thread name" This reverts commit 73c6674306599000281064b599545aaaa24a5448. Signed-off-by: Kevin O'Connor --- klippy/queuelogger.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/klippy/queuelogger.py b/klippy/queuelogger.py index 7fa93acec..c6447f8e5 100644 --- a/klippy/queuelogger.py +++ b/klippy/queuelogger.py @@ -4,7 +4,6 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import logging, logging.handlers, threading, queue, time -import chelper # Class to forward all messages through a queue to a background thread class QueueHandler(logging.Handler): @@ -26,15 +25,11 @@ class QueueListener(logging.handlers.TimedRotatingFileHandler): def __init__(self, filename): logging.handlers.TimedRotatingFileHandler.__init__( self, filename, when='midnight', backupCount=5) - _, ffi_lib = chelper.get_ffi() - self._set_thread_name = ffi_lib.set_thread_name self.bg_queue = queue.Queue() self.bg_thread = threading.Thread(target=self._bg_thread) self.bg_thread.start() self.rollover_info = {} def _bg_thread(self): - name_short = "queuelogger"[:15] - self._set_thread_name(name_short.encode('utf-8')) while 1: record = self.bg_queue.get(True) if record is None: From 5eb07966b5d7e1534aa40df3b0ea305f5c6d9ae2 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Fri, 1 Aug 2025 15:46:24 +0200 Subject: [PATCH 039/411] idex_modes: Fixed dual_carriage axis range calculation after homing Signed-off-by: Dmitry Butyugin --- klippy/kinematics/idex_modes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/kinematics/idex_modes.py b/klippy/kinematics/idex_modes.py index 19d5a655c..46b0be08b 100644 --- a/klippy/kinematics/idex_modes.py +++ b/klippy/kinematics/idex_modes.py @@ -124,7 +124,7 @@ class DualCarriages: self.toggle_active_dc_rail(dc) kin.home_axis(homing_state, axis, dc.rail) # Restore the original rails ordering - self.toggle_active_dc_rail(dcs[0]) + self.activate_dc_mode(dcs[0], PRIMARY) def get_status(self, eventtime=None): status = {'carriages' : {dc.get_name() : dc.mode for dc in self.dc_rails.values()}} From cfc58d3ce7347002c43eaa92574e09095a039dae Mon Sep 17 00:00:00 2001 From: Ben Lye Date: Mon, 11 Aug 2025 22:49:35 +0100 Subject: [PATCH 040/411] spi_flash: Add qidi-x7 to board_defs.py (#6979) Added board definition for stm32f401xc-based Qidi X-7 board used in Qidi Q1 Pro and Plus4. Signed-off-by: Ben Lye --- scripts/spi_flash/board_defs.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index 44eefa4f3..0e82d27bf 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -169,6 +169,14 @@ BOARD_DEFS = { "cs_pin": "PB12", "firmware_path": "ZNP_ROBIN_NANO.bin", "current_firmware_path": "ZNP_ROBIN_NANO.CUR" + }, + 'qidi-x7': { + 'mcu': "stm32f401xc", + 'spi_bus': "spi2", + 'cs_pin': "PB12", + 'skip_verify': False, + 'firmware_path': 'qd_mcu.bin', + 'current_firmware_path': 'qd_mcu.CUR' } } @@ -219,7 +227,9 @@ BOARD_ALIASES = { 'fysetc-s6-v2': BOARD_DEFS['fysetc-spider'], 'robin_v3': BOARD_DEFS['monster8'], 'btt-skrat-v1.0': BOARD_DEFS['btt-skrat'], - 'chitu-v6': BOARD_DEFS['chitu-v6'] + 'chitu-v6': BOARD_DEFS['chitu-v6'], + 'qidi-q1-pro': BOARD_DEFS['qidi-x7'], + 'qidi-plus4': BOARD_DEFS['qidi-x7'] } def list_boards(): From 3ef760c18ff8dfd9710ab4f0fae6cf644d3d7c34 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 4 Aug 2025 12:02:35 -0400 Subject: [PATCH 041/411] toolhead: Remove support for max_accel_to_decel This support was deprecated on 20240313. Remove the remaining compatibility code. Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 5 +++++ docs/Config_Reference.md | 3 --- klippy/toolhead.py | 19 +------------------ test/klippy/commands.test | 2 +- 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 4ab7d33d3..838b23273 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,11 @@ All dates in this document are approximate. ## Changes +20250811: Support for the `max_accel_to_decel` parameter in the +`[printer]` config section has been removed and support for the +`ACCEL_TO_DECEL` parameter in the `SET_VELOCITY_LIMIT` command has +been removed. These capabilities were deprecated on 20240313. + 20250721: The `[pca9632]` and `[mcp4018]` modules no longer accept the `scl_pin` and `sda_pin` options. Use `i2c_software_scl_pin` and `i2c_software_sda_pin` instead. diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index a9b6db6ce..83de96096 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -126,8 +126,6 @@ max_accel: # decelerate to zero at each corner. The value specified here may be # changed at runtime using the SET_VELOCITY_LIMIT command. The # default is 5mm/s. -#max_accel_to_decel: -# This parameter is deprecated and should no longer be used. ``` ### [stepper] @@ -740,7 +738,6 @@ max_velocity: max_accel: #minimum_cruise_ratio: #square_corner_velocity: -#max_accel_to_decel: #max_z_velocity: #max_z_accel: diff --git a/klippy/toolhead.py b/klippy/toolhead.py index f835977c9..f45fe8392 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -219,17 +219,8 @@ class ToolHead: # Velocity and acceleration control self.max_velocity = config.getfloat('max_velocity', above=0.) self.max_accel = config.getfloat('max_accel', above=0.) - min_cruise_ratio = 0.5 - if config.getfloat('minimum_cruise_ratio', None) is None: - req_accel_to_decel = config.getfloat('max_accel_to_decel', None, - above=0.) - if req_accel_to_decel is not None: - config.deprecate('max_accel_to_decel') - min_cruise_ratio = 1. - min(1., (req_accel_to_decel - / self.max_accel)) self.min_cruise_ratio = config.getfloat('minimum_cruise_ratio', - min_cruise_ratio, - below=1., minval=0.) + 0.5, below=1., minval=0.) self.square_corner_velocity = config.getfloat( 'square_corner_velocity', 5., minval=0.) self.junction_deviation = self.max_accel_to_decel = 0. @@ -686,14 +677,6 @@ class ToolHead: 'SQUARE_CORNER_VELOCITY', None, minval=0.) min_cruise_ratio = gcmd.get_float( 'MINIMUM_CRUISE_RATIO', None, minval=0., below=1.) - if min_cruise_ratio is None: - req_accel_to_decel = gcmd.get_float('ACCEL_TO_DECEL', - None, above=0.) - if req_accel_to_decel is not None and max_accel is not None: - min_cruise_ratio = 1. - min(1., req_accel_to_decel / max_accel) - elif req_accel_to_decel is not None and max_accel is None: - min_cruise_ratio = 1. - min(1., (req_accel_to_decel - / self.max_accel)) if max_velocity is not None: self.max_velocity = max_velocity if max_accel is not None: diff --git a/test/klippy/commands.test b/test/klippy/commands.test index 33c599614..cb3b17d97 100644 --- a/test/klippy/commands.test +++ b/test/klippy/commands.test @@ -36,7 +36,7 @@ SET_GCODE_OFFSET Z=.1 M206 Z-.2 SET_GCODE_OFFSET Z_ADJUST=-.1 -SET_VELOCITY_LIMIT ACCEL=100 VELOCITY=20 SQUARE_CORNER_VELOCITY=1 ACCEL_TO_DECEL=200 +SET_VELOCITY_LIMIT ACCEL=100 VELOCITY=20 SQUARE_CORNER_VELOCITY=1 MINIMUM_CRUISE_RATIO=0 M204 S500 SET_PRESSURE_ADVANCE EXTRUDER=extruder ADVANCE=.001 From bcd4510958b4e2280266b403051894ed43fa0308 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 4 Aug 2025 13:21:55 -0400 Subject: [PATCH 042/411] toolhead: Move extra module loading out of core Toolhead() class Load these extra modules from add_printer_objects() instead. 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 f45fe8392..bb65c1a9f 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -279,11 +279,6 @@ class ToolHead: gcode.register_command('M204', self.cmd_M204) self.printer.register_event_handler("klippy:shutdown", self._handle_shutdown) - # Load some default modules - modules = ["gcode_move", "homing", "idle_timeout", "statistics", - "manual_probe", "tuning_tower", "garbage_collection"] - for module_name in modules: - self.printer.load_object(config, module_name) # Print time and flush tracking def _advance_flush_time(self, flush_time): flush_time = max(flush_time, self.last_flush_time) @@ -712,5 +707,11 @@ class ToolHead: self._calc_junction_deviation() def add_printer_objects(config): - config.get_printer().add_object('toolhead', ToolHead(config)) + printer = config.get_printer() + printer.add_object('toolhead', ToolHead(config)) kinematics.extruder.add_printer_objects(config) + # Load some default modules + modules = ["gcode_move", "homing", "idle_timeout", "statistics", + "manual_probe", "tuning_tower", "garbage_collection"] + for module_name in modules: + printer.load_object(config, module_name) From f8da8099d5e5cdec366841728a28da23d3f9bb07 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 4 Aug 2025 13:45:59 -0400 Subject: [PATCH 043/411] toolhead: Move g-code command handlers to new ToolHeadCommandHelper() class Move the g-code command handlers to a new class. This reduces the size of the main Toolhead() class. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 67 +++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index bb65c1a9f..c78b215b1 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -270,13 +270,7 @@ class ToolHead: msg = "Error loading kinematics '%s'" % (kin_name,) logging.exception(msg) raise config.error(msg) - # Register commands - gcode.register_command('G4', self.cmd_G4) - gcode.register_command('M400', self.cmd_M400) - gcode.register_command('SET_VELOCITY_LIMIT', - self.cmd_SET_VELOCITY_LIMIT, - desc=self.cmd_SET_VELOCITY_LIMIT_help) - gcode.register_command('M204', self.cmd_M204) + # Register handlers self.printer.register_event_handler("klippy:shutdown", self._handle_shutdown) # Print time and flush tracking @@ -657,21 +651,8 @@ class ToolHead: scv2 = self.square_corner_velocity**2 self.junction_deviation = scv2 * (math.sqrt(2.) - 1.) / self.max_accel self.max_accel_to_decel = self.max_accel * (1. - self.min_cruise_ratio) - def cmd_G4(self, gcmd): - # Dwell - delay = gcmd.get_float('P', 0., minval=0.) / 1000. - self.dwell(delay) - def cmd_M400(self, gcmd): - # Wait for current moves to finish - self.wait_moves() - cmd_SET_VELOCITY_LIMIT_help = "Set printer velocity limits" - def cmd_SET_VELOCITY_LIMIT(self, gcmd): - max_velocity = gcmd.get_float('VELOCITY', None, above=0.) - max_accel = gcmd.get_float('ACCEL', None, above=0.) - square_corner_velocity = gcmd.get_float( - 'SQUARE_CORNER_VELOCITY', None, minval=0.) - min_cruise_ratio = gcmd.get_float( - 'MINIMUM_CRUISE_RATIO', None, minval=0., below=1.) + def set_max_velocities(self, max_velocity, max_accel, + square_corner_velocity, min_cruise_ratio): if max_velocity is not None: self.max_velocity = max_velocity if max_accel is not None: @@ -681,12 +662,43 @@ class ToolHead: if min_cruise_ratio is not None: self.min_cruise_ratio = min_cruise_ratio self._calc_junction_deviation() + return (self.max_velocity, self.max_accel, + self.square_corner_velocity, self.min_cruise_ratio) + +# Support common G-Code commands relative to the toolhead +class ToolHeadCommandHelper: + def __init__(self, config): + self.printer = config.get_printer() + self.toolhead = self.printer.lookup_object("toolhead") + # Register commands + gcode = self.printer.lookup_object('gcode') + gcode.register_command('G4', self.cmd_G4) + gcode.register_command('M400', self.cmd_M400) + gcode.register_command('SET_VELOCITY_LIMIT', + self.cmd_SET_VELOCITY_LIMIT, + desc=self.cmd_SET_VELOCITY_LIMIT_help) + gcode.register_command('M204', self.cmd_M204) + def cmd_G4(self, gcmd): + # Dwell + delay = gcmd.get_float('P', 0., minval=0.) / 1000. + self.toolhead.dwell(delay) + def cmd_M400(self, gcmd): + # Wait for current moves to finish + self.toolhead.wait_moves() + cmd_SET_VELOCITY_LIMIT_help = "Set printer velocity limits" + def cmd_SET_VELOCITY_LIMIT(self, gcmd): + max_velocity = gcmd.get_float('VELOCITY', None, above=0.) + max_accel = gcmd.get_float('ACCEL', None, above=0.) + square_corner_velocity = gcmd.get_float( + 'SQUARE_CORNER_VELOCITY', None, minval=0.) + min_cruise_ratio = gcmd.get_float( + 'MINIMUM_CRUISE_RATIO', None, minval=0., below=1.) + mv, ma, scv, mcr = self.toolhead.set_max_velocities( + max_velocity, max_accel, square_corner_velocity, min_cruise_ratio) msg = ("max_velocity: %.6f\n" "max_accel: %.6f\n" "minimum_cruise_ratio: %.6f\n" - "square_corner_velocity: %.6f" % ( - self.max_velocity, self.max_accel, - self.min_cruise_ratio, self.square_corner_velocity)) + "square_corner_velocity: %.6f" % (mv, ma, scv, mcr)) 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): @@ -703,12 +715,13 @@ class ToolHead: % (gcmd.get_commandline(),)) return accel = min(p, t) - self.max_accel = accel - self._calc_junction_deviation() + self.toolhead.set_max_velocities(None, accel, None, None) def add_printer_objects(config): printer = config.get_printer() printer.add_object('toolhead', ToolHead(config)) + ToolHeadCommandHelper(config) + # Load default extruder objects kinematics.extruder.add_printer_objects(config) # Load some default modules modules = ["gcode_move", "homing", "idle_timeout", "statistics", From 126275d1f4921b60d249c352169fe7ffd860b4ac Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 6 Aug 2025 21:20:55 -0400 Subject: [PATCH 044/411] toolhead: Default note_mcu_movequeue_activity() to set step_gen_time Change note_mcu_movequeue_activity() to default to setting the step_gen_time (instead of the previous default to not set it). Most users of the mcu "move queue" will be for stepper activity. There is also little harm in incrementing the tracking of the last possible step generation time, but accidentally generating a step without incrementing the tracking can lead to very hard to debug failures. The two cases (output_pin.py and pwm_tool.py) where note_mcu_movequeue_activity() is called and definitely not related to step generation can explicitly pass 'is_step_gen=False'. Signed-off-by: Kevin O'Connor --- klippy/extras/output_pin.py | 5 +++-- klippy/extras/pwm_tool.py | 3 ++- klippy/toolhead.py | 10 ++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index bde7ea698..052663821 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -50,10 +50,11 @@ 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) + self.toolhead.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) + self.toolhead.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 46873203d..53e101ae9 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -115,7 +115,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) + self._toolhead.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 c78b215b1..bddba4c7b 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -342,8 +342,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, - set_step_gen_time=True) + self.note_mcu_movequeue_activity(next_move_time + self.kin_flush_delay) self._advance_move_time(next_move_time) def _flush_lookahead(self): # Transit from "NeedPrime"/"Priming"/"Drip"/main state to "NeedPrime" @@ -539,8 +538,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, - set_step_gen_time=True) + self.note_mcu_movequeue_activity(npt + self.kin_flush_delay) for stepper in addstepper: stepper.generate_steps(npt) self._advance_move_time(npt) @@ -638,9 +636,9 @@ class ToolHead: callback(self.get_last_move_time()) return last_move.timing_callbacks.append(callback) - def note_mcu_movequeue_activity(self, mq_time, set_step_gen_time=False): + def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True): self.need_flush_time = max(self.need_flush_time, mq_time) - if set_step_gen_time: + if is_step_gen: self.step_gen_time = max(self.step_gen_time, mq_time) if self.do_kick_flush_timer: self.do_kick_flush_timer = False From 9399e738bc21e70cdd5a60bc9f8e1d51fb20cd75 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 4 Aug 2025 21:57:45 -0400 Subject: [PATCH 045/411] motion_queuing: Add new module to help with motion queues and flushing Create a new module to assist with host management of motion queues. Register all MCU_stepper objects with this module and use the module for step generation. All steppers will now automatically generate steps whenever toolhead._advance_flush_time() is invoked. It is no longer necessary for callers to individually call stepper.generate_steps(). Signed-off-by: Kevin O'Connor --- klippy/extras/force_move.py | 9 ++++----- klippy/extras/manual_stepper.py | 5 +---- klippy/extras/motion_queuing.py | 19 +++++++++++++++++++ klippy/kinematics/cartesian.py | 1 - klippy/kinematics/corexy.py | 1 - klippy/kinematics/corexz.py | 1 - klippy/kinematics/delta.py | 1 - klippy/kinematics/deltesian.py | 1 - klippy/kinematics/extruder.py | 2 -- klippy/kinematics/generic_cartesian.py | 1 - klippy/kinematics/hybrid_corexy.py | 1 - klippy/kinematics/hybrid_corexz.py | 1 - klippy/kinematics/polar.py | 1 - klippy/kinematics/rotary_delta.py | 1 - klippy/kinematics/winch.py | 1 - klippy/stepper.py | 6 ++---- klippy/toolhead.py | 14 +++----------- 17 files changed, 29 insertions(+), 37 deletions(-) create mode 100644 klippy/extras/motion_queuing.py diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py index 0be0eb9b4..54b07d59b 100644 --- a/klippy/extras/force_move.py +++ b/klippy/extras/force_move.py @@ -85,14 +85,13 @@ 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 - stepper.generate_steps(print_time) - self.trapq_finalize_moves(self.trapq, print_time + 99999.9, - print_time + 99999.9) - stepper.set_trapq(prev_trapq) - stepper.set_stepper_kinematics(prev_sk) toolhead.note_mcu_movequeue_activity(print_time) toolhead.dwell(accel_t + cruise_t + accel_t) toolhead.flush_step_generation() + stepper.set_trapq(prev_trapq) + stepper.set_stepper_kinematics(prev_sk) + self.trapq_finalize_moves(self.trapq, print_time + 99999.9, + print_time + 99999.9) def _lookup_stepper(self, gcmd): name = gcmd.get('STEPPER') if name not in self.steppers: diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 5a8949b96..03816b5cc 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -76,7 +76,6 @@ class ManualStepper: self.sync_print_time() self.next_cmd_time = self._submit_move(self.next_cmd_time, movepos, speed, accel) - self.rail.generate_steps(self.next_cmd_time) self.trapq_finalize_moves(self.trapq, self.next_cmd_time + 99999.9, self.next_cmd_time + 99999.9) toolhead = self.printer.lookup_object('toolhead') @@ -138,7 +137,6 @@ class ManualStepper: raise gcmd.error("Must unregister axis first") # Unregister toolhead.remove_extra_axis(self) - toolhead.unregister_step_generator(self.rail.generate_steps) self.axis_gcode_id = None return if (len(gcode_axis) != 1 or not gcode_axis.isupper() @@ -155,7 +153,6 @@ class ManualStepper: self.gaxis_limit_velocity = limit_velocity self.gaxis_limit_accel = limit_accel toolhead.add_extra_axis(self, self.get_position()[0]) - toolhead.register_step_generator(self.rail.generate_steps) def process_move(self, print_time, move, ea_index): axis_r = move.axes_r[ea_index] start_pos = move.start_pos[ea_index] @@ -208,7 +205,7 @@ class ManualStepper: speed, self.homing_accel) # Drip updates to motors toolhead = self.printer.lookup_object('toolhead') - toolhead.drip_update_time(maxtime, drip_completion, self.steppers) + toolhead.drip_update_time(maxtime, drip_completion) # Clear trapq of any remaining parts of movement reactor = self.printer.get_reactor() self.trapq_finalize_moves(self.trapq, reactor.NEVER, 0) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py new file mode 100644 index 000000000..4b5bd017d --- /dev/null +++ b/klippy/extras/motion_queuing.py @@ -0,0 +1,19 @@ +# Helper code for low-level motion queuing and flushing +# +# Copyright (C) 2025 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging + +class PrinterMotionQueuing: + def __init__(self, config): + self.printer = config.get_printer() + self.steppers = [] + def register_stepper(self, config, stepper): + self.steppers.append(stepper) + def flush_motion_queues(self, must_flush_time, max_step_gen_time): + for stepper in self.steppers: + stepper.generate_steps(max_step_gen_time) + +def load_config(config): + return PrinterMotionQueuing(config) diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index f9030275e..edd9c1a04 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -36,7 +36,6 @@ class CartKinematics: 'safe_distance', None, minval=0.)) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat('max_z_velocity', max_velocity, diff --git a/klippy/kinematics/corexy.py b/klippy/kinematics/corexy.py index d68f8854f..f940c57fa 100644 --- a/klippy/kinematics/corexy.py +++ b/klippy/kinematics/corexy.py @@ -20,7 +20,6 @@ class CoreXYKinematics: self.rails[2].setup_itersolve('cartesian_stepper_alloc', b'z') for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( diff --git a/klippy/kinematics/corexz.py b/klippy/kinematics/corexz.py index 8d3e027c3..d4a0eb350 100644 --- a/klippy/kinematics/corexz.py +++ b/klippy/kinematics/corexz.py @@ -20,7 +20,6 @@ class CoreXZKinematics: self.rails[2].setup_itersolve('corexz_stepper_alloc', b'-') for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( diff --git a/klippy/kinematics/delta.py b/klippy/kinematics/delta.py index aa244a8f3..5ec4d54cc 100644 --- a/klippy/kinematics/delta.py +++ b/klippy/kinematics/delta.py @@ -52,7 +52,6 @@ class DeltaKinematics: r.setup_itersolve('delta_stepper_alloc', a, t[0], t[1]) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) # Setup boundary checks self.need_home = True self.limit_xy2 = -1. diff --git a/klippy/kinematics/deltesian.py b/klippy/kinematics/deltesian.py index 54b013a5c..a62a58bd2 100644 --- a/klippy/kinematics/deltesian.py +++ b/klippy/kinematics/deltesian.py @@ -40,7 +40,6 @@ class DeltesianKinematics: self.rails[2].setup_itersolve('cartesian_stepper_alloc', b'y') for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) self.limits = [(1.0, -1.0)] * 3 # X axis limits min_angle = config.getfloat('min_angle', MIN_ANGLE, diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 58ccdbec4..e3e375bcc 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -39,8 +39,6 @@ class ExtruderStepper: self.name, self.cmd_SYNC_EXTRUDER_MOTION, desc=self.cmd_SYNC_EXTRUDER_MOTION_help) def _handle_connect(self): - toolhead = self.printer.lookup_object('toolhead') - toolhead.register_step_generator(self.stepper.generate_steps) self._set_pressure_advance(self.config_pa, self.config_smooth_time) def get_status(self, eventtime): return {'pressure_advance': self.pressure_advance, diff --git a/klippy/kinematics/generic_cartesian.py b/klippy/kinematics/generic_cartesian.py index b8cabb775..49e16eec3 100644 --- a/klippy/kinematics/generic_cartesian.py +++ b/klippy/kinematics/generic_cartesian.py @@ -108,7 +108,6 @@ class GenericCartesianKinematics: self._load_kinematics(config) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) self.dc_module = None if self.dc_carriages: pcs = [dc.get_dual_carriage() for dc in self.dc_carriages] diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index fd2621d50..22884b93c 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -39,7 +39,6 @@ class HybridCoreXYKinematics: 'safe_distance', None, minval=0.)) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index b96999827..1cd646e28 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -39,7 +39,6 @@ class HybridCoreXZKinematics: 'safe_distance', None, minval=0.)) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( diff --git a/klippy/kinematics/polar.py b/klippy/kinematics/polar.py index ffa15d839..2e467d50e 100644 --- a/klippy/kinematics/polar.py +++ b/klippy/kinematics/polar.py @@ -21,7 +21,6 @@ class PolarKinematics: for s in r.get_steppers() ] for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( diff --git a/klippy/kinematics/rotary_delta.py b/klippy/kinematics/rotary_delta.py index 732a89a81..42b0a706d 100644 --- a/klippy/kinematics/rotary_delta.py +++ b/klippy/kinematics/rotary_delta.py @@ -52,7 +52,6 @@ class RotaryDeltaKinematics: math.radians(a), ua, la) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) # Setup boundary checks self.need_home = True self.limit_xy2 = -1. diff --git a/klippy/kinematics/winch.py b/klippy/kinematics/winch.py index 47bc68558..2fa06ef22 100644 --- a/klippy/kinematics/winch.py +++ b/klippy/kinematics/winch.py @@ -21,7 +21,6 @@ class WinchKinematics: self.anchors.append(a) s.setup_itersolve('winch_stepper_alloc', *a) s.set_trapq(toolhead.get_trapq()) - toolhead.register_step_generator(s.generate_steps) # Setup boundary checks acoords = list(zip(*self.anchors)) self.axes_min = toolhead.Coord(*[min(a) for a in acoords], e=0.) diff --git a/klippy/stepper.py b/klippy/stepper.py index 86a923581..5ba76a5b0 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -273,7 +273,8 @@ def PrinterStepper(config, units_in_radians=False): rotation_dist, steps_per_rotation, step_pulse_duration, units_in_radians) # Register with helper modules - for mname in ['stepper_enable', 'force_move', 'motion_report']: + mods = ['stepper_enable', 'force_move', 'motion_report', 'motion_queuing'] + for mname in mods: m = printer.load_object(config, mname) m.register_stepper(config, mcu_stepper) return mcu_stepper @@ -442,9 +443,6 @@ class GenericPrinterRail: def setup_itersolve(self, alloc_func, *params): for stepper in self.steppers: stepper.setup_itersolve(alloc_func, *params) - def generate_steps(self, flush_time): - for stepper in self.steppers: - stepper.generate_steps(flush_time) def set_trapq(self, trapq): for stepper in self.steppers: stepper.set_trapq(trapq) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index bddba4c7b..3b9c0e8e3 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -251,7 +251,7 @@ class ToolHead: self.trapq_append = ffi_lib.trapq_append self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves # Motion flushing - self.step_generators = [] + self.motion_queuing = self.printer.load_object(config, 'motion_queuing') self.flush_trapqs = [self.trapq] # Create kinematics class gcode = self.printer.lookup_object('gcode') @@ -280,8 +280,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) - for sg in self.step_generators: - sg(sg_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) # Free trapq entries that are no longer needed clear_history_time = self.clear_history_time @@ -517,7 +516,7 @@ 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, addstepper=()): + 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 @@ -539,8 +538,6 @@ class ToolHead: continue npt = min(self.print_time + DRIP_SEGMENT_TIME, next_print_time) self.note_mcu_movequeue_activity(npt + self.kin_flush_delay) - for stepper in addstepper: - stepper.generate_steps(npt) self._advance_move_time(npt) # Exit "Drip" state self.reactor.update_timer(self.flush_timer, self.reactor.NOW) @@ -617,11 +614,6 @@ class ToolHead: return self.kin def get_trapq(self): return self.trapq - def register_step_generator(self, handler): - self.step_generators.append(handler) - def unregister_step_generator(self, handler): - if handler in self.step_generators: - self.step_generators.remove(handler) def note_step_generation_scan_time(self, delay, old_delay=0.): self.flush_step_generation() if old_delay: From 5cbe7d83e8b10cc0986b12ddf49b2838655843d9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 4 Aug 2025 22:41:15 -0400 Subject: [PATCH 046/411] motion_queuing: Track all trapqs and globally flush all trapqs Add an allocate_trapq() helper function to facilitate the creation of a low-level C trapq object. Track all trapq objects and clear history on them globally when the main motion queues are flushed. Signed-off-by: Kevin O'Connor --- klippy/extras/force_move.py | 9 ++++----- klippy/extras/manual_stepper.py | 11 ++++------- klippy/extras/motion_queuing.py | 20 ++++++++++++++++++++ klippy/kinematics/extruder.py | 7 +++---- klippy/toolhead.py | 27 +++++---------------------- 5 files changed, 36 insertions(+), 38 deletions(-) diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py index 54b07d59b..00f835f5b 100644 --- a/klippy/extras/force_move.py +++ b/klippy/extras/force_move.py @@ -33,10 +33,10 @@ class ForceMove: self.printer = config.get_printer() self.steppers = {} # Setup iterative solver + self.motion_queuing = self.printer.load_object(config, 'motion_queuing') + self.trapq = self.motion_queuing.allocate_trapq() + self.trapq_append = self.motion_queuing.lookup_trapq_append() ffi_main, ffi_lib = chelper.get_ffi() - self.trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) - self.trapq_append = ffi_lib.trapq_append - self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves self.stepper_kinematics = ffi_main.gc( ffi_lib.cartesian_stepper_alloc(b'x'), ffi_lib.free) # Register commands @@ -90,8 +90,7 @@ class ForceMove: toolhead.flush_step_generation() stepper.set_trapq(prev_trapq) stepper.set_stepper_kinematics(prev_sk) - self.trapq_finalize_moves(self.trapq, print_time + 99999.9, - print_time + 99999.9) + self.motion_queuing.wipe_trapq(self.trapq) def _lookup_stepper(self, gcmd): name = gcmd.get('STEPPER') if name not in self.steppers: diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 03816b5cc..05899f58a 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -25,10 +25,9 @@ class ManualStepper: self.pos_min = config.getfloat('position_min', None) self.pos_max = config.getfloat('position_max', None) # Setup iterative solver - ffi_main, ffi_lib = chelper.get_ffi() - self.trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) - self.trapq_append = ffi_lib.trapq_append - self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves + self.motion_queuing = self.printer.load_object(config, 'motion_queuing') + self.trapq = self.motion_queuing.allocate_trapq() + self.trapq_append = self.motion_queuing.lookup_trapq_append() self.rail.setup_itersolve('cartesian_stepper_alloc', b'x') self.rail.set_trapq(self.trapq) # Registered with toolhead as an axtra axis @@ -76,8 +75,6 @@ class ManualStepper: self.sync_print_time() self.next_cmd_time = self._submit_move(self.next_cmd_time, movepos, speed, accel) - self.trapq_finalize_moves(self.trapq, self.next_cmd_time + 99999.9, - self.next_cmd_time + 99999.9) toolhead = self.printer.lookup_object('toolhead') toolhead.note_mcu_movequeue_activity(self.next_cmd_time) if sync: @@ -208,7 +205,7 @@ class ManualStepper: toolhead.drip_update_time(maxtime, drip_completion) # Clear trapq of any remaining parts of movement reactor = self.printer.get_reactor() - self.trapq_finalize_moves(self.trapq, reactor.NEVER, 0) + self.motion_queuing.wipe_trapq(self.trapq) self.rail.set_position([newpos[0], 0., 0.]) self.sync_print_time() def get_kinematics(self): diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 4b5bd017d..d8db880ab 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -4,16 +4,36 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import logging +import chelper class PrinterMotionQueuing: def __init__(self, config): self.printer = config.get_printer() self.steppers = [] + self.trapqs = [] + ffi_main, ffi_lib = chelper.get_ffi() + self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves + def allocate_trapq(self): + ffi_main, ffi_lib = chelper.get_ffi() + trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) + self.trapqs.append(trapq) + return trapq def register_stepper(self, config, stepper): self.steppers.append(stepper) def flush_motion_queues(self, must_flush_time, max_step_gen_time): for stepper in self.steppers: stepper.generate_steps(max_step_gen_time) + def clean_motion_queues(self, trapq_free_time, clear_history_time): + for trapq in self.trapqs: + self.trapq_finalize_moves(trapq, trapq_free_time, + clear_history_time) + 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.) + def lookup_trapq_append(self): + ffi_main, ffi_lib = chelper.get_ffi() + return ffi_lib.trapq_append def load_config(config): return PrinterMotionQueuing(config) diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index e3e375bcc..a89e3bdfa 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -163,10 +163,9 @@ class PrinterExtruder: self.instant_corner_v = config.getfloat( 'instantaneous_corner_velocity', 1., minval=0.) # Setup extruder trapq (trapezoidal motion queue) - ffi_main, ffi_lib = chelper.get_ffi() - self.trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) - self.trapq_append = ffi_lib.trapq_append - self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves + self.motion_queuing = self.printer.load_object(config, 'motion_queuing') + self.trapq = self.motion_queuing.allocate_trapq() + self.trapq_append = self.motion_queuing.lookup_trapq_append() # Setup extruder stepper self.extruder_stepper = None if (config.get('step_pin', None) is not None diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 3b9c0e8e3..bc8c254d1 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -245,14 +245,10 @@ class ToolHead: # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME self.kin_flush_times = [] - # Setup iterative solver - ffi_main, ffi_lib = chelper.get_ffi() - self.trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) - self.trapq_append = ffi_lib.trapq_append - self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves - # Motion flushing + # Setup for generating moves self.motion_queuing = self.printer.load_object(config, 'motion_queuing') - self.flush_trapqs = [self.trapq] + self.trapq = self.motion_queuing.allocate_trapq() + self.trapq_append = self.motion_queuing.lookup_trapq_append() # Create kinematics class gcode = self.printer.lookup_object('gcode') self.Coord = gcode.Coord @@ -287,8 +283,7 @@ class ToolHead: if not self.can_pause: clear_history_time = flush_time - MOVE_HISTORY_EXPIRE free_time = sg_flush_time - self.kin_flush_delay - for trapq in self.flush_trapqs: - self.trapq_finalize_moves(trapq, free_time, clear_history_time) + self.motion_queuing.clean_motion_queues(free_time, clear_history_time) # Flush stepcompress and mcu steppersync for m in self.all_mcus: m.flush_moves(flush_time, clear_history_time) @@ -484,32 +479,20 @@ class ToolHead: eventtime = self.reactor.pause(eventtime + 0.100) def set_extruder(self, extruder, extrude_pos): # XXX - should use add_extra_axis - prev_ea_trapq = self.extra_axes[0].get_trapq() - if prev_ea_trapq in self.flush_trapqs: - self.flush_trapqs.remove(prev_ea_trapq) self.extra_axes[0] = extruder self.commanded_pos[3] = extrude_pos - ea_trapq = extruder.get_trapq() - if ea_trapq is not None: - self.flush_trapqs.append(ea_trapq) def get_extruder(self): return self.extra_axes[0] def add_extra_axis(self, ea, axis_pos): self._flush_lookahead() self.extra_axes.append(ea) self.commanded_pos.append(axis_pos) - ea_trapq = ea.get_trapq() - if ea_trapq is not None: - self.flush_trapqs.append(ea_trapq) self.printer.send_event("toolhead:update_extra_axes") def remove_extra_axis(self, ea): self._flush_lookahead() if ea not in self.extra_axes: return ea_index = self.extra_axes.index(ea) + 3 - ea_trapq = ea.get_trapq() - if ea_trapq in self.flush_trapqs: - self.flush_trapqs.remove(ea_trapq) self.commanded_pos.pop(ea_index) self.extra_axes.pop(ea_index - 3) self.printer.send_event("toolhead:update_extra_axes") @@ -574,7 +557,7 @@ class ToolHead: next_move_time = self._drip_load_trapq(move) self.drip_update_time(next_move_time, drip_completion) # Move finished; cleanup any remnants on trapq - self.trapq_finalize_moves(self.trapq, self.reactor.NEVER, 0) + self.motion_queuing.wipe_trapq(self.trapq) # Misc commands def stats(self, eventtime): max_queue_time = max(self.print_time, self.last_flush_time) From 128226fe8af1834ed424e509bde185202a61c4dc Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 4 Aug 2025 23:26:30 -0400 Subject: [PATCH 047/411] motion_queuing: Add allocate_steppersync() call Allocate the low-level C steppersync object from the motion_queuing module. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 9 +++++++++ klippy/mcu.py | 14 +++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index d8db880ab..21d5465ed 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -11,6 +11,7 @@ class PrinterMotionQueuing: self.printer = config.get_printer() self.steppers = [] self.trapqs = [] + self.steppersyncs = [] ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves def allocate_trapq(self): @@ -18,6 +19,14 @@ class PrinterMotionQueuing: trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) self.trapqs.append(trapq) return trapq + def allocate_steppersync(self, mcu, serialqueue, stepqueues, move_count): + ffi_main, ffi_lib = chelper.get_ffi() + ss = ffi_main.gc( + ffi_lib.steppersync_alloc(serialqueue, stepqueues, len(stepqueues), + move_count), + ffi_lib.steppersync_free) + self.steppersyncs.append((mcu, ss)) + return ss def register_stepper(self, config, stepper): self.steppers.append(stepper) def flush_motion_queues(self, must_flush_time, max_step_gen_time): diff --git a/klippy/mcu.py b/klippy/mcu.py index d3b8ffd79..48eee4e5a 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -770,13 +770,13 @@ class MCU: move_count = config_params['move_count'] if move_count < self._reserved_move_slots: raise error("Too few moves available on MCU '%s'" % (self._name,)) - ffi_main, ffi_lib = chelper.get_ffi() - self._steppersync = ffi_main.gc( - ffi_lib.steppersync_alloc(self._serial.get_serialqueue(), - self._stepqueues, len(self._stepqueues), - move_count-self._reserved_move_slots), - ffi_lib.steppersync_free) - ffi_lib.steppersync_set_time(self._steppersync, 0., self._mcu_freq) + ss_move_count = move_count - self._reserved_move_slots + motion_queuing = self._printer.lookup_object('motion_queuing') + self._steppersync = motion_queuing.allocate_steppersync( + self, self._serial.get_serialqueue(), + self._stepqueues, ss_move_count) + self._ffi_lib.steppersync_set_time(self._steppersync, + 0., self._mcu_freq) # Log config information move_msg = "Configured MCU '%s' (%d moves)" % (self._name, move_count) logging.info(move_msg) From 6f685e9e01ba1581473fd340a3c3d91765626870 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 4 Aug 2025 23:52:59 -0400 Subject: [PATCH 048/411] motion_queuing: Add allocate_stepcompress() call Allocate the low-level C stepcompress object in the motion_queuing module. This simplifies the mcu.py code as it no longer needs to track the stepqueues for the steppersync object. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 13 ++++++++++++- klippy/extras/pwm_tool.py | 17 ++++++++--------- klippy/mcu.py | 6 +----- klippy/stepper.py | 27 +++++++++++++-------------- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 21d5465ed..910600199 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -11,6 +11,7 @@ class PrinterMotionQueuing: self.printer = config.get_printer() self.steppers = [] self.trapqs = [] + self.stepcompress = [] self.steppersyncs = [] ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves @@ -19,7 +20,17 @@ class PrinterMotionQueuing: trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) self.trapqs.append(trapq) return trapq - def allocate_steppersync(self, mcu, serialqueue, stepqueues, move_count): + def allocate_stepcompress(self, mcu, oid): + ffi_main, ffi_lib = chelper.get_ffi() + sc = ffi_main.gc(ffi_lib.stepcompress_alloc(oid), + ffi_lib.stepcompress_free) + self.stepcompress.append((mcu, sc)) + return sc + def allocate_steppersync(self, mcu, serialqueue, move_count): + stepqueues = [] + for sc_mcu, sc in self.stepcompress: + if sc_mcu is mcu: + stepqueues.append(sc) ffi_main, ffi_lib = chelper.get_ffi() ss = ffi_main.gc( ffi_lib.steppersync_alloc(serialqueue, stepqueues, len(stepqueues), diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index 53e101ae9..a0739b5b6 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -9,18 +9,18 @@ class error(Exception): pass class MCU_queued_pwm: - def __init__(self, pin_params): - self._mcu = pin_params['chip'] + def __init__(self, config, pin_params): + self._mcu = mcu = pin_params['chip'] self._hardware_pwm = False self._cycle_time = 0.100 self._max_duration = 2. - self._oid = self._mcu.create_oid() + 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) ffi_main, ffi_lib = chelper.get_ffi() - self._stepqueue = ffi_main.gc(ffi_lib.stepcompress_alloc(self._oid), - ffi_lib.stepcompress_free) - self._mcu.register_stepqueue(self._stepqueue) self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg - self._mcu.register_config_callback(self._build_config) + mcu.register_config_callback(self._build_config) self._pin = pin_params['pin'] self._invert = pin_params['invert'] self._start_value = self._shutdown_value = float(self._invert) @@ -29,7 +29,6 @@ class MCU_queued_pwm: self._pwm_max = 0. self._set_cmd_tag = None self._toolhead = None - printer = self._mcu.get_printer() printer.register_event_handler("klippy:connect", self._handle_connect) def _handle_connect(self): self._toolhead = self._mcu.get_printer().lookup_object("toolhead") @@ -135,7 +134,7 @@ class PrinterOutputPin: ppins = self.printer.lookup_object('pins') # Determine pin type pin_params = ppins.lookup_pin(config.get('pin'), can_invert=True) - self.mcu_pin = MCU_queued_pwm(pin_params) + self.mcu_pin = MCU_queued_pwm(config, pin_params) max_duration = self.mcu_pin.get_mcu().max_nominal_duration() cycle_time = config.getfloat('cycle_time', 0.100, above=0., maxval=max_duration) diff --git a/klippy/mcu.py b/klippy/mcu.py index 48eee4e5a..d7665036b 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -605,7 +605,6 @@ class MCU: self._max_stepper_error = config.getfloat('max_stepper_error', 0.000025, minval=0.) self._reserved_move_slots = 0 - self._stepqueues = [] self._steppersync = None self._flush_callbacks = [] # Stats @@ -773,8 +772,7 @@ class MCU: ss_move_count = move_count - self._reserved_move_slots motion_queuing = self._printer.lookup_object('motion_queuing') self._steppersync = motion_queuing.allocate_steppersync( - self, self._serial.get_serialqueue(), - self._stepqueues, ss_move_count) + self, self._serial.get_serialqueue(), ss_move_count) self._ffi_lib.steppersync_set_time(self._steppersync, 0., self._mcu_freq) # Log config information @@ -971,8 +969,6 @@ class MCU: def _firmware_restart_bridge(self): self._firmware_restart(True) # Move queue tracking - def register_stepqueue(self, stepqueue): - self._stepqueues.append(stepqueue) def request_move_queue_slot(self): self._reserved_move_slots += 1 def register_flush_callback(self, callback): diff --git a/klippy/stepper.py b/klippy/stepper.py index 5ba76a5b0..6ce9130ff 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -19,22 +19,23 @@ MIN_OPTIMIZED_BOTH_EDGE_DURATION = 0.000000150 # Interface to low-level mcu and chelper code class MCU_stepper: - def __init__(self, name, step_pin_params, dir_pin_params, + def __init__(self, config, step_pin_params, dir_pin_params, rotation_dist, steps_per_rotation, step_pulse_duration=None, units_in_radians=False): - self._name = name + self._name = config.get_name() self._rotation_dist = rotation_dist self._steps_per_rotation = steps_per_rotation self._step_pulse_duration = step_pulse_duration self._units_in_radians = units_in_radians self._step_dist = rotation_dist / steps_per_rotation - self._mcu = step_pin_params['chip'] - self._oid = oid = self._mcu.create_oid() - self._mcu.register_config_callback(self._build_config) + self._mcu = mcu = step_pin_params['chip'] + self._oid = oid = mcu.create_oid() + mcu.register_config_callback(self._build_config) self._step_pin = step_pin_params['pin'] self._invert_step = step_pin_params['invert'] - if dir_pin_params['chip'] is not self._mcu: - raise self._mcu.get_printer().config_error( + printer = mcu.get_printer() + if dir_pin_params['chip'] is not mcu: + raise printer.config_error( "Stepper dir pin must be on same mcu as step pin") self._dir_pin = dir_pin_params['pin'] self._invert_dir = self._orig_invert_dir = dir_pin_params['invert'] @@ -42,17 +43,16 @@ class MCU_stepper: self._mcu_position_offset = 0. self._reset_cmd_tag = self._get_position_cmd = None self._active_callbacks = [] + motion_queuing = printer.load_object(config, 'motion_queuing') + self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid) ffi_main, ffi_lib = chelper.get_ffi() - self._stepqueue = ffi_main.gc(ffi_lib.stepcompress_alloc(oid), - ffi_lib.stepcompress_free) ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir) - self._mcu.register_stepqueue(self._stepqueue) self._stepper_kinematics = None self._itersolve_generate_steps = ffi_lib.itersolve_generate_steps self._itersolve_check_active = ffi_lib.itersolve_check_active self._trapq = ffi_main.NULL - self._mcu.get_printer().register_event_handler('klippy:connect', - self._query_mcu_position) + printer.register_event_handler('klippy:connect', + self._query_mcu_position) def get_mcu(self): return self._mcu def get_name(self, short=False): @@ -258,7 +258,6 @@ class MCU_stepper: # Helper code to build a stepper object from a config section def PrinterStepper(config, units_in_radians=False): printer = config.get_printer() - name = config.get_name() # Stepper definition ppins = printer.lookup_object('pins') step_pin = config.get('step_pin') @@ -269,7 +268,7 @@ def PrinterStepper(config, units_in_radians=False): config, units_in_radians, True) step_pulse_duration = config.getfloat('step_pulse_duration', None, minval=0., maxval=.001) - mcu_stepper = MCU_stepper(name, step_pin_params, dir_pin_params, + mcu_stepper = MCU_stepper(config, step_pin_params, dir_pin_params, rotation_dist, steps_per_rotation, step_pulse_duration, units_in_radians) # Register with helper modules From c09ca4cf5a547f82c00e974e35a577ebcc5b2a4f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 5 Aug 2025 00:06:32 -0400 Subject: [PATCH 049/411] motion_queuing: Add register_flush_callback() Move register_flush_callback() from mcu.py code to motion_queuing module. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 5 +++++ klippy/extras/output_pin.py | 5 +++-- klippy/extras/pwm_tool.py | 11 +++++++---- klippy/mcu.py | 5 ----- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 910600199..55a7b9295 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -13,6 +13,7 @@ class PrinterMotionQueuing: self.trapqs = [] self.stepcompress = [] self.steppersyncs = [] + self.flush_callbacks = [] ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves def allocate_trapq(self): @@ -40,7 +41,11 @@ class PrinterMotionQueuing: return ss def register_stepper(self, config, stepper): self.steppers.append(stepper) + def register_flush_callback(self, callback): + self.flush_callbacks.append(callback) def flush_motion_queues(self, must_flush_time, max_step_gen_time): + for cb in self.flush_callbacks: + cb(must_flush_time) for stepper in self.steppers: stepper.generate_steps(max_step_gen_time) def clean_motion_queues(self, trapq_free_time, clear_history_time): diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 052663821..9eb8ea8ba 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -20,11 +20,12 @@ class GCodeRequestQueue: self.rqueue = [] self.next_min_flush_time = 0. self.toolhead = None - mcu.register_flush_callback(self._flush_notification) + motion_queuing = printer.load_object(config, 'motion_queuing') + 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') - def _flush_notification(self, print_time, clock): + def _flush_notification(self, print_time): min_sched_time = self.mcu.min_schedule_time() rqueue = self.rqueue while rqueue: diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index a0739b5b6..69fc1a468 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -46,12 +46,13 @@ class MCU_queued_pwm: self._start_value = max(0., min(1., start_value)) self._shutdown_value = max(0., min(1., shutdown_value)) def _build_config(self): - config_error = self._mcu.get_printer().config_error + printer = self._mcu.get_printer() + config_error = printer.config_error if self._max_duration and self._start_value != self._shutdown_value: raise config_error("Pin with max duration must have start" " value equal to shutdown value") cmd_queue = self._mcu.alloc_command_queue() - curtime = self._mcu.get_printer().get_reactor().monotonic() + curtime = printer.get_reactor().monotonic() printtime = self._mcu.estimated_print_time(curtime) self._last_clock = self._mcu.print_time_to_clock(printtime + 0.200) cycle_ticks = self._mcu.seconds_to_clock(self._cycle_time) @@ -61,7 +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: - self._mcu.register_flush_callback(self._flush_notification) + motion_queuing = printer.lookup_object('motion_queuing') + 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 @@ -122,7 +124,8 @@ class MCU_queued_pwm: value = 1. - value v = int(max(0., min(1., value)) * self._pwm_max + 0.5) self._send_update(clock, v) - def _flush_notification(self, print_time, clock): + def _flush_notification(self, print_time): + clock = self._mcu.print_time_to_clock(print_time) if self._last_value != self._default_value: while clock >= self._last_clock + self._duration_ticks: self._send_update(self._last_clock + self._duration_ticks, diff --git a/klippy/mcu.py b/klippy/mcu.py index d7665036b..c8bc9c9bc 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -606,7 +606,6 @@ class MCU: minval=0.) self._reserved_move_slots = 0 self._steppersync = None - self._flush_callbacks = [] # Stats self._get_status_info = {} self._stats_sumsq_base = 0. @@ -971,16 +970,12 @@ class MCU: # Move queue tracking def request_move_queue_slot(self): self._reserved_move_slots += 1 - def register_flush_callback(self, callback): - self._flush_callbacks.append(callback) def flush_moves(self, print_time, clear_history_time): if self._steppersync is None: return clock = self.print_time_to_clock(print_time) if clock < 0: return - for cb in self._flush_callbacks: - cb(print_time, clock) clear_history_clock = \ max(0, self.print_time_to_clock(clear_history_time)) ret = self._ffi_lib.steppersync_flush(self._steppersync, clock, From 864c78f24a0dbfe120313ab996c6e4257f644344 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 5 Aug 2025 00:16:13 -0400 Subject: [PATCH 050/411] motion_queueing: Add flush_steppersync() Move the mcu.flush_moves() code to motion_queuing.flush_steppersync(). Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 12 ++++++++++++ klippy/mcu.py | 13 ------------- klippy/toolhead.py | 3 +-- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 55a7b9295..fdbb5df29 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -16,6 +16,7 @@ class PrinterMotionQueuing: self.flush_callbacks = [] ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves + self.steppersync_flush = ffi_lib.steppersync_flush def allocate_trapq(self): ffi_main, ffi_lib = chelper.get_ffi() trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) @@ -52,6 +53,17 @@ class PrinterMotionQueuing: for trapq in self.trapqs: self.trapq_finalize_moves(trapq, trapq_free_time, clear_history_time) + def flush_steppersync(self, print_time, clear_history_time): + for mcu, ss in self.steppersyncs: + clock = mcu.print_time_to_clock(print_time) + if clock < 0: + continue + clear_history_clock = \ + max(0, mcu.print_time_to_clock(clear_history_time)) + ret = self.steppersync_flush(ss, clock, clear_history_clock) + if ret: + raise mcu.error("Internal error in MCU '%s' stepcompress" + % (mcu.get_name(),)) def wipe_trapq(self, trapq): # Expire any remaining movement in the trapq (force to history list) NEVER = 9999999999999999. diff --git a/klippy/mcu.py b/klippy/mcu.py index c8bc9c9bc..db02e2a47 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -970,19 +970,6 @@ class MCU: # Move queue tracking def request_move_queue_slot(self): self._reserved_move_slots += 1 - def flush_moves(self, print_time, clear_history_time): - if self._steppersync is None: - return - clock = self.print_time_to_clock(print_time) - if clock < 0: - return - clear_history_clock = \ - max(0, self.print_time_to_clock(clear_history_time)) - ret = self._ffi_lib.steppersync_flush(self._steppersync, clock, - clear_history_clock) - if ret: - raise error("Internal error in MCU '%s' stepcompress" - % (self._name,)) def check_active(self, print_time, eventtime): if self._steppersync is None: return diff --git a/klippy/toolhead.py b/klippy/toolhead.py index bc8c254d1..6fd5f2deb 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -285,8 +285,7 @@ class ToolHead: free_time = sg_flush_time - self.kin_flush_delay self.motion_queuing.clean_motion_queues(free_time, clear_history_time) # Flush stepcompress and mcu steppersync - for m in self.all_mcus: - m.flush_moves(flush_time, clear_history_time) + self.motion_queuing.flush_steppersync(flush_time, clear_history_time) self.last_flush_time = flush_time def _advance_move_time(self, next_print_time): pt_delay = self.kin_flush_delay + STEPCOMPRESS_FLUSH_TIME From 7b25d1c06f1b31fd78972383d4c1ee402cb4c737 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 5 Aug 2025 14:07:59 -0400 Subject: [PATCH 051/411] stepcompress: Export steppersync_history_expire() Don't implement history expiration from the main steppersync_flush() code. Instead, have callers directly invoke steppersync_history_expire(). Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 4 ++-- klippy/chelper/stepcompress.c | 22 ++++++---------------- klippy/chelper/stepcompress.h | 4 ++-- klippy/extras/motion_queuing.py | 8 +++++--- 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 622cc9a72..d56b720c8 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -60,8 +60,8 @@ defs_stepcompress = """ void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); - int steppersync_flush(struct steppersync *ss, uint64_t move_clock - , uint64_t clear_history_clock); + void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); + int steppersync_flush(struct steppersync *ss, uint64_t move_clock); """ defs_itersolve = """ diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 310f2bf31..939ac2c1b 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -276,9 +276,9 @@ stepcompress_set_invert_sdir(struct stepcompress *sc, uint32_t invert_sdir) } } -// Helper to free items from the history_list +// Expire the stepcompress history older than the given clock static void -free_history(struct stepcompress *sc, uint64_t end_clock) +stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock) { while (!list_empty(&sc->history_list)) { struct history_steps *hs = list_last_entry( @@ -290,13 +290,6 @@ free_history(struct stepcompress *sc, uint64_t end_clock) } } -// Expire the stepcompress history older than the given clock -static void -stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock) -{ - free_history(sc, end_clock); -} - // Free memory associated with a 'stepcompress' object void __visible stepcompress_free(struct stepcompress *sc) @@ -305,7 +298,7 @@ stepcompress_free(struct stepcompress *sc) return; free(sc->queue); message_queue_free(&sc->msg_queue); - free_history(sc, UINT64_MAX); + stepcompress_history_expire(sc, UINT64_MAX); free(sc); } @@ -734,12 +727,11 @@ steppersync_set_time(struct steppersync *ss, double time_offset } // Expire the stepcompress history before the given clock time -static void +void __visible steppersync_history_expire(struct steppersync *ss, uint64_t end_clock) { int i; - for (i = 0; i < ss->sc_num; i++) - { + for (i = 0; i < ss->sc_num; i++) { struct stepcompress *sc = ss->sc_list[i]; stepcompress_history_expire(sc, end_clock); } @@ -772,8 +764,7 @@ heap_replace(struct steppersync *ss, uint64_t req_clock) // Find and transmit any scheduled steps prior to the given 'move_clock' int __visible -steppersync_flush(struct steppersync *ss, uint64_t move_clock - , uint64_t clear_history_clock) +steppersync_flush(struct steppersync *ss, uint64_t move_clock) { // Flush each stepcompress to the specified move_clock int i; @@ -822,6 +813,5 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock if (!list_empty(&msgs)) serialqueue_send_batch(ss->sq, ss->cq, &msgs); - steppersync_history_expire(ss, clear_history_clock); return 0; } diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index c5b40383f..06bc7a497 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -42,7 +42,7 @@ struct steppersync *steppersync_alloc( void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); -int steppersync_flush(struct steppersync *ss, uint64_t move_clock - , uint64_t clear_history_clock); +void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); +int steppersync_flush(struct steppersync *ss, uint64_t move_clock); #endif // stepcompress.h diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index fdbb5df29..3a7bba460 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -17,6 +17,7 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves self.steppersync_flush = ffi_lib.steppersync_flush + self.steppersync_history_expire = ffi_lib.steppersync_history_expire def allocate_trapq(self): ffi_main, ffi_lib = chelper.get_ffi() trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) @@ -58,12 +59,13 @@ class PrinterMotionQueuing: clock = mcu.print_time_to_clock(print_time) if clock < 0: continue - clear_history_clock = \ - max(0, mcu.print_time_to_clock(clear_history_time)) - ret = self.steppersync_flush(ss, clock, clear_history_clock) + ret = self.steppersync_flush(ss, clock) if ret: raise mcu.error("Internal error in MCU '%s' stepcompress" % (mcu.get_name(),)) + clear_history_clock = \ + max(0, mcu.print_time_to_clock(clear_history_time)) + self.steppersync_history_expire(ss, clear_history_clock) def wipe_trapq(self, trapq): # Expire any remaining movement in the trapq (force to history list) NEVER = 9999999999999999. From 1d569a6631781ed8b3d39eadaf203272d93769e1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 5 Aug 2025 14:27:45 -0400 Subject: [PATCH 052/411] motion_queuing: Remove flush_steppersync() Move code from flush_steppersync() to existing flush_motion_queues() and clean_motion_queues() functions. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 24 +++++++++++++----------- klippy/toolhead.py | 2 -- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 3a7bba460..c0de615d7 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -46,26 +46,28 @@ class PrinterMotionQueuing: def register_flush_callback(self, callback): self.flush_callbacks.append(callback) 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) + # Generate itersolve steps for stepper in self.steppers: stepper.generate_steps(max_step_gen_time) - def clean_motion_queues(self, trapq_free_time, clear_history_time): - for trapq in self.trapqs: - self.trapq_finalize_moves(trapq, trapq_free_time, - clear_history_time) - def flush_steppersync(self, print_time, clear_history_time): + # Flush steps from stepcompress and steppersync for mcu, ss in self.steppersyncs: - clock = mcu.print_time_to_clock(print_time) - if clock < 0: - continue + clock = max(0, mcu.print_time_to_clock(must_flush_time)) ret = self.steppersync_flush(ss, clock) if ret: raise mcu.error("Internal error in MCU '%s' stepcompress" % (mcu.get_name(),)) - clear_history_clock = \ - max(0, mcu.print_time_to_clock(clear_history_time)) - self.steppersync_history_expire(ss, clear_history_clock) + def clean_motion_queues(self, trapq_free_time, clear_history_time): + # Move processed trapq moves to history list, and expire old history + for trapq in self.trapqs: + self.trapq_finalize_moves(trapq, trapq_free_time, + clear_history_time) + # Clean up old history entries in stepcompress objects + for mcu, ss in self.steppersyncs: + clock = max(0, mcu.print_time_to_clock(clear_history_time)) + self.steppersync_history_expire(ss, clock) def wipe_trapq(self, trapq): # Expire any remaining movement in the trapq (force to history list) NEVER = 9999999999999999. diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 6fd5f2deb..9a6c23455 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -284,8 +284,6 @@ class ToolHead: clear_history_time = flush_time - MOVE_HISTORY_EXPIRE free_time = sg_flush_time - self.kin_flush_delay self.motion_queuing.clean_motion_queues(free_time, clear_history_time) - # Flush stepcompress and mcu steppersync - self.motion_queuing.flush_steppersync(flush_time, clear_history_time) self.last_flush_time = flush_time def _advance_move_time(self, next_print_time): pt_delay = self.kin_flush_delay + STEPCOMPRESS_FLUSH_TIME From 6d592794381af0e93338b272dce3f1262499db31 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 5 Aug 2025 13:56:17 -0400 Subject: [PATCH 053/411] statistics: Avoid adding extra blank spaces on empty stats reports Signed-off-by: Kevin O'Connor --- klippy/extras/statistics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klippy/extras/statistics.py b/klippy/extras/statistics.py index 90cd53f8d..9e6188a81 100644 --- a/klippy/extras/statistics.py +++ b/klippy/extras/statistics.py @@ -65,8 +65,8 @@ class PrinterStats: def generate_stats(self, eventtime): stats = [cb(eventtime) for cb in self.stats_cb] if max([s[0] for s in stats]): - logging.info("Stats %.1f: %s", eventtime, - ' '.join([s[1] for s in stats])) + stats_str = ' '.join([s[1] for s in stats if s[1]]) + logging.info("Stats %.1f: %s", eventtime, stats_str) return eventtime + 1. def load_config(config): From b5e573957cb8b18871ae81fe873402bcc39fac44 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 5 Aug 2025 14:39:04 -0400 Subject: [PATCH 054/411] motion_queuing: Move clear_history_time from toolhead to motion_queuing Implement the 30 second clear_history_time offset checking directly in the motion_queuing module. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 15 ++++++++++++++- klippy/toolhead.py | 9 ++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index c0de615d7..6fadbef15 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -6,6 +6,8 @@ import logging import chelper +MOVE_HISTORY_EXPIRE = 30. + class PrinterMotionQueuing: def __init__(self, config): self.printer = config.get_printer() @@ -18,6 +20,9 @@ class PrinterMotionQueuing: self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves self.steppersync_flush = ffi_lib.steppersync_flush self.steppersync_history_expire = ffi_lib.steppersync_history_expire + self.clear_history_time = 0. + is_debug = self.printer.get_start_args().get('debugoutput') is not None + self.is_debugoutput = is_debug def allocate_trapq(self): ffi_main, ffi_lib = chelper.get_ffi() trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) @@ -59,7 +64,10 @@ class PrinterMotionQueuing: if ret: raise mcu.error("Internal error in MCU '%s' stepcompress" % (mcu.get_name(),)) - def clean_motion_queues(self, trapq_free_time, clear_history_time): + def clean_motion_queues(self, trapq_free_time): + clear_history_time = self.clear_history_time + if self.is_debugoutput: + 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: self.trapq_finalize_moves(trapq, trapq_free_time, @@ -75,6 +83,11 @@ class PrinterMotionQueuing: def lookup_trapq_append(self): ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append + def stats(self, eventtime): + mcu = self.printer.lookup_object('mcu') + est_print_time = mcu.estimated_print_time(eventtime) + self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE + return False, "" def load_config(config): return PrinterMotionQueuing(config) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 9a6c23455..7f53ef44e 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -200,7 +200,6 @@ 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 -MOVE_HISTORY_EXPIRE = 30. DRIP_SEGMENT_TIME = 0.050 DRIP_TIME = 0.100 @@ -241,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.min_restart_time = 0. - self.need_flush_time = self.step_gen_time = self.clear_history_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 self.kin_flush_times = [] @@ -279,11 +278,8 @@ class ToolHead: self.motion_queuing.flush_motion_queues(flush_time, sg_flush_time) self.min_restart_time = max(self.min_restart_time, sg_flush_time) # Free trapq entries that are no longer needed - clear_history_time = self.clear_history_time - if not self.can_pause: - clear_history_time = flush_time - MOVE_HISTORY_EXPIRE free_time = sg_flush_time - self.kin_flush_delay - self.motion_queuing.clean_motion_queues(free_time, clear_history_time) + self.motion_queuing.clean_motion_queues(free_time) self.last_flush_time = flush_time def _advance_move_time(self, next_print_time): pt_delay = self.kin_flush_delay + STEPCOMPRESS_FLUSH_TIME @@ -561,7 +557,6 @@ class ToolHead: for m in self.all_mcus: m.check_active(max_queue_time, eventtime) est_print_time = self.mcu.estimated_print_time(eventtime) - self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE 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": From c454e88d9aec86b149e1e3fa9daa75944872e74a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 5 Aug 2025 14:50:36 -0400 Subject: [PATCH 055/411] stepper: Implement active callbacks via motion_queuing.register_flush_callback() Use the existing register_flush_callback() system to implement motor activity checking. This simplifies the generate_steps() code. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 7 ++++++- klippy/extras/output_pin.py | 4 ++-- klippy/extras/pwm_tool.py | 4 ++-- klippy/stepper.py | 28 +++++++++++++++++++--------- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 6fadbef15..06fa5a9b4 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -50,10 +50,15 @@ class PrinterMotionQueuing: self.steppers.append(stepper) def register_flush_callback(self, callback): self.flush_callbacks.append(callback) + def unregister_flush_callback(self, callback): + if callback in self.flush_callbacks: + fcbs = list(self.flush_callbacks) + fcbs.remove(callback) + self.flush_callbacks = fcbs 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) + cb(must_flush_time, max_step_gen_time) # Generate itersolve steps for stepper in self.steppers: stepper.generate_steps(max_step_gen_time) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 9eb8ea8ba..63862d978 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -25,12 +25,12 @@ class GCodeRequestQueue: printer.register_event_handler("klippy:connect", self._handle_connect) def _handle_connect(self): self.toolhead = self.printer.lookup_object('toolhead') - def _flush_notification(self, print_time): + def _flush_notification(self, must_flush_time, max_step_gen_time): min_sched_time = self.mcu.min_schedule_time() rqueue = self.rqueue while rqueue: next_time = max(rqueue[0][0], self.next_min_flush_time) - if next_time > print_time: + if next_time > must_flush_time: return # Skip requests that have been overridden with a following request pos = 0 diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index 69fc1a468..6d401c0b0 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -124,8 +124,8 @@ class MCU_queued_pwm: value = 1. - value v = int(max(0., min(1., value)) * self._pwm_max + 0.5) self._send_update(clock, v) - def _flush_notification(self, print_time): - clock = self._mcu.print_time_to_clock(print_time) + def _flush_notification(self, must_flush_time, max_step_gen_time): + clock = self._mcu.print_time_to_clock(must_flush_time) if self._last_value != self._default_value: while clock >= self._last_clock + self._duration_ticks: self._send_update(self._last_clock + self._duration_ticks, diff --git a/klippy/stepper.py b/klippy/stepper.py index 6ce9130ff..8e6a883d7 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -235,16 +235,26 @@ class MCU_stepper: return old_tq def add_active_callback(self, cb): self._active_callbacks.append(cb) + if len(self._active_callbacks) == 1: + printer = self._mcu.get_printer() + motion_queuing = printer.lookup_object('motion_queuing') + motion_queuing.register_flush_callback(self._check_active) + def _check_active(self, must_flush_time, max_step_gen_time): + sk = self._stepper_kinematics + ret = self._itersolve_check_active(sk, max_step_gen_time) + if not ret: + # Stepper motor still not active + return + # Motor is active, disable future checking + printer = self._mcu.get_printer() + motion_queuing = printer.lookup_object('motion_queuing') + motion_queuing.unregister_flush_callback(self._check_active) + cbs = self._active_callbacks + self._active_callbacks = [] + # Invoke callbacks + for cb in cbs: + cb(ret) def generate_steps(self, flush_time): - # Check for activity if necessary - if self._active_callbacks: - sk = self._stepper_kinematics - ret = self._itersolve_check_active(sk, flush_time) - if ret: - cbs = self._active_callbacks - self._active_callbacks = [] - for cb in cbs: - cb(ret) # Generate steps sk = self._stepper_kinematics ret = self._itersolve_generate_steps(sk, flush_time) From c520bf981dc37bb3dccdad6dcd78012ad13abd1f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 5 Aug 2025 16:32:27 -0400 Subject: [PATCH 056/411] steppersync: Split steppersync code from stepcompress.c to new file Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 12 ++- klippy/chelper/stepcompress.c | 173 ++-------------------------------- klippy/chelper/stepcompress.h | 15 +-- klippy/chelper/steppersync.c | 168 +++++++++++++++++++++++++++++++++ klippy/chelper/steppersync.h | 16 ++++ 5 files changed, 206 insertions(+), 178 deletions(-) create mode 100644 klippy/chelper/steppersync.c create mode 100644 klippy/chelper/steppersync.h diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index d56b720c8..3800834bb 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -17,16 +17,16 @@ COMPILE_ARGS = ("-Wall -g -O2 -shared -fPIC" " -o %s %s") SSE_FLAGS = "-mfpmath=sse -msse2" SOURCE_FILES = [ - 'pyhelper.c', 'serialqueue.c', 'stepcompress.c', 'itersolve.c', 'trapq.c', - 'pollreactor.c', 'msgblock.c', 'trdispatch.c', + 'pyhelper.c', 'serialqueue.c', 'stepcompress.c', 'steppersync.c', + 'itersolve.c', 'trapq.c', 'pollreactor.c', 'msgblock.c', 'trdispatch.c', 'kin_cartesian.c', 'kin_corexy.c', 'kin_corexz.c', 'kin_delta.c', 'kin_deltesian.c', 'kin_polar.c', 'kin_rotary_delta.c', 'kin_winch.c', 'kin_extruder.c', 'kin_shaper.c', 'kin_idex.c', 'kin_generic.c' ] DEST_LIB = "c_helper.so" OTHER_FILES = [ - 'list.h', 'serialqueue.h', 'stepcompress.h', 'itersolve.h', 'pyhelper.h', - 'trapq.h', 'pollreactor.h', 'msgblock.h' + 'list.h', 'serialqueue.h', 'stepcompress.h', 'steppersync.h', + 'itersolve.h', 'pyhelper.h', 'trapq.h', 'pollreactor.h', 'msgblock.h' ] defs_stepcompress = """ @@ -54,7 +54,9 @@ defs_stepcompress = """ int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); +""" +defs_steppersync = """ struct steppersync *steppersync_alloc(struct serialqueue *sq , struct stepcompress **sc_list, int sc_num, int move_num); void steppersync_free(struct steppersync *ss); @@ -228,7 +230,7 @@ defs_std = """ defs_all = [ defs_pyhelper, defs_serialqueue, defs_std, defs_stepcompress, - defs_itersolve, defs_trapq, defs_trdispatch, + defs_steppersync, defs_itersolve, defs_trapq, defs_trdispatch, defs_kin_cartesian, defs_kin_corexy, defs_kin_corexz, defs_kin_delta, defs_kin_deltesian, defs_kin_polar, defs_kin_rotary_delta, defs_kin_winch, defs_kin_extruder, defs_kin_shaper, defs_kin_idex, diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 939ac2c1b..470feb71b 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -1,6 +1,6 @@ // Stepper pulse schedule compression // -// Copyright (C) 2016-2021 Kevin O'Connor +// Copyright (C) 2016-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -277,7 +277,7 @@ stepcompress_set_invert_sdir(struct stepcompress *sc, uint32_t invert_sdir) } // Expire the stepcompress history older than the given clock -static void +void stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock) { while (!list_empty(&sc->history_list)) { @@ -314,6 +314,12 @@ stepcompress_get_step_dir(struct stepcompress *sc) return sc->next_step_dir; } +struct list_head * +stepcompress_get_msg_queue(struct stepcompress *sc) +{ + return &sc->msg_queue; +} + // Determine the "print time" of the last_step_clock static void calc_last_step_print_time(struct stepcompress *sc) @@ -323,7 +329,7 @@ calc_last_step_print_time(struct stepcompress *sc) } // Set the conversion rate of 'print_time' to mcu clock -static void +void stepcompress_set_time(struct stepcompress *sc , double time_offset, double mcu_freq) { @@ -532,7 +538,7 @@ stepcompress_commit(struct stepcompress *sc) } // Flush pending steps -static int +int stepcompress_flush(struct stepcompress *sc, uint64_t move_clock) { if (sc->next_step_clock && move_clock >= sc->next_step_clock) { @@ -656,162 +662,3 @@ stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p } return res; } - - -/**************************************************************** - * Step compress synchronization - ****************************************************************/ - -// The steppersync object is used to synchronize the output of mcu -// step commands. The mcu can only queue a limited number of step -// commands - this code tracks when items on the mcu step queue become -// free so that new commands can be transmitted. It also ensures the -// mcu step queue is ordered between steppers so that no stepper -// starves the other steppers of space in the mcu step queue. - -struct steppersync { - // Serial port - struct serialqueue *sq; - struct command_queue *cq; - // Storage for associated stepcompress objects - struct stepcompress **sc_list; - int sc_num; - // Storage for list of pending move clocks - uint64_t *move_clocks; - int num_move_clocks; -}; - -// Allocate a new 'steppersync' object -struct steppersync * __visible -steppersync_alloc(struct serialqueue *sq, struct stepcompress **sc_list - , int sc_num, int move_num) -{ - struct steppersync *ss = malloc(sizeof(*ss)); - memset(ss, 0, sizeof(*ss)); - ss->sq = sq; - ss->cq = serialqueue_alloc_commandqueue(); - - ss->sc_list = malloc(sizeof(*sc_list)*sc_num); - memcpy(ss->sc_list, sc_list, sizeof(*sc_list)*sc_num); - ss->sc_num = sc_num; - - ss->move_clocks = malloc(sizeof(*ss->move_clocks)*move_num); - memset(ss->move_clocks, 0, sizeof(*ss->move_clocks)*move_num); - ss->num_move_clocks = move_num; - - return ss; -} - -// Free memory associated with a 'steppersync' object -void __visible -steppersync_free(struct steppersync *ss) -{ - if (!ss) - return; - free(ss->sc_list); - free(ss->move_clocks); - serialqueue_free_commandqueue(ss->cq); - free(ss); -} - -// Set the conversion rate of 'print_time' to mcu clock -void __visible -steppersync_set_time(struct steppersync *ss, double time_offset - , double mcu_freq) -{ - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_set_time(sc, time_offset, mcu_freq); - } -} - -// Expire the stepcompress history before the given clock time -void __visible -steppersync_history_expire(struct steppersync *ss, uint64_t end_clock) -{ - int i; - for (i = 0; i < ss->sc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_history_expire(sc, end_clock); - } -} - -// Implement a binary heap algorithm to track when the next available -// 'struct move' in the mcu will be available -static void -heap_replace(struct steppersync *ss, uint64_t req_clock) -{ - uint64_t *mc = ss->move_clocks; - int nmc = ss->num_move_clocks, pos = 0; - for (;;) { - int child1_pos = 2*pos+1, child2_pos = 2*pos+2; - uint64_t child2_clock = child2_pos < nmc ? mc[child2_pos] : UINT64_MAX; - uint64_t child1_clock = child1_pos < nmc ? mc[child1_pos] : UINT64_MAX; - if (req_clock <= child1_clock && req_clock <= child2_clock) { - mc[pos] = req_clock; - break; - } - if (child1_clock < child2_clock) { - mc[pos] = child1_clock; - pos = child1_pos; - } else { - mc[pos] = child2_clock; - pos = child2_pos; - } - } -} - -// Find and transmit any scheduled steps prior to the given 'move_clock' -int __visible -steppersync_flush(struct steppersync *ss, uint64_t move_clock) -{ - // Flush each stepcompress to the specified move_clock - int i; - for (i=0; isc_num; i++) { - int ret = stepcompress_flush(ss->sc_list[i], move_clock); - if (ret) - return ret; - } - - // Order commands by the reqclock of each pending command - struct list_head msgs; - list_init(&msgs); - for (;;) { - // Find message with lowest reqclock - uint64_t req_clock = MAX_CLOCK; - struct queue_message *qm = NULL; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - if (!list_empty(&sc->msg_queue)) { - struct queue_message *m = list_first_entry( - &sc->msg_queue, struct queue_message, node); - if (m->req_clock < req_clock) { - qm = m; - req_clock = m->req_clock; - } - } - } - if (!qm || (qm->min_clock && req_clock > move_clock)) - break; - - uint64_t next_avail = ss->move_clocks[0]; - if (qm->min_clock) - // The qm->min_clock field is overloaded to indicate that - // the command uses the 'move queue' and to store the time - // that move queue item becomes available. - heap_replace(ss, qm->min_clock); - // Reset the min_clock to its normal meaning (minimum transmit time) - qm->min_clock = next_avail; - - // Batch this command - list_del(&qm->node); - list_add_tail(&qm->node, &msgs); - } - - // Transmit commands - if (!list_empty(&msgs)) - serialqueue_send_batch(ss->sq, ss->cq, &msgs); - - return 0; -} diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 06bc7a497..1850436e5 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -17,12 +17,17 @@ void stepcompress_fill(struct stepcompress *sc, uint32_t max_error , int32_t set_next_step_dir_msgtag); void stepcompress_set_invert_sdir(struct stepcompress *sc , uint32_t invert_sdir); +void stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock); void stepcompress_free(struct stepcompress *sc); uint32_t stepcompress_get_oid(struct stepcompress *sc); int stepcompress_get_step_dir(struct stepcompress *sc); +struct list_head *stepcompress_get_msg_queue(struct stepcompress *sc); +void stepcompress_set_time(struct stepcompress *sc + , double time_offset, double mcu_freq); int stepcompress_append(struct stepcompress *sc, int sdir , double print_time, double step_time); int stepcompress_commit(struct stepcompress *sc); +int stepcompress_flush(struct stepcompress *sc, uint64_t move_clock); int stepcompress_reset(struct stepcompress *sc, uint64_t last_step_clock); int stepcompress_set_last_position(struct stepcompress *sc, uint64_t clock , int64_t last_position); @@ -35,14 +40,4 @@ int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); -struct serialqueue; -struct steppersync *steppersync_alloc( - struct serialqueue *sq, struct stepcompress **sc_list, int sc_num - , int move_num); -void steppersync_free(struct steppersync *ss); -void steppersync_set_time(struct steppersync *ss, double time_offset - , double mcu_freq); -void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); -int steppersync_flush(struct steppersync *ss, uint64_t move_clock); - #endif // stepcompress.h diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c new file mode 100644 index 000000000..0542ee4ff --- /dev/null +++ b/klippy/chelper/steppersync.c @@ -0,0 +1,168 @@ +// Stepper step transmit synchronization +// +// Copyright (C) 2016-2025 Kevin O'Connor +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +// The steppersync object is used to synchronize the output of mcu +// step commands. The mcu can only queue a limited number of step +// commands - this code tracks when items on the mcu step queue become +// free so that new commands can be transmitted. It also ensures the +// mcu step queue is ordered between steppers so that no stepper +// starves the other steppers of space in the mcu step queue. + +#include // offsetof +#include // malloc +#include // memset +#include "compiler.h" // __visible +#include "serialqueue.h" // struct queue_message +#include "stepcompress.h" // stepcompress_flush +#include "steppersync.h" // steppersync_alloc + +struct steppersync { + // Serial port + struct serialqueue *sq; + struct command_queue *cq; + // Storage for associated stepcompress objects + struct stepcompress **sc_list; + int sc_num; + // Storage for list of pending move clocks + uint64_t *move_clocks; + int num_move_clocks; +}; + +// Allocate a new 'steppersync' object +struct steppersync * __visible +steppersync_alloc(struct serialqueue *sq, struct stepcompress **sc_list + , int sc_num, int move_num) +{ + struct steppersync *ss = malloc(sizeof(*ss)); + memset(ss, 0, sizeof(*ss)); + ss->sq = sq; + ss->cq = serialqueue_alloc_commandqueue(); + + ss->sc_list = malloc(sizeof(*sc_list)*sc_num); + memcpy(ss->sc_list, sc_list, sizeof(*sc_list)*sc_num); + ss->sc_num = sc_num; + + ss->move_clocks = malloc(sizeof(*ss->move_clocks)*move_num); + memset(ss->move_clocks, 0, sizeof(*ss->move_clocks)*move_num); + ss->num_move_clocks = move_num; + + return ss; +} + +// Free memory associated with a 'steppersync' object +void __visible +steppersync_free(struct steppersync *ss) +{ + if (!ss) + return; + free(ss->sc_list); + free(ss->move_clocks); + serialqueue_free_commandqueue(ss->cq); + free(ss); +} + +// Set the conversion rate of 'print_time' to mcu clock +void __visible +steppersync_set_time(struct steppersync *ss, double time_offset + , double mcu_freq) +{ + int i; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + stepcompress_set_time(sc, time_offset, mcu_freq); + } +} + +// Expire the stepcompress history before the given clock time +void __visible +steppersync_history_expire(struct steppersync *ss, uint64_t end_clock) +{ + int i; + for (i = 0; i < ss->sc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + stepcompress_history_expire(sc, end_clock); + } +} + +// Implement a binary heap algorithm to track when the next available +// 'struct move' in the mcu will be available +static void +heap_replace(struct steppersync *ss, uint64_t req_clock) +{ + uint64_t *mc = ss->move_clocks; + int nmc = ss->num_move_clocks, pos = 0; + for (;;) { + int child1_pos = 2*pos+1, child2_pos = 2*pos+2; + uint64_t child2_clock = child2_pos < nmc ? mc[child2_pos] : UINT64_MAX; + uint64_t child1_clock = child1_pos < nmc ? mc[child1_pos] : UINT64_MAX; + if (req_clock <= child1_clock && req_clock <= child2_clock) { + mc[pos] = req_clock; + break; + } + if (child1_clock < child2_clock) { + mc[pos] = child1_clock; + pos = child1_pos; + } else { + mc[pos] = child2_clock; + pos = child2_pos; + } + } +} + +// Find and transmit any scheduled steps prior to the given 'move_clock' +int __visible +steppersync_flush(struct steppersync *ss, uint64_t move_clock) +{ + // Flush each stepcompress to the specified move_clock + int i; + for (i=0; isc_num; i++) { + int ret = stepcompress_flush(ss->sc_list[i], move_clock); + if (ret) + return ret; + } + + // Order commands by the reqclock of each pending command + struct list_head msgs; + list_init(&msgs); + for (;;) { + // Find message with lowest reqclock + uint64_t req_clock = MAX_CLOCK; + struct queue_message *qm = NULL; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + struct list_head *sc_mq = stepcompress_get_msg_queue(sc); + if (!list_empty(sc_mq)) { + struct queue_message *m = list_first_entry( + sc_mq, struct queue_message, node); + if (m->req_clock < req_clock) { + qm = m; + req_clock = m->req_clock; + } + } + } + if (!qm || (qm->min_clock && req_clock > move_clock)) + break; + + uint64_t next_avail = ss->move_clocks[0]; + if (qm->min_clock) + // The qm->min_clock field is overloaded to indicate that + // the command uses the 'move queue' and to store the time + // that move queue item becomes available. + heap_replace(ss, qm->min_clock); + // Reset the min_clock to its normal meaning (minimum transmit time) + qm->min_clock = next_avail; + + // Batch this command + list_del(&qm->node); + list_add_tail(&qm->node, &msgs); + } + + // Transmit commands + if (!list_empty(&msgs)) + serialqueue_send_batch(ss->sq, ss->cq, &msgs); + + return 0; +} diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h new file mode 100644 index 000000000..ffc8d5699 --- /dev/null +++ b/klippy/chelper/steppersync.h @@ -0,0 +1,16 @@ +#ifndef STEPPERSYNC_H +#define STEPPERSYNC_H + +#include // uint64_t + +struct serialqueue; +struct steppersync *steppersync_alloc( + struct serialqueue *sq, struct stepcompress **sc_list, int sc_num + , int move_num); +void steppersync_free(struct steppersync *ss); +void steppersync_set_time(struct steppersync *ss, double time_offset + , double mcu_freq); +void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); +int steppersync_flush(struct steppersync *ss, uint64_t move_clock); + +#endif // steppersync.h From dd4cc8eb4cba7ef6c510d52148589824d24043a8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 5 Aug 2025 17:07:19 -0400 Subject: [PATCH 057/411] itersolve: Do not store a reference to 'struct stepcompress' Pass in the 'struct stepcompress' reference to each call of itersolve_generate_steps(). Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 7 +++---- klippy/chelper/itersolve.c | 33 ++++++++++++++------------------- klippy/chelper/itersolve.h | 7 +++---- klippy/stepper.py | 9 ++++----- 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 3800834bb..175b10be4 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -68,13 +68,12 @@ defs_steppersync = """ defs_itersolve = """ int32_t itersolve_generate_steps(struct stepper_kinematics *sk - , double flush_time); + , struct stepcompress *sc, double flush_time); 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); - void itersolve_set_stepcompress(struct stepper_kinematics *sk - , struct stepcompress *sc, double step_dist); + void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq + , double step_dist); double itersolve_calc_position_from_coord(struct stepper_kinematics *sk , double x, double y, double z); void itersolve_set_position(struct stepper_kinematics *sk diff --git a/klippy/chelper/itersolve.c b/klippy/chelper/itersolve.c index 0dbc6c512..968c8cfb5 100644 --- a/klippy/chelper/itersolve.c +++ b/klippy/chelper/itersolve.c @@ -26,8 +26,8 @@ struct timepos { // Generate step times for a portion of a move static int32_t -itersolve_gen_steps_range(struct stepper_kinematics *sk, struct move *m - , double abs_start, double abs_end) +itersolve_gen_steps_range(struct stepper_kinematics *sk, struct stepcompress *sc + , struct move *m, double abs_start, double abs_end) { sk_calc_callback calc_position_cb = sk->calc_position_cb; double half_step = .5 * sk->step_dist; @@ -37,7 +37,7 @@ itersolve_gen_steps_range(struct stepper_kinematics *sk, struct move *m if (end > m->move_t) end = m->move_t; struct timepos old_guess = {start, sk->commanded_pos}, guess = old_guess; - int sdir = stepcompress_get_step_dir(sk->sc); + int sdir = stepcompress_get_step_dir(sc); int is_dir_change = 0, have_bracket = 0, check_oscillate = 0; double target = sk->commanded_pos + (sdir ? half_step : -half_step); double last_time=start, low_time=start, high_time=start + SEEK_TIME_RESET; @@ -99,13 +99,13 @@ itersolve_gen_steps_range(struct stepper_kinematics *sk, struct move *m if (!have_bracket || high_time - low_time > .000000001) { if (!is_dir_change && rel_dist >= -half_step) // Avoid rollback if stepper fully reaches step position - stepcompress_commit(sk->sc); + stepcompress_commit(sc); // Guess is not close enough - guess again with new time continue; } } // Found next step - submit it - int ret = stepcompress_append(sk->sc, sdir, m->print_time, guess.time); + int ret = stepcompress_append(sc, sdir, m->print_time, guess.time); if (ret) return ret; target = sdir ? target+half_step+half_step : target-half_step-half_step; @@ -144,7 +144,8 @@ check_active(struct stepper_kinematics *sk, struct move *m) // Generate step times for a range of moves on the trapq int32_t __visible -itersolve_generate_steps(struct stepper_kinematics *sk, double flush_time) +itersolve_generate_steps(struct stepper_kinematics *sk, struct stepcompress *sc + , double flush_time) { double last_flush_time = sk->last_flush_time; sk->last_flush_time = flush_time; @@ -170,15 +171,15 @@ itersolve_generate_steps(struct stepper_kinematics *sk, double flush_time) while (--skip_count && pm->print_time > abs_start) pm = list_prev_entry(pm, node); do { - int32_t ret = itersolve_gen_steps_range(sk, pm, abs_start - , flush_time); + int32_t ret = itersolve_gen_steps_range( + sk, sc, pm, abs_start, flush_time); if (ret) return ret; pm = list_next_entry(pm, node); } while (pm != m); } // Generate steps for this move - int32_t ret = itersolve_gen_steps_range(sk, m, last_flush_time + int32_t ret = itersolve_gen_steps_range(sk, sc, m, last_flush_time , flush_time); if (ret) return ret; @@ -195,8 +196,8 @@ itersolve_generate_steps(struct stepper_kinematics *sk, double flush_time) double abs_end = force_steps_time; if (abs_end > flush_time) abs_end = flush_time; - int32_t ret = itersolve_gen_steps_range(sk, m, last_flush_time - , abs_end); + int32_t ret = itersolve_gen_steps_range( + sk, sc, m, last_flush_time, abs_end); if (ret) return ret; skip_count = 1; @@ -240,16 +241,10 @@ itersolve_is_active_axis(struct stepper_kinematics *sk, char axis) } void __visible -itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq) +itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq + , double step_dist) { sk->tq = tq; -} - -void __visible -itersolve_set_stepcompress(struct stepper_kinematics *sk - , struct stepcompress *sc, double step_dist) -{ - sk->sc = sc; sk->step_dist = step_dist; } diff --git a/klippy/chelper/itersolve.h b/klippy/chelper/itersolve.h index adb480557..e2e46ebe3 100644 --- a/klippy/chelper/itersolve.h +++ b/klippy/chelper/itersolve.h @@ -26,12 +26,11 @@ struct stepper_kinematics { }; int32_t itersolve_generate_steps(struct stepper_kinematics *sk - , double flush_time); + , struct stepcompress *sc, double flush_time); 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); -void itersolve_set_stepcompress(struct stepper_kinematics *sk - , struct stepcompress *sc, double step_dist); +void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq + , double step_dist); double itersolve_calc_position_from_coord(struct stepper_kinematics *sk , double x, double y, double z); void itersolve_set_position(struct stepper_kinematics *sk diff --git a/klippy/stepper.py b/klippy/stepper.py index 8e6a883d7..8e87c538e 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -135,7 +135,7 @@ class MCU_stepper: mcu_pos = self.get_mcu_position() self._rotation_dist = rotation_dist self._step_dist = rotation_dist / self._steps_per_rotation - self.set_stepper_kinematics(self._stepper_kinematics) + self.set_trapq(self._trapq) self._set_mcu_position(mcu_pos) def get_dir_inverted(self): return self._invert_dir, self._orig_invert_dir @@ -192,8 +192,6 @@ class MCU_stepper: if old_sk is not None: mcu_pos = self.get_mcu_position() self._stepper_kinematics = sk - ffi_main, ffi_lib = chelper.get_ffi() - ffi_lib.itersolve_set_stepcompress(sk, self._stepqueue, self._step_dist) self.set_trapq(self._trapq) self._set_mcu_position(mcu_pos) return old_sk @@ -229,7 +227,8 @@ class MCU_stepper: ffi_main, ffi_lib = chelper.get_ffi() if tq is None: tq = ffi_main.NULL - ffi_lib.itersolve_set_trapq(self._stepper_kinematics, tq) + ffi_lib.itersolve_set_trapq(self._stepper_kinematics, + tq, self._step_dist) old_tq = self._trapq self._trapq = tq return old_tq @@ -257,7 +256,7 @@ class MCU_stepper: def generate_steps(self, flush_time): # Generate steps sk = self._stepper_kinematics - ret = self._itersolve_generate_steps(sk, flush_time) + ret = self._itersolve_generate_steps(sk, self._stepqueue, flush_time) if ret: raise error("Internal error in stepcompress") def is_active_axis(self, axis): From 2919f373438cc4e6268842113f2a0c40d9f8cde0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 5 Aug 2025 17:51:59 -0400 Subject: [PATCH 058/411] stepcompress: Store a reference to 'struct stepper_kinematics' Support storing a reference to 'struct stepper_kinematics' in 'struct stepcompress' and support globally generating steps via the steppersync mechanism. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 6 ++++-- klippy/chelper/itersolve.c | 2 +- klippy/chelper/stepcompress.c | 28 +++++++++++++++++++++++++++- klippy/chelper/stepcompress.h | 7 ++++++- klippy/chelper/steppersync.c | 25 +++++++++++++++++-------- klippy/chelper/steppersync.h | 2 ++ klippy/extras/motion_queuing.py | 14 +++++++------- klippy/stepper.py | 12 +++--------- 8 files changed, 67 insertions(+), 29 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 175b10be4..6ed6ed5b5 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -54,6 +54,8 @@ defs_stepcompress = """ int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); + void stepcompress_set_stepper_kinematics(struct stepcompress *sc + , struct stepper_kinematics *sk); """ defs_steppersync = """ @@ -62,13 +64,13 @@ defs_steppersync = """ void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); + int32_t steppersync_generate_steps(struct steppersync *ss + , double gen_steps_time, uint64_t flush_clock); void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); int steppersync_flush(struct steppersync *ss, uint64_t move_clock); """ defs_itersolve = """ - int32_t itersolve_generate_steps(struct stepper_kinematics *sk - , struct stepcompress *sc, double flush_time); double itersolve_check_active(struct stepper_kinematics *sk , double flush_time); int32_t itersolve_is_active_axis(struct stepper_kinematics *sk, char axis); diff --git a/klippy/chelper/itersolve.c b/klippy/chelper/itersolve.c index 968c8cfb5..eba1deef4 100644 --- a/klippy/chelper/itersolve.c +++ b/klippy/chelper/itersolve.c @@ -143,7 +143,7 @@ check_active(struct stepper_kinematics *sk, struct move *m) } // Generate step times for a range of moves on the trapq -int32_t __visible +int32_t itersolve_generate_steps(struct stepper_kinematics *sk, struct stepcompress *sc , double flush_time) { diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 470feb71b..2889570dc 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -21,6 +21,7 @@ #include // malloc #include // memset #include "compiler.h" // DIV_ROUND_UP +#include "itersolve.h" // itersolve_generate_steps #include "pyhelper.h" // errorf #include "serialqueue.h" // struct queue_message #include "stepcompress.h" // stepcompress_alloc @@ -46,6 +47,8 @@ struct stepcompress { // History tracking int64_t last_position; struct list_head history_list; + // Itersolve reference + struct stepper_kinematics *sk; }; struct step_move { @@ -538,7 +541,7 @@ stepcompress_commit(struct stepcompress *sc) } // Flush pending steps -int +static int stepcompress_flush(struct stepcompress *sc, uint64_t move_clock) { if (sc->next_step_clock && move_clock >= sc->next_step_clock) { @@ -662,3 +665,26 @@ stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p } return res; } + +// Store a reference to stepper_kinematics +void __visible +stepcompress_set_stepper_kinematics(struct stepcompress *sc + , struct stepper_kinematics *sk) +{ + sc->sk = sk; +} + +// Generate steps (via itersolve) and flush +int32_t +stepcompress_generate_steps(struct stepcompress *sc, double gen_steps_time + , uint64_t flush_clock) +{ + if (!sc->sk) + return 0; + // Generate steps + int32_t ret = itersolve_generate_steps(sc->sk, sc, gen_steps_time); + if (ret) + return ret; + // Flush steps + return stepcompress_flush(sc, flush_clock); +} diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 1850436e5..e21c4fd96 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -27,7 +27,6 @@ void stepcompress_set_time(struct stepcompress *sc int stepcompress_append(struct stepcompress *sc, int sdir , double print_time, double step_time); int stepcompress_commit(struct stepcompress *sc); -int stepcompress_flush(struct stepcompress *sc, uint64_t move_clock); int stepcompress_reset(struct stepcompress *sc, uint64_t last_step_clock); int stepcompress_set_last_position(struct stepcompress *sc, uint64_t clock , int64_t last_position); @@ -39,5 +38,11 @@ int stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); +struct stepper_kinematics; +void stepcompress_set_stepper_kinematics(struct stepcompress *sc + , struct stepper_kinematics *sk); +int32_t stepcompress_generate_steps(struct stepcompress *sc + , double gen_steps_time + , uint64_t flush_clock); #endif // stepcompress.h diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 0542ee4ff..745578c75 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -76,6 +76,22 @@ steppersync_set_time(struct steppersync *ss, double time_offset } } +// Generate steps and flush stepcompress objects +int32_t __visible +steppersync_generate_steps(struct steppersync *ss, double gen_steps_time + , uint64_t flush_clock) +{ + int i; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + int32_t ret = stepcompress_generate_steps(sc, gen_steps_time + , flush_clock); + if (ret) + return ret; + } + return 0; +} + // Expire the stepcompress history before the given clock time void __visible steppersync_history_expire(struct steppersync *ss, uint64_t end_clock) @@ -116,14 +132,6 @@ heap_replace(struct steppersync *ss, uint64_t req_clock) int __visible steppersync_flush(struct steppersync *ss, uint64_t move_clock) { - // Flush each stepcompress to the specified move_clock - int i; - for (i=0; isc_num; i++) { - int ret = stepcompress_flush(ss->sc_list[i], move_clock); - if (ret) - return ret; - } - // Order commands by the reqclock of each pending command struct list_head msgs; list_init(&msgs); @@ -131,6 +139,7 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock) // Find message with lowest reqclock uint64_t req_clock = MAX_CLOCK; struct queue_message *qm = NULL; + int i; for (i=0; isc_num; i++) { struct stepcompress *sc = ss->sc_list[i]; struct list_head *sc_mq = stepcompress_get_msg_queue(sc); diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index ffc8d5699..1320bbaa0 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -10,6 +10,8 @@ struct steppersync *steppersync_alloc( void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); +int32_t steppersync_generate_steps(struct steppersync *ss, double gen_steps_time + , uint64_t flush_clock); void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); int steppersync_flush(struct steppersync *ss, uint64_t move_clock); diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 06fa5a9b4..c13b49703 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -11,13 +11,13 @@ MOVE_HISTORY_EXPIRE = 30. class PrinterMotionQueuing: def __init__(self, config): self.printer = config.get_printer() - self.steppers = [] self.trapqs = [] self.stepcompress = [] self.steppersyncs = [] self.flush_callbacks = [] 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 self.clear_history_time = 0. @@ -46,8 +46,6 @@ class PrinterMotionQueuing: ffi_lib.steppersync_free) self.steppersyncs.append((mcu, ss)) return ss - def register_stepper(self, config, stepper): - self.steppers.append(stepper) def register_flush_callback(self, callback): self.flush_callbacks.append(callback) def unregister_flush_callback(self, callback): @@ -59,12 +57,14 @@ class PrinterMotionQueuing: # Invoke flush callbacks (if any) for cb in self.flush_callbacks: cb(must_flush_time, max_step_gen_time) - # Generate itersolve steps - for stepper in self.steppers: - stepper.generate_steps(max_step_gen_time) - # Flush steps from stepcompress and steppersync for mcu, ss in self.steppersyncs: clock = max(0, mcu.print_time_to_clock(must_flush_time)) + # Generate steps + ret = self.steppersync_generate_steps(ss, max_step_gen_time, clock) + if ret: + raise mcu.error("Internal error in MCU '%s' stepcompress" + % (mcu.get_name(),)) + # Flush steps from steppersync ret = self.steppersync_flush(ss, clock) if ret: raise mcu.error("Internal error in MCU '%s' stepcompress" diff --git a/klippy/stepper.py b/klippy/stepper.py index 8e87c538e..d5b3cecde 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -48,7 +48,6 @@ class MCU_stepper: ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir) self._stepper_kinematics = None - self._itersolve_generate_steps = ffi_lib.itersolve_generate_steps self._itersolve_check_active = ffi_lib.itersolve_check_active self._trapq = ffi_main.NULL printer.register_event_handler('klippy:connect', @@ -192,6 +191,8 @@ class MCU_stepper: if old_sk is not None: mcu_pos = self.get_mcu_position() self._stepper_kinematics = sk + ffi_main, ffi_lib = chelper.get_ffi() + ffi_lib.stepcompress_set_stepper_kinematics(self._stepqueue, sk); self.set_trapq(self._trapq) self._set_mcu_position(mcu_pos) return old_sk @@ -253,12 +254,6 @@ class MCU_stepper: # Invoke callbacks for cb in cbs: cb(ret) - def generate_steps(self, flush_time): - # Generate steps - sk = self._stepper_kinematics - ret = self._itersolve_generate_steps(sk, self._stepqueue, flush_time) - if ret: - raise error("Internal error in stepcompress") def is_active_axis(self, axis): ffi_main, ffi_lib = chelper.get_ffi() a = axis.encode() @@ -281,8 +276,7 @@ def PrinterStepper(config, units_in_radians=False): rotation_dist, steps_per_rotation, step_pulse_duration, units_in_radians) # Register with helper modules - mods = ['stepper_enable', 'force_move', 'motion_report', 'motion_queuing'] - for mname in mods: + for mname in ['stepper_enable', 'force_move', 'motion_report']: m = printer.load_object(config, mname) m.register_stepper(config, mcu_stepper) return mcu_stepper From d6d85872896dc6f938cfd8d9c19af6d308e8e7b0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 6 Aug 2025 13:27:24 -0400 Subject: [PATCH 059/411] motion_queuing: Remove clean_motion_queues() Merge the clean_motion_queues() code into the existing flush_motion_queues() code. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 6 ++++-- klippy/toolhead.py | 7 +++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index c13b49703..a06b556e1 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -53,10 +53,12 @@ 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, + trapq_free_time): # Invoke flush callbacks (if any) for cb in self.flush_callbacks: cb(must_flush_time, max_step_gen_time) + # Generate stepper movement and transmit for mcu, ss in self.steppersyncs: clock = max(0, mcu.print_time_to_clock(must_flush_time)) # Generate steps @@ -69,7 +71,7 @@ class PrinterMotionQueuing: if ret: raise mcu.error("Internal error in MCU '%s' stepcompress" % (mcu.get_name(),)) - def clean_motion_queues(self, trapq_free_time): + # Determine maximum history to keep clear_history_time = self.clear_history_time if self.is_debugoutput: clear_history_time = trapq_free_time - MOVE_HISTORY_EXPIRE diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 7f53ef44e..21aeca3df 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -275,11 +275,10 @@ 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) - self.motion_queuing.flush_motion_queues(flush_time, sg_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.min_restart_time = max(self.min_restart_time, sg_flush_time) - # Free trapq entries that are no longer needed - free_time = sg_flush_time - self.kin_flush_delay - self.motion_queuing.clean_motion_queues(free_time) self.last_flush_time = flush_time def _advance_move_time(self, next_print_time): pt_delay = self.kin_flush_delay + STEPCOMPRESS_FLUSH_TIME From edbfc6f8567b40b7c884f543a0b7cbe49ca05f24 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 14 Aug 2025 20:45:40 +0200 Subject: [PATCH 060/411] resonance_tester: replace missing M204 call Signed-off-by: Timofey Titovets --- klippy/extras/resonance_tester.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index ff32dcacf..c6171dc42 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -148,8 +148,7 @@ class ResonanceTestExecutor: last_v = last_t = last_accel = last_freq = 0. for next_t, accel, freq in test_seq: t_seg = next_t - last_t - toolhead.cmd_M204(self.gcode.create_gcode_command( - "M204", "M204", {"S": abs(accel)})) + toolhead.set_max_velocities(None, abs(accel), None, None) v = last_v + accel * t_seg abs_v = abs(v) if abs_v < 0.000001: @@ -182,8 +181,7 @@ class ResonanceTestExecutor: if last_v: d_decel = -.5 * last_v2 / old_max_accel decel_X, decel_Y = axis.get_point(d_decel) - toolhead.cmd_M204(self.gcode.create_gcode_command( - "M204", "M204", {"S": old_max_accel})) + toolhead.set_max_velocities(None, old_max_accel, None, None) toolhead.move([X + decel_X, Y + decel_Y] + tpos[2:], abs(last_v)) # Restore the original acceleration values self.gcode.run_script_from_command( From 78462cff4c1656893b61d97e30bfc20e0e9398c7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 4 Aug 2025 11:35:33 -0400 Subject: [PATCH 061/411] docs: Remove "relative_reference_index" documentation from Bed_Mesh.md The "relative_reference_index" was deprecated on 20230619 and removed on 20240215. So, remove the last references from the documentation. Signed-off-by: Kevin O'Connor --- docs/Bed_Mesh.md | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/docs/Bed_Mesh.md b/docs/Bed_Mesh.md index 5df081d15..0859913fd 100644 --- a/docs/Bed_Mesh.md +++ b/docs/Bed_Mesh.md @@ -292,33 +292,6 @@ probe_count: 5, 3 z-offset. Note that this coordinate must NOT be in a location specified as a `faulty_region` if a probe is necessary. -#### The deprecated relative_reference_index - -Existing configurations using the `relative_reference_index` option must be -updated to use the `zero_reference_position`. The response to the -[BED_MESH_OUTPUT PGP=1](#output) gcode command will include the (X, Y) -coordinate associated with the index; this position may be used as the value for -the `zero_reference_position`. The output will look similar to the following: - -``` -// bed_mesh: generated points -// Index | Tool Adjusted | Probe -// 0 | (1.0, 1.0) | (24.0, 6.0) -// 1 | (36.7, 1.0) | (59.7, 6.0) -// 2 | (72.3, 1.0) | (95.3, 6.0) -// 3 | (108.0, 1.0) | (131.0, 6.0) -... (additional generated points) -// bed_mesh: relative_reference_index 24 is (131.5, 108.0) -``` - -_Note: The above output is also printed in `klippy.log` during initialization._ - -Using the example above we see that the `relative_reference_index` is -printed along with its coordinate. Thus the `zero_reference_position` -is `131.5, 108`. - - - ### Faulty Regions It is possible for some areas of a bed to report inaccurate results when From d34d3b05b89b3bb3d56b61b0029a42af9704b2f2 Mon Sep 17 00:00:00 2001 From: BIGTREETECH <38851044+bigtreetech@users.noreply.github.com> Date: Fri, 15 Aug 2025 19:43:43 +0200 Subject: [PATCH 062/411] stm32: Add i2c2_PA7_PA6 and i2c3_PA7_PA6 for stm32g0 (#7007) Signed-off-by: Alan.Ma from BigTreeTech --- src/stm32/stm32f0_i2c.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/stm32/stm32f0_i2c.c b/src/stm32/stm32f0_i2c.c index 381fe8b40..61c848e45 100644 --- a/src/stm32/stm32f0_i2c.c +++ b/src/stm32/stm32f0_i2c.c @@ -44,11 +44,15 @@ struct i2c_info { DECL_CONSTANT_STR("BUS_PINS_i2c2_PB10_PB11", "PB10,PB11"); DECL_ENUMERATION("i2c_bus", "i2c2_PB13_PB14", 4); DECL_CONSTANT_STR("BUS_PINS_i2c2_PB13_PB14", "PB13,PB14"); + DECL_ENUMERATION("i2c_bus", "i2c2_PA7_PA6", 5); + DECL_CONSTANT_STR("BUS_PINS_i2c2_PA7_PA6", "PA7,PA6"); #ifdef I2C3 - DECL_ENUMERATION("i2c_bus", "i2c3_PB3_PB4", 5); + DECL_ENUMERATION("i2c_bus", "i2c3_PB3_PB4", 6); DECL_CONSTANT_STR("BUS_PINS_i2c3_PB3_PB4", "PB3,PB4"); - DECL_ENUMERATION("i2c_bus", "i2c3_PC0_PC1", 6); + DECL_ENUMERATION("i2c_bus", "i2c3_PC0_PC1", 7); DECL_CONSTANT_STR("BUS_PINS_i2c3_PC0_PC1", "PC0,PC1"); + DECL_ENUMERATION("i2c_bus", "i2c3_PA7_PA6", 8); + DECL_CONSTANT_STR("BUS_PINS_i2c3_PA7_PA6", "PA7,PA6"); #endif #elif CONFIG_MACH_STM32L4 DECL_ENUMERATION("i2c_bus", "i2c1_PB6_PB7", 0); @@ -105,9 +109,11 @@ static const struct i2c_info i2c_bus[] = { { I2C1, GPIO('A', 9), GPIO('A', 10), GPIO_FUNCTION(6) }, { I2C2, GPIO('B', 10), GPIO('B', 11), GPIO_FUNCTION(6) }, { I2C2, GPIO('B', 13), GPIO('B', 14), GPIO_FUNCTION(6) }, + { I2C2, GPIO('A', 7), GPIO('A', 6), GPIO_FUNCTION(8) }, #ifdef I2C3 { I2C3, GPIO('B', 3), GPIO('B', 4), GPIO_FUNCTION(6) }, { I2C3, GPIO('C', 0), GPIO('C', 1), GPIO_FUNCTION(6) }, + { I2C3, GPIO('A', 7), GPIO('A', 6), GPIO_FUNCTION(9) }, #endif #elif CONFIG_MACH_STM32L4 { I2C1, GPIO('B', 6), GPIO('B', 7), GPIO_FUNCTION(4) }, From 91b5e8e9425b5063c8a5bab9358d4c45c0e3430c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 20 Aug 2025 16:20:12 -0400 Subject: [PATCH 063/411] 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 05899f58a..3d3e614a6 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 064/411] 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 d19c39c5e..6ab45c2da 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 065/411] 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 fce3c49a9..fcec44fcd 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 066/411] 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 35d2ce838..d57a93b83 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 067/411] 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 0e82d27bf..4fdba64cc 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 068/411] 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 d27e70c9e..52cee2f5f 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 069/411] 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 8a7afc3d8..1e0df93d2 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 0ee71dcf1..3c158d518 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 070/411] 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 070b91335..82b41d812 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 071/411] 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 82b41d812..fd36c7fe1 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 072/411] 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 4121c1c86..4490efe4b 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 073/411] 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 6ed6ed5b5..60ba91e7d 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 074/411] 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 4490efe4b..66b8330ea 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 075/411] 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 1c26bbee7..284380d2e 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 076/411] 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 c76ceb767..c4fb22960 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 077/411] 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 66b8330ea..033e93415 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 078/411] 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 033e93415..9fb466390 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 db02e2a47..146b5baca 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 079/411] gcode_move: Fix M114 when extra axes are defined Commit d40fd219 added support for defining extra axes, however that change could break the M114 command. Update the code to fix M114. Signed-off-by: Kevin O'Connor --- klippy/extras/gcode_move.py | 4 ++-- test/klippy/manual_stepper.test | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/klippy/extras/gcode_move.py b/klippy/extras/gcode_move.py index 94a0ce422..655b8108f 100644 --- a/klippy/extras/gcode_move.py +++ b/klippy/extras/gcode_move.py @@ -92,7 +92,7 @@ class GCodeMove: def _get_gcode_position(self): p = [lp - bp for lp, bp in zip(self.last_position, self.base_position)] p[3] /= self.extrude_factor - return p + return p[:4] def _get_gcode_speed(self): return self.speed / self.speed_factor def _get_gcode_speed_override(self): @@ -107,7 +107,7 @@ class GCodeMove: 'absolute_extrude': self.absolute_extrude, 'homing_origin': self.Coord(*self.homing_position[:4]), 'position': self.Coord(*self.last_position[:4]), - 'gcode_position': self.Coord(*move_position[:4]), + 'gcode_position': self.Coord(*move_position), } def reset_last_position(self): if self.is_printer_ready: diff --git a/test/klippy/manual_stepper.test b/test/klippy/manual_stepper.test index 6ab45c2da..127264113 100644 --- a/test/klippy/manual_stepper.test +++ b/test/klippy/manual_stepper.test @@ -33,6 +33,10 @@ G28 G1 X20 Y20 Z10 G1 A10 X22 +# Verify position query commands work with extra axis +GET_POSITION +M114 + # Test unregistering MANUAL_STEPPER STEPPER=basic_stepper GCODE_AXIS= G1 X15 From bab5f8031c8a5dfcfcb20e299da5630ddca0949c Mon Sep 17 00:00:00 2001 From: JamesH1978 <87171443+JamesH1978@users.noreply.github.com> Date: Sun, 31 Aug 2025 19:26:15 +0100 Subject: [PATCH 080/411] docs: Update FAQ.md - Recomendation adjustment (#7025) This doc still says the Pi 2 is an option for Klipper, in this day and age, i am not sure it is. From anecdotal evidence, the lowest pi recommended should be the zero2w. I also changed the wording and removed some Octoprint wording in that section to better reflect how things are today, as i don't think even with virtual_sdcard these older devices will keep up. Signed-off-by: James Hartley --- docs/FAQ.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 417fb1d4f..7c8214b32 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -98,17 +98,16 @@ bootloaders. ## Can I run Klipper on something other than a Raspberry Pi 3? -The recommended hardware is a Raspberry Pi 2, Raspberry Pi 3, or -Raspberry Pi 4. +The recommended hardware is a Raspberry Pi Zero2w, Raspberry Pi 3, +Raspberry Pi 4 or Raspberry Pi 5. Klipper will also run on other SBC +devices as well as x86 hardware, as described below. -Klipper will run on a Raspberry Pi 1 and on the Raspberry Pi Zero, but -these boards don't have enough processing power to run OctoPrint +Klipper will run on a Raspberry Pi 1, 2 and on the Raspberry Pi Zero1, +but these boards don't have enough processing power to run Klipper well. It is common for print stalls to occur on these slower machines -when printing directly from OctoPrint. (The printer may move faster -than OctoPrint can send movement commands.) If you wish to run on one -one of these slower boards anyway, consider using the "virtual_sdcard" -feature when printing (see -[config reference](Config_Reference.md#virtual_sdcard) for details). +when printing (The printer may move faster than Klipper can send +movement commands.) It is not reccomended to run Kliper on these older +machines. For running on the Beaglebone, see the [Beaglebone specific installation instructions](Beaglebone.md). From e4c66452dc00aa448fc5771433121a44688684d3 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 31 Aug 2025 16:02:44 -0400 Subject: [PATCH 081/411] temperature_probe: Fix python2 incompatibility It seems python2 string.split() method does not accept a "maxsplit" parameter. Use a format compatible with both python2 and python3. Signed-off-by: Kevin O'Connor --- klippy/extras/temperature_probe.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py index c480ddae8..aebb10764 100644 --- a/klippy/extras/temperature_probe.py +++ b/klippy/extras/temperature_probe.py @@ -132,7 +132,7 @@ class TemperatureProbe: self.start_pos = [] # Register GCode Commands - pname = self.name.split(maxsplit=1)[-1] + pname = self.name.split(None, 1)[-1] self.gcode.register_mux_command( "TEMPERATURE_PROBE_CALIBRATE", "PROBE", pname, self.cmd_TEMPERATURE_PROBE_CALIBRATE, @@ -357,8 +357,8 @@ class TemperatureProbe: self._check_homed() probe = self._get_probe() probe_name = probe.get_status(None)["name"] - short_name = probe_name.split(maxsplit=1)[-1] - if short_name != self.name.split(maxsplit=1)[-1]: + short_name = probe_name.split(None, 1)[-1] + if short_name != self.name.split(None, 1)[-1]: raise self.gcode.error( "[%s] not linked to registered probe [%s]." % (self.name, probe_name) @@ -588,7 +588,7 @@ class EddyDriftCompensation: temps[idx] = cur_temp probe_samples[idx].append(sample) return True - sect_name = "probe_eddy_current " + self.name.split(maxsplit=1)[-1] + sect_name = "probe_eddy_current " + self.name.split(None, 1)[-1] self.printer.lookup_object(sect_name).add_client(_on_bulk_data_recd) for i in range(DRIFT_SAMPLE_COUNT): if i == 0: From 20d9c84a9f78f418da3b098147002a8edff02dda Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 1 Sep 2025 21:20:00 -0400 Subject: [PATCH 082/411] toolhead: Fix incorrect response message in SET_VELOCITY_LIMIT Commit f8da8099 incorrectly changed the order of variables in the log/response message of the SET_VELOCITY_LIMIT command. Restore the correct order. Reported by @berkakinci. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 21aeca3df..0b66e8184 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -662,7 +662,7 @@ class ToolHeadCommandHelper: msg = ("max_velocity: %.6f\n" "max_accel: %.6f\n" "minimum_cruise_ratio: %.6f\n" - "square_corner_velocity: %.6f" % (mv, ma, scv, mcr)) + "square_corner_velocity: %.6f" % (mv, ma, mcr, scv)) self.printer.set_rollover_info("toolhead", "toolhead: %s" % (msg,)) if (max_velocity is None and max_accel is None and square_corner_velocity is None and min_cruise_ratio is None): From 77d4cdbae42b97a3e1981e2a00d98f63350fe25d Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 7 Aug 2025 16:23:05 +0200 Subject: [PATCH 083/411] pyhelper: drop linux/prctl header for compability with musl Signed-off-by: Timofey Titovets --- klippy/chelper/pyhelper.c | 1 - 1 file changed, 1 deletion(-) diff --git a/klippy/chelper/pyhelper.c b/klippy/chelper/pyhelper.c index 60c6de9b3..8d4e4ee8c 100644 --- a/klippy/chelper/pyhelper.c +++ b/klippy/chelper/pyhelper.c @@ -10,7 +10,6 @@ #include // fprintf #include // strerror #include // struct timespec -#include // PR_SET_NAME #include // prctl #include "compiler.h" // __visible #include "pyhelper.h" // get_monotonic From 1f14e950e7e0c08dcc5553dd4c6c1071ff14eef7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 1 Sep 2025 13:34:59 -0400 Subject: [PATCH 084/411] stepper_enable: Unify explicit stepper enable/disable code There were several slightly different implementations of explicit stepper motor enabling/disabling in the force_move, stepper_enable, and manual_stepper modules. Introduce a new set_motors_enable() method and use this in all implementations. This simplifies the code and reduces the chance of obscure timing issues. This fixes a manual_stepper error introduced in commit 9399e738. That commit changed the manual_stepper class to no longer explicitly flush and clear all steps after each move, which broke the expectations of manual_stepper's custom enable code. Using the more robust implementation in stepper_enable fixes that issue. Signed-off-by: Kevin O'Connor --- klippy/extras/force_move.py | 33 +++++++++---------------- klippy/extras/manual_stepper.py | 10 ++------ klippy/extras/stepper_enable.py | 44 ++++++++++++++++++--------------- klippy/extras/z_tilt.py | 2 +- 4 files changed, 39 insertions(+), 50 deletions(-) diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py index 00f835f5b..3947292b9 100644 --- a/klippy/extras/force_move.py +++ b/klippy/extras/force_move.py @@ -1,6 +1,6 @@ # Utility for manually moving a stepper for diagnostic purposes # -# Copyright (C) 2018-2019 Kevin O'Connor +# Copyright (C) 2018-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import math, logging @@ -10,7 +10,6 @@ BUZZ_DISTANCE = 1. BUZZ_VELOCITY = BUZZ_DISTANCE / .250 BUZZ_RADIANS_DISTANCE = math.radians(1.) BUZZ_RADIANS_VELOCITY = BUZZ_RADIANS_DISTANCE / .250 -STALL_TIME = 0.100 # Calculate a move's accel_t, cruise_t, and cruise_v def calc_move_time(dist, speed, accel): @@ -56,24 +55,16 @@ class ForceMove: raise self.printer.config_error("Unknown stepper %s" % (name,)) return self.steppers[name] def _force_enable(self, stepper): - toolhead = self.printer.lookup_object('toolhead') - print_time = toolhead.get_last_move_time() + stepper_name = stepper.get_name() stepper_enable = self.printer.lookup_object('stepper_enable') - enable = stepper_enable.lookup_enable(stepper.get_name()) - was_enable = enable.is_motor_enabled() - if not was_enable: - enable.motor_enable(print_time) - toolhead.dwell(STALL_TIME) - return was_enable - def _restore_enable(self, stepper, was_enable): - if not was_enable: - toolhead = self.printer.lookup_object('toolhead') - toolhead.dwell(STALL_TIME) - print_time = toolhead.get_last_move_time() - stepper_enable = self.printer.lookup_object('stepper_enable') - enable = stepper_enable.lookup_enable(stepper.get_name()) - enable.motor_disable(print_time) - toolhead.dwell(STALL_TIME) + did_enable = stepper_enable.set_motors_enable([stepper_name], True) + return did_enable + def _restore_enable(self, stepper, did_enable): + if not did_enable: + return + stepper_name = stepper.get_name() + stepper_enable = self.printer.lookup_object('stepper_enable') + stepper_enable.set_motors_enable([stepper_name], False) def manual_move(self, stepper, dist, speed, accel=0.): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() @@ -100,7 +91,7 @@ class ForceMove: def cmd_STEPPER_BUZZ(self, gcmd): stepper = self._lookup_stepper(gcmd) logging.info("Stepper buzz %s", stepper.get_name()) - was_enable = self._force_enable(stepper) + did_enable = self._force_enable(stepper) toolhead = self.printer.lookup_object('toolhead') dist, speed = BUZZ_DISTANCE, BUZZ_VELOCITY if stepper.units_in_radians(): @@ -110,7 +101,7 @@ class ForceMove: toolhead.dwell(.050) self.manual_move(stepper, -dist, speed) toolhead.dwell(.450) - self._restore_enable(stepper, was_enable) + self._restore_enable(stepper, did_enable) cmd_FORCE_MOVE_help = "Manually move a stepper; invalidates kinematics" def cmd_FORCE_MOVE(self, gcmd): stepper = self._lookup_stepper(gcmd) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 3d3e614a6..a2ce57da1 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -50,15 +50,9 @@ class ManualStepper: self.next_cmd_time = print_time def do_enable(self, enable): self.sync_print_time() + stepper_names = [s.get_name() for s in self.steppers] stepper_enable = self.printer.lookup_object('stepper_enable') - if enable: - for s in self.steppers: - se = stepper_enable.lookup_enable(s.get_name()) - se.motor_enable(self.next_cmd_time) - else: - for s in self.steppers: - se = stepper_enable.lookup_enable(s.get_name()) - se.motor_disable(self.next_cmd_time) + stepper_enable.set_motors_enable(stepper_names, enable) self.sync_print_time() def do_set_position(self, setpos): toolhead = self.printer.lookup_object('toolhead') diff --git a/klippy/extras/stepper_enable.py b/klippy/extras/stepper_enable.py index 2bad75552..cd3f4f731 100644 --- a/klippy/extras/stepper_enable.py +++ b/klippy/extras/stepper_enable.py @@ -88,30 +88,30 @@ class PrinterStepperEnable: name = mcu_stepper.get_name() enable = setup_enable_pin(self.printer, config.get('enable_pin', None)) self.enable_lines[name] = EnableTracking(mcu_stepper, enable) + def set_motors_enable(self, stepper_names, enable): + toolhead = self.printer.lookup_object('toolhead') + toolhead.dwell(DISABLE_STALL_TIME) + print_time = toolhead.get_last_move_time() + did_change = False + for stepper_name in stepper_names: + el = self.enable_lines[stepper_name] + was_enabled = el.is_motor_enabled() + if enable: + el.motor_enable(print_time) + else: + el.motor_disable(print_time) + if was_enabled != el.is_motor_enabled(): + did_change = True + toolhead.dwell(DISABLE_STALL_TIME) + return did_change def motor_off(self): + self.set_motors_enable(self.get_steppers(), False) toolhead = self.printer.lookup_object('toolhead') - toolhead.dwell(DISABLE_STALL_TIME) - print_time = toolhead.get_last_move_time() - for el in self.enable_lines.values(): - el.motor_disable(print_time) toolhead.get_kinematics().clear_homing_state("xyz") - self.printer.send_event("stepper_enable:motor_off", print_time) - toolhead.dwell(DISABLE_STALL_TIME) - def motor_debug_enable(self, stepper, enable): - toolhead = self.printer.lookup_object('toolhead') - toolhead.dwell(DISABLE_STALL_TIME) - print_time = toolhead.get_last_move_time() - el = self.enable_lines[stepper] - if enable: - el.motor_enable(print_time) - logging.info("%s has been manually enabled", stepper) - else: - el.motor_disable(print_time) - logging.info("%s has been manually disabled", stepper) - toolhead.dwell(DISABLE_STALL_TIME) + self.printer.send_event("stepper_enable:motor_off") def get_status(self, eventtime): steppers = { name: et.is_motor_enabled() - for (name, et) in self.enable_lines.items() } + for (name, et) in self.enable_lines.items() } return {'steppers': steppers} def _handle_request_restart(self, print_time): self.motor_off() @@ -126,7 +126,11 @@ class PrinterStepperEnable: % (stepper_name,)) return stepper_enable = gcmd.get_int('ENABLE', 1) - self.motor_debug_enable(stepper_name, stepper_enable) + self.set_motors_enable([stepper_name], stepper_enable) + if stepper_enable: + logging.info("%s has been manually enabled", stepper_name) + else: + logging.info("%s has been manually disabled", stepper_name) def lookup_enable(self, name): if name not in self.enable_lines: raise self.printer.config_error("Unknown stepper '%s'" % (name,)) diff --git a/klippy/extras/z_tilt.py b/klippy/extras/z_tilt.py index 9f5ea0b9c..0316ee721 100644 --- a/klippy/extras/z_tilt.py +++ b/klippy/extras/z_tilt.py @@ -79,7 +79,7 @@ class ZAdjustStatus: self.applied = False def get_status(self, eventtime): return {'applied': self.applied} - def _motor_off(self, print_time): + def _motor_off(self): self.reset() class RetryHelper: From 5056e1031c92f279caafe49fb8e2fb961dcbb9ad Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 1 Sep 2025 14:22:36 -0400 Subject: [PATCH 085/411] stepper_enable: Improve timing of manual stepper enable/disable commands Invoke flush_step_generation() prior to checking motor enable state as this is the best way to ensure all stepper active callbacks have been invoked (which could change the enable line state). Also, there is no longer a reason to add additional toolhead dwells when enabling a stepper motor. Signed-off-by: Kevin O'Connor --- klippy/extras/manual_stepper.py | 2 -- klippy/extras/stepper_enable.py | 22 +++++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index a2ce57da1..3c1b29b65 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -49,11 +49,9 @@ class ManualStepper: else: self.next_cmd_time = print_time def do_enable(self, enable): - self.sync_print_time() stepper_names = [s.get_name() for s in self.steppers] stepper_enable = self.printer.lookup_object('stepper_enable') stepper_enable.set_motors_enable(stepper_names, enable) - self.sync_print_time() def do_set_position(self, setpos): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() diff --git a/klippy/extras/stepper_enable.py b/klippy/extras/stepper_enable.py index cd3f4f731..926e95922 100644 --- a/klippy/extras/stepper_enable.py +++ b/klippy/extras/stepper_enable.py @@ -1,6 +1,6 @@ # Support for enable pins on stepper motor drivers # -# Copyright (C) 2019-2021 Kevin O'Connor +# Copyright (C) 2019-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import logging @@ -90,19 +90,27 @@ class PrinterStepperEnable: self.enable_lines[name] = EnableTracking(mcu_stepper, enable) def set_motors_enable(self, stepper_names, enable): toolhead = self.printer.lookup_object('toolhead') - toolhead.dwell(DISABLE_STALL_TIME) - print_time = toolhead.get_last_move_time() + # Flush steps to ensure all auto enable callbacks invoked + toolhead.flush_step_generation() + print_time = None did_change = False for stepper_name in stepper_names: el = self.enable_lines[stepper_name] - was_enabled = el.is_motor_enabled() + if el.is_motor_enabled() == enable: + continue + if print_time is None: + # Dwell for sufficient delay from any previous auto enable + if not enable: + toolhead.dwell(DISABLE_STALL_TIME) + print_time = toolhead.get_last_move_time() if enable: el.motor_enable(print_time) else: el.motor_disable(print_time) - if was_enabled != el.is_motor_enabled(): - did_change = True - toolhead.dwell(DISABLE_STALL_TIME) + did_change = True + # Dwell to ensure sufficient delay prior to a future auto enable + if did_change and not enable: + toolhead.dwell(DISABLE_STALL_TIME) return did_change def motor_off(self): self.set_motors_enable(self.get_steppers(), False) From a5218619b725c849c3c371b4d3d4549e636db5e3 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 11 Aug 2025 20:01:33 -0400 Subject: [PATCH 086/411] motion_queuing: Track kin_flush_delay locally Track the kin_flush_delay in both toolhead.py and motion_queuing.py . Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 15 ++++++++++++--- klippy/toolhead.py | 5 ++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index a06b556e1..49fafd6b0 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -7,22 +7,29 @@ import logging import chelper MOVE_HISTORY_EXPIRE = 30. +SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c class PrinterMotionQueuing: def __init__(self, config): self.printer = config.get_printer() + # Low level C allocations self.trapqs = [] self.stepcompress = [] self.steppersyncs = [] - self.flush_callbacks = [] + # Low-level C flushing calls ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves self.steppersync_generate_steps = ffi_lib.steppersync_generate_steps self.steppersync_flush = ffi_lib.steppersync_flush self.steppersync_history_expire = ffi_lib.steppersync_history_expire + # Flush notification callbacks + self.flush_callbacks = [] + # History expiration self.clear_history_time = 0. is_debug = self.printer.get_start_args().get('debugoutput') is not None self.is_debugoutput = is_debug + # Kinematic step generation scan window time tracking + self.kin_flush_delay = SDS_CHECK_TIME def allocate_trapq(self): ffi_main, ffi_lib = chelper.get_ffi() trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) @@ -53,8 +60,7 @@ class PrinterMotionQueuing: fcbs = list(self.flush_callbacks) fcbs.remove(callback) self.flush_callbacks = fcbs - def flush_motion_queues(self, must_flush_time, max_step_gen_time, - trapq_free_time): + def flush_motion_queues(self, must_flush_time, max_step_gen_time): # Invoke flush callbacks (if any) for cb in self.flush_callbacks: cb(must_flush_time, max_step_gen_time) @@ -72,6 +78,7 @@ class PrinterMotionQueuing: raise mcu.error("Internal error in MCU '%s' stepcompress" % (mcu.get_name(),)) # Determine maximum history to keep + trapq_free_time = max_step_gen_time - self.kin_flush_delay clear_history_time = self.clear_history_time if self.is_debugoutput: clear_history_time = trapq_free_time - MOVE_HISTORY_EXPIRE @@ -90,6 +97,8 @@ class PrinterMotionQueuing: def lookup_trapq_append(self): ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append + def set_step_generate_scan_time(self, delay): + self.kin_flush_delay = delay def stats(self, eventtime): mcu = self.printer.lookup_object('mcu') est_print_time = mcu.estimated_print_time(eventtime) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 0b66e8184..f68d49726 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -275,9 +275,7 @@ class ToolHead: sg_flush_want = min(flush_time + STEPCOMPRESS_FLUSH_TIME, self.print_time - self.kin_flush_delay) sg_flush_time = max(sg_flush_want, flush_time) - trapq_free_time = sg_flush_time - self.kin_flush_delay - self.motion_queuing.flush_motion_queues(flush_time, sg_flush_time, - trapq_free_time) + self.motion_queuing.flush_motion_queues(flush_time, sg_flush_time) self.min_restart_time = max(self.min_restart_time, sg_flush_time) self.last_flush_time = flush_time def _advance_move_time(self, next_print_time): @@ -596,6 +594,7 @@ class ToolHead: self.kin_flush_times.append(delay) new_delay = max(self.kin_flush_times + [SDS_CHECK_TIME]) self.kin_flush_delay = new_delay + self.motion_queuing.set_step_generate_scan_time(new_delay) def register_lookahead_callback(self, callback): last_move = self.lookahead.get_last() if last_move is None: From 1e7c67919ec4a9f8716426b8e9e024a2ae0076fe Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 11:38:16 -0400 Subject: [PATCH 087/411] toolhead: Rework min_restart_time to last_step_gen_time Commit 3d3b87f9 renamed last_sg_flush_time to min_restart_time to ensure that flush_step_generation() would fully flush out moves generated from the force_move module. However, now that force_move calls note_mcu_movequeue_activity() with is_step_gen=True, this is no longer necessary. Rework min_restart_time to last_step_gen_time. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index f68d49726..2f9909f51 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -239,7 +239,7 @@ class ToolHead: # Flush tracking self.flush_timer = self.reactor.register_timer(self._flush_handler) self.do_kick_flush_timer = True - self.last_flush_time = self.min_restart_time = 0. + self.last_flush_time = self.last_step_gen_time = 0. self.need_flush_time = self.step_gen_time = 0. # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME @@ -274,10 +274,10 @@ class ToolHead: # Generate steps via itersolve sg_flush_want = min(flush_time + STEPCOMPRESS_FLUSH_TIME, self.print_time - self.kin_flush_delay) - sg_flush_time = max(sg_flush_want, flush_time) - self.motion_queuing.flush_motion_queues(flush_time, sg_flush_time) - self.min_restart_time = max(self.min_restart_time, sg_flush_time) + step_gen_time = max(self.last_step_gen_time, sg_flush_want, flush_time) + self.motion_queuing.flush_motion_queues(flush_time, step_gen_time) self.last_flush_time = flush_time + self.last_step_gen_time = step_gen_time def _advance_move_time(self, next_print_time): pt_delay = self.kin_flush_delay + STEPCOMPRESS_FLUSH_TIME flush_time = max(self.last_flush_time, self.print_time - pt_delay) @@ -291,7 +291,7 @@ class ToolHead: def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - kin_time = max(est_print_time + MIN_KIN_TIME, self.min_restart_time) + kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) kin_time += self.kin_flush_delay min_print_time = max(est_print_time + BUFFER_TIME_START, kin_time) if min_print_time > self.print_time: @@ -338,7 +338,6 @@ class ToolHead: def flush_step_generation(self): self._flush_lookahead() self._advance_flush_time(self.step_gen_time) - self.min_restart_time = max(self.min_restart_time, self.print_time) def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() From b0a642a8ea9beb1d76c4aaaf5aee4923f20c8243 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 11:43:26 -0400 Subject: [PATCH 088/411] toolhead: Add kin_flush_delay in note_mcu_movequeue_activity() Automatically add kin_flush_delay to the requested flush time if is_step_gen=True. This simplifies the callers. Also, rename self.step_gen_time to self.need_step_gen_time. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 2f9909f51..167e43c0c 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -240,7 +240,7 @@ class ToolHead: self.flush_timer = self.reactor.register_timer(self._flush_handler) self.do_kick_flush_timer = True self.last_flush_time = self.last_step_gen_time = 0. - self.need_flush_time = self.step_gen_time = 0. + self.need_flush_time = self.need_step_gen_time = 0. # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME self.kin_flush_times = [] @@ -326,7 +326,7 @@ class ToolHead: for cb in move.timing_callbacks: cb(next_move_time) # Generate steps for moves - self.note_mcu_movequeue_activity(next_move_time + self.kin_flush_delay) + self.note_mcu_movequeue_activity(next_move_time) self._advance_move_time(next_move_time) def _flush_lookahead(self): # Transit from "NeedPrime"/"Priming"/"Drip"/main state to "NeedPrime" @@ -337,7 +337,7 @@ class ToolHead: self.check_stall_time = 0. def flush_step_generation(self): self._flush_lookahead() - self._advance_flush_time(self.step_gen_time) + self._advance_flush_time(self.need_step_gen_time) def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() @@ -509,7 +509,7 @@ class ToolHead: drip_completion.wait(curtime + wait_time) continue npt = min(self.print_time + DRIP_SEGMENT_TIME, next_print_time) - self.note_mcu_movequeue_activity(npt + self.kin_flush_delay) + self.note_mcu_movequeue_activity(npt) self._advance_move_time(npt) # Exit "Drip" state self.reactor.update_timer(self.flush_timer, self.reactor.NOW) @@ -601,9 +601,10 @@ class ToolHead: return last_move.timing_callbacks.append(callback) def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True): - self.need_flush_time = max(self.need_flush_time, mq_time) if is_step_gen: - self.step_gen_time = max(self.step_gen_time, mq_time) + mq_time += self.kin_flush_delay + self.need_step_gen_time = max(self.need_step_gen_time, mq_time) + self.need_flush_time = max(self.need_flush_time, mq_time) if self.do_kick_flush_timer: self.do_kick_flush_timer = False self.reactor.update_timer(self.flush_timer, self.reactor.NOW) From a64207aac3aad0bf3679c7619a3941382700e689 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 12:23:27 -0400 Subject: [PATCH 089/411] toolhead: Implement flush "waves" in _advance_flush_time() Move the code that implements flushing in waves from _advance_move_time() to _advance_flush_time(). This also separates print_time tracking from the _advance_flush_time() implementation. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 167e43c0c..e9746b54c 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -269,25 +269,38 @@ class ToolHead: self.printer.register_event_handler("klippy:shutdown", self._handle_shutdown) # Print time and flush tracking - def _advance_flush_time(self, flush_time): - flush_time = max(flush_time, self.last_flush_time) - # Generate steps via itersolve - sg_flush_want = min(flush_time + STEPCOMPRESS_FLUSH_TIME, - self.print_time - self.kin_flush_delay) - step_gen_time = max(self.last_step_gen_time, sg_flush_want, flush_time) - self.motion_queuing.flush_motion_queues(flush_time, step_gen_time) - self.last_flush_time = flush_time - self.last_step_gen_time = step_gen_time - def _advance_move_time(self, next_print_time): - pt_delay = self.kin_flush_delay + STEPCOMPRESS_FLUSH_TIME - flush_time = max(self.last_flush_time, self.print_time - pt_delay) - self.print_time = max(self.print_time, next_print_time) - want_flush_time = max(flush_time, self.print_time - pt_delay) + def _advance_flush_time(self, target_time=None, lazy_target=False): + if target_time is None: + # This is a full flush + target_time = self.need_step_gen_time + want_flush_time = want_step_gen_time = target_time + if lazy_target: + # Account for step gen scan windows and optimize step compression + want_step_gen_time -= self.kin_flush_delay + want_flush_time = want_step_gen_time - STEPCOMPRESS_FLUSH_TIME + want_flush_time = max(want_flush_time, self.last_flush_time) + flush_time = self.last_flush_time + if want_flush_time > flush_time + 10. * MOVE_BATCH_TIME: + # Use closer startup time when coming out of idle state + curtime = self.reactor.monotonic() + est_print_time = self.mcu.estimated_print_time(curtime) + flush_time = max(flush_time, est_print_time) + flush_motion_queues = self.motion_queuing.flush_motion_queues while 1: flush_time = min(flush_time + MOVE_BATCH_TIME, want_flush_time) - self._advance_flush_time(flush_time) + # Generate steps via itersolve + want_sg_wave = min(flush_time + STEPCOMPRESS_FLUSH_TIME, + want_step_gen_time) + step_gen_time = max(self.last_step_gen_time, want_sg_wave, + flush_time) + flush_motion_queues(flush_time, step_gen_time) + self.last_flush_time = flush_time + self.last_step_gen_time = step_gen_time if flush_time >= want_flush_time: break + def _advance_move_time(self, next_print_time): + self.print_time = max(self.print_time, next_print_time) + self._advance_flush_time(self.print_time, lazy_target=True) def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) @@ -337,7 +350,7 @@ class ToolHead: self.check_stall_time = 0. def flush_step_generation(self): self._flush_lookahead() - self._advance_flush_time(self.need_step_gen_time) + self._advance_flush_time() def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() From 4b9a0b4f82bbebf77a6475e7d4b3dca224ad45be Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 14:27:21 -0400 Subject: [PATCH 090/411] toolhead: Separate lookahead timer flushing to new _check_flush_lookahead() Separate out the lookahead specific flushing logic from the _flush_handler() code. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index e9746b54c..dbafdb924 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -401,21 +401,29 @@ class ToolHead: logging.exception("Exception in priming_handler") self.printer.invoke_shutdown("Exception in priming_handler") return self.reactor.NEVER + def _check_flush_lookahead(self, eventtime): + if self.special_queuing_state: + return None + # In "main" state - flush lookahead if buffer runs low + est_print_time = self.mcu.estimated_print_time(eventtime) + print_time = self.print_time + buffer_time = print_time - est_print_time + if buffer_time > BUFFER_TIME_LOW: + # Running normally - reschedule check + return eventtime + buffer_time - BUFFER_TIME_LOW + # Under ran low buffer mark - flush lookahead queue + self._flush_lookahead() + if print_time != self.print_time: + self.check_stall_time = self.print_time + return None def _flush_handler(self, eventtime): try: + # Check if flushing is done via lookahead queue + ret = self._check_flush_lookahead(eventtime) + if ret is not None: + return ret + # Flush motion queues est_print_time = self.mcu.estimated_print_time(eventtime) - if not self.special_queuing_state: - # In "main" state - flush lookahead if buffer runs low - print_time = self.print_time - buffer_time = print_time - est_print_time - if buffer_time > BUFFER_TIME_LOW: - # Running normally - reschedule check - return eventtime + buffer_time - BUFFER_TIME_LOW - # Under ran low buffer mark - flush lookahead queue - self._flush_lookahead() - if print_time != self.print_time: - self.check_stall_time = self.print_time - # In "NeedPrime"/"Priming" state - flush queues if needed while 1: end_flush = self.need_flush_time + BGFLUSH_EXTRA_TIME if self.last_flush_time >= end_flush: From 872615cfcf3939473fc4a081bb7e75a66e8e0bd8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 14:28:18 -0400 Subject: [PATCH 091/411] toolhead: Add new _calc_step_gen_restart() helper Separate out step generation specific handling from _calc_print_time() code. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index dbafdb924..cf8ee583a 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -298,14 +298,16 @@ class ToolHead: self.last_step_gen_time = step_gen_time if flush_time >= want_flush_time: break + def _calc_step_gen_restart(self, est_print_time): + kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) + return kin_time + self.kin_flush_delay def _advance_move_time(self, next_print_time): self.print_time = max(self.print_time, next_print_time) self._advance_flush_time(self.print_time, lazy_target=True) def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) - kin_time += self.kin_flush_delay + kin_time = self._calc_step_gen_restart(est_print_time) min_print_time = max(est_print_time + BUFFER_TIME_START, kin_time) if min_print_time > self.print_time: self.print_time = min_print_time From db5cbe56d330e87918be2a3c24b51e4be3e4adfd Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 13 Aug 2025 14:07:03 -0400 Subject: [PATCH 092/411] toolhead: Do not modify print_time in drip_update_time() Implement drip_update_time() using _advance_flush_time() instead of _advance_move_time(). Signed-off-by: Kevin O'Connor --- klippy/extras/manual_stepper.py | 7 +++--- klippy/toolhead.py | 42 ++++++++++++++------------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 3c1b29b65..f57bbbbb1 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -197,11 +197,12 @@ class ManualStepper: def drip_move(self, newpos, speed, drip_completion): # Submit move to trapq self.sync_print_time() - maxtime = self._submit_move(self.next_cmd_time, newpos[0], - speed, self.homing_accel) + start_time = self.next_cmd_time + end_time = self._submit_move(start_time, newpos[0], + speed, self.homing_accel) # Drip updates to motors toolhead = self.printer.lookup_object('toolhead') - toolhead.drip_update_time(maxtime, drip_completion) + toolhead.drip_update_time(start_time, end_time, drip_completion) # Clear trapq of any remaining parts of movement reactor = self.printer.get_reactor() self.motion_queuing.wipe_trapq(self.trapq) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index cf8ee583a..62756dfdc 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -344,7 +344,7 @@ class ToolHead: self.note_mcu_movequeue_activity(next_move_time) self._advance_move_time(next_move_time) def _flush_lookahead(self): - # Transit from "NeedPrime"/"Priming"/"Drip"/main state to "NeedPrime" + # Transit from "NeedPrime"/"Priming"/main state to "NeedPrime" self._process_lookahead() self.special_queuing_state = "NeedPrime" self.need_check_pause = -1. @@ -511,32 +511,29 @@ class ToolHead: def get_extra_axes(self): return [None, None, None] + self.extra_axes # Homing "drip move" handling - def drip_update_time(self, next_print_time, drip_completion): - # Transition from "NeedPrime"/"Priming"/main state to "Drip" state - self.special_queuing_state = "Drip" - self.need_check_pause = self.reactor.NEVER + def drip_update_time(self, start_time, end_time, drip_completion): + # Disable background flushing from timer self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) self.do_kick_flush_timer = False - self.lookahead.set_flush_time(BUFFER_TIME_HIGH) - self.check_stall_time = 0. - # Update print_time in segments until drip_completion signal + # Flush in segments until drip_completion signal flush_delay = DRIP_TIME + STEPCOMPRESS_FLUSH_TIME + self.kin_flush_delay - while self.print_time < next_print_time: + flush_time = start_time + while flush_time < end_time: if drip_completion.test(): break curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - wait_time = self.print_time - est_print_time - flush_delay + wait_time = flush_time - est_print_time - flush_delay if wait_time > 0. and self.can_pause: # Pause before sending more steps drip_completion.wait(curtime + wait_time) continue - npt = min(self.print_time + DRIP_SEGMENT_TIME, next_print_time) - self.note_mcu_movequeue_activity(npt) - self._advance_move_time(npt) - # Exit "Drip" state + flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) + self.note_mcu_movequeue_activity(flush_time) + self._advance_flush_time(flush_time, lazy_target=True) + # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) - self.flush_step_generation() + self._advance_flush_time() def _drip_load_trapq(self, submit_move): # Queue move into trapezoid motion queue (trapq) if submit_move.move_d: @@ -544,18 +541,17 @@ class ToolHead: self.lookahead.add_move(submit_move) moves = self.lookahead.flush() self._calc_print_time() - next_move_time = self.print_time + start_time = end_time = self.print_time for move in moves: self.trapq_append( - self.trapq, next_move_time, + self.trapq, end_time, move.accel_t, move.cruise_t, move.decel_t, move.start_pos[0], move.start_pos[1], move.start_pos[2], move.axes_r[0], move.axes_r[1], move.axes_r[2], move.start_v, move.cruise_v, move.accel) - next_move_time = (next_move_time + move.accel_t - + move.cruise_t + move.decel_t) + end_time = end_time + move.accel_t + move.cruise_t + move.decel_t self.lookahead.reset() - return next_move_time + return start_time, end_time def drip_move(self, newpos, speed, drip_completion): # Create and verify move is valid newpos = newpos[:3] + self.commanded_pos[3:] @@ -566,8 +562,8 @@ class ToolHead: self.dwell(self.kin_flush_delay) # Transmit move in "drip" mode self._process_lookahead() - next_move_time = self._drip_load_trapq(move) - self.drip_update_time(next_move_time, drip_completion) + start_time, end_time = self._drip_load_trapq(move) + self.drip_update_time(start_time, end_time, drip_completion) # Move finished; cleanup any remnants on trapq self.motion_queuing.wipe_trapq(self.trapq) # Misc commands @@ -578,8 +574,6 @@ class ToolHead: est_print_time = self.mcu.estimated_print_time(eventtime) buffer_time = self.print_time - est_print_time is_active = buffer_time > -60. or not self.special_queuing_state - if self.special_queuing_state == "Drip": - buffer_time = 0. return is_active, "print_time=%.3f buffer_time=%.3f print_stall=%d" % ( self.print_time, max(buffer_time, 0.), self.print_stall) def check_busy(self, eventtime): From 8c13811c3bbf1b4e1a48413775b60c0d50e6a5f4 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 13 Aug 2025 15:28:28 -0400 Subject: [PATCH 093/411] toolhead: Avoid using print_time when calling mcu.check_active() Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 62756dfdc..bd9f67da0 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -568,9 +568,8 @@ class ToolHead: self.motion_queuing.wipe_trapq(self.trapq) # Misc commands def stats(self, eventtime): - max_queue_time = max(self.print_time, self.last_flush_time) for m in self.all_mcus: - m.check_active(max_queue_time, eventtime) + m.check_active(self.last_step_gen_time, eventtime) est_print_time = self.mcu.estimated_print_time(eventtime) buffer_time = self.print_time - est_print_time is_active = buffer_time > -60. or not self.special_queuing_state From d1974c0d3d743e0d0ab9b30b699309d9082bbb14 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 12 Aug 2025 20:07:08 -0400 Subject: [PATCH 094/411] motion_queuing: Move flushing logic from toolhead to motion_queuing module Move low-level step generation timing code to the motion_queing module. This helps simplify the toolhead module. It also helps centralize the step generation code into the motion_queing module. Signed-off-by: Kevin O'Connor --- klippy/extras/force_move.py | 2 +- klippy/extras/manual_stepper.py | 7 +- klippy/extras/motion_queuing.py | 131 +++++++++++++++++++++++++++++--- klippy/extras/output_pin.py | 11 +-- klippy/extras/pwm_tool.py | 12 +-- klippy/toolhead.py | 121 +++-------------------------- 6 files changed, 149 insertions(+), 135 deletions(-) diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py index 3947292b9..277c68e3c 100644 --- a/klippy/extras/force_move.py +++ b/klippy/extras/force_move.py @@ -76,7 +76,7 @@ class ForceMove: self.trapq_append(self.trapq, print_time, accel_t, cruise_t, accel_t, 0., 0., 0., axis_r, 0., 0., 0., cruise_v, accel) print_time = print_time + accel_t + cruise_t + accel_t - toolhead.note_mcu_movequeue_activity(print_time) + self.motion_queuing.note_mcu_movequeue_activity(print_time) toolhead.dwell(accel_t + cruise_t + accel_t) toolhead.flush_step_generation() stepper.set_trapq(prev_trapq) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index f57bbbbb1..9c775567f 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -72,8 +72,7 @@ class ManualStepper: self.sync_print_time() self.next_cmd_time = self._submit_move(self.next_cmd_time, movepos, speed, accel) - toolhead = self.printer.lookup_object('toolhead') - toolhead.note_mcu_movequeue_activity(self.next_cmd_time) + self.motion_queuing.note_mcu_movequeue_activity(self.next_cmd_time) if sync: self.sync_print_time() def do_homing_move(self, movepos, speed, accel, triggered, check_trigger): @@ -201,8 +200,8 @@ class ManualStepper: end_time = self._submit_move(start_time, newpos[0], speed, self.homing_accel) # Drip updates to motors - toolhead = self.printer.lookup_object('toolhead') - toolhead.drip_update_time(start_time, end_time, drip_completion) + self.motion_queuing.drip_update_time(start_time, end_time, + drip_completion) # Clear trapq of any remaining parts of movement reactor = self.printer.get_reactor() self.motion_queuing.wipe_trapq(self.trapq) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 49fafd6b0..226aa9f2a 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -6,12 +6,22 @@ import logging import chelper +BGFLUSH_LOW_TIME = 0.200 +BGFLUSH_BATCH_TIME = 0.200 +BGFLUSH_EXTRA_TIME = 0.250 MOVE_HISTORY_EXPIRE = 30. +MIN_KIN_TIME = 0.100 +MOVE_BATCH_TIME = 0.500 +STEPCOMPRESS_FLUSH_TIME = 0.050 SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c +DRIP_SEGMENT_TIME = 0.050 +DRIP_TIME = 0.100 + class PrinterMotionQueuing: def __init__(self, config): - self.printer = config.get_printer() + self.printer = printer = config.get_printer() + self.reactor = printer.get_reactor() # Low level C allocations self.trapqs = [] self.stepcompress = [] @@ -26,10 +36,22 @@ class PrinterMotionQueuing: self.flush_callbacks = [] # History expiration self.clear_history_time = 0. - is_debug = self.printer.get_start_args().get('debugoutput') is not None - self.is_debugoutput = is_debug + # Flush tracking + self.flush_timer = self.reactor.register_timer(self._flush_handler) + self.do_kick_flush_timer = True + self.last_flush_time = self.last_step_gen_time = 0. + self.need_flush_time = self.need_step_gen_time = 0. + self.check_flush_lookahead_cb = (lambda e: None) + # MCU tracking + self.all_mcus = [m for n, m in printer.lookup_objects(module='mcu')] + self.mcu = self.all_mcus[0] + self.can_pause = True + if self.mcu.is_fileoutput(): + self.can_pause = False # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME + # Register handlers + printer.register_event_handler("klippy:shutdown", self._handle_shutdown) def allocate_trapq(self): ffi_main, ffi_lib = chelper.get_ffi() trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) @@ -60,7 +82,7 @@ class PrinterMotionQueuing: fcbs = list(self.flush_callbacks) fcbs.remove(callback) self.flush_callbacks = fcbs - def flush_motion_queues(self, must_flush_time, max_step_gen_time): + def _flush_motion_queues(self, must_flush_time, max_step_gen_time): # Invoke flush callbacks (if any) for cb in self.flush_callbacks: cb(must_flush_time, max_step_gen_time) @@ -80,7 +102,7 @@ class PrinterMotionQueuing: # Determine maximum history to keep trapq_free_time = max_step_gen_time - self.kin_flush_delay clear_history_time = self.clear_history_time - if self.is_debugoutput: + if not self.can_pause: clear_history_time = trapq_free_time - MOVE_HISTORY_EXPIRE # Move processed trapq moves to history list, and expire old history for trapq in self.trapqs: @@ -92,18 +114,109 @@ class PrinterMotionQueuing: self.steppersync_history_expire(ss, clock) def wipe_trapq(self, trapq): # Expire any remaining movement in the trapq (force to history list) - NEVER = 9999999999999999. - self.trapq_finalize_moves(trapq, NEVER, 0.) + self.trapq_finalize_moves(trapq, self.reactor.NEVER, 0.) def lookup_trapq_append(self): ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append def set_step_generate_scan_time(self, delay): self.kin_flush_delay = delay def stats(self, eventtime): - mcu = self.printer.lookup_object('mcu') - est_print_time = mcu.estimated_print_time(eventtime) + # Hack to globally invoke mcu check_active() + for m in self.all_mcus: + m.check_active(self.last_step_gen_time, eventtime) + # Calculate history expiration + est_print_time = self.mcu.estimated_print_time(eventtime) self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE return False, "" + # Flush tracking + def _handle_shutdown(self): + self.can_pause = False + def setup_lookahead_flush_callback(self, check_flush_lookahead_cb): + self.check_flush_lookahead_cb = check_flush_lookahead_cb + def advance_flush_time(self, target_time=None, lazy_target=False): + if target_time is None: + # This is a full flush + target_time = self.need_step_gen_time + want_flush_time = want_step_gen_time = target_time + if lazy_target: + # Account for step gen scan windows and optimize step compression + want_step_gen_time -= self.kin_flush_delay + want_flush_time = want_step_gen_time - STEPCOMPRESS_FLUSH_TIME + want_flush_time = max(want_flush_time, self.last_flush_time) + flush_time = self.last_flush_time + if want_flush_time > flush_time + 10. * MOVE_BATCH_TIME: + # Use closer startup time when coming out of idle state + curtime = self.reactor.monotonic() + est_print_time = self.mcu.estimated_print_time(curtime) + flush_time = max(flush_time, est_print_time) + while 1: + flush_time = min(flush_time + MOVE_BATCH_TIME, want_flush_time) + # Generate steps via itersolve + want_sg_wave = min(flush_time + STEPCOMPRESS_FLUSH_TIME, + want_step_gen_time) + step_gen_time = max(self.last_step_gen_time, want_sg_wave, + flush_time) + self._flush_motion_queues(flush_time, step_gen_time) + self.last_flush_time = flush_time + self.last_step_gen_time = step_gen_time + if flush_time >= want_flush_time: + break + def calc_step_gen_restart(self, est_print_time): + kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) + return kin_time + self.kin_flush_delay + def _flush_handler(self, eventtime): + try: + # Check if flushing is done via lookahead queue + ret = self.check_flush_lookahead_cb(eventtime) + if ret is not None: + return ret + # Flush motion queues + est_print_time = self.mcu.estimated_print_time(eventtime) + while 1: + end_flush = self.need_flush_time + BGFLUSH_EXTRA_TIME + if self.last_flush_time >= end_flush: + self.do_kick_flush_timer = True + return self.reactor.NEVER + buffer_time = self.last_flush_time - est_print_time + if buffer_time > BGFLUSH_LOW_TIME: + return eventtime + buffer_time - BGFLUSH_LOW_TIME + ftime = est_print_time + BGFLUSH_LOW_TIME + BGFLUSH_BATCH_TIME + self.advance_flush_time(min(end_flush, ftime)) + except: + logging.exception("Exception in flush_handler") + self.printer.invoke_shutdown("Exception in flush_handler") + return self.reactor.NEVER + def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True): + if is_step_gen: + mq_time += self.kin_flush_delay + self.need_step_gen_time = max(self.need_step_gen_time, mq_time) + self.need_flush_time = max(self.need_flush_time, mq_time) + if self.do_kick_flush_timer: + self.do_kick_flush_timer = False + self.reactor.update_timer(self.flush_timer, self.reactor.NOW) + def drip_update_time(self, start_time, end_time, drip_completion): + # Disable background flushing from timer + self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) + self.do_kick_flush_timer = False + # Flush in segments until drip_completion signal + flush_delay = DRIP_TIME + STEPCOMPRESS_FLUSH_TIME + self.kin_flush_delay + flush_time = start_time + while flush_time < end_time: + if drip_completion.test(): + break + curtime = self.reactor.monotonic() + est_print_time = self.mcu.estimated_print_time(curtime) + wait_time = flush_time - est_print_time - flush_delay + if wait_time > 0. and self.can_pause: + # Pause before sending more steps + drip_completion.wait(curtime + wait_time) + continue + flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) + self.note_mcu_movequeue_activity(flush_time) + self.advance_flush_time(flush_time, lazy_target=True) + # Restore background flushing + self.reactor.update_timer(self.flush_timer, self.reactor.NOW) + self.advance_flush_time() def load_config(config): return PrinterMotionQueuing(config) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 63862d978..a51292990 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -20,8 +20,8 @@ class GCodeRequestQueue: self.rqueue = [] self.next_min_flush_time = 0. self.toolhead = None - motion_queuing = printer.load_object(config, 'motion_queuing') - motion_queuing.register_flush_callback(self._flush_notification) + self.motion_queuing = printer.load_object(config, 'motion_queuing') + self.motion_queuing.register_flush_callback(self._flush_notification) printer.register_event_handler("klippy:connect", self._handle_connect) def _handle_connect(self): self.toolhead = self.printer.lookup_object('toolhead') @@ -51,11 +51,12 @@ class GCodeRequestQueue: del rqueue[:pos+1] self.next_min_flush_time = next_time + max(min_wait, min_sched_time) # Ensure following queue items are flushed - self.toolhead.note_mcu_movequeue_activity(self.next_min_flush_time, - is_step_gen=False) + self.motion_queuing.note_mcu_movequeue_activity( + self.next_min_flush_time, is_step_gen=False) def _queue_request(self, print_time, value): self.rqueue.append((print_time, value)) - self.toolhead.note_mcu_movequeue_activity(print_time, is_step_gen=False) + self.motion_queuing.note_mcu_movequeue_activity( + print_time, is_step_gen=False) def queue_gcode_request(self, value): self.toolhead.register_lookahead_callback( (lambda pt: self._queue_request(pt, value))) diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index 6d401c0b0..d9e72c5e1 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -16,8 +16,8 @@ class MCU_queued_pwm: self._max_duration = 2. self._oid = oid = mcu.create_oid() printer = mcu.get_printer() - motion_queuing = printer.load_object(config, 'motion_queuing') - self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid) + self._motion_queuing = printer.load_object(config, 'motion_queuing') + self._stepqueue = self._motion_queuing.allocate_stepcompress(mcu, oid) ffi_main, ffi_lib = chelper.get_ffi() self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg mcu.register_config_callback(self._build_config) @@ -62,8 +62,8 @@ class MCU_queued_pwm: if self._duration_ticks >= 1<<31: raise config_error("PWM pin max duration too large") if self._duration_ticks: - motion_queuing = printer.lookup_object('motion_queuing') - motion_queuing.register_flush_callback(self._flush_notification) + self._motion_queuing.register_flush_callback( + self._flush_notification) if self._hardware_pwm: self._pwm_max = self._mcu.get_constant_float("PWM_MAX") self._default_value = self._shutdown_value * self._pwm_max @@ -116,8 +116,8 @@ class MCU_queued_pwm: # Continue flushing to resend time wakeclock += self._duration_ticks wake_print_time = self._mcu.clock_to_print_time(wakeclock) - self._toolhead.note_mcu_movequeue_activity(wake_print_time, - is_step_gen=False) + self._motion_queuing.note_mcu_movequeue_activity(wake_print_time, + is_step_gen=False) def set_pwm(self, print_time, value): clock = self._mcu.print_time_to_clock(print_time) if self._invert: diff --git a/klippy/toolhead.py b/klippy/toolhead.py index bd9f67da0..3eadbb3d3 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -193,25 +193,14 @@ class LookAheadQueue: BUFFER_TIME_LOW = 1.0 BUFFER_TIME_HIGH = 2.0 BUFFER_TIME_START = 0.250 -BGFLUSH_LOW_TIME = 0.200 -BGFLUSH_BATCH_TIME = 0.200 -BGFLUSH_EXTRA_TIME = 0.250 -MIN_KIN_TIME = 0.100 -MOVE_BATCH_TIME = 0.500 -STEPCOMPRESS_FLUSH_TIME = 0.050 SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c -DRIP_SEGMENT_TIME = 0.050 -DRIP_TIME = 0.100 - # Main code to track events (and their timing) on the printer toolhead class ToolHead: def __init__(self, config): self.printer = config.get_printer() self.reactor = self.printer.get_reactor() - self.all_mcus = [ - m for n, m in self.printer.lookup_objects(module='mcu')] - self.mcu = self.all_mcus[0] + self.mcu = self.printer.lookup_object('mcu') self.lookahead = LookAheadQueue() self.lookahead.set_flush_time(BUFFER_TIME_HIGH) self.commanded_pos = [0., 0., 0., 0.] @@ -236,16 +225,13 @@ class ToolHead: self.print_time = 0. self.special_queuing_state = "NeedPrime" self.priming_timer = None - # Flush tracking - self.flush_timer = self.reactor.register_timer(self._flush_handler) - self.do_kick_flush_timer = True - self.last_flush_time = self.last_step_gen_time = 0. - self.need_flush_time = self.need_step_gen_time = 0. # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME self.kin_flush_times = [] # Setup for generating moves self.motion_queuing = self.printer.load_object(config, 'motion_queuing') + self.motion_queuing.setup_lookahead_flush_callback( + self._check_flush_lookahead) self.trapq = self.motion_queuing.allocate_trapq() self.trapq_append = self.motion_queuing.lookup_trapq_append() # Create kinematics class @@ -268,46 +254,15 @@ class ToolHead: # Register handlers self.printer.register_event_handler("klippy:shutdown", self._handle_shutdown) - # Print time and flush tracking - def _advance_flush_time(self, target_time=None, lazy_target=False): - if target_time is None: - # This is a full flush - target_time = self.need_step_gen_time - want_flush_time = want_step_gen_time = target_time - if lazy_target: - # Account for step gen scan windows and optimize step compression - want_step_gen_time -= self.kin_flush_delay - want_flush_time = want_step_gen_time - STEPCOMPRESS_FLUSH_TIME - want_flush_time = max(want_flush_time, self.last_flush_time) - flush_time = self.last_flush_time - if want_flush_time > flush_time + 10. * MOVE_BATCH_TIME: - # Use closer startup time when coming out of idle state - curtime = self.reactor.monotonic() - est_print_time = self.mcu.estimated_print_time(curtime) - flush_time = max(flush_time, est_print_time) - flush_motion_queues = self.motion_queuing.flush_motion_queues - while 1: - flush_time = min(flush_time + MOVE_BATCH_TIME, want_flush_time) - # Generate steps via itersolve - want_sg_wave = min(flush_time + STEPCOMPRESS_FLUSH_TIME, - want_step_gen_time) - step_gen_time = max(self.last_step_gen_time, want_sg_wave, - flush_time) - flush_motion_queues(flush_time, step_gen_time) - self.last_flush_time = flush_time - self.last_step_gen_time = step_gen_time - if flush_time >= want_flush_time: - break - def _calc_step_gen_restart(self, est_print_time): - kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) - return kin_time + self.kin_flush_delay + # Print time tracking def _advance_move_time(self, next_print_time): self.print_time = max(self.print_time, next_print_time) - self._advance_flush_time(self.print_time, lazy_target=True) + self.motion_queuing.advance_flush_time(self.print_time, + lazy_target=True) def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - kin_time = self._calc_step_gen_restart(est_print_time) + kin_time = self.motion_queuing.calc_step_gen_restart(est_print_time) min_print_time = max(est_print_time + BUFFER_TIME_START, kin_time) if min_print_time > self.print_time: self.print_time = min_print_time @@ -341,7 +296,7 @@ class ToolHead: for cb in move.timing_callbacks: cb(next_move_time) # Generate steps for moves - self.note_mcu_movequeue_activity(next_move_time) + self.motion_queuing.note_mcu_movequeue_activity(next_move_time) self._advance_move_time(next_move_time) def _flush_lookahead(self): # Transit from "NeedPrime"/"Priming"/main state to "NeedPrime" @@ -352,7 +307,7 @@ class ToolHead: self.check_stall_time = 0. def flush_step_generation(self): self._flush_lookahead() - self._advance_flush_time() + self.motion_queuing.advance_flush_time() def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() @@ -418,28 +373,6 @@ class ToolHead: if print_time != self.print_time: self.check_stall_time = self.print_time return None - def _flush_handler(self, eventtime): - try: - # Check if flushing is done via lookahead queue - ret = self._check_flush_lookahead(eventtime) - if ret is not None: - return ret - # Flush motion queues - est_print_time = self.mcu.estimated_print_time(eventtime) - while 1: - end_flush = self.need_flush_time + BGFLUSH_EXTRA_TIME - if self.last_flush_time >= end_flush: - self.do_kick_flush_timer = True - return self.reactor.NEVER - buffer_time = self.last_flush_time - est_print_time - if buffer_time > BGFLUSH_LOW_TIME: - return eventtime + buffer_time - BGFLUSH_LOW_TIME - ftime = est_print_time + BGFLUSH_LOW_TIME + BGFLUSH_BATCH_TIME - self._advance_flush_time(min(end_flush, ftime)) - except: - logging.exception("Exception in flush_handler") - self.printer.invoke_shutdown("Exception in flush_handler") - return self.reactor.NEVER # Movement commands def get_position(self): return list(self.commanded_pos) @@ -511,29 +444,6 @@ class ToolHead: def get_extra_axes(self): return [None, None, None] + self.extra_axes # Homing "drip move" handling - def drip_update_time(self, start_time, end_time, drip_completion): - # Disable background flushing from timer - self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) - self.do_kick_flush_timer = False - # Flush in segments until drip_completion signal - flush_delay = DRIP_TIME + STEPCOMPRESS_FLUSH_TIME + self.kin_flush_delay - flush_time = start_time - while flush_time < end_time: - if drip_completion.test(): - break - curtime = self.reactor.monotonic() - est_print_time = self.mcu.estimated_print_time(curtime) - wait_time = flush_time - est_print_time - flush_delay - if wait_time > 0. and self.can_pause: - # Pause before sending more steps - drip_completion.wait(curtime + wait_time) - continue - flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) - self.note_mcu_movequeue_activity(flush_time) - self._advance_flush_time(flush_time, lazy_target=True) - # Restore background flushing - self.reactor.update_timer(self.flush_timer, self.reactor.NOW) - self._advance_flush_time() def _drip_load_trapq(self, submit_move): # Queue move into trapezoid motion queue (trapq) if submit_move.move_d: @@ -563,13 +473,12 @@ class ToolHead: # Transmit move in "drip" mode self._process_lookahead() start_time, end_time = self._drip_load_trapq(move) - self.drip_update_time(start_time, end_time, drip_completion) + self.motion_queuing.drip_update_time(start_time, end_time, + drip_completion) # Move finished; cleanup any remnants on trapq self.motion_queuing.wipe_trapq(self.trapq) # Misc commands def stats(self, eventtime): - for m in self.all_mcus: - m.check_active(self.last_step_gen_time, eventtime) est_print_time = self.mcu.estimated_print_time(eventtime) buffer_time = self.print_time - est_print_time is_active = buffer_time > -60. or not self.special_queuing_state @@ -616,14 +525,6 @@ class ToolHead: callback(self.get_last_move_time()) return last_move.timing_callbacks.append(callback) - def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True): - if is_step_gen: - mq_time += self.kin_flush_delay - self.need_step_gen_time = max(self.need_step_gen_time, mq_time) - self.need_flush_time = max(self.need_flush_time, mq_time) - if self.do_kick_flush_timer: - self.do_kick_flush_timer = False - self.reactor.update_timer(self.flush_timer, self.reactor.NOW) def get_max_velocity(self): return self.max_velocity, self.max_accel def _calc_junction_deviation(self): From 5426943501bf716903d4797d334634c4b0e28e49 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 29 Nov 2023 01:04:28 -0500 Subject: [PATCH 095/411] motion_queuing: Automatically detect changes to kin_flush_delay Remove the toolhead note_step_generation_scan_time() code and automatically detect the itersolve scan windows that are in use. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 7 +++++-- klippy/chelper/itersolve.c | 18 ++++++++++++++++++ klippy/chelper/itersolve.h | 3 +++ klippy/chelper/kin_shaper.c | 8 -------- klippy/chelper/stepcompress.c | 7 +++++++ klippy/chelper/stepcompress.h | 2 ++ klippy/extras/input_shaper.py | 11 +---------- klippy/extras/motion_queuing.py | 24 ++++++++++++++++++++++-- klippy/kinematics/extruder.py | 14 ++++++++------ klippy/toolhead.py | 16 ++-------------- 10 files changed, 68 insertions(+), 42 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 60ba91e7d..59971c1c4 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -56,6 +56,8 @@ defs_stepcompress = """ , uint64_t start_clock, uint64_t end_clock); void stepcompress_set_stepper_kinematics(struct stepcompress *sc , struct stepper_kinematics *sk); + struct stepper_kinematics *stepcompress_get_stepper_kinematics( + struct stepcompress *sc); """ defs_steppersync = """ @@ -76,11 +78,14 @@ defs_itersolve = """ int32_t itersolve_is_active_axis(struct stepper_kinematics *sk, char axis); void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq , double step_dist); + struct trapq *itersolve_get_trapq(struct stepper_kinematics *sk); double itersolve_calc_position_from_coord(struct stepper_kinematics *sk , double x, double y, double z); void itersolve_set_position(struct stepper_kinematics *sk , double x, double y, double z); double itersolve_get_commanded_pos(struct stepper_kinematics *sk); + double itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk); + double itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk); """ defs_trapq = """ @@ -157,8 +162,6 @@ defs_kin_extruder = """ """ defs_kin_shaper = """ - double input_shaper_get_step_generation_window( - struct stepper_kinematics *sk); int input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis , int n, double a[], double t[]); int input_shaper_set_sk(struct stepper_kinematics *sk diff --git a/klippy/chelper/itersolve.c b/klippy/chelper/itersolve.c index eba1deef4..9b1206249 100644 --- a/klippy/chelper/itersolve.c +++ b/klippy/chelper/itersolve.c @@ -248,6 +248,12 @@ itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq sk->step_dist = step_dist; } +struct trapq * __visible +itersolve_get_trapq(struct stepper_kinematics *sk) +{ + return sk->tq; +} + double __visible itersolve_calc_position_from_coord(struct stepper_kinematics *sk , double x, double y, double z) @@ -273,3 +279,15 @@ itersolve_get_commanded_pos(struct stepper_kinematics *sk) { return sk->commanded_pos; } + +double __visible +itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk) +{ + return sk->gen_steps_pre_active; +} + +double __visible +itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk) +{ + return sk->gen_steps_post_active; +} diff --git a/klippy/chelper/itersolve.h b/klippy/chelper/itersolve.h index e2e46ebe3..50a30f7da 100644 --- a/klippy/chelper/itersolve.h +++ b/klippy/chelper/itersolve.h @@ -31,10 +31,13 @@ double itersolve_check_active(struct stepper_kinematics *sk, double flush_time); int32_t itersolve_is_active_axis(struct stepper_kinematics *sk, char axis); void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq , double step_dist); +struct trapq *itersolve_get_trapq(struct stepper_kinematics *sk); double itersolve_calc_position_from_coord(struct stepper_kinematics *sk , double x, double y, double z); void itersolve_set_position(struct stepper_kinematics *sk , double x, double y, double z); double itersolve_get_commanded_pos(struct stepper_kinematics *sk); +double itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk); +double itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk); #endif // itersolve.h diff --git a/klippy/chelper/kin_shaper.c b/klippy/chelper/kin_shaper.c index 42d572d02..d5138ff04 100644 --- a/klippy/chelper/kin_shaper.c +++ b/klippy/chelper/kin_shaper.c @@ -239,14 +239,6 @@ input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis return status; } -double __visible -input_shaper_get_step_generation_window(struct stepper_kinematics *sk) -{ - struct input_shaper *is = container_of(sk, struct input_shaper, sk); - return is->sk.gen_steps_pre_active > is->sk.gen_steps_post_active - ? is->sk.gen_steps_pre_active : is->sk.gen_steps_post_active; -} - struct stepper_kinematics * __visible input_shaper_alloc(void) { diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 2889570dc..52dd40773 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -674,6 +674,13 @@ stepcompress_set_stepper_kinematics(struct stepcompress *sc sc->sk = sk; } +// Report current stepper_kinematics +struct stepper_kinematics * __visible +stepcompress_get_stepper_kinematics(struct stepcompress *sc) +{ + return sc->sk; +} + // Generate steps (via itersolve) and flush int32_t stepcompress_generate_steps(struct stepcompress *sc, double gen_steps_time diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index e21c4fd96..7ca0f2e43 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -41,6 +41,8 @@ int stepcompress_extract_old(struct stepcompress *sc struct stepper_kinematics; void stepcompress_set_stepper_kinematics(struct stepcompress *sc , struct stepper_kinematics *sk); +struct stepper_kinematics *stepcompress_get_stepper_kinematics( + struct stepcompress *sc); int32_t stepcompress_generate_steps(struct stepcompress *sc , double gen_steps_time , uint64_t flush_clock); diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py index 67a287cdd..cb9027d98 100644 --- a/klippy/extras/input_shaper.py +++ b/klippy/extras/input_shaper.py @@ -146,12 +146,8 @@ class InputShaper: is_sk = self._get_input_shaper_stepper_kinematics(s) if is_sk is None: continue - old_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk) + self.toolhead.flush_step_generation() ffi_lib.input_shaper_update_sk(is_sk) - new_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk) - if old_delay != new_delay: - self.toolhead.note_step_generation_scan_time(new_delay, - old_delay) def _update_input_shaping(self, error=None): self.toolhead.flush_step_generation() ffi_main, ffi_lib = chelper.get_ffi() @@ -163,16 +159,11 @@ class InputShaper: is_sk = self._get_input_shaper_stepper_kinematics(s) if is_sk is None: continue - old_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk) for shaper in self.shapers: if shaper in failed_shapers: continue if not shaper.set_shaper_kinematics(is_sk): failed_shapers.append(shaper) - new_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk) - if old_delay != new_delay: - self.toolhead.note_step_generation_scan_time(new_delay, - old_delay) if failed_shapers: error = error or self.printer.command_error raise error("Failed to configure shaper(s) %s with given parameters" diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 226aa9f2a..0b0981f1c 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -49,6 +49,7 @@ class PrinterMotionQueuing: if self.mcu.is_fileoutput(): self.can_pause = False # Kinematic step generation scan window time tracking + self.need_calc_kin_flush_delay = True self.kin_flush_delay = SDS_CHECK_TIME # Register handlers printer.register_event_handler("klippy:shutdown", self._handle_shutdown) @@ -118,8 +119,6 @@ class PrinterMotionQueuing: def lookup_trapq_append(self): ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append - def set_step_generate_scan_time(self, delay): - self.kin_flush_delay = delay def stats(self, eventtime): # Hack to globally invoke mcu check_active() for m in self.all_mcus: @@ -128,6 +127,24 @@ class PrinterMotionQueuing: est_print_time = self.mcu.estimated_print_time(eventtime) self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE return False, "" + # Kinematic step generation scan window time tracking + def get_kin_flush_delay(self): + return self.kin_flush_delay + def _calc_kin_flush_delay(self): + self.need_calc_kin_flush_delay = False + ffi_main, ffi_lib = chelper.get_ffi() + kin_flush_delay = SDS_CHECK_TIME + for mcu, sc in self.stepcompress: + sk = ffi_lib.stepcompress_get_stepper_kinematics(sc) + if sk == ffi_main.NULL: + continue + trapq = ffi_lib.itersolve_get_trapq(sk) + if trapq == ffi_main.NULL: + continue + pre_active = ffi_lib.itersolve_get_gen_steps_pre_active(sk) + post_active = ffi_lib.itersolve_get_gen_steps_post_active(sk) + kin_flush_delay = max(kin_flush_delay, pre_active, post_active) + self.kin_flush_delay = kin_flush_delay # Flush tracking def _handle_shutdown(self): self.can_pause = False @@ -137,6 +154,7 @@ class PrinterMotionQueuing: if target_time is None: # This is a full flush target_time = self.need_step_gen_time + self.need_calc_kin_flush_delay = True want_flush_time = want_step_gen_time = target_time if lazy_target: # Account for step gen scan windows and optimize step compression @@ -162,6 +180,8 @@ class PrinterMotionQueuing: if flush_time >= want_flush_time: break def calc_step_gen_restart(self, est_print_time): + if self.need_calc_kin_flush_delay: + self._calc_kin_flush_delay() kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) return kin_time + self.kin_flush_delay def _flush_handler(self, eventtime): diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index a89e3bdfa..4e6f14e41 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -69,14 +69,16 @@ class ExtruderStepper: if not pressure_advance: new_smooth_time = 0. toolhead = self.printer.lookup_object("toolhead") - if new_smooth_time != old_smooth_time: - toolhead.note_step_generation_scan_time( - new_smooth_time * .5, old_delay=old_smooth_time * .5) ffi_main, ffi_lib = chelper.get_ffi() espa = ffi_lib.extruder_set_pressure_advance - toolhead.register_lookahead_callback( - lambda print_time: espa(self.sk_extruder, print_time, - pressure_advance, new_smooth_time)) + if new_smooth_time != old_smooth_time: + # Need full kinematic flush to change the smooth time + toolhead.flush_step_generation() + espa(self.sk_extruder, 0., pressure_advance, new_smooth_time) + else: + toolhead.register_lookahead_callback( + lambda print_time: espa(self.sk_extruder, print_time, + pressure_advance, new_smooth_time)) self.pressure_advance = pressure_advance self.pressure_advance_smooth_time = smooth_time cmd_SET_PRESSURE_ADVANCE_help = "Set pressure advance parameters" diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 3eadbb3d3..877e4f34c 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -193,7 +193,6 @@ class LookAheadQueue: BUFFER_TIME_LOW = 1.0 BUFFER_TIME_HIGH = 2.0 BUFFER_TIME_START = 0.250 -SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c # Main code to track events (and their timing) on the printer toolhead class ToolHead: @@ -225,9 +224,6 @@ class ToolHead: self.print_time = 0. self.special_queuing_state = "NeedPrime" self.priming_timer = None - # Kinematic step generation scan window time tracking - self.kin_flush_delay = SDS_CHECK_TIME - self.kin_flush_times = [] # Setup for generating moves self.motion_queuing = self.printer.load_object(config, 'motion_queuing') self.motion_queuing.setup_lookahead_flush_callback( @@ -469,7 +465,8 @@ class ToolHead: if move.move_d: self.kin.check_move(move) # Make sure stepper movement doesn't start before nominal start time - self.dwell(self.kin_flush_delay) + kin_flush_delay = self.motion_queuing.get_kin_flush_delay() + self.dwell(kin_flush_delay) # Transmit move in "drip" mode self._process_lookahead() start_time, end_time = self._drip_load_trapq(move) @@ -510,15 +507,6 @@ class ToolHead: return self.kin def get_trapq(self): return self.trapq - def note_step_generation_scan_time(self, delay, old_delay=0.): - self.flush_step_generation() - if old_delay: - self.kin_flush_times.pop(self.kin_flush_times.index(old_delay)) - if delay: - self.kin_flush_times.append(delay) - new_delay = max(self.kin_flush_times + [SDS_CHECK_TIME]) - self.kin_flush_delay = new_delay - self.motion_queuing.set_step_generate_scan_time(new_delay) def register_lookahead_callback(self, callback): last_move = self.lookahead.get_last() if last_move is None: From 8a833175a534fcb33e5c78f8d8787a5d18c1274f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 2 Sep 2025 13:23:02 -0400 Subject: [PATCH 096/411] motion_queuing: Introduce flush_all_steps() helper Move the "full flush" code from advance_flush_time() to a new flush_all_steps() method. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 11 +++++------ klippy/toolhead.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 0b0981f1c..9d2ef5137 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -150,11 +150,7 @@ class PrinterMotionQueuing: self.can_pause = False def setup_lookahead_flush_callback(self, check_flush_lookahead_cb): self.check_flush_lookahead_cb = check_flush_lookahead_cb - def advance_flush_time(self, target_time=None, lazy_target=False): - if target_time is None: - # This is a full flush - target_time = self.need_step_gen_time - self.need_calc_kin_flush_delay = True + def advance_flush_time(self, target_time, lazy_target=False): want_flush_time = want_step_gen_time = target_time if lazy_target: # Account for step gen scan windows and optimize step compression @@ -179,6 +175,9 @@ class PrinterMotionQueuing: self.last_step_gen_time = step_gen_time if flush_time >= want_flush_time: break + def flush_all_steps(self): + self.need_calc_kin_flush_delay = True + self.advance_flush_time(self.need_step_gen_time) def calc_step_gen_restart(self, est_print_time): if self.need_calc_kin_flush_delay: self._calc_kin_flush_delay() @@ -236,7 +235,7 @@ class PrinterMotionQueuing: self.advance_flush_time(flush_time, lazy_target=True) # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) - self.advance_flush_time() + self.advance_flush_time(self.need_step_gen_time) def load_config(config): return PrinterMotionQueuing(config) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 877e4f34c..cf648695a 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -303,7 +303,7 @@ class ToolHead: self.check_stall_time = 0. def flush_step_generation(self): self._flush_lookahead() - self.motion_queuing.advance_flush_time() + self.motion_queuing.flush_all_steps() def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() From 93ea9ddfa95665fc0619f3311c93e6e07ddea51f Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 3 Sep 2025 03:27:13 +0200 Subject: [PATCH 097/411] extruder_stepper: define missing public methods methods Other modules could access the extruderN by the printer lookup_object(). That would return this wrapper class. Specifically, filament_motion_sensor will. They can try to access missing methods and klippy would crash. Signed-off-by: Timofey Titovets --- klippy/extras/extruder_stepper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/klippy/extras/extruder_stepper.py b/klippy/extras/extruder_stepper.py index 4ac5289f8..28293a3c0 100644 --- a/klippy/extras/extruder_stepper.py +++ b/klippy/extras/extruder_stepper.py @@ -15,6 +15,8 @@ class PrinterExtruderStepper: self.handle_connect) def handle_connect(self): self.extruder_stepper.sync_to_extruder(self.extruder_name) + def find_past_position(self, print_time): + return self.extruder_stepper.find_past_position(print_time) def get_status(self, eventtime): return self.extruder_stepper.get_status(eventtime) From 58e179d1281f92d3f1f73448c0eab961f5c8a958 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 3 Sep 2025 03:48:57 +0200 Subject: [PATCH 098/411] filament_motion_sensor: define tests Signed-off-by: Timofey Titovets --- test/klippy/extruders.cfg | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/klippy/extruders.cfg b/test/klippy/extruders.cfg index d7123d08e..7384617ef 100644 --- a/test/klippy/extruders.cfg +++ b/test/klippy/extruders.cfg @@ -66,3 +66,19 @@ max_velocity: 300 max_accel: 3000 max_z_velocity: 5 max_z_accel: 100 + +[filament_switch_sensor runout_switch] +switch_pin = PD4 + +[filament_motion_sensor runout_encoder] +switch_pin = PD5 +detection_length = 4 +extruder = extruder + +[filament_switch_sensor runout_switch1] +switch_pin = PL4 + +[filament_motion_sensor runout_encoder1] +switch_pin = PL6 +detection_length = 4 +extruder = extruder_stepper my_extra_stepper From 6c1a4a825d4e2fdbc0a5c9aed580e297c0d1601b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 4 Sep 2025 14:21:47 -0400 Subject: [PATCH 099/411] docs: Note filemant_motion_sensor can be associated with extruder_stepper Signed-off-by: Kevin O'Connor --- docs/Config_Reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 83de96096..ca21bcf21 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -4944,8 +4944,8 @@ detection_length: 7.0 # a state change on the switch_pin # Default is 7 mm. extruder: -# The name of the extruder section this sensor is associated with. -# This parameter must be provided. +# The name of the extruder or extruder_stepper section this sensor +# is associated with. This parameter must be provided. switch_pin: #pause_on_runout: #runout_gcode: From aa59b32031ce72dcfd8c8e96dc933f2ad37b16ed Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 3 Sep 2025 13:45:13 -0400 Subject: [PATCH 100/411] reactor: Prevent update_timer() from running a single timer multiple times The "lazy" greenlet implementation could allow the same timer to run multiple times in parallel if the first timer instance calls pause() and another task calls update_timer(). This is confusing and can cause hard to debug errors. Add a new timer_is_running flag to prevent it. Signed-off-by: Kevin O'Connor --- klippy/reactor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/klippy/reactor.py b/klippy/reactor.py index 412d53edf..f9bedcf3f 100644 --- a/klippy/reactor.py +++ b/klippy/reactor.py @@ -1,6 +1,6 @@ # File descriptor and timer event helper # -# Copyright (C) 2016-2020 Kevin O'Connor +# Copyright (C) 2016-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import os, gc, select, math, time, logging, queue @@ -14,6 +14,7 @@ class ReactorTimer: def __init__(self, callback, waketime): self.callback = callback self.waketime = waketime + self.timer_is_running = False class ReactorCompletion: class sentinel: pass @@ -118,6 +119,8 @@ class SelectReactor: return tuple(self._last_gc_times) # Timers def update_timer(self, timer_handler, waketime): + if timer_handler.timer_is_running: + return timer_handler.waketime = waketime self._next_timer = min(self._next_timer, waketime) def register_timer(self, callback, waketime=NEVER): @@ -155,7 +158,9 @@ class SelectReactor: waketime = t.waketime if eventtime >= waketime: t.waketime = self.NEVER + t.timer_is_running = True t.waketime = waketime = t.callback(eventtime) + t.timer_is_running = False if g_dispatch is not self._g_dispatch: self._next_timer = min(self._next_timer, waketime) self._end_greenlet(g_dispatch) From 68b67a16d6d7f006ac165419f5dddae16a970dd7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 4 Sep 2025 10:08:41 -0400 Subject: [PATCH 101/411] display: Check for redraw_request_pending at end of screen_update_event() Signed-off-by: Kevin O'Connor --- klippy/extras/display/display.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/klippy/extras/display/display.py b/klippy/extras/display/display.py index e9ba31d6d..cc33bc154 100644 --- a/klippy/extras/display/display.py +++ b/klippy/extras/display/display.py @@ -236,6 +236,8 @@ class PrinterLCD: except: logging.exception("Error during display screen update") self.lcd_chip.flush() + if self.redraw_request_pending: + return self.redraw_time return eventtime + REDRAW_TIME def request_redraw(self): if self.redraw_request_pending: From 96c3ca160e881dbeef8ccbe80f804572b9170d8c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 6 Sep 2025 14:06:25 -0400 Subject: [PATCH 102/411] gcode: Fix out-of-order check for M112 when read from gcode pseudo-tty Make sure to check for an out-of-order M112 command on the gcode pseudo-tty even if there is no pending commands being processed from that gcode pseudo-tty. There could be long running commands pending from webhooks, virtual_sdcard, or similar. Signed-off-by: Kevin O'Connor --- klippy/gcode.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/klippy/gcode.py b/klippy/gcode.py index 975da792b..1c50695d2 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -430,18 +430,17 @@ class GCodeIO: self.gcode.request_restart('exit') pending_commands.append("") # Handle case where multiple commands pending - if self.is_processing_data or len(pending_commands) > 1: - if len(pending_commands) < 20: - # Check for M112 out-of-order - for line in lines: - if self.m112_r.match(line) is not None: - self.gcode.cmd_M112(None) - if self.is_processing_data: - if len(pending_commands) >= 20: - # Stop reading input - self.reactor.unregister_fd(self.fd_handle) - self.fd_handle = None - return + if len(pending_commands) < 20: + # Check for M112 out-of-order + for line in lines: + if self.m112_r.match(line) is not None: + self.gcode.cmd_M112(None) + if self.is_processing_data: + if len(pending_commands) >= 20: + # Stop reading input + self.reactor.unregister_fd(self.fd_handle) + self.fd_handle = None + return # Process commands self.is_processing_data = True while pending_commands: From a89694ac68dbfe5b150a46aa90f542ba1b53ee04 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 3 Sep 2025 15:29:24 -0400 Subject: [PATCH 103/411] stepcompress: Generate steps in a per-stepper background thread Create a thread for each stepper and use it for step generation and step compression. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 9 +-- klippy/chelper/stepcompress.c | 121 +++++++++++++++++++++++++++++++- klippy/chelper/stepcompress.h | 8 +-- klippy/chelper/steppersync.c | 46 +++++++----- klippy/chelper/steppersync.h | 7 +- klippy/extras/motion_queuing.py | 21 +++--- klippy/extras/pwm_tool.py | 4 +- klippy/stepper.py | 3 +- 8 files changed, 175 insertions(+), 44 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 59971c1c4..b9ad9747d 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -36,7 +36,7 @@ defs_stepcompress = """ int step_count, interval, add; }; - struct stepcompress *stepcompress_alloc(uint32_t oid); + struct stepcompress *stepcompress_alloc(uint32_t oid, char name[16]); void stepcompress_fill(struct stepcompress *sc, uint32_t max_error , int32_t queue_step_msgtag, int32_t set_next_step_dir_msgtag); void stepcompress_set_invert_sdir(struct stepcompress *sc @@ -66,10 +66,11 @@ defs_steppersync = """ void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); - int32_t steppersync_generate_steps(struct steppersync *ss - , double gen_steps_time, uint64_t flush_clock); void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); - int steppersync_flush(struct steppersync *ss, uint64_t move_clock); + void steppersync_start_gen_steps(struct steppersync *ss + , double gen_steps_time, uint64_t flush_clock); + int32_t steppersync_finalize_gen_steps(struct steppersync *ss + , uint64_t flush_clock); """ defs_itersolve = """ diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 52dd40773..ab261d129 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -15,6 +15,7 @@ // efficiency - the repetitive integer math is vastly faster in C. #include // sqrt +#include // pthread_mutex_lock #include // offsetof #include // uint32_t #include // fprintf @@ -47,8 +48,16 @@ struct stepcompress { // History tracking int64_t last_position; struct list_head history_list; - // Itersolve reference + // Thread for step generation struct stepper_kinematics *sk; + char name[16]; + pthread_t tid; + pthread_mutex_t lock; // protects variables below + pthread_cond_t cond; + int have_work; + double bg_gen_steps_time; + uint64_t bg_flush_clock; + int32_t bg_result; }; struct step_move { @@ -244,9 +253,12 @@ check_line(struct stepcompress *sc, struct step_move move) * Step compress interface ****************************************************************/ +static int sc_thread_alloc(struct stepcompress *sc, char name[16]); +static void sc_thread_free(struct stepcompress *sc); + // Allocate a new 'stepcompress' object struct stepcompress * __visible -stepcompress_alloc(uint32_t oid) +stepcompress_alloc(uint32_t oid, char name[16]) { struct stepcompress *sc = malloc(sizeof(*sc)); memset(sc, 0, sizeof(*sc)); @@ -254,6 +266,10 @@ stepcompress_alloc(uint32_t oid) list_init(&sc->history_list); sc->oid = oid; sc->sdir = -1; + + int ret = sc_thread_alloc(sc, name); + if (ret) + return NULL; return sc; } @@ -299,6 +315,7 @@ stepcompress_free(struct stepcompress *sc) { if (!sc) return; + sc_thread_free(sc); free(sc->queue); message_queue_free(&sc->msg_queue); stepcompress_history_expire(sc, UINT64_MAX); @@ -666,6 +683,11 @@ stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p return res; } + +/**************************************************************** + * Step generation thread + ****************************************************************/ + // Store a reference to stepper_kinematics void __visible stepcompress_set_stepper_kinematics(struct stepcompress *sc @@ -682,7 +704,7 @@ stepcompress_get_stepper_kinematics(struct stepcompress *sc) } // Generate steps (via itersolve) and flush -int32_t +static int32_t stepcompress_generate_steps(struct stepcompress *sc, double gen_steps_time , uint64_t flush_clock) { @@ -695,3 +717,96 @@ stepcompress_generate_steps(struct stepcompress *sc, double gen_steps_time // Flush steps return stepcompress_flush(sc, flush_clock); } + +// Main background thread for generating steps +static void * +sc_background_thread(void *data) +{ + struct stepcompress *sc = data; + set_thread_name(sc->name); + + pthread_mutex_lock(&sc->lock); + for (;;) { + if (!sc->have_work) { + pthread_cond_wait(&sc->cond, &sc->lock); + continue; + } + if (sc->have_work < 0) + // Exit request + break; + + // Request to generate steps + sc->bg_result = stepcompress_generate_steps(sc, sc->bg_gen_steps_time + , sc->bg_flush_clock); + sc->have_work = 0; + pthread_cond_signal(&sc->cond); + } + pthread_mutex_unlock(&sc->lock); + + return NULL; +} + +// Signal background thread to start step generation +void +stepcompress_start_gen_steps(struct stepcompress *sc, double gen_steps_time + , uint64_t flush_clock) +{ + if (!sc->sk) + return; + pthread_mutex_lock(&sc->lock); + while (sc->have_work) + pthread_cond_wait(&sc->cond, &sc->lock); + sc->bg_gen_steps_time = gen_steps_time; + sc->bg_flush_clock = flush_clock; + sc->have_work = 1; + pthread_mutex_unlock(&sc->lock); + pthread_cond_signal(&sc->cond); +} + +// Wait for background thread to complete last step generation request +int32_t +stepcompress_finalize_gen_steps(struct stepcompress *sc) +{ + pthread_mutex_lock(&sc->lock); + while (sc->have_work) + pthread_cond_wait(&sc->cond, &sc->lock); + int32_t res = sc->bg_result; + pthread_mutex_unlock(&sc->lock); + return res; +} + +// Internal helper to start thread +static int +sc_thread_alloc(struct stepcompress *sc, char name[16]) +{ + strncpy(sc->name, name, sizeof(sc->name)); + sc->name[sizeof(sc->name)-1] = '\0'; + int ret = pthread_mutex_init(&sc->lock, NULL); + if (ret) + goto fail; + ret = pthread_cond_init(&sc->cond, NULL); + if (ret) + goto fail; + ret = pthread_create(&sc->tid, NULL, sc_background_thread, sc); + if (ret) + goto fail; + return 0; +fail: + report_errno("sc init", ret); + return -1; +} + +// Request background thread to exit +static void +sc_thread_free(struct stepcompress *sc) +{ + pthread_mutex_lock(&sc->lock); + while (sc->have_work) + pthread_cond_wait(&sc->cond, &sc->lock); + sc->have_work = -1; + pthread_cond_signal(&sc->cond); + pthread_mutex_unlock(&sc->lock); + int ret = pthread_join(sc->tid, NULL); + if (ret) + report_errno("sc pthread_join", ret); +} diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 7ca0f2e43..5ebf8bf08 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -11,7 +11,7 @@ struct pull_history_steps { int step_count, interval, add; }; -struct stepcompress *stepcompress_alloc(uint32_t oid); +struct stepcompress *stepcompress_alloc(uint32_t oid, char name[16]); void stepcompress_fill(struct stepcompress *sc, uint32_t max_error , int32_t queue_step_msgtag , int32_t set_next_step_dir_msgtag); @@ -43,8 +43,8 @@ void stepcompress_set_stepper_kinematics(struct stepcompress *sc , struct stepper_kinematics *sk); struct stepper_kinematics *stepcompress_get_stepper_kinematics( struct stepcompress *sc); -int32_t stepcompress_generate_steps(struct stepcompress *sc - , double gen_steps_time - , uint64_t flush_clock); +void stepcompress_start_gen_steps(struct stepcompress *sc, double gen_steps_time + , uint64_t flush_clock); +int32_t stepcompress_finalize_gen_steps(struct stepcompress *sc); #endif // stepcompress.h diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 745578c75..0ff5bcab1 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -76,22 +76,6 @@ steppersync_set_time(struct steppersync *ss, double time_offset } } -// Generate steps and flush stepcompress objects -int32_t __visible -steppersync_generate_steps(struct steppersync *ss, double gen_steps_time - , uint64_t flush_clock) -{ - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - int32_t ret = stepcompress_generate_steps(sc, gen_steps_time - , flush_clock); - if (ret) - return ret; - } - return 0; -} - // Expire the stepcompress history before the given clock time void __visible steppersync_history_expire(struct steppersync *ss, uint64_t end_clock) @@ -129,7 +113,7 @@ heap_replace(struct steppersync *ss, uint64_t req_clock) } // Find and transmit any scheduled steps prior to the given 'move_clock' -int __visible +static void steppersync_flush(struct steppersync *ss, uint64_t move_clock) { // Order commands by the reqclock of each pending command @@ -172,6 +156,34 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock) // Transmit commands if (!list_empty(&msgs)) serialqueue_send_batch(ss->sq, ss->cq, &msgs); +} +// Start generating steps in stepcompress objects +void __visible +steppersync_start_gen_steps(struct steppersync *ss, double gen_steps_time + , uint64_t flush_clock) +{ + int i; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + stepcompress_start_gen_steps(sc, gen_steps_time, flush_clock); + } +} + +// Finalize step generation and flush +int32_t __visible +steppersync_finalize_gen_steps(struct steppersync *ss, uint64_t flush_clock) +{ + int i; + int32_t res = 0; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + int32_t ret = stepcompress_finalize_gen_steps(sc); + if (ret) + res = ret; + } + if (res) + return res; + steppersync_flush(ss, flush_clock); return 0; } diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index 1320bbaa0..41cd03bbd 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -10,9 +10,10 @@ struct steppersync *steppersync_alloc( void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); -int32_t steppersync_generate_steps(struct steppersync *ss, double gen_steps_time - , uint64_t flush_clock); void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); -int steppersync_flush(struct steppersync *ss, uint64_t move_clock); +void steppersync_start_gen_steps(struct steppersync *ss, double gen_steps_time + , uint64_t flush_clock); +int32_t steppersync_finalize_gen_steps(struct steppersync *ss + , uint64_t flush_clock); #endif // steppersync.h diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 9d2ef5137..a61ba5cc0 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -29,8 +29,9 @@ class PrinterMotionQueuing: # Low-level C flushing calls ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves - self.steppersync_generate_steps = ffi_lib.steppersync_generate_steps - self.steppersync_flush = ffi_lib.steppersync_flush + self.steppersync_start_gen_steps = ffi_lib.steppersync_start_gen_steps + self.steppersync_finalize_gen_steps = \ + ffi_lib.steppersync_finalize_gen_steps self.steppersync_history_expire = ffi_lib.steppersync_history_expire # Flush notification callbacks self.flush_callbacks = [] @@ -58,9 +59,10 @@ class PrinterMotionQueuing: trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) self.trapqs.append(trapq) return trapq - def allocate_stepcompress(self, mcu, oid): + def allocate_stepcompress(self, mcu, oid, name): + name = name.encode("utf-8")[:15] ffi_main, ffi_lib = chelper.get_ffi() - sc = ffi_main.gc(ffi_lib.stepcompress_alloc(oid), + sc = ffi_main.gc(ffi_lib.stepcompress_alloc(oid, name), ffi_lib.stepcompress_free) self.stepcompress.append((mcu, sc)) return sc @@ -90,13 +92,10 @@ class PrinterMotionQueuing: # Generate stepper movement and transmit for mcu, ss in self.steppersyncs: clock = max(0, mcu.print_time_to_clock(must_flush_time)) - # Generate steps - ret = self.steppersync_generate_steps(ss, max_step_gen_time, clock) - if ret: - raise mcu.error("Internal error in MCU '%s' stepcompress" - % (mcu.get_name(),)) - # Flush steps from steppersync - ret = self.steppersync_flush(ss, clock) + self.steppersync_start_gen_steps(ss, max_step_gen_time, clock) + for mcu, ss in self.steppersyncs: + clock = max(0, mcu.print_time_to_clock(must_flush_time)) + ret = self.steppersync_finalize_gen_steps(ss, clock) if ret: raise mcu.error("Internal error in MCU '%s' stepcompress" % (mcu.get_name(),)) diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index d9e72c5e1..cec7e3791 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -16,8 +16,10 @@ class MCU_queued_pwm: self._max_duration = 2. self._oid = oid = mcu.create_oid() printer = mcu.get_printer() + sname = config.get_name().split()[-1] self._motion_queuing = printer.load_object(config, 'motion_queuing') - self._stepqueue = self._motion_queuing.allocate_stepcompress(mcu, oid) + self._stepqueue = self._motion_queuing.allocate_stepcompress( + mcu, oid, sname) ffi_main, ffi_lib = chelper.get_ffi() self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg mcu.register_config_callback(self._build_config) diff --git a/klippy/stepper.py b/klippy/stepper.py index d5b3cecde..046b5280d 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -44,7 +44,8 @@ class MCU_stepper: self._reset_cmd_tag = self._get_position_cmd = None self._active_callbacks = [] motion_queuing = printer.load_object(config, 'motion_queuing') - self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid) + sname = self._name.split()[-1] + self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid, sname) ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir) self._stepper_kinematics = None From bb88985b8d48fa7505fee116eec1c4902361f95d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 5 Sep 2025 13:34:48 -0400 Subject: [PATCH 104/411] reactor: Unify handling of fd events The SelectReactor has a different event dispatch system from the PollReactor and EPollReactor. However, in practice the PollReactor code is always used, so there is no reason to maintain a different implementation for SelectReactor. Rework the code so that SelectReactor file dispatch handling is done the same way as PollReactor (and EPollReactor). This simplfiies the code. Introduce a new _check_fds() method that is shared between Reactor implementations. Also, fix some cut-and-paste bugs in SelectReactor and EPollReactor. Signed-off-by: Kevin O'Connor --- klippy/reactor.py | 129 ++++++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 74 deletions(-) diff --git a/klippy/reactor.py b/klippy/reactor.py index f9bedcf3f..db6a089e3 100644 --- a/klippy/reactor.py +++ b/klippy/reactor.py @@ -55,8 +55,6 @@ class ReactorFileHandler: self.fd = fd self.read_callback = read_callback self.write_callback = write_callback - def fileno(self): - return self.fd class ReactorGreenlet(greenlet.greenlet): def __init__(self, run): @@ -109,8 +107,13 @@ class SelectReactor: self._pipe_fds = None self._async_queue = queue.Queue() # File descriptors + self._dummy_fd_hdl = ReactorFileHandler(-1, (lambda e: None), + (lambda e: None)) + self._fds = {} self._read_fds = [] self._write_fds = [] + self._READ = 1 + self._WRITE = 2 # Greenlets self._g_dispatch = None self._greenlets = [] @@ -245,48 +248,54 @@ class SelectReactor: # File descriptors def register_fd(self, fd, read_callback, write_callback=None): file_handler = ReactorFileHandler(fd, read_callback, write_callback) + self._fds[fd] = file_handler self.set_fd_wake(file_handler, True, False) return file_handler def unregister_fd(self, file_handler): - if file_handler in self._read_fds: - self._read_fds.pop(self._read_fds.index(file_handler)) - if file_handler in self._write_fds: - self._write_fds.pop(self._write_fds.index(file_handler)) + self.set_fd_wake(file_handler, False, False) + del self._fds[file_handler.fd] def set_fd_wake(self, file_handler, is_readable=True, is_writeable=False): - if file_handler in self._read_fds: + fd = file_handler.fd + if fd in self._read_fds: if not is_readable: - self._read_fds.pop(self._read_fds.index(file_handler)) + self._read_fds.remove(fd) elif is_readable: - self._read_fds.append(file_handler) - if file_handler in self._write_fds: + self._read_fds.append(fd) + if fd in self._write_fds: if not is_writeable: - self._write_fds.pop(self._write_fds.index(file_handler)) + self._write_fds.remove(fd) elif is_writeable: - self._write_fds.append(file_handler) + self._write_fds.append(fd) + def _check_fds(self, eventtime, hdls): + g_dispatch = self._g_dispatch + for fd, event in hdls: + hdl = self._fds.get(fd, self._dummy_fd_hdl) + if event & self._READ: + hdl.read_callback(eventtime) + if g_dispatch is not self._g_dispatch: + self._end_greenlet(g_dispatch) + return self.monotonic() + if event & self._WRITE: + hdl.write_callback(eventtime) + if g_dispatch is not self._g_dispatch: + self._end_greenlet(g_dispatch) + return self.monotonic() + return eventtime # Main loop def _dispatch_loop(self): - self._g_dispatch = g_dispatch = greenlet.getcurrent() + self._g_dispatch = greenlet.getcurrent() busy = True eventtime = self.monotonic() while self._process: timeout = self._check_timers(eventtime, busy) busy = False - res = select.select(self._read_fds, self.write_fds, [], timeout) + res = select.select(self._read_fds, self._write_fds, [], timeout) eventtime = self.monotonic() - for fd in res[0]: + if res[0] or res[1]: busy = True - fd.read_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break - for fd in res[1]: - busy = True - fd.write_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break + hdls = ([(fd, self._READ) for fd in res[0]] + + [(fd, self._WRITE) for fd in res[1]]) + eventtime = self._check_fds(eventtime, hdls) self._g_dispatch = None def run(self): if self._pipe_fds is None: @@ -315,30 +324,27 @@ class PollReactor(SelectReactor): def __init__(self, gc_checking=False): SelectReactor.__init__(self, gc_checking) self._poll = select.poll() - self._fds = {} + self._READ = select.POLLIN | select.POLLHUP + self._WRITE = select.POLLOUT # File descriptors def register_fd(self, fd, read_callback, write_callback=None): file_handler = ReactorFileHandler(fd, read_callback, write_callback) - fds = self._fds.copy() - fds[fd] = file_handler - self._fds = fds - self._poll.register(file_handler, select.POLLIN | select.POLLHUP) + self._fds[fd] = file_handler + self._poll.register(file_handler.fd, select.POLLIN | select.POLLHUP) return file_handler def unregister_fd(self, file_handler): - self._poll.unregister(file_handler) - fds = self._fds.copy() - del fds[file_handler.fd] - self._fds = fds + self._poll.unregister(file_handler.fd) + del self._fds[file_handler.fd] def set_fd_wake(self, file_handler, is_readable=True, is_writeable=False): flags = select.POLLHUP if is_readable: flags |= select.POLLIN if is_writeable: flags |= select.POLLOUT - self._poll.modify(file_handler, flags) + self._poll.modify(file_handler.fd, flags) # Main loop def _dispatch_loop(self): - self._g_dispatch = g_dispatch = greenlet.getcurrent() + self._g_dispatch = greenlet.getcurrent() busy = True eventtime = self.monotonic() while self._process: @@ -346,50 +352,36 @@ class PollReactor(SelectReactor): busy = False res = self._poll.poll(int(math.ceil(timeout * 1000.))) eventtime = self.monotonic() - for fd, event in res: + if res: busy = True - if event & (select.POLLIN | select.POLLHUP): - self._fds[fd].read_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break - if event & select.POLLOUT: - self._fds[fd].write_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break + eventtime = self._check_fds(eventtime, res) self._g_dispatch = None class EPollReactor(SelectReactor): def __init__(self, gc_checking=False): SelectReactor.__init__(self, gc_checking) self._epoll = select.epoll() - self._fds = {} + self._READ = select.EPOLLIN | select.EPOLLHUP + self._WRITE = select.EPOLLOUT # File descriptors def register_fd(self, fd, read_callback, write_callback=None): file_handler = ReactorFileHandler(fd, read_callback, write_callback) - fds = self._fds.copy() - fds[fd] = read_callback - self._fds = fds + self._fds[fd] = file_handler self._epoll.register(fd, select.EPOLLIN | select.EPOLLHUP) return file_handler def unregister_fd(self, file_handler): self._epoll.unregister(file_handler.fd) - fds = self._fds.copy() - del fds[file_handler.fd] - self._fds = fds + del self._fds[file_handler.fd] def set_fd_wake(self, file_handler, is_readable=True, is_writeable=False): - flags = select.POLLHUP + flags = select.EPOLLHUP if is_readable: flags |= select.EPOLLIN if is_writeable: flags |= select.EPOLLOUT - self._epoll.modify(file_handler, flags) + self._epoll.modify(file_handler.fd, flags) # Main loop def _dispatch_loop(self): - self._g_dispatch = g_dispatch = greenlet.getcurrent() + self._g_dispatch = greenlet.getcurrent() busy = True eventtime = self.monotonic() while self._process: @@ -397,20 +389,9 @@ class EPollReactor(SelectReactor): busy = False res = self._epoll.poll(timeout) eventtime = self.monotonic() - for fd, event in res: + if res: busy = True - if event & (select.EPOLLIN | select.EPOLLHUP): - self._fds[fd].read_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break - if event & select.EPOLLOUT: - self._fds[fd].write_callback(eventtime) - if g_dispatch is not self._g_dispatch: - self._end_greenlet(g_dispatch) - eventtime = self.monotonic() - break + eventtime = self._check_fds(eventtime, res) self._g_dispatch = None # Use the poll based reactor if it is available From cde57bdcfd7b5eda44da4ad7c263cbb40789e139 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 6 Sep 2025 22:22:12 -0400 Subject: [PATCH 105/411] toolhead: Set check_stall_time from _flush_lookahead() Add a new is_runout parameter to _flush_lookahead() and use that in places that could set check_stall_time. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index cf648695a..9cb82388e 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -294,13 +294,16 @@ class ToolHead: # Generate steps for moves self.motion_queuing.note_mcu_movequeue_activity(next_move_time) self._advance_move_time(next_move_time) - def _flush_lookahead(self): + def _flush_lookahead(self, is_runout=False): # Transit from "NeedPrime"/"Priming"/main state to "NeedPrime" + prev_print_time = self.print_time self._process_lookahead() self.special_queuing_state = "NeedPrime" self.need_check_pause = -1. self.lookahead.set_flush_time(BUFFER_TIME_HIGH) self.check_stall_time = 0. + if is_runout and prev_print_time != self.print_time: + self.check_stall_time = self.print_time def flush_step_generation(self): self._flush_lookahead() self.motion_queuing.flush_all_steps() @@ -348,8 +351,7 @@ class ToolHead: self.priming_timer = None try: if self.special_queuing_state == "Priming": - self._flush_lookahead() - self.check_stall_time = self.print_time + self._flush_lookahead(is_runout=True) except: logging.exception("Exception in priming_handler") self.printer.invoke_shutdown("Exception in priming_handler") @@ -359,15 +361,12 @@ class ToolHead: return None # In "main" state - flush lookahead if buffer runs low est_print_time = self.mcu.estimated_print_time(eventtime) - print_time = self.print_time - buffer_time = print_time - est_print_time + buffer_time = self.print_time - est_print_time if buffer_time > BUFFER_TIME_LOW: # Running normally - reschedule check return eventtime + buffer_time - BUFFER_TIME_LOW # Under ran low buffer mark - flush lookahead queue - self._flush_lookahead() - if print_time != self.print_time: - self.check_stall_time = self.print_time + self._flush_lookahead(is_runout=True) return None # Movement commands def get_position(self): From 22db9bb84e26eeef5b1281707fd536829c5933af Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 7 Sep 2025 12:18:28 -0400 Subject: [PATCH 106/411] motion_queuing: Require explicit notification on a scan window change Don't try to infer when the step generation scan window may change. Instead, require the input_shaper and pressure_advance code call motion_queuing.check_step_generation_scan_windows() any time a scanning window may change. Signed-off-by: Kevin O'Connor --- klippy/extras/input_shaper.py | 6 +++++- klippy/extras/motion_queuing.py | 7 +------ klippy/kinematics/extruder.py | 2 ++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py index cb9027d98..c79cdcf2c 100644 --- a/klippy/extras/input_shaper.py +++ b/klippy/extras/input_shaper.py @@ -138,6 +138,7 @@ class InputShaper: if self.toolhead is None: # Klipper initialization is not yet completed return + self.toolhead.flush_step_generation() ffi_main, ffi_lib = chelper.get_ffi() kin = self.toolhead.get_kinematics() for s in kin.get_steppers(): @@ -146,8 +147,9 @@ class InputShaper: is_sk = self._get_input_shaper_stepper_kinematics(s) if is_sk is None: continue - self.toolhead.flush_step_generation() ffi_lib.input_shaper_update_sk(is_sk) + motion_queuing = self.printer.lookup_object("motion_queuing") + motion_queuing.check_step_generation_scan_windows() def _update_input_shaping(self, error=None): self.toolhead.flush_step_generation() ffi_main, ffi_lib = chelper.get_ffi() @@ -164,6 +166,8 @@ class InputShaper: continue if not shaper.set_shaper_kinematics(is_sk): failed_shapers.append(shaper) + motion_queuing = self.printer.lookup_object("motion_queuing") + motion_queuing.check_step_generation_scan_windows() if failed_shapers: error = error or self.printer.command_error raise error("Failed to configure shaper(s) %s with given parameters" diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index a61ba5cc0..99035f06f 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -50,7 +50,6 @@ class PrinterMotionQueuing: if self.mcu.is_fileoutput(): self.can_pause = False # Kinematic step generation scan window time tracking - self.need_calc_kin_flush_delay = True self.kin_flush_delay = SDS_CHECK_TIME # Register handlers printer.register_event_handler("klippy:shutdown", self._handle_shutdown) @@ -129,8 +128,7 @@ class PrinterMotionQueuing: # Kinematic step generation scan window time tracking def get_kin_flush_delay(self): return self.kin_flush_delay - def _calc_kin_flush_delay(self): - self.need_calc_kin_flush_delay = False + def check_step_generation_scan_windows(self): ffi_main, ffi_lib = chelper.get_ffi() kin_flush_delay = SDS_CHECK_TIME for mcu, sc in self.stepcompress: @@ -175,11 +173,8 @@ class PrinterMotionQueuing: if flush_time >= want_flush_time: break def flush_all_steps(self): - self.need_calc_kin_flush_delay = True self.advance_flush_time(self.need_step_gen_time) def calc_step_gen_restart(self, est_print_time): - if self.need_calc_kin_flush_delay: - self._calc_kin_flush_delay() kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) return kin_time + self.kin_flush_delay def _flush_handler(self, eventtime): diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 4e6f14e41..9b1ec2d20 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -75,6 +75,8 @@ class ExtruderStepper: # Need full kinematic flush to change the smooth time toolhead.flush_step_generation() espa(self.sk_extruder, 0., pressure_advance, new_smooth_time) + motion_queuing = self.printer.lookup_object('motion_queuing') + motion_queuing.check_step_generation_scan_windows() else: toolhead.register_lookahead_callback( lambda print_time: espa(self.sk_extruder, print_time, From 3bed65f10ffff5dd61f666acf974e05c59e498e5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 4 Sep 2025 12:54:03 -0400 Subject: [PATCH 107/411] motion_queuing: Move remaining steppersync logic from mcu module Move the last parts of the steppersync logic into the motion_queuing module. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 15 ++++++++++----- klippy/mcu.py | 17 ++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 99035f06f..ab3836fee 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -65,7 +65,8 @@ class PrinterMotionQueuing: ffi_lib.stepcompress_free) self.stepcompress.append((mcu, sc)) return sc - def allocate_steppersync(self, mcu, serialqueue, move_count): + def setup_mcu_movequeue(self, mcu, serialqueue, move_count): + # Setup steppersync object for the mcu's main movequeue stepqueues = [] for sc_mcu, sc in self.stepcompress: if sc_mcu is mcu: @@ -76,7 +77,8 @@ class PrinterMotionQueuing: move_count), ffi_lib.steppersync_free) self.steppersyncs.append((mcu, ss)) - return ss + mcu_freq = float(mcu.seconds_to_clock(1.)) + ffi_lib.steppersync_set_time(ss, 0., mcu_freq) def register_flush_callback(self, callback): self.flush_callbacks.append(callback) def unregister_flush_callback(self, callback): @@ -118,9 +120,12 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append def stats(self, eventtime): - # Hack to globally invoke mcu check_active() - for m in self.all_mcus: - m.check_active(self.last_step_gen_time, eventtime) + # Globally calibrate mcu clocks (and step generation clocks) + sync_time = self.last_step_gen_time + ffi_main, ffi_lib = chelper.get_ffi() + for mcu, ss in self.steppersyncs: + offset, freq = mcu.calibrate_clock(sync_time, eventtime) + ffi_lib.steppersync_set_time(ss, offset, freq) # Calculate history expiration est_print_time = self.mcu.estimated_print_time(eventtime) self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE diff --git a/klippy/mcu.py b/klippy/mcu.py index 146b5baca..74e2164e7 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -604,11 +604,9 @@ class MCU: self._init_cmds = [] self._mcu_freq = 0. # Move command queuing - ffi_main, self._ffi_lib = chelper.get_ffi() self._max_stepper_error = config.getfloat('max_stepper_error', 0.000025, minval=0.) self._reserved_move_slots = 0 - self._steppersync = None # Stats self._get_status_info = {} self._stats_sumsq_base = 0. @@ -773,10 +771,8 @@ class MCU: raise error("Too few moves available on MCU '%s'" % (self._name,)) ss_move_count = move_count - self._reserved_move_slots motion_queuing = self._printer.lookup_object('motion_queuing') - self._steppersync = motion_queuing.allocate_steppersync( + motion_queuing.setup_mcu_movequeue( self, self._serial.get_serialqueue(), ss_move_count) - self._ffi_lib.steppersync_set_time(self._steppersync, - 0., self._mcu_freq) # Log config information move_msg = "Configured MCU '%s' (%d moves)" % (self._name, move_count) logging.info(move_msg) @@ -919,7 +915,6 @@ class MCU: # Restarts def _disconnect(self): self._serial.disconnect() - self._steppersync = None def _shutdown(self, force=False): if (self._emergency_stop_cmd is None or (self._is_shutdown and not force)): @@ -974,11 +969,7 @@ class MCU: # Move queue tracking def request_move_queue_slot(self): self._reserved_move_slots += 1 - def check_active(self, print_time, eventtime): - if self._steppersync is None: - return - offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) - self._ffi_lib.steppersync_set_time(self._steppersync, offset, freq) + def _check_timeout(self, eventtime): if (self._clocksync.is_active() or self.is_fileoutput() or self._is_timeout): return @@ -987,6 +978,10 @@ class MCU: self._name, eventtime) self._printer.invoke_shutdown("Lost communication with MCU '%s'" % ( self._name,)) + def calibrate_clock(self, print_time, eventtime): + offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) + self._check_timeout(eventtime) + return offset, freq # Misc external commands def is_fileoutput(self): return self._printer.get_start_args().get('debugoutput') is not None From 32bd03703b3cff2ade12c76fd8c2b11d0b185488 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 2 Sep 2025 13:03:00 -0400 Subject: [PATCH 108/411] motion_queuing: Don't use lazy_target in drip_update_time() Using separate flush_time and step_gen_time is a minor optimization. Using it in drip_update_time() complicates the code and may reduce the time needed to schedule post homing/probing movements. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index ab3836fee..fe93b6207 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -217,24 +217,23 @@ class PrinterMotionQueuing: self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) self.do_kick_flush_timer = False # Flush in segments until drip_completion signal - flush_delay = DRIP_TIME + STEPCOMPRESS_FLUSH_TIME + self.kin_flush_delay flush_time = start_time while flush_time < end_time: if drip_completion.test(): break curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - wait_time = flush_time - est_print_time - flush_delay + wait_time = flush_time - est_print_time - DRIP_TIME if wait_time > 0. and self.can_pause: # Pause before sending more steps drip_completion.wait(curtime + wait_time) continue flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) self.note_mcu_movequeue_activity(flush_time) - self.advance_flush_time(flush_time, lazy_target=True) + self.advance_flush_time(flush_time) # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) - self.advance_flush_time(self.need_step_gen_time) + self.advance_flush_time(flush_time + self.kin_flush_delay) def load_config(config): return PrinterMotionQueuing(config) From b60804bb662a56bcbb61a0ae5271756d70b24e91 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 3 Sep 2025 21:59:26 -0400 Subject: [PATCH 109/411] trapq: Set the head sentinel to a negative print_time If a stepper kinematics has a "scan window" defined during its first flush then the iterative solver may walk past the head sentinel. Set a small negative print_time for the head sentinel to avoid this corner case. Signed-off-by: Kevin O'Connor --- klippy/chelper/trapq.c | 1 + 1 file changed, 1 insertion(+) diff --git a/klippy/chelper/trapq.c b/klippy/chelper/trapq.c index b9930e997..a21969414 100644 --- a/klippy/chelper/trapq.c +++ b/klippy/chelper/trapq.c @@ -49,6 +49,7 @@ trapq_alloc(void) list_init(&tq->moves); list_init(&tq->history); struct move *head_sentinel = move_alloc(), *tail_sentinel = move_alloc(); + head_sentinel->print_time = -1.0; tail_sentinel->print_time = tail_sentinel->move_t = NEVER_TIME; list_add_head(&head_sentinel->node, &tq->moves); list_add_tail(&tail_sentinel->node, &tq->moves); From 7ea5f5d25ecc1c6acd30873c035f150054bbdfe0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 2 Sep 2025 13:45:29 -0400 Subject: [PATCH 110/411] motion_queuing: Generate steps from timer instead of from lookahead Don't tie the step generation logic to the toolhead lookahead logic. Instead, use regular timers to generate steps with a goal of staying 500-750ms ahead of the micro-controllers. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 111 ++++++++++++++++++++++---------- klippy/toolhead.py | 26 +++----- 2 files changed, 85 insertions(+), 52 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index fe93b6207..67609eee7 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -7,8 +7,11 @@ import logging import chelper BGFLUSH_LOW_TIME = 0.200 -BGFLUSH_BATCH_TIME = 0.200 +BGFLUSH_HIGH_TIME = 0.400 +BGFLUSH_SG_LOW_TIME = 0.450 +BGFLUSH_SG_HIGH_TIME = 0.700 BGFLUSH_EXTRA_TIME = 0.250 + MOVE_HISTORY_EXPIRE = 30. MIN_KIN_TIME = 0.100 MOVE_BATCH_TIME = 0.500 @@ -37,18 +40,20 @@ class PrinterMotionQueuing: self.flush_callbacks = [] # History expiration self.clear_history_time = 0. - # Flush tracking - self.flush_timer = self.reactor.register_timer(self._flush_handler) - self.do_kick_flush_timer = True - self.last_flush_time = self.last_step_gen_time = 0. - self.need_flush_time = self.need_step_gen_time = 0. - self.check_flush_lookahead_cb = (lambda e: None) # MCU tracking self.all_mcus = [m for n, m in printer.lookup_objects(module='mcu')] self.mcu = self.all_mcus[0] self.can_pause = True if self.mcu.is_fileoutput(): self.can_pause = False + # Flush tracking + flush_handler = self._flush_handler + if not self.can_pause: + flush_handler = self._flush_handler_debug + self.flush_timer = self.reactor.register_timer(flush_handler) + self.do_kick_flush_timer = True + self.last_flush_time = self.last_step_gen_time = 0. + self.need_flush_time = self.need_step_gen_time = 0. # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME # Register handlers @@ -79,8 +84,11 @@ class PrinterMotionQueuing: self.steppersyncs.append((mcu, ss)) mcu_freq = float(mcu.seconds_to_clock(1.)) ffi_lib.steppersync_set_time(ss, 0., mcu_freq) - def register_flush_callback(self, callback): - self.flush_callbacks.append(callback) + def register_flush_callback(self, callback, can_add_trapq=False): + if can_add_trapq: + self.flush_callbacks = [callback] + self.flush_callbacks + else: + self.flush_callbacks = self.flush_callbacks + [callback] def unregister_flush_callback(self, callback): if callback in self.flush_callbacks: fcbs = list(self.flush_callbacks) @@ -150,15 +158,10 @@ class PrinterMotionQueuing: # Flush tracking def _handle_shutdown(self): self.can_pause = False - def setup_lookahead_flush_callback(self, check_flush_lookahead_cb): - self.check_flush_lookahead_cb = check_flush_lookahead_cb - def advance_flush_time(self, target_time, lazy_target=False): - want_flush_time = want_step_gen_time = target_time - if lazy_target: - # Account for step gen scan windows and optimize step compression - want_step_gen_time -= self.kin_flush_delay - want_flush_time = want_step_gen_time - STEPCOMPRESS_FLUSH_TIME - want_flush_time = max(want_flush_time, self.last_flush_time) + def _advance_flush_time(self, want_flush_time, want_step_gen_time=0.): + want_flush_time = max(want_flush_time, self.last_flush_time, + want_step_gen_time - STEPCOMPRESS_FLUSH_TIME) + want_step_gen_time = max(want_step_gen_time, want_flush_time) flush_time = self.last_flush_time if want_flush_time > flush_time + 10. * MOVE_BATCH_TIME: # Use closer startup time when coming out of idle state @@ -178,32 +181,70 @@ class PrinterMotionQueuing: if flush_time >= want_flush_time: break def flush_all_steps(self): - self.advance_flush_time(self.need_step_gen_time) + self._advance_flush_time(self.need_step_gen_time) def calc_step_gen_restart(self, est_print_time): kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) return kin_time + self.kin_flush_delay def _flush_handler(self, eventtime): try: - # Check if flushing is done via lookahead queue - ret = self.check_flush_lookahead_cb(eventtime) - if ret is not None: - return ret - # Flush motion queues est_print_time = self.mcu.estimated_print_time(eventtime) - while 1: - end_flush = self.need_flush_time + BGFLUSH_EXTRA_TIME - if self.last_flush_time >= end_flush: - self.do_kick_flush_timer = True + aggr_sg_time = self.need_step_gen_time - 2.*self.kin_flush_delay + if self.last_step_gen_time < aggr_sg_time: + # Actively stepping - want more aggressive flushing + want_sg_time = est_print_time + BGFLUSH_SG_HIGH_TIME + want_sg_time = min(want_sg_time, aggr_sg_time) + # Try improving run-to-run reproducibility by batching from last + batch_time = BGFLUSH_SG_HIGH_TIME - BGFLUSH_SG_LOW_TIME + next_batch_time = self.last_step_gen_time + batch_time + if next_batch_time > est_print_time + BGFLUSH_SG_LOW_TIME: + want_sg_time = min(want_sg_time, next_batch_time) + # Flush motion queues (if needed) + if want_sg_time > self.last_step_gen_time: + self._advance_flush_time(0., want_sg_time) + else: + # Not stepping (or only step remnants) - use relaxed flushing + want_flush_time = est_print_time + BGFLUSH_HIGH_TIME + max_flush_time = self.need_flush_time + BGFLUSH_EXTRA_TIME + want_flush_time = min(want_flush_time, max_flush_time) + # Flush motion queues (if needed) + if want_flush_time > self.last_flush_time: + self._advance_flush_time(want_flush_time) + # Reschedule timer + aggr_sg_time = self.need_step_gen_time - 2.*self.kin_flush_delay + if self.last_step_gen_time < aggr_sg_time: + waketime = self.last_step_gen_time - BGFLUSH_SG_LOW_TIME + else: + self.do_kick_flush_timer = True + max_flush_time = self.need_flush_time + BGFLUSH_EXTRA_TIME + if self.last_flush_time >= max_flush_time: return self.reactor.NEVER - buffer_time = self.last_flush_time - est_print_time - if buffer_time > BGFLUSH_LOW_TIME: - return eventtime + buffer_time - BGFLUSH_LOW_TIME - ftime = est_print_time + BGFLUSH_LOW_TIME + BGFLUSH_BATCH_TIME - self.advance_flush_time(min(end_flush, ftime)) + waketime = self.last_flush_time - BGFLUSH_LOW_TIME + return eventtime + waketime - est_print_time except: logging.exception("Exception in flush_handler") self.printer.invoke_shutdown("Exception in flush_handler") return self.reactor.NEVER + def _flush_handler_debug(self, eventtime): + # Use custom flushing code when in batch output mode + try: + faux_time = self.need_flush_time - 1.5 + batch_time = BGFLUSH_SG_HIGH_TIME - BGFLUSH_SG_LOW_TIME + flush_count = 0 + while self.last_step_gen_time < faux_time: + target = self.last_step_gen_time + batch_time + if flush_count > 100.: + target = faux_time + self._advance_flush_time(0., target) + flush_count += 1 + if flush_count: + return self.reactor.NOW + self._advance_flush_time(self.need_flush_time + BGFLUSH_EXTRA_TIME) + self.do_kick_flush_timer = True + return self.reactor.NEVER + except: + logging.exception("Exception in flush_handler_debug") + self.printer.invoke_shutdown("Exception in flush_handler_debug") + return self.reactor.NEVER def note_mcu_movequeue_activity(self, mq_time, is_step_gen=True): if is_step_gen: mq_time += self.kin_flush_delay @@ -230,10 +271,10 @@ class PrinterMotionQueuing: continue flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) self.note_mcu_movequeue_activity(flush_time) - self.advance_flush_time(flush_time) + self._advance_flush_time(flush_time) # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) - self.advance_flush_time(flush_time + self.kin_flush_delay) + self._advance_flush_time(flush_time + self.kin_flush_delay) def load_config(config): return PrinterMotionQueuing(config) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 9cb82388e..c4264051d 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -190,7 +190,6 @@ class LookAheadQueue: # Check if enough moves have been queued to reach the target flush time. return self.junction_flush <= 0. -BUFFER_TIME_LOW = 1.0 BUFFER_TIME_HIGH = 2.0 BUFFER_TIME_START = 0.250 @@ -226,8 +225,8 @@ class ToolHead: self.priming_timer = None # Setup for generating moves self.motion_queuing = self.printer.load_object(config, 'motion_queuing') - self.motion_queuing.setup_lookahead_flush_callback( - self._check_flush_lookahead) + self.motion_queuing.register_flush_callback(self._handle_step_flush, + can_add_trapq=True) self.trapq = self.motion_queuing.allocate_trapq() self.trapq_append = self.motion_queuing.lookup_trapq_append() # Create kinematics class @@ -253,8 +252,6 @@ class ToolHead: # Print time tracking def _advance_move_time(self, next_print_time): self.print_time = max(self.print_time, next_print_time) - self.motion_queuing.advance_flush_time(self.print_time, - lazy_target=True) def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) @@ -292,8 +289,8 @@ class ToolHead: for cb in move.timing_callbacks: cb(next_move_time) # Generate steps for moves - self.motion_queuing.note_mcu_movequeue_activity(next_move_time) self._advance_move_time(next_move_time) + self.motion_queuing.note_mcu_movequeue_activity(next_move_time) def _flush_lookahead(self, is_runout=False): # Transit from "NeedPrime"/"Priming"/main state to "NeedPrime" prev_print_time = self.print_time @@ -330,7 +327,7 @@ class ToolHead: if self.priming_timer is None: self.priming_timer = self.reactor.register_timer( self._priming_handler) - wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_LOW) + wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_HIGH) self.reactor.update_timer(self.priming_timer, wtime) # Check if there are lots of queued moves and pause if so while 1: @@ -356,18 +353,13 @@ class ToolHead: logging.exception("Exception in priming_handler") self.printer.invoke_shutdown("Exception in priming_handler") return self.reactor.NEVER - def _check_flush_lookahead(self, eventtime): + def _handle_step_flush(self, flush_time, step_gen_time): if self.special_queuing_state: - return None + return # In "main" state - flush lookahead if buffer runs low - est_print_time = self.mcu.estimated_print_time(eventtime) - buffer_time = self.print_time - est_print_time - if buffer_time > BUFFER_TIME_LOW: - # Running normally - reschedule check - return eventtime + buffer_time - BUFFER_TIME_LOW - # Under ran low buffer mark - flush lookahead queue - self._flush_lookahead(is_runout=True) - return None + kin_flush_delay = self.motion_queuing.get_kin_flush_delay() + if step_gen_time >= self.print_time - kin_flush_delay: + self._flush_lookahead(is_runout=True) # Movement commands def get_position(self): return list(self.commanded_pos) From 42d149b40fc3a4443133a2369c2a2b0a184660d0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 6 Sep 2025 23:37:48 -0400 Subject: [PATCH 111/411] motion_queuing: Avoid flushing far into the future If a flush_all_steps() request is for a time far in the future, then wait for that time to become close prior to flushing steps. This avoids committing to a step schedule that is far in the future. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 67609eee7..d85f173c4 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -180,8 +180,20 @@ class PrinterMotionQueuing: self.last_step_gen_time = step_gen_time if flush_time >= want_flush_time: break + def _await_flush_time(self, want_flush_time): + while 1: + if self.last_flush_time >= want_flush_time or not self.can_pause: + return + systime = self.reactor.monotonic() + est_print_time = self.mcu.estimated_print_time(systime) + wait = want_flush_time - BGFLUSH_HIGH_TIME - est_print_time + if wait <= 0.: + return + self.reactor.pause(systime + min(1., wait)) def flush_all_steps(self): - self._advance_flush_time(self.need_step_gen_time) + flush_time = self.need_step_gen_time + self._await_flush_time(flush_time) + self._advance_flush_time(flush_time) def calc_step_gen_restart(self, est_print_time): kin_time = max(est_print_time + MIN_KIN_TIME, self.last_step_gen_time) return kin_time + self.kin_flush_delay @@ -254,9 +266,11 @@ class PrinterMotionQueuing: self.do_kick_flush_timer = False self.reactor.update_timer(self.flush_timer, self.reactor.NOW) def drip_update_time(self, start_time, end_time, drip_completion): + self._await_flush_time(start_time) # Disable background flushing from timer self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) self.do_kick_flush_timer = False + self._advance_flush_time(start_time) # Flush in segments until drip_completion signal flush_time = start_time while flush_time < end_time: From 950aa103e46204395dbe035f12356cb5d9d21b74 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 6 Sep 2025 23:52:12 -0400 Subject: [PATCH 112/411] motion_queuing: It is no longer necessary to loop in _advance_flush_time() Now that the host code does not flush far into the future, it is no longer necessary to flush in waves. Integrate _advance_flush_time() into _flush_motion_queues(). Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 41 ++++++++++----------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index d85f173c4..a22598093 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -14,7 +14,6 @@ BGFLUSH_EXTRA_TIME = 0.250 MOVE_HISTORY_EXPIRE = 30. MIN_KIN_TIME = 0.100 -MOVE_BATCH_TIME = 0.500 STEPCOMPRESS_FLUSH_TIME = 0.050 SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c @@ -94,22 +93,28 @@ class PrinterMotionQueuing: fcbs = list(self.flush_callbacks) fcbs.remove(callback) self.flush_callbacks = fcbs - def _flush_motion_queues(self, must_flush_time, max_step_gen_time): + def _advance_flush_time(self, want_flush_time, want_step_gen_time=0.): + flush_time = max(want_flush_time, self.last_flush_time, + want_step_gen_time - STEPCOMPRESS_FLUSH_TIME) + step_gen_time = max(want_step_gen_time, self.last_step_gen_time, + flush_time) # Invoke flush callbacks (if any) for cb in self.flush_callbacks: - cb(must_flush_time, max_step_gen_time) + cb(flush_time, step_gen_time) # Generate stepper movement and transmit for mcu, ss in self.steppersyncs: - clock = max(0, mcu.print_time_to_clock(must_flush_time)) - self.steppersync_start_gen_steps(ss, max_step_gen_time, clock) + clock = max(0, mcu.print_time_to_clock(flush_time)) + self.steppersync_start_gen_steps(ss, step_gen_time, clock) for mcu, ss in self.steppersyncs: - clock = max(0, mcu.print_time_to_clock(must_flush_time)) + clock = max(0, mcu.print_time_to_clock(flush_time)) ret = self.steppersync_finalize_gen_steps(ss, clock) if ret: raise mcu.error("Internal error in MCU '%s' stepcompress" % (mcu.get_name(),)) + self.last_flush_time = flush_time + self.last_step_gen_time = step_gen_time # Determine maximum history to keep - trapq_free_time = max_step_gen_time - self.kin_flush_delay + trapq_free_time = step_gen_time - self.kin_flush_delay clear_history_time = self.clear_history_time if not self.can_pause: clear_history_time = trapq_free_time - MOVE_HISTORY_EXPIRE @@ -158,28 +163,6 @@ class PrinterMotionQueuing: # Flush tracking def _handle_shutdown(self): self.can_pause = False - def _advance_flush_time(self, want_flush_time, want_step_gen_time=0.): - want_flush_time = max(want_flush_time, self.last_flush_time, - want_step_gen_time - STEPCOMPRESS_FLUSH_TIME) - want_step_gen_time = max(want_step_gen_time, want_flush_time) - flush_time = self.last_flush_time - if want_flush_time > flush_time + 10. * MOVE_BATCH_TIME: - # Use closer startup time when coming out of idle state - curtime = self.reactor.monotonic() - est_print_time = self.mcu.estimated_print_time(curtime) - flush_time = max(flush_time, est_print_time) - while 1: - flush_time = min(flush_time + MOVE_BATCH_TIME, want_flush_time) - # Generate steps via itersolve - want_sg_wave = min(flush_time + STEPCOMPRESS_FLUSH_TIME, - want_step_gen_time) - step_gen_time = max(self.last_step_gen_time, want_sg_wave, - flush_time) - self._flush_motion_queues(flush_time, step_gen_time) - self.last_flush_time = flush_time - self.last_step_gen_time = step_gen_time - if flush_time >= want_flush_time: - break def _await_flush_time(self, want_flush_time): while 1: if self.last_flush_time >= want_flush_time or not self.can_pause: From f34103183453093f432b8d5565068cdd5b2c7999 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Sep 2025 13:42:32 -0400 Subject: [PATCH 113/411] motion_queuing: Try harder to use next_batch_time when flushing Use the next_batch_time even if it is slightly past or before the ideal flushing window. This should improve run to run reproducibility of flush timing. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index a22598093..c2b306436 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -187,12 +187,13 @@ class PrinterMotionQueuing: if self.last_step_gen_time < aggr_sg_time: # Actively stepping - want more aggressive flushing want_sg_time = est_print_time + BGFLUSH_SG_HIGH_TIME - want_sg_time = min(want_sg_time, aggr_sg_time) - # Try improving run-to-run reproducibility by batching from last batch_time = BGFLUSH_SG_HIGH_TIME - BGFLUSH_SG_LOW_TIME next_batch_time = self.last_step_gen_time + batch_time - if next_batch_time > est_print_time + BGFLUSH_SG_LOW_TIME: - want_sg_time = min(want_sg_time, next_batch_time) + if (next_batch_time > est_print_time + and next_batch_time < want_sg_time + 0.005): + # Improve run-to-run reproducibility by batching from last + want_sg_time = next_batch_time + want_sg_time = min(want_sg_time, aggr_sg_time) # Flush motion queues (if needed) if want_sg_time > self.last_step_gen_time: self._advance_flush_time(0., want_sg_time) From 4c46b80f38670e0b0547db5aa88ca3579a4ceaf7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Sep 2025 14:19:02 -0400 Subject: [PATCH 114/411] motion_queuing: Further improve step flushing in batches Further encourage flushing steps in batches by delaying flushing if a batch isn't needed yet. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index c2b306436..2371e9a2c 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -189,9 +189,11 @@ class PrinterMotionQueuing: want_sg_time = est_print_time + BGFLUSH_SG_HIGH_TIME batch_time = BGFLUSH_SG_HIGH_TIME - BGFLUSH_SG_LOW_TIME next_batch_time = self.last_step_gen_time + batch_time - if (next_batch_time > est_print_time - and next_batch_time < want_sg_time + 0.005): + if next_batch_time > est_print_time: # Improve run-to-run reproducibility by batching from last + if next_batch_time > want_sg_time: + # Delay flushing until next wakeup + next_batch_time = self.last_step_gen_time want_sg_time = next_batch_time want_sg_time = min(want_sg_time, aggr_sg_time) # Flush motion queues (if needed) From df29a380110651919464a694491f33703f5916b7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Sep 2025 14:30:27 -0400 Subject: [PATCH 115/411] motion_queuing: Further tune flushing in batches Avoid unnecessary reactor wakeups if a batch is close to being ready. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 2371e9a2c..ec279a698 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -191,7 +191,7 @@ class PrinterMotionQueuing: next_batch_time = self.last_step_gen_time + batch_time if next_batch_time > est_print_time: # Improve run-to-run reproducibility by batching from last - if next_batch_time > want_sg_time: + if next_batch_time > want_sg_time + 0.005: # Delay flushing until next wakeup next_batch_time = self.last_step_gen_time want_sg_time = next_batch_time From 636380e4f3cacf89b777f580d69ae672e612f501 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Sep 2025 19:20:00 -0400 Subject: [PATCH 116/411] toolhead: Avoid numerical stability in _handle_step_flush() comparison Don't rely on an exact floating point number match to detect when a forced lookahead flush is needed. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index c4264051d..f54743b0a 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -358,7 +358,7 @@ class ToolHead: return # In "main" state - flush lookahead if buffer runs low kin_flush_delay = self.motion_queuing.get_kin_flush_delay() - if step_gen_time >= self.print_time - kin_flush_delay: + if step_gen_time >= self.print_time - kin_flush_delay - 0.001: self._flush_lookahead(is_runout=True) # Movement commands def get_position(self): From c7365c8c585bb3127280651b95b0e9b87e5bdf77 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 16 Sep 2025 11:28:16 -0400 Subject: [PATCH 117/411] extruder: Recheck the step generation scan windows on sync_to_extruder() Signed-off-by: Kevin O'Connor --- klippy/kinematics/extruder.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 9b1ec2d20..684f4be71 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -50,9 +50,11 @@ class ExtruderStepper: def sync_to_extruder(self, extruder_name): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() + motion_queuing = self.printer.lookup_object('motion_queuing') if not extruder_name: self.stepper.set_trapq(None) self.motion_queue = None + motion_queuing.check_step_generation_scan_windows() return extruder = self.printer.lookup_object(extruder_name, None) if extruder is None or not isinstance(extruder, PrinterExtruder): @@ -61,6 +63,7 @@ class ExtruderStepper: self.stepper.set_position([extruder.last_position, 0., 0.]) self.stepper.set_trapq(extruder.get_trapq()) self.motion_queue = extruder_name + motion_queuing.check_step_generation_scan_windows() def _set_pressure_advance(self, pressure_advance, smooth_time): old_smooth_time = self.pressure_advance_smooth_time if not self.pressure_advance: From 8db5d254e077e10583cfaff0d9e70e08263383e5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 16 Sep 2025 11:35:11 -0400 Subject: [PATCH 118/411] docs: Update Code_Overview.md with recent motion_queuing changes Update the documentation to reflect the new threads and new movement code flow. Signed-off-by: Kevin O'Connor --- docs/Code_Overview.md | 84 +++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index fd0e90a38..6b5df5595 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -102,20 +102,35 @@ some functionality in C code. Initial execution starts in **klippy/klippy.py**. This reads the command-line arguments, opens the printer config file, instantiates the main printer objects, and starts the serial connection. The main -execution of G-code commands is in the process_commands() method in +execution of G-code commands is in the _process_commands() method in **klippy/gcode.py**. This code translates the G-code commands into printer object calls, which frequently translate the actions to commands to be executed on the micro-controller (as declared via the DECL_COMMAND macro in the micro-controller code). -There are four threads in the Klippy host code. The main thread -handles incoming gcode commands. A second thread (which resides -entirely in the **klippy/chelper/serialqueue.c** C code) handles -low-level IO with the serial port. The third thread is used to process -response messages from the micro-controller in the Python code (see -**klippy/serialhdl.py**). The fourth thread writes debug messages to -the log (see **klippy/queuelogger.py**) so that the other threads -never block on log writes. +There are several threads in the Klipper host code: +* There is a Python "main thread" that handles incoming G-Code + commands and is the starting point for most actions. This thread + runs the [reactor](https://en.wikipedia.org/wiki/Reactor_pattern) + (**klippy/reactor.py**) and most high-level actions originate from + IO and timer event callbacks from that reactor. +* A thread for writing messages to the log so that the other threads + do not block on log writes. This thread resides entirely in the + **klippy/queuelogger.py** code and its operation is generally not + exposed to the main Python thread. +* A thread per micro-controller that performs the low-level reading + and writing of messages to that micro-controller. It resides in the + **klippy/chelper/serialqueue.c** C code and its operation is + generally not exposed to the Python code. +* A thread per micro-controller for processing messages received from + that micro-controller in the Python code. This thread is created in + **klippy/serialhdl.py**. Care must be taken in Python callbacks + invoked from this thread as this thread may directly interact with + the main Python thread. +* A thread per stepper motor that calculates the timing of stepper + motor step pulses and compresses those times. This thread resides in + the **klippy/chelper/stepcompress.c** C code and its operation is + generally not exposed to the Python code. ## Code flow of a move command @@ -138,7 +153,7 @@ provides further information on the mechanics of moves. the timing of printing actions. The main codepath for a move is: `ToolHead.move() -> LookAheadQueue.add_move() -> LookAheadQueue.flush() -> Move.set_junction() -> - ToolHead._process_moves()`. + ToolHead._process_moves() -> trapq_append()`. * ToolHead.move() creates a Move() object with the parameters of the move (in cartesian space and in units of seconds and millimeters). * The kinematics class is given the opportunity to audit each move @@ -163,34 +178,41 @@ provides further information on the mechanics of moves. during acceleration/cruising/deceleration. All the information is stored in the Move() class and is in cartesian space in units of millimeters and seconds. - -* Klipper uses an - [iterative solver](https://en.wikipedia.org/wiki/Root-finding_algorithm) - to generate the step times for each stepper. For efficiency reasons, - the stepper pulse times are generated in C code. The moves are first - placed on a "trapezoid motion queue": `ToolHead._process_moves() -> - trapq_append()` (in klippy/chelper/trapq.c). The step times are then - generated: `ToolHead._process_moves() -> - ToolHead._advance_move_time() -> ToolHead._advance_flush_time() -> - MCU_Stepper.generate_steps() -> itersolve_generate_steps() -> - itersolve_gen_steps_range()` (in klippy/chelper/itersolve.c). The - goal of the iterative solver is to find step times given a function - that calculates a stepper position from a time. This is done by - repeatedly "guessing" various times until the stepper position - formula returns the desired position of the next step on the - stepper. The feedback produced from each guess is used to improve - future guesses so that the process rapidly converges to the desired - time. The kinematic stepper position formulas are located in the - klippy/chelper/ directory (eg, kin_cart.c, kin_corexy.c, - kin_delta.c, kin_extruder.c). + * The moves are then placed on a "trapezoid motion queue" via + trapq_append() (in klippy/chelper/trapq.c). The trapq stores all the + information in the Move() class in a C struct accessible to the host + C code. * Note that the extruder is handled in its own kinematic class: - `ToolHead._process_moves() -> PrinterExtruder.move()`. Since + `ToolHead._process_moves() -> PrinterExtruder.process_move()`. Since the Move() class specifies the exact movement time and since step pulses are sent to the micro-controller with specific timing, stepper movements produced by the extruder class will be in sync with head movement even though the code is kept separate. +* Klipper uses an + [iterative solver](https://en.wikipedia.org/wiki/Root-finding_algorithm) + to generate the step times for each stepper. For efficiency reasons, + the stepper pulse times are generated in C code in a thread per + stepper motor. The threads are notified of new activity by the + motion_queuing module (klippy/extras/motion_queuing.py): + `PrinterMotionQueuing._flush_handler() -> + PrinterMotionQueuing._advance_move_time() -> + steppersync_start_gen_steps() -> + stepcompress_start_gen_steps()`. The step times are then generated + from that thread (klippy/chelper/stepcompress.c): + `sc_background_thread() -> stepcompress_generate_steps() -> + itersolve_generate_steps() -> itersolve_gen_steps_range()` (in + klippy/chelper/itersolve.c). The goal of the iterative solver is to + find step times given a function that calculates a stepper position + from a time. This is done by repeatedly "guessing" various times + until the stepper position formula returns the desired position of + the next step on the stepper. The feedback produced from each guess + is used to improve future guesses so that the process rapidly + converges to the desired time. The kinematic stepper position + formulas are located in the klippy/chelper/ directory (eg, + kin_cart.c, kin_corexy.c, kin_delta.c, kin_extruder.c). + * After the iterative solver calculates the step times they are added to an array: `itersolve_gen_steps_range() -> stepcompress_append()` (in klippy/chelper/stepcompress.c). The array (struct From e8e88415ea097dc4e3873cd4ed85d26c07939784 Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Tue, 2 Sep 2025 19:08:00 +0800 Subject: [PATCH 119/411] stm32: Clean up SPI code on stm32h7_spi.c Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- src/stm32/stm32h7_spi.c | 56 +++++++++++------------------------------ 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/src/stm32/stm32h7_spi.c b/src/stm32/stm32h7_spi.c index d1e514e7a..1d8c2afdf 100644 --- a/src/stm32/stm32h7_spi.c +++ b/src/stm32/stm32h7_spi.c @@ -16,67 +16,41 @@ struct spi_info { uint8_t miso_pin, mosi_pin, sck_pin, function; }; -DECL_ENUMERATION("spi_bus", "spi2", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi2", 0); DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); - -DECL_ENUMERATION("spi_bus", "spi1", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi1", 1); DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); -DECL_ENUMERATION("spi_bus", "spi1a", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi1a", 2); DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); - -#if !CONFIG_MACH_STM32F1 -DECL_ENUMERATION("spi_bus", "spi2a", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi2a", 3); DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); -#endif - -#ifdef SPI3 -DECL_ENUMERATION("spi_bus", "spi3a", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi3a", 4); DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10"); -#endif - -#ifdef SPI4 -DECL_ENUMERATION("spi_bus", "spi4", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi4", 5); DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12"); -#endif - +DECL_ENUMERATION("spi_bus", "spi5", 6); +DECL_CONSTANT_STR("BUS_PINS_spi5", "PF8,PF9,PF7"); +DECL_ENUMERATION("spi_bus", "spi5a", 7); +DECL_CONSTANT_STR("BUS_PINS_spi5a", "PH7,PF11,PH6"); +DECL_ENUMERATION("spi_bus", "spi6", 8); +DECL_CONSTANT_STR("BUS_PINS_spi6", "PG12,PG14,PG13"); #ifdef GPIOI -DECL_ENUMERATION("spi_bus", "spi2b", __COUNTER__); +DECL_ENUMERATION("spi_bus", "spi2b", 9); DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); #endif -#ifdef SPI5 -DECL_ENUMERATION("spi_bus", "spi5", __COUNTER__); -DECL_CONSTANT_STR("BUS_PINS_spi5", "PF8,PF9,PF7"); -DECL_ENUMERATION("spi_bus", "spi5a", __COUNTER__); -DECL_CONSTANT_STR("BUS_PINS_spi5a", "PH7,PF11,PH6"); -#endif - -#ifdef SPI6 -DECL_ENUMERATION("spi_bus", "spi6", __COUNTER__); -DECL_CONSTANT_STR("BUS_PINS_spi6", "PG12,PG14,PG13"); -#endif - - static const struct spi_info spi_bus[] = { { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION(5) }, { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION(5) }, { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION(5) }, -#if !CONFIG_MACH_STM32F1 { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION(5) }, -#endif -#ifdef SPI3 { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION(6) }, -#endif -#ifdef SPI4 { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION(5) }, -#endif - { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION(5) }, -#ifdef SPI5 { SPI5, GPIO('F', 8), GPIO('F', 9), GPIO('F', 7), GPIO_FUNCTION(5) }, { SPI5, GPIO('H', 7), GPIO('F', 11), GPIO('H', 6), GPIO_FUNCTION(5) }, -#endif -#ifdef SPI6 { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), GPIO_FUNCTION(5)}, +#ifdef GPIOI + { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION(5) }, #endif }; From 61252819e3aaf9fb5995242ce132860f83578c42 Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Wed, 10 Sep 2025 15:39:51 +0800 Subject: [PATCH 120/411] stm32: Clean up SPI code on spi.c Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- src/stm32/spi.c | 324 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 266 insertions(+), 58 deletions(-) diff --git a/src/stm32/spi.c b/src/stm32/spi.c index f64326813..0e8034a9c 100644 --- a/src/stm32/spi.c +++ b/src/stm32/spi.c @@ -15,79 +15,287 @@ struct spi_info { uint8_t miso_pin, mosi_pin, sck_pin, miso_af, mosi_af, sck_af; }; -DECL_ENUMERATION("spi_bus", "spi2", 0); -DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); -DECL_ENUMERATION("spi_bus", "spi1", 1); -DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); -DECL_ENUMERATION("spi_bus", "spi1a", 2); -DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); - -#if CONFIG_MACH_STM32G4 - DECL_ENUMERATION("spi_bus", "spi2_PA10_PA11_PF1", 3); - DECL_CONSTANT_STR("BUS_PINS_spi2_PA10_PA11_PF1", "PA10,PA11,PF1"); -#elif !CONFIG_MACH_STM32F1 - DECL_ENUMERATION("spi_bus", "spi2a", 3); - DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); -#endif -#ifdef SPI3 - DECL_ENUMERATION("spi_bus", "spi3", 4); - DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); - #if CONFIG_MACH_STM32F4 || CONFIG_MACH_STM32G4 +#if CONFIG_MACH_STM32F0 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); +#elif CONFIG_MACH_STM32F1 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 3); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3", 3); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); +#elif CONFIG_MACH_STM32F2 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); +#elif CONFIG_MACH_STM32F4 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 5); + DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10"); + #ifdef GPIOI + DECL_ENUMERATION("spi_bus", "spi2_PI2_PI3_PI1", 6); + DECL_CONSTANT_STR("BUS_PINS_spi2_PI2_PI3_PI1", "PI2,PI3,PI1"); + #define SPI4_INDEX (1 + 6) + #else + #define SPI4_INDEX (0 + 6) + #endif + #ifdef SPI4 + DECL_ENUMERATION("spi_bus", "spi4_PE13_PE14_PE12", SPI4_INDEX); + DECL_CONSTANT_STR("BUS_PINS_spi4_PE13_PE14_PE12", "PE13,PE14,PE12"); + #define SPI6_INDEX (1 + SPI4_INDEX) + #else + #define SPI6_INDEX (0 + SPI4_INDEX) + #endif + #ifdef SPI6 + DECL_ENUMERATION("spi_bus", "spi6_PG12_PG14_PG13", SPI6_INDEX); + DECL_CONSTANT_STR("BUS_PINS_spi6_PG12_PG14_PG13", "PG12,PG14,PG13"); + #endif + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3a", 5); + DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10"); + #ifdef GPIOI + DECL_ENUMERATION("spi_bus", "spi2b", 6); + DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); + #endif + #ifdef SPI4 + DECL_ENUMERATION("spi_bus", "spi4", SPI4_INDEX); + DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12"); + #endif +#elif CONFIG_MACH_STM32F7 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); +#elif CONFIG_MACH_STM32G0 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + DECL_ENUMERATION("spi_bus", "spi2_PB2_PB11_PB10", 4); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB2_PB11_PB10", "PB2,PB11,PB10"); + #ifdef SPI3 + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 5); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + #endif + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); + #ifdef SPI3 + DECL_ENUMERATION("spi_bus", "spi3", 5); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); + #endif +#elif CONFIG_MACH_STM32G4 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PA10_PA11_PF1", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PA10_PA11_PF1", "PA10,PA11,PF1"); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 5); + DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10"); + #ifdef SPI4 + DECL_ENUMERATION("spi_bus", "spi4_PE13_PE14_PE12", 6); + DECL_CONSTANT_STR("BUS_PINS_spi4_PE13_PE14_PE12", "PE13,PE14,PE12"); + #endif + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3", 4); + DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); DECL_ENUMERATION("spi_bus", "spi3a", 5); DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10"); #ifdef SPI4 - DECL_ENUMERATION("spi_bus", "spi4", 6); - DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12"); - #elif defined(GPIOI) - DECL_ENUMERATION("spi_bus", "spi2b", 6); - DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); + DECL_ENUMERATION("spi_bus", "spi4", 6); + DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12"); #endif - #ifdef SPI6 - DECL_ENUMERATION("spi_bus", "spi6_PG12_PG14_PG13", 7); - DECL_CONSTANT_STR("BUS_PINS_spi6_PG12_PG14_PG13", "PG12,PG14,PG13"); - #endif - #endif - #if CONFIG_MACH_STM32G0B1 - DECL_ENUMERATION("spi_bus", "spi2_PB2_PB11_PB10", 5); - DECL_CONSTANT_STR("BUS_PINS_spi2_PB2_PB11_PB10", "PB2,PB11,PB10"); -#endif +#elif CONFIG_MACH_STM32L4 + DECL_ENUMERATION("spi_bus", "spi2_PB14_PB15_PB13", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB14_PB15_PB13", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1_PA6_PA7_PA5", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1_PA6_PA7_PA5", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1_PB4_PB5_PB3", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2_PC2_PC3_PB10", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); + // Deprecated "spi1" style mappings + DECL_ENUMERATION("spi_bus", "spi2", 0); + DECL_CONSTANT_STR("BUS_PINS_spi2", "PB14,PB15,PB13"); + DECL_ENUMERATION("spi_bus", "spi1", 1); + DECL_CONSTANT_STR("BUS_PINS_spi1", "PA6,PA7,PA5"); + DECL_ENUMERATION("spi_bus", "spi1a", 2); + DECL_CONSTANT_STR("BUS_PINS_spi1a", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi2a", 3); + DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); #endif #define GPIO_FUNCTION_ALL(fn) GPIO_FUNCTION(fn), \ GPIO_FUNCTION(fn), GPIO_FUNCTION(fn) -#if CONFIG_MACH_STM32F0 || CONFIG_MACH_STM32G0 - #define SPI_FUNCTION_ALL GPIO_FUNCTION_ALL(0) -#else - #define SPI_FUNCTION_ALL GPIO_FUNCTION_ALL(5) -#endif - static const struct spi_info spi_bus[] = { - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION_ALL }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION_ALL }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION_ALL }, -#if CONFIG_MACH_STM32G4 - { SPI2, GPIO('A', 10), GPIO('A', 11), GPIO('F', 1), SPI_FUNCTION_ALL }, -#else - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION_ALL }, -#endif -#ifdef SPI3 +#if CONFIG_MACH_STM32F0 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), + GPIO_FUNCTION(1), GPIO_FUNCTION(1), GPIO_FUNCTION(5) }, +#elif CONFIG_MACH_STM32F1 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, +#elif CONFIG_MACH_STM32F2 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, +#elif CONFIG_MACH_STM32F4 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, + { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION_ALL(6) }, + #ifdef GPIOI + { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION_ALL(5) }, + #endif + #ifdef SPI4 + { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION_ALL(5) }, + #endif + #ifdef SPI6 + { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), GPIO_FUNCTION_ALL(5)}, + #endif +#elif CONFIG_MACH_STM32F7 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, +#elif CONFIG_MACH_STM32G0 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), + GPIO_FUNCTION(1), GPIO_FUNCTION(1), GPIO_FUNCTION(5) }, + { SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), + GPIO_FUNCTION(1), GPIO_FUNCTION(0), GPIO_FUNCTION(5) }, + #ifdef SPI3 + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(9) }, + #endif +#elif CONFIG_MACH_STM32G4 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('A', 10), GPIO('A', 11), GPIO('F', 1), GPIO_FUNCTION_ALL(5) }, { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, - #if CONFIG_MACH_STM32F4 || CONFIG_MACH_STM32G4 { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION_ALL(6) }, #ifdef SPI4 { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION_ALL(5) }, - #elif defined(GPIOI) - { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION_ALL(5) }, #endif - #ifdef SPI6 - { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), SPI_FUNCTION_ALL} - #endif - #endif - #if CONFIG_MACH_STM32G0B1 - { SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), - GPIO_FUNCTION(1), GPIO_FUNCTION(0), GPIO_FUNCTION(5) }, - #endif +#elif CONFIG_MACH_STM32L4 + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, #endif }; From 1be6c0fce0a53b314f43a934d66cf64cf95e6176 Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Wed, 10 Sep 2025 18:20:06 +0800 Subject: [PATCH 121/411] stm32: change `GPIO_FUNCTION_ALL` to `SPI_FUNCTION` Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- src/stm32/spi.c | 115 +++++++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/src/stm32/spi.c b/src/stm32/spi.c index 0e8034a9c..5e3e428be 100644 --- a/src/stm32/spi.c +++ b/src/stm32/spi.c @@ -227,75 +227,72 @@ struct spi_info { DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); #endif -#define GPIO_FUNCTION_ALL(fn) GPIO_FUNCTION(fn), \ - GPIO_FUNCTION(fn), GPIO_FUNCTION(fn) +#define SPI_FUNCTION(miso, mosi, sck) GPIO_FUNCTION(miso), \ + GPIO_FUNCTION(mosi), GPIO_FUNCTION(sck) static const struct spi_info spi_bus[] = { #if CONFIG_MACH_STM32F0 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), - GPIO_FUNCTION(1), GPIO_FUNCTION(1), GPIO_FUNCTION(5) }, + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(1, 1, 5) }, #elif CONFIG_MACH_STM32F1 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) }, #elif CONFIG_MACH_STM32F2 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) }, #elif CONFIG_MACH_STM32F4 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, - { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION_ALL(6) }, - #ifdef GPIOI - { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION_ALL(5) }, - #endif - #ifdef SPI4 - { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION_ALL(5) }, - #endif - #ifdef SPI6 - { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), GPIO_FUNCTION_ALL(5)}, - #endif + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) }, + { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(6, 6, 6) }, + #ifdef GPIOI + { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), SPI_FUNCTION(5, 5, 5) }, + #endif + #ifdef SPI4 + { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), SPI_FUNCTION(5, 5, 5) }, + #endif + #ifdef SPI6 + { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), SPI_FUNCTION(5, 5, 5)}, + #endif #elif CONFIG_MACH_STM32F7 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) }, #elif CONFIG_MACH_STM32G0 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(0) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(0) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), - GPIO_FUNCTION(1), GPIO_FUNCTION(1), GPIO_FUNCTION(5) }, - { SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), - GPIO_FUNCTION(1), GPIO_FUNCTION(0), GPIO_FUNCTION(5) }, - #ifdef SPI3 - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(9) }, - #endif + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(0, 0, 0) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(1, 1, 5) }, + { SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), SPI_FUNCTION(1, 0, 5) }, + #ifdef SPI3 + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(9, 9, 9) }, + #endif #elif CONFIG_MACH_STM32G4 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI2, GPIO('A', 10), GPIO('A', 11), GPIO('F', 1), GPIO_FUNCTION_ALL(5) }, - { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(6) }, - { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), GPIO_FUNCTION_ALL(6) }, - #ifdef SPI4 - { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), GPIO_FUNCTION_ALL(5) }, - #endif + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI2, GPIO('A', 10), GPIO('A', 11), GPIO('F', 1), SPI_FUNCTION(5, 5, 5) }, + { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) }, + { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(6, 6, 6) }, + #ifdef SPI4 + { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), SPI_FUNCTION(5, 5, 5) }, + #endif #elif CONFIG_MACH_STM32L4 - { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), GPIO_FUNCTION_ALL(5) }, - { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), GPIO_FUNCTION_ALL(5) }, - { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), GPIO_FUNCTION_ALL(5) }, + { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('A', 6), GPIO('A', 7), GPIO('A', 5), SPI_FUNCTION(5, 5, 5) }, + { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(5, 5, 5) }, + { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) }, #endif }; From 5da026a337a681c29c66e47ca6d30dc52e9acf58 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Wed, 14 Feb 2024 01:55:21 +0100 Subject: [PATCH 122/411] input_shaper: Updated definitions of *EI input shapers Signed-off-by: Dmitry Butyugin --- docs/Config_Changes.md | 4 ++ klippy/extras/shaper_defs.py | 82 +++++++++++++++++++++--------------- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 838b23273..296343b12 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,10 @@ All dates in this document are approximate. ## Changes +20250916: The definitions of EI, 2HUMP_EI, and 3HUMP_EI input shapers +were updated. For best performance it is recommended to recalibrate +input shapers, especially if some of these shapers are currently used. + 20250811: Support for the `max_accel_to_decel` parameter in the `[printer]` config section has been removed and support for the `ACCEL_TO_DECEL` parameter in the `SET_VELOCITY_LIMIT` command has diff --git a/klippy/extras/shaper_defs.py b/klippy/extras/shaper_defs.py index 611fed16a..8a0d93962 100644 --- a/klippy/extras/shaper_defs.py +++ b/klippy/extras/shaper_defs.py @@ -46,50 +46,62 @@ def get_mzv_shaper(shaper_freq, damping_ratio): def get_ei_shaper(shaper_freq, damping_ratio): v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance df = math.sqrt(1. - damping_ratio**2) - K = math.exp(-damping_ratio * math.pi / df) t_d = 1. / (shaper_freq * df) + dr = damping_ratio - a1 = .25 * (1. + v_tol) - a2 = .5 * (1. - v_tol) * K - a3 = a1 * K * K + a1 = (0.24968 + 0.24961 * v_tol) + (( 0.80008 + 1.23328 * v_tol) + + ( 0.49599 + 3.17316 * v_tol) * dr) * dr + a3 = (0.25149 + 0.21474 * v_tol) + ((-0.83249 + 1.41498 * v_tol) + + ( 0.85181 - 4.90094 * v_tol) * dr) * dr + a2 = 1. - a1 - a3 + + t2 = 0.4999 + ((( 0.46159 + 8.57843 * v_tol) * v_tol) + + (((4.26169 - 108.644 * v_tol) * v_tol) + + ((1.75601 + 336.989 * v_tol) * v_tol) * dr) * dr) * dr A = [a1, a2, a3] - T = [0., .5*t_d, t_d] + T = [0., t2 * t_d, t_d] + return (A, T) + +def _get_shaper_from_expansion_coeffs(shaper_freq, damping_ratio, t, a): + tau = 1. / shaper_freq + T = [] + A = [] + n = len(a) + k = len(a[0]) + for i in range(n): + u = t[i][k-1] + v = a[i][k-1] + for j in range(k-1): + u = u * damping_ratio + t[i][k-j-2] + v = v * damping_ratio + a[i][k-j-2] + T.append(u * tau) + A.append(v) return (A, T) def get_2hump_ei_shaper(shaper_freq, damping_ratio): - v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance - df = math.sqrt(1. - damping_ratio**2) - K = math.exp(-damping_ratio * math.pi / df) - t_d = 1. / (shaper_freq * df) - - V2 = v_tol**2 - X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.) - a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X) - a2 = (.5 - a1) * K - a3 = a2 * K - a4 = a1 * K * K * K - - A = [a1, a2, a3, a4] - T = [0., .5*t_d, t_d, 1.5*t_d] - return (A, T) + t = [[0., 0., 0., 0.], + [0.49890, 0.16270, -0.54262, 6.16180], + [0.99748, 0.18382, -1.58270, 8.17120], + [1.49920, -0.09297, -0.28338, 1.85710]] + a = [[0.16054, 0.76699, 2.26560, -1.22750], + [0.33911, 0.45081, -2.58080, 1.73650], + [0.34089, -0.61533, -0.68765, 0.42261], + [0.15997, -0.60246, 1.00280, -0.93145]] + return _get_shaper_from_expansion_coeffs(shaper_freq, damping_ratio, t, a) def get_3hump_ei_shaper(shaper_freq, damping_ratio): - v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance - df = math.sqrt(1. - damping_ratio**2) - K = math.exp(-damping_ratio * math.pi / df) - t_d = 1. / (shaper_freq * df) - - K2 = K*K - a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol)) - a2 = 0.25 * (1. - v_tol) * K - a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2 - a4 = a2 * K2 - a5 = a1 * K2 * K2 - - A = [a1, a2, a3, a4, a5] - T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d] - return (A, T) + t = [[0., 0., 0., 0.], + [0.49974, 0.23834, 0.44559, 12.4720], + [0.99849, 0.29808, -2.36460, 23.3990], + [1.49870, 0.10306, -2.01390, 17.0320], + [1.99960, -0.28231, 0.61536, 5.40450]] + a = [[0.11275, 0.76632, 3.29160 -1.44380], + [0.23698, 0.61164, -2.57850, 4.85220], + [0.30008, -0.19062, -2.14560, 0.13744], + [0.23775, -0.73297, 0.46885, -2.08650], + [0.11244, -0.45439, 0.96382, -1.46000]] + return _get_shaper_from_expansion_coeffs(shaper_freq, damping_ratio, t, a) # min_freq for each shaper is chosen to have projected max_accel ~= 1500 INPUT_SHAPERS = [ From 599dcd176c1eed673b418d95501f4aba19e09e9c Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sun, 21 Sep 2025 01:50:54 +0200 Subject: [PATCH 123/411] resonance_tester: Correctly handle incorrect accelerometer chip names Signed-off-by: Dmitry Butyugin --- klippy/extras/resonance_tester.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index c6171dc42..3e49b7b15 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -223,9 +223,13 @@ class ResonanceTester: self.printer.register_event_handler("klippy:connect", self.connect) def connect(self): - self.accel_chips = [ - (chip_axis, self.printer.lookup_object(chip_name)) - for chip_axis, chip_name in self.accel_chip_names] + self.accel_chips = [] + for chip_axis, chip_name in self.accel_chip_names: + chip = self.printer.lookup_object(chip_name) + if not hasattr(chip, 'start_internal_client'): + raise self.printer.config_error( + "'%s' is not an accelerometer" % chip_name) + self.accel_chips.append((chip_axis, chip)) def _run_test(self, gcmd, axes, helper, raw_name_suffix=None, accel_chips=None, test_point=None): @@ -288,7 +292,13 @@ class ResonanceTester: def _parse_chips(self, accel_chips): parsed_chips = [] for chip_name in accel_chips.split(','): - chip = self.printer.lookup_object(chip_name.strip()) + chip = self.printer.lookup_object(chip_name.strip(), None) + if chip is None: + raise self.printer.command_error("Name '%s' is not valid for" + " CHIPS parameter" % chip_name) + if not hasattr(chip, 'start_internal_client'): + raise self.printer.command_error( + "'%s' is not an accelerometer" % chip_name) parsed_chips.append(chip) return parsed_chips def _get_max_calibration_freq(self): From 1c76ed1dc90fdd346e783e6c24c4aafefdbbd5a8 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sun, 21 Sep 2025 17:10:26 +0200 Subject: [PATCH 124/411] resonance_tester: Gracefully handle zero accelerations during the test Signed-off-by: Dmitry Butyugin --- klippy/extras/resonance_tester.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index 3e49b7b15..4f4ef7bb4 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -145,19 +145,27 @@ class ResonanceTestExecutor: gcmd.respond_info("Disabled [input_shaper] for resonance testing") else: input_shaper = None - last_v = last_t = last_accel = last_freq = 0. + last_v = last_t = last_freq = 0. for next_t, accel, freq in test_seq: t_seg = next_t - last_t - toolhead.set_max_velocities(None, abs(accel), None, None) - v = last_v + accel * t_seg - abs_v = abs(v) - if abs_v < 0.000001: - v = abs_v = 0. abs_last_v = abs(last_v) - v2 = v * v last_v2 = last_v * last_v - half_inv_accel = .5 / accel - d = (v2 - last_v2) * half_inv_accel + if abs(accel) < 0.000001: + v, abs_v = last_v, abs_last_v + if abs_v < 0.000001: + toolhead.dwell(t_seg) + last_t, last_freq = next_t, freq + continue + half_inv_accel = 0. + d = v * t_seg + else: + toolhead.set_max_velocities(None, abs(accel), None, None) + v = last_v + accel * t_seg + abs_v = abs(v) + if abs_v < 0.000001: + v = abs_v = 0. + half_inv_accel = .5 / accel + d = (v * v - last_v2) * half_inv_accel dX, dY = axis.get_point(d) nX = X + dX nY = Y + dY @@ -176,7 +184,6 @@ class ResonanceTestExecutor: X, Y = nX, nY last_t = next_t last_v = v - last_accel = accel last_freq = freq if last_v: d_decel = -.5 * last_v2 / old_max_accel From 13cfdf57114f2915724d85c8182360601e0622b5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 14:58:36 -0400 Subject: [PATCH 125/411] motion_queuing: Reorganize code into sections Only code movement - no code changes. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 93 +++++++++++++++++---------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index ec279a698..4679e801d 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -24,21 +24,23 @@ class PrinterMotionQueuing: def __init__(self, config): self.printer = printer = config.get_printer() self.reactor = printer.get_reactor() - # Low level C allocations + # C trapq tracking self.trapqs = [] - self.stepcompress = [] - self.steppersyncs = [] - # Low-level C flushing calls ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves + # C steppersync tracking + self.stepcompress = [] + self.steppersyncs = [] self.steppersync_start_gen_steps = ffi_lib.steppersync_start_gen_steps self.steppersync_finalize_gen_steps = \ ffi_lib.steppersync_finalize_gen_steps self.steppersync_history_expire = ffi_lib.steppersync_history_expire - # Flush notification callbacks - self.flush_callbacks = [] # History expiration self.clear_history_time = 0. + # Flush notification callbacks + self.flush_callbacks = [] + # Kinematic step generation scan window time tracking + self.kin_flush_delay = SDS_CHECK_TIME # MCU tracking self.all_mcus = [m for n, m in printer.lookup_objects(module='mcu')] self.mcu = self.all_mcus[0] @@ -53,15 +55,21 @@ class PrinterMotionQueuing: self.do_kick_flush_timer = True self.last_flush_time = self.last_step_gen_time = 0. self.need_flush_time = self.need_step_gen_time = 0. - # Kinematic step generation scan window time tracking - self.kin_flush_delay = SDS_CHECK_TIME # Register handlers printer.register_event_handler("klippy:shutdown", self._handle_shutdown) + # C trapq tracking def allocate_trapq(self): ffi_main, ffi_lib = chelper.get_ffi() trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) self.trapqs.append(trapq) return trapq + def wipe_trapq(self, trapq): + # Expire any remaining movement in the trapq (force to history list) + self.trapq_finalize_moves(trapq, self.reactor.NEVER, 0.) + def lookup_trapq_append(self): + ffi_main, ffi_lib = chelper.get_ffi() + return ffi_lib.trapq_append + # C steppersync tracking def allocate_stepcompress(self, mcu, oid, name): name = name.encode("utf-8")[:15] ffi_main, ffi_lib = chelper.get_ffi() @@ -83,6 +91,18 @@ class PrinterMotionQueuing: self.steppersyncs.append((mcu, ss)) mcu_freq = float(mcu.seconds_to_clock(1.)) ffi_lib.steppersync_set_time(ss, 0., mcu_freq) + def stats(self, eventtime): + # Globally calibrate mcu clocks (and step generation clocks) + sync_time = self.last_step_gen_time + ffi_main, ffi_lib = chelper.get_ffi() + for mcu, ss in self.steppersyncs: + offset, freq = mcu.calibrate_clock(sync_time, eventtime) + ffi_lib.steppersync_set_time(ss, offset, freq) + # Calculate history expiration + est_print_time = self.mcu.estimated_print_time(eventtime) + self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE + return False, "" + # Flush notification callbacks def register_flush_callback(self, callback, can_add_trapq=False): if can_add_trapq: self.flush_callbacks = [callback] + self.flush_callbacks @@ -93,6 +113,26 @@ class PrinterMotionQueuing: fcbs = list(self.flush_callbacks) fcbs.remove(callback) self.flush_callbacks = fcbs + # Kinematic step generation scan window time tracking + def get_kin_flush_delay(self): + return self.kin_flush_delay + def check_step_generation_scan_windows(self): + ffi_main, ffi_lib = chelper.get_ffi() + kin_flush_delay = SDS_CHECK_TIME + for mcu, sc in self.stepcompress: + sk = ffi_lib.stepcompress_get_stepper_kinematics(sc) + if sk == ffi_main.NULL: + continue + trapq = ffi_lib.itersolve_get_trapq(sk) + if trapq == ffi_main.NULL: + continue + pre_active = ffi_lib.itersolve_get_gen_steps_pre_active(sk) + post_active = ffi_lib.itersolve_get_gen_steps_post_active(sk) + kin_flush_delay = max(kin_flush_delay, pre_active, post_active) + self.kin_flush_delay = kin_flush_delay + # Flush tracking + def _handle_shutdown(self): + self.can_pause = False def _advance_flush_time(self, want_flush_time, want_step_gen_time=0.): flush_time = max(want_flush_time, self.last_flush_time, want_step_gen_time - STEPCOMPRESS_FLUSH_TIME) @@ -126,43 +166,6 @@ class PrinterMotionQueuing: for mcu, ss in self.steppersyncs: clock = max(0, mcu.print_time_to_clock(clear_history_time)) self.steppersync_history_expire(ss, clock) - def wipe_trapq(self, trapq): - # Expire any remaining movement in the trapq (force to history list) - self.trapq_finalize_moves(trapq, self.reactor.NEVER, 0.) - def lookup_trapq_append(self): - ffi_main, ffi_lib = chelper.get_ffi() - return ffi_lib.trapq_append - def stats(self, eventtime): - # Globally calibrate mcu clocks (and step generation clocks) - sync_time = self.last_step_gen_time - ffi_main, ffi_lib = chelper.get_ffi() - for mcu, ss in self.steppersyncs: - offset, freq = mcu.calibrate_clock(sync_time, eventtime) - ffi_lib.steppersync_set_time(ss, offset, freq) - # Calculate history expiration - est_print_time = self.mcu.estimated_print_time(eventtime) - self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE - return False, "" - # Kinematic step generation scan window time tracking - def get_kin_flush_delay(self): - return self.kin_flush_delay - def check_step_generation_scan_windows(self): - ffi_main, ffi_lib = chelper.get_ffi() - kin_flush_delay = SDS_CHECK_TIME - for mcu, sc in self.stepcompress: - sk = ffi_lib.stepcompress_get_stepper_kinematics(sc) - if sk == ffi_main.NULL: - continue - trapq = ffi_lib.itersolve_get_trapq(sk) - if trapq == ffi_main.NULL: - continue - pre_active = ffi_lib.itersolve_get_gen_steps_pre_active(sk) - post_active = ffi_lib.itersolve_get_gen_steps_post_active(sk) - kin_flush_delay = max(kin_flush_delay, pre_active, post_active) - self.kin_flush_delay = kin_flush_delay - # Flush tracking - def _handle_shutdown(self): - self.can_pause = False def _await_flush_time(self, want_flush_time): while 1: if self.last_flush_time >= want_flush_time or not self.can_pause: From a66f5cec520e2959d22be3a870e63e23f5032144 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 15:11:20 -0400 Subject: [PATCH 126/411] msgblock: Add new clock_fill() function Add a new function for filling the fields of 'struct clock_estimate'. Signed-off-by: Kevin O'Connor --- klippy/chelper/msgblock.c | 11 +++++++++++ klippy/chelper/msgblock.h | 2 ++ klippy/chelper/serialqueue.c | 5 +---- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/klippy/chelper/msgblock.c b/klippy/chelper/msgblock.c index e6cb298b4..a7385c893 100644 --- a/klippy/chelper/msgblock.c +++ b/klippy/chelper/msgblock.c @@ -207,3 +207,14 @@ clock_from_time(struct clock_estimate *ce, double time) { return (int64_t)((time - ce->conv_time)*ce->est_freq + .5) + ce->conv_clock; } + +// Fill the fields of a 'struct clock_estimate' +void +clock_fill(struct clock_estimate *ce, double est_freq, double conv_time + , uint64_t conv_clock, uint64_t last_clock) +{ + ce->est_freq = est_freq; + ce->conv_time = conv_time; + ce->conv_clock = conv_clock; + ce->last_clock = last_clock; +} diff --git a/klippy/chelper/msgblock.h b/klippy/chelper/msgblock.h index 43ee95325..9bb066313 100644 --- a/klippy/chelper/msgblock.h +++ b/klippy/chelper/msgblock.h @@ -50,5 +50,7 @@ void message_queue_free(struct list_head *root); uint64_t clock_from_clock32(struct clock_estimate *ce, uint32_t clock32); double clock_to_time(struct clock_estimate *ce, uint64_t clock); uint64_t clock_from_time(struct clock_estimate *ce, double time); +void clock_fill(struct clock_estimate *ce, double est_freq, double conv_time + , uint64_t conv_clock, uint64_t last_clock); #endif // msgblock.h diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index ed1215df9..914d4c395 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -909,10 +909,7 @@ serialqueue_set_clock_est(struct serialqueue *sq, double est_freq , uint64_t last_clock) { pthread_mutex_lock(&sq->lock); - sq->ce.est_freq = est_freq; - sq->ce.conv_time = conv_time; - sq->ce.conv_clock = conv_clock; - sq->ce.last_clock = last_clock; + clock_fill(&sq->ce, est_freq, conv_time, conv_clock, last_clock); pthread_mutex_unlock(&sq->lock); } From bd747872c3c917cd6677305c412a2e6c2ae34b87 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 15:13:14 -0400 Subject: [PATCH 127/411] steppersync: Add new 'struct steppersyncmgr' Add a new C based mechanism for tracking all the 'struct steppersync' instances. This simplifies memory management. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 8 +++-- klippy/chelper/steppersync.c | 56 +++++++++++++++++++++++++++++++-- klippy/chelper/steppersync.h | 13 +++++--- klippy/extras/motion_queuing.py | 9 +++--- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index b9ad9747d..efc2d303b 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -61,9 +61,6 @@ defs_stepcompress = """ """ defs_steppersync = """ - struct steppersync *steppersync_alloc(struct serialqueue *sq - , struct stepcompress **sc_list, int sc_num, int move_num); - void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); @@ -71,6 +68,11 @@ defs_steppersync = """ , double gen_steps_time, uint64_t flush_clock); int32_t steppersync_finalize_gen_steps(struct steppersync *ss , uint64_t flush_clock); + struct steppersyncmgr *steppersyncmgr_alloc(void); + void steppersyncmgr_free(struct steppersyncmgr *ssm); + struct steppersync *steppersyncmgr_alloc_steppersync( + struct steppersyncmgr *ssm, struct serialqueue *sq + , struct stepcompress **sc_list, int sc_num, int move_num); """ defs_itersolve = """ diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 0ff5bcab1..35010322d 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -19,7 +19,14 @@ #include "stepcompress.h" // stepcompress_flush #include "steppersync.h" // steppersync_alloc + +/**************************************************************** + * StepperSync - sort move queue for a micro-controller + ****************************************************************/ + struct steppersync { + // List node for storage in steppersyncmgr list + struct list_node ssm_node; // Serial port struct serialqueue *sq; struct command_queue *cq; @@ -32,7 +39,7 @@ struct steppersync { }; // Allocate a new 'steppersync' object -struct steppersync * __visible +static struct steppersync * steppersync_alloc(struct serialqueue *sq, struct stepcompress **sc_list , int sc_num, int move_num) { @@ -53,7 +60,7 @@ steppersync_alloc(struct serialqueue *sq, struct stepcompress **sc_list } // Free memory associated with a 'steppersync' object -void __visible +static void steppersync_free(struct steppersync *ss) { if (!ss) @@ -187,3 +194,48 @@ steppersync_finalize_gen_steps(struct steppersync *ss, uint64_t flush_clock) steppersync_flush(ss, flush_clock); return 0; } + + +/**************************************************************** + * StepperSyncMgr - manage a list of steppersync + ****************************************************************/ + +struct steppersyncmgr { + struct list_head ss_list; +}; + +// Allocate a new 'steppersyncmgr' object +struct steppersyncmgr * __visible +steppersyncmgr_alloc(void) +{ + struct steppersyncmgr *ssm = malloc(sizeof(*ssm)); + memset(ssm, 0, sizeof(*ssm)); + list_init(&ssm->ss_list); + return ssm; +} + +// Free memory associated with a 'steppersync' object +void __visible +steppersyncmgr_free(struct steppersyncmgr *ssm) +{ + if (!ssm) + return; + while (!list_empty(&ssm->ss_list)) { + struct steppersync *ss = list_first_entry( + &ssm->ss_list, struct steppersync, ssm_node); + list_del(&ss->ssm_node); + steppersync_free(ss); + } + free(ssm); +} + +// Allocate a new 'steppersync' object +struct steppersync * __visible +steppersyncmgr_alloc_steppersync( + struct steppersyncmgr *ssm, struct serialqueue *sq + , struct stepcompress **sc_list, int sc_num, int move_num) +{ + struct steppersync *ss = steppersync_alloc(sq, sc_list, sc_num, move_num); + list_add_tail(&ss->ssm_node, &ssm->ss_list); + return ss; +} diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index 41cd03bbd..0931d9240 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -3,11 +3,7 @@ #include // uint64_t -struct serialqueue; -struct steppersync *steppersync_alloc( - struct serialqueue *sq, struct stepcompress **sc_list, int sc_num - , int move_num); -void steppersync_free(struct steppersync *ss); +struct steppersync; void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); @@ -16,4 +12,11 @@ void steppersync_start_gen_steps(struct steppersync *ss, double gen_steps_time int32_t steppersync_finalize_gen_steps(struct steppersync *ss , uint64_t flush_clock); +struct steppersyncmgr *steppersyncmgr_alloc(void); +void steppersyncmgr_free(struct steppersyncmgr *ssm); +struct serialqueue; +struct steppersync *steppersyncmgr_alloc_steppersync( + struct steppersyncmgr *ssm, struct serialqueue *sq + , struct stepcompress **sc_list, int sc_num, int move_num); + #endif // steppersync.h diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 4679e801d..a44cb625a 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -29,6 +29,8 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves # C steppersync tracking + self.steppersyncmgr = ffi_main.gc(ffi_lib.steppersyncmgr_alloc(), + ffi_lib.steppersyncmgr_free) self.stepcompress = [] self.steppersyncs = [] self.steppersync_start_gen_steps = ffi_lib.steppersync_start_gen_steps @@ -84,10 +86,9 @@ class PrinterMotionQueuing: if sc_mcu is mcu: stepqueues.append(sc) ffi_main, ffi_lib = chelper.get_ffi() - ss = ffi_main.gc( - ffi_lib.steppersync_alloc(serialqueue, stepqueues, len(stepqueues), - move_count), - ffi_lib.steppersync_free) + ss = ffi_lib.steppersyncmgr_alloc_steppersync( + self.steppersyncmgr, serialqueue, stepqueues, len(stepqueues), + move_count) self.steppersyncs.append((mcu, ss)) mcu_freq = float(mcu.seconds_to_clock(1.)) ffi_lib.steppersync_set_time(ss, 0., mcu_freq) From f21cca049f6f0ec45ba1c9b7e8a96a135f057d26 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 15:31:44 -0400 Subject: [PATCH 128/411] steppersync: Add new steppersyncmgr_gen_steps() function Generate and flush all the steppersync instances from a single steppersyncmgr_gen_steps() call. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 7 +-- klippy/chelper/steppersync.c | 88 ++++++++++++++++++--------------- klippy/chelper/steppersync.h | 8 ++- klippy/extras/motion_queuing.py | 34 +++++-------- 4 files changed, 63 insertions(+), 74 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index efc2d303b..3a7ddf4eb 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -63,16 +63,13 @@ defs_stepcompress = """ defs_steppersync = """ void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); - void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); - void steppersync_start_gen_steps(struct steppersync *ss - , double gen_steps_time, uint64_t flush_clock); - int32_t steppersync_finalize_gen_steps(struct steppersync *ss - , uint64_t flush_clock); struct steppersyncmgr *steppersyncmgr_alloc(void); void steppersyncmgr_free(struct steppersyncmgr *ssm); struct steppersync *steppersyncmgr_alloc_steppersync( struct steppersyncmgr *ssm, struct serialqueue *sq , struct stepcompress **sc_list, int sc_num, int move_num); + int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm + , double flush_time, double gen_steps_time, double clear_history_time); """ defs_itersolve = """ diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 35010322d..ef2df415b 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -33,6 +33,8 @@ struct steppersync { // Storage for associated stepcompress objects struct stepcompress **sc_list; int sc_num; + // Convert from time to clock + struct clock_estimate ce; // Storage for list of pending move clocks uint64_t *move_clocks; int num_move_clocks; @@ -76,6 +78,7 @@ void __visible steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq) { + clock_fill(&ss->ce, mcu_freq, time_offset, 0, 0); int i; for (i=0; isc_num; i++) { struct stepcompress *sc = ss->sc_list[i]; @@ -83,17 +86,6 @@ steppersync_set_time(struct steppersync *ss, double time_offset } } -// Expire the stepcompress history before the given clock time -void __visible -steppersync_history_expire(struct steppersync *ss, uint64_t end_clock) -{ - int i; - for (i = 0; i < ss->sc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_history_expire(sc, end_clock); - } -} - // Implement a binary heap algorithm to track when the next available // 'struct move' in the mcu will be available static void @@ -165,36 +157,6 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock) serialqueue_send_batch(ss->sq, ss->cq, &msgs); } -// Start generating steps in stepcompress objects -void __visible -steppersync_start_gen_steps(struct steppersync *ss, double gen_steps_time - , uint64_t flush_clock) -{ - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_start_gen_steps(sc, gen_steps_time, flush_clock); - } -} - -// Finalize step generation and flush -int32_t __visible -steppersync_finalize_gen_steps(struct steppersync *ss, uint64_t flush_clock) -{ - int i; - int32_t res = 0; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - int32_t ret = stepcompress_finalize_gen_steps(sc); - if (ret) - res = ret; - } - if (res) - return res; - steppersync_flush(ss, flush_clock); - return 0; -} - /**************************************************************** * StepperSyncMgr - manage a list of steppersync @@ -239,3 +201,47 @@ steppersyncmgr_alloc_steppersync( list_add_tail(&ss->ssm_node, &ssm->ss_list); return ss; } + +// Generate and flush steps +int32_t __visible +steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time + , double gen_steps_time, double clear_history_time) +{ + struct steppersync *ss; + // Start step generation threads + list_for_each_entry(ss, &ssm->ss_list, ssm_node) { + uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); + int i; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + stepcompress_start_gen_steps(sc, gen_steps_time, flush_clock); + } + } + // Wait for step generation threads to complete + int32_t res = 0; + list_for_each_entry(ss, &ssm->ss_list, ssm_node) { + int i; + for (i=0; isc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + int32_t ret = stepcompress_finalize_gen_steps(sc); + if (ret) + res = ret; + } + if (res) + continue; + uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); + steppersync_flush(ss, flush_clock); + } + if (res) + return res; + // Clear history + list_for_each_entry(ss, &ssm->ss_list, ssm_node) { + uint64_t end_clock = clock_from_time(&ss->ce, clear_history_time); + int i; + for (i = 0; i < ss->sc_num; i++) { + struct stepcompress *sc = ss->sc_list[i]; + stepcompress_history_expire(sc, end_clock); + } + } + return 0; +} diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index 0931d9240..f7a89230d 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -6,11 +6,6 @@ struct steppersync; void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); -void steppersync_history_expire(struct steppersync *ss, uint64_t end_clock); -void steppersync_start_gen_steps(struct steppersync *ss, double gen_steps_time - , uint64_t flush_clock); -int32_t steppersync_finalize_gen_steps(struct steppersync *ss - , uint64_t flush_clock); struct steppersyncmgr *steppersyncmgr_alloc(void); void steppersyncmgr_free(struct steppersyncmgr *ssm); @@ -18,5 +13,8 @@ struct serialqueue; struct steppersync *steppersyncmgr_alloc_steppersync( struct steppersyncmgr *ssm, struct serialqueue *sq , struct stepcompress **sc_list, int sc_num, int move_num); +int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time + , double gen_steps_time + , double clear_history_time); #endif // steppersync.h diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index a44cb625a..d981dcb58 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -33,10 +33,7 @@ class PrinterMotionQueuing: ffi_lib.steppersyncmgr_free) self.stepcompress = [] self.steppersyncs = [] - self.steppersync_start_gen_steps = ffi_lib.steppersync_start_gen_steps - self.steppersync_finalize_gen_steps = \ - ffi_lib.steppersync_finalize_gen_steps - self.steppersync_history_expire = ffi_lib.steppersync_history_expire + self.steppersyncmgr_gen_steps = ffi_lib.steppersyncmgr_gen_steps # History expiration self.clear_history_time = 0. # Flush notification callbacks @@ -101,7 +98,7 @@ class PrinterMotionQueuing: ffi_lib.steppersync_set_time(ss, offset, freq) # Calculate history expiration est_print_time = self.mcu.estimated_print_time(eventtime) - self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE + self.clear_history_time = max(0., est_print_time - MOVE_HISTORY_EXPIRE) return False, "" # Flush notification callbacks def register_flush_callback(self, callback, can_add_trapq=False): @@ -142,31 +139,22 @@ class PrinterMotionQueuing: # Invoke flush callbacks (if any) for cb in self.flush_callbacks: cb(flush_time, step_gen_time) - # Generate stepper movement and transmit - for mcu, ss in self.steppersyncs: - clock = max(0, mcu.print_time_to_clock(flush_time)) - self.steppersync_start_gen_steps(ss, step_gen_time, clock) - for mcu, ss in self.steppersyncs: - clock = max(0, mcu.print_time_to_clock(flush_time)) - ret = self.steppersync_finalize_gen_steps(ss, clock) - if ret: - raise mcu.error("Internal error in MCU '%s' stepcompress" - % (mcu.get_name(),)) - self.last_flush_time = flush_time - self.last_step_gen_time = step_gen_time # Determine maximum history to keep trapq_free_time = step_gen_time - self.kin_flush_delay clear_history_time = self.clear_history_time if not self.can_pause: - clear_history_time = trapq_free_time - MOVE_HISTORY_EXPIRE - # Move processed trapq moves to history list, and expire old history + clear_history_time = max(0., trapq_free_time - MOVE_HISTORY_EXPIRE) + # Generate stepper movement and transmit + ret = self.steppersyncmgr_gen_steps(self.steppersyncmgr, flush_time, + step_gen_time, clear_history_time) + if ret: + raise self.mcu.error("Internal error in stepcompress") + self.last_flush_time = flush_time + self.last_step_gen_time = step_gen_time + # Move processed trapq entries to history list, and expire old history for trapq in self.trapqs: self.trapq_finalize_moves(trapq, trapq_free_time, clear_history_time) - # Clean up old history entries in stepcompress objects - for mcu, ss in self.steppersyncs: - clock = max(0, mcu.print_time_to_clock(clear_history_time)) - self.steppersync_history_expire(ss, clock) def _await_flush_time(self, want_flush_time): while 1: if self.last_flush_time >= want_flush_time or not self.can_pause: From a29cfc170148f92adee6ec2fb226b5ded627eb71 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 18:45:18 -0400 Subject: [PATCH 129/411] stepcompress: Pass oid in stepcompress_fill() instead of stepcompress_alloc() Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 7 ++++--- klippy/chelper/stepcompress.c | 6 +++--- klippy/chelper/stepcompress.h | 4 ++-- klippy/extras/motion_queuing.py | 4 ++-- klippy/extras/pwm_tool.py | 5 ++--- klippy/stepper.py | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 3a7ddf4eb..d3366044d 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -36,9 +36,10 @@ defs_stepcompress = """ int step_count, interval, add; }; - struct stepcompress *stepcompress_alloc(uint32_t oid, char name[16]); - void stepcompress_fill(struct stepcompress *sc, uint32_t max_error - , int32_t queue_step_msgtag, int32_t set_next_step_dir_msgtag); + struct stepcompress *stepcompress_alloc(char name[16]); + void stepcompress_fill(struct stepcompress *sc, uint32_t oid + , uint32_t max_error, int32_t queue_step_msgtag + , int32_t set_next_step_dir_msgtag); void stepcompress_set_invert_sdir(struct stepcompress *sc , uint32_t invert_sdir); void stepcompress_free(struct stepcompress *sc); diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index ab261d129..1a592173e 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -258,13 +258,12 @@ static void sc_thread_free(struct stepcompress *sc); // Allocate a new 'stepcompress' object struct stepcompress * __visible -stepcompress_alloc(uint32_t oid, char name[16]) +stepcompress_alloc(char name[16]) { struct stepcompress *sc = malloc(sizeof(*sc)); memset(sc, 0, sizeof(*sc)); list_init(&sc->msg_queue); list_init(&sc->history_list); - sc->oid = oid; sc->sdir = -1; int ret = sc_thread_alloc(sc, name); @@ -275,9 +274,10 @@ stepcompress_alloc(uint32_t oid, char name[16]) // Fill message id information void __visible -stepcompress_fill(struct stepcompress *sc, uint32_t max_error +stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error , int32_t queue_step_msgtag, int32_t set_next_step_dir_msgtag) { + sc->oid = oid; sc->max_error = max_error; sc->queue_step_msgtag = queue_step_msgtag; sc->set_next_step_dir_msgtag = set_next_step_dir_msgtag; diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 5ebf8bf08..413446daf 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -11,8 +11,8 @@ struct pull_history_steps { int step_count, interval, add; }; -struct stepcompress *stepcompress_alloc(uint32_t oid, char name[16]); -void stepcompress_fill(struct stepcompress *sc, uint32_t max_error +struct stepcompress *stepcompress_alloc(char name[16]); +void stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error , int32_t queue_step_msgtag , int32_t set_next_step_dir_msgtag); void stepcompress_set_invert_sdir(struct stepcompress *sc diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index d981dcb58..8fa562621 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -69,10 +69,10 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append # C steppersync tracking - def allocate_stepcompress(self, mcu, oid, name): + def allocate_stepcompress(self, mcu, name): name = name.encode("utf-8")[:15] ffi_main, ffi_lib = chelper.get_ffi() - sc = ffi_main.gc(ffi_lib.stepcompress_alloc(oid, name), + sc = ffi_main.gc(ffi_lib.stepcompress_alloc(name), ffi_lib.stepcompress_free) self.stepcompress.append((mcu, sc)) return sc diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index cec7e3791..dfa5c682b 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -14,12 +14,11 @@ class MCU_queued_pwm: self._hardware_pwm = False self._cycle_time = 0.100 self._max_duration = 2. - self._oid = oid = mcu.create_oid() + self._oid = mcu.create_oid() printer = mcu.get_printer() sname = config.get_name().split()[-1] self._motion_queuing = printer.load_object(config, 'motion_queuing') - self._stepqueue = self._motion_queuing.allocate_stepcompress( - mcu, oid, sname) + self._stepqueue = self._motion_queuing.allocate_stepcompress(mcu, sname) ffi_main, ffi_lib = chelper.get_ffi() self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg mcu.register_config_callback(self._build_config) diff --git a/klippy/stepper.py b/klippy/stepper.py index 046b5280d..deb73769e 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -29,7 +29,7 @@ class MCU_stepper: self._units_in_radians = units_in_radians self._step_dist = rotation_dist / steps_per_rotation self._mcu = mcu = step_pin_params['chip'] - self._oid = oid = mcu.create_oid() + self._oid = mcu.create_oid() mcu.register_config_callback(self._build_config) self._step_pin = step_pin_params['pin'] self._invert_step = step_pin_params['invert'] @@ -45,7 +45,7 @@ class MCU_stepper: self._active_callbacks = [] motion_queuing = printer.load_object(config, 'motion_queuing') sname = self._name.split()[-1] - self._stepqueue = motion_queuing.allocate_stepcompress(mcu, oid, sname) + self._stepqueue = motion_queuing.allocate_stepcompress(mcu, sname) ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir) self._stepper_kinematics = None @@ -123,7 +123,7 @@ class MCU_stepper: max_error = self._mcu.get_max_stepper_error() max_error_ticks = self._mcu.seconds_to_clock(max_error) ffi_main, ffi_lib = chelper.get_ffi() - ffi_lib.stepcompress_fill(self._stepqueue, max_error_ticks, + ffi_lib.stepcompress_fill(self._stepqueue, self._oid, max_error_ticks, step_cmd_tag, dir_cmd_tag) def get_oid(self): return self._oid From d831d66c114efd3df051e6586030b15544d1e951 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 19:43:00 -0400 Subject: [PATCH 130/411] steppersync: Introduce new 'struct syncemitter' Create a new 'struct syncemitter' for each object that can generate messages for a 'struct steppersync' and store in a regular linked list. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 10 ++- klippy/chelper/stepcompress.c | 4 +- klippy/chelper/steppersync.c | 128 +++++++++++++++++++------------- klippy/chelper/steppersync.h | 10 ++- klippy/extras/motion_queuing.py | 33 ++++---- 5 files changed, 111 insertions(+), 74 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index d3366044d..e7340b204 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -36,13 +36,11 @@ defs_stepcompress = """ int step_count, interval, add; }; - struct stepcompress *stepcompress_alloc(char name[16]); void stepcompress_fill(struct stepcompress *sc, uint32_t oid , uint32_t max_error, int32_t queue_step_msgtag , int32_t set_next_step_dir_msgtag); void stepcompress_set_invert_sdir(struct stepcompress *sc , uint32_t invert_sdir); - void stepcompress_free(struct stepcompress *sc); int stepcompress_reset(struct stepcompress *sc, uint64_t last_step_clock); int stepcompress_set_last_position(struct stepcompress *sc , uint64_t clock, int64_t last_position); @@ -62,13 +60,17 @@ defs_stepcompress = """ """ defs_steppersync = """ + struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); + struct syncemitter *steppersync_alloc_syncemitter(struct steppersync *ss + , char name[16], int alloc_stepcompress); + void steppersync_setup_movequeue(struct steppersync *ss + , struct serialqueue *sq, int move_num); void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); struct steppersyncmgr *steppersyncmgr_alloc(void); void steppersyncmgr_free(struct steppersyncmgr *ssm); struct steppersync *steppersyncmgr_alloc_steppersync( - struct steppersyncmgr *ssm, struct serialqueue *sq - , struct stepcompress **sc_list, int sc_num, int move_num); + struct steppersyncmgr *ssm); int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm , double flush_time, double gen_steps_time, double clear_history_time); """ diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 1a592173e..1c74380fb 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -257,7 +257,7 @@ static int sc_thread_alloc(struct stepcompress *sc, char name[16]); static void sc_thread_free(struct stepcompress *sc); // Allocate a new 'stepcompress' object -struct stepcompress * __visible +struct stepcompress * stepcompress_alloc(char name[16]) { struct stepcompress *sc = malloc(sizeof(*sc)); @@ -310,7 +310,7 @@ stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock) } // Free memory associated with a 'stepcompress' object -void __visible +void stepcompress_free(struct stepcompress *sc) { if (!sc) diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index ef2df415b..047fde3c3 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -20,6 +20,24 @@ #include "steppersync.h" // steppersync_alloc +/**************************************************************** + * SyncEmitter - message generation for each stepper + ****************************************************************/ + +struct syncemitter { + // List node for storage in steppersync list + struct list_node ss_node; + // Step compression and generation + struct stepcompress *sc; +}; + +struct stepcompress * __visible +syncemitter_get_stepcompress(struct syncemitter *se) +{ + return se->sc; +} + + /**************************************************************** * StepperSync - sort move queue for a micro-controller ****************************************************************/ @@ -30,9 +48,8 @@ struct steppersync { // Serial port struct serialqueue *sq; struct command_queue *cq; - // Storage for associated stepcompress objects - struct stepcompress **sc_list; - int sc_num; + // The syncemitters that generate messages on this mcu + struct list_head se_list; // Convert from time to clock struct clock_estimate ce; // Storage for list of pending move clocks @@ -40,37 +57,33 @@ struct steppersync { int num_move_clocks; }; -// Allocate a new 'steppersync' object -static struct steppersync * -steppersync_alloc(struct serialqueue *sq, struct stepcompress **sc_list - , int sc_num, int move_num) +// Allocate a new syncemitter instance +struct syncemitter * __visible +steppersync_alloc_syncemitter(struct steppersync *ss, char name[16] + , int alloc_stepcompress) { - struct steppersync *ss = malloc(sizeof(*ss)); - memset(ss, 0, sizeof(*ss)); + struct syncemitter *se = malloc(sizeof(*se)); + memset(se, 0, sizeof(*se)); + list_add_tail(&se->ss_node, &ss->se_list); + if (alloc_stepcompress) + se->sc = stepcompress_alloc(name); + return se; +} + +// Fill information on mcu move queue +void __visible +steppersync_setup_movequeue(struct steppersync *ss, struct serialqueue *sq + , int move_num) +{ + serialqueue_free_commandqueue(ss->cq); + free(ss->move_clocks); + ss->sq = sq; ss->cq = serialqueue_alloc_commandqueue(); - ss->sc_list = malloc(sizeof(*sc_list)*sc_num); - memcpy(ss->sc_list, sc_list, sizeof(*sc_list)*sc_num); - ss->sc_num = sc_num; - ss->move_clocks = malloc(sizeof(*ss->move_clocks)*move_num); memset(ss->move_clocks, 0, sizeof(*ss->move_clocks)*move_num); ss->num_move_clocks = move_num; - - return ss; -} - -// Free memory associated with a 'steppersync' object -static void -steppersync_free(struct steppersync *ss) -{ - if (!ss) - return; - free(ss->sc_list); - free(ss->move_clocks); - serialqueue_free_commandqueue(ss->cq); - free(ss); } // Set the conversion rate of 'print_time' to mcu clock @@ -79,10 +92,10 @@ steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq) { clock_fill(&ss->ce, mcu_freq, time_offset, 0, 0); - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_set_time(sc, time_offset, mcu_freq); + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + if (se->sc) + stepcompress_set_time(se->sc, time_offset, mcu_freq); } } @@ -122,10 +135,9 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock) // Find message with lowest reqclock uint64_t req_clock = MAX_CLOCK; struct queue_message *qm = NULL; - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - struct list_head *sc_mq = stepcompress_get_msg_queue(sc); + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + struct list_head *sc_mq = stepcompress_get_msg_queue(se->sc); if (!list_empty(sc_mq)) { struct queue_message *m = list_first_entry( sc_mq, struct queue_message, node); @@ -186,18 +198,27 @@ steppersyncmgr_free(struct steppersyncmgr *ssm) struct steppersync *ss = list_first_entry( &ssm->ss_list, struct steppersync, ssm_node); list_del(&ss->ssm_node); - steppersync_free(ss); + free(ss->move_clocks); + serialqueue_free_commandqueue(ss->cq); + while (!list_empty(&ss->se_list)) { + struct syncemitter *se = list_first_entry( + &ss->se_list, struct syncemitter, ss_node); + list_del(&se->ss_node); + stepcompress_free(se->sc); + free(se); + } + free(ss); } free(ssm); } // Allocate a new 'steppersync' object struct steppersync * __visible -steppersyncmgr_alloc_steppersync( - struct steppersyncmgr *ssm, struct serialqueue *sq - , struct stepcompress **sc_list, int sc_num, int move_num) +steppersyncmgr_alloc_steppersync(struct steppersyncmgr *ssm) { - struct steppersync *ss = steppersync_alloc(sq, sc_list, sc_num, move_num); + struct steppersync *ss = malloc(sizeof(*ss)); + memset(ss, 0, sizeof(*ss)); + list_init(&ss->se_list); list_add_tail(&ss->ssm_node, &ssm->ss_list); return ss; } @@ -211,19 +232,21 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time // Start step generation threads list_for_each_entry(ss, &ssm->ss_list, ssm_node) { uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_start_gen_steps(sc, gen_steps_time, flush_clock); + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + if (!se->sc) + continue; + stepcompress_start_gen_steps(se->sc, gen_steps_time, flush_clock); } } // Wait for step generation threads to complete int32_t res = 0; list_for_each_entry(ss, &ssm->ss_list, ssm_node) { - int i; - for (i=0; isc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - int32_t ret = stepcompress_finalize_gen_steps(sc); + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + if (!se->sc) + continue; + int32_t ret = stepcompress_finalize_gen_steps(se->sc); if (ret) res = ret; } @@ -237,10 +260,11 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time // Clear history list_for_each_entry(ss, &ssm->ss_list, ssm_node) { uint64_t end_clock = clock_from_time(&ss->ce, clear_history_time); - int i; - for (i = 0; i < ss->sc_num; i++) { - struct stepcompress *sc = ss->sc_list[i]; - stepcompress_history_expire(sc, end_clock); + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + if (!se->sc) + continue; + stepcompress_history_expire(se->sc, end_clock); } } return 0; diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index f7a89230d..f42d3c85d 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -3,7 +3,14 @@ #include // uint64_t +struct syncemitter; +struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); + struct steppersync; +struct syncemitter *steppersync_alloc_syncemitter( + struct steppersync *ss, char name[16], int alloc_stepcompress); +void steppersync_setup_movequeue(struct steppersync *ss, struct serialqueue *sq + , int move_num); void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); @@ -11,8 +18,7 @@ struct steppersyncmgr *steppersyncmgr_alloc(void); void steppersyncmgr_free(struct steppersyncmgr *ssm); struct serialqueue; struct steppersync *steppersyncmgr_alloc_steppersync( - struct steppersyncmgr *ssm, struct serialqueue *sq - , struct stepcompress **sc_list, int sc_num, int move_num); + struct steppersyncmgr *ssm); int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time , double gen_steps_time , double clear_history_time); diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 8fa562621..da1a5a33b 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -31,7 +31,7 @@ class PrinterMotionQueuing: # C steppersync tracking self.steppersyncmgr = ffi_main.gc(ffi_lib.steppersyncmgr_alloc(), ffi_lib.steppersyncmgr_free) - self.stepcompress = [] + self.syncemitters = [] self.steppersyncs = [] self.steppersyncmgr_gen_steps = ffi_lib.steppersyncmgr_gen_steps # History expiration @@ -69,24 +69,26 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.trapq_append # C steppersync tracking + def _lookup_steppersync(self, mcu): + for ss_mcu, ss in self.steppersyncs: + if ss_mcu is mcu: + return ss + ffi_main, ffi_lib = chelper.get_ffi() + ss = ffi_lib.steppersyncmgr_alloc_steppersync(self.steppersyncmgr) + self.steppersyncs.append((mcu, ss)) + return ss def allocate_stepcompress(self, mcu, name): name = name.encode("utf-8")[:15] + ss = self._lookup_steppersync(mcu) ffi_main, ffi_lib = chelper.get_ffi() - sc = ffi_main.gc(ffi_lib.stepcompress_alloc(name), - ffi_lib.stepcompress_free) - self.stepcompress.append((mcu, sc)) - return sc + se = ffi_lib.steppersync_alloc_syncemitter(ss, name, True) + self.syncemitters.append(se) + return ffi_lib.syncemitter_get_stepcompress(se) def setup_mcu_movequeue(self, mcu, serialqueue, move_count): # Setup steppersync object for the mcu's main movequeue - stepqueues = [] - for sc_mcu, sc in self.stepcompress: - if sc_mcu is mcu: - stepqueues.append(sc) ffi_main, ffi_lib = chelper.get_ffi() - ss = ffi_lib.steppersyncmgr_alloc_steppersync( - self.steppersyncmgr, serialqueue, stepqueues, len(stepqueues), - move_count) - self.steppersyncs.append((mcu, ss)) + ss = self._lookup_steppersync(mcu) + ffi_lib.steppersync_setup_movequeue(ss, serialqueue, move_count) mcu_freq = float(mcu.seconds_to_clock(1.)) ffi_lib.steppersync_set_time(ss, 0., mcu_freq) def stats(self, eventtime): @@ -117,7 +119,10 @@ class PrinterMotionQueuing: def check_step_generation_scan_windows(self): ffi_main, ffi_lib = chelper.get_ffi() kin_flush_delay = SDS_CHECK_TIME - for mcu, sc in self.stepcompress: + for se in self.syncemitters: + sc = ffi_lib.syncemitter_get_stepcompress(se) + if sc == ffi_main.NULL: + continue sk = ffi_lib.stepcompress_get_stepper_kinematics(sc) if sk == ffi_main.NULL: continue From e78d11bc6fe9d78c5c108e056d28cc2525ed66a9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 21:03:39 -0400 Subject: [PATCH 131/411] steppersync: Support sending messages directly from syncemitter Move msg_queue allocation from stepcompress to syncemitter. With this change the pwm_tool module does not need to allocate a stepcompress object. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 4 ++-- klippy/chelper/stepcompress.c | 34 ++++++--------------------------- klippy/chelper/stepcompress.h | 7 +++---- klippy/chelper/steppersync.c | 22 +++++++++++++++++---- klippy/chelper/steppersync.h | 2 ++ klippy/extras/motion_queuing.py | 6 +++--- klippy/extras/pwm_tool.py | 10 ++++------ klippy/stepper.py | 3 ++- 8 files changed, 40 insertions(+), 48 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index e7340b204..c0324a07c 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -48,8 +48,6 @@ defs_stepcompress = """ , uint64_t clock); int stepcompress_queue_msg(struct stepcompress *sc , uint32_t *data, int len); - int stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock - , uint32_t *data, int len); int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); @@ -61,6 +59,8 @@ defs_stepcompress = """ defs_steppersync = """ struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); + void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock + , uint32_t *data, int len); struct syncemitter *steppersync_alloc_syncemitter(struct steppersync *ss , char name[16], int alloc_stepcompress); void steppersync_setup_movequeue(struct steppersync *ss diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 1c74380fb..1426d963e 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -38,7 +38,7 @@ struct stepcompress { double mcu_time_offset, mcu_freq, last_step_print_time; // Message generation uint64_t last_step_clock; - struct list_head msg_queue; + struct list_head *msg_queue; uint32_t oid; int32_t queue_step_msgtag, set_next_step_dir_msgtag; int sdir, invert_sdir; @@ -258,13 +258,13 @@ static void sc_thread_free(struct stepcompress *sc); // Allocate a new 'stepcompress' object struct stepcompress * -stepcompress_alloc(char name[16]) +stepcompress_alloc(char name[16], struct list_head *msg_queue) { struct stepcompress *sc = malloc(sizeof(*sc)); memset(sc, 0, sizeof(*sc)); - list_init(&sc->msg_queue); list_init(&sc->history_list); sc->sdir = -1; + sc->msg_queue = msg_queue; int ret = sc_thread_alloc(sc, name); if (ret) @@ -317,7 +317,6 @@ stepcompress_free(struct stepcompress *sc) return; sc_thread_free(sc); free(sc->queue); - message_queue_free(&sc->msg_queue); stepcompress_history_expire(sc, UINT64_MAX); free(sc); } @@ -334,12 +333,6 @@ stepcompress_get_step_dir(struct stepcompress *sc) return sc->next_step_dir; } -struct list_head * -stepcompress_get_msg_queue(struct stepcompress *sc) -{ - return &sc->msg_queue; -} - // Determine the "print time" of the last_step_clock static void calc_last_step_print_time(struct stepcompress *sc) @@ -377,7 +370,7 @@ add_move(struct stepcompress *sc, uint64_t first_clock, struct step_move *move) qm->min_clock = qm->req_clock = sc->last_step_clock; if (move->count == 1 && first_clock >= sc->last_step_clock + CLOCK_DIFF_MAX) qm->req_clock = first_clock; - list_add_tail(&qm->node, &sc->msg_queue); + list_add_tail(&qm->node, sc->msg_queue); sc->last_step_clock = last_clock; // Create and store move in history tracking @@ -441,7 +434,7 @@ set_next_step_dir(struct stepcompress *sc, int sdir) }; struct queue_message *qm = message_alloc_and_encode(msg, 3); qm->req_clock = sc->last_step_clock; - list_add_tail(&qm->node, &sc->msg_queue); + list_add_tail(&qm->node, sc->msg_queue); return 0; } @@ -640,22 +633,7 @@ stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len) struct queue_message *qm = message_alloc_and_encode(data, len); qm->req_clock = sc->last_step_clock; - list_add_tail(&qm->node, &sc->msg_queue); - return 0; -} - -// Queue an mcu command that will consume space in the mcu move queue -int __visible -stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock - , uint32_t *data, int len) -{ - int ret = stepcompress_flush(sc, UINT64_MAX); - if (ret) - return ret; - - struct queue_message *qm = message_alloc_and_encode(data, len); - qm->min_clock = qm->req_clock = req_clock; - list_add_tail(&qm->node, &sc->msg_queue); + list_add_tail(&qm->node, sc->msg_queue); return 0; } diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 413446daf..25521fa0c 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -11,7 +11,9 @@ struct pull_history_steps { int step_count, interval, add; }; -struct stepcompress *stepcompress_alloc(char name[16]); +struct list_head; +struct stepcompress *stepcompress_alloc(char name[16] + , struct list_head *msg_queue); void stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error , int32_t queue_step_msgtag , int32_t set_next_step_dir_msgtag); @@ -21,7 +23,6 @@ void stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock); void stepcompress_free(struct stepcompress *sc); uint32_t stepcompress_get_oid(struct stepcompress *sc); int stepcompress_get_step_dir(struct stepcompress *sc); -struct list_head *stepcompress_get_msg_queue(struct stepcompress *sc); void stepcompress_set_time(struct stepcompress *sc , double time_offset, double mcu_freq); int stepcompress_append(struct stepcompress *sc, int sdir @@ -33,8 +34,6 @@ int stepcompress_set_last_position(struct stepcompress *sc, uint64_t clock int64_t stepcompress_find_past_position(struct stepcompress *sc , uint64_t clock); int stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len); -int stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock - , uint32_t *data, int len); int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 047fde3c3..bf3a55393 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -27,16 +27,29 @@ struct syncemitter { // List node for storage in steppersync list struct list_node ss_node; + // Transmit message queue + struct list_head msg_queue; // Step compression and generation struct stepcompress *sc; }; +// Return this emitters 'struct stepcompress' (or NULL if not allocated) struct stepcompress * __visible syncemitter_get_stepcompress(struct syncemitter *se) { return se->sc; } +// Queue an mcu command that will consume space in the mcu move queue +void __visible +syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock + , uint32_t *data, int len) +{ + struct queue_message *qm = message_alloc_and_encode(data, len); + qm->min_clock = qm->req_clock = req_clock; + list_add_tail(&qm->node, &se->msg_queue); +} + /**************************************************************** * StepperSync - sort move queue for a micro-controller @@ -65,8 +78,9 @@ steppersync_alloc_syncemitter(struct steppersync *ss, char name[16] struct syncemitter *se = malloc(sizeof(*se)); memset(se, 0, sizeof(*se)); list_add_tail(&se->ss_node, &ss->se_list); + list_init(&se->msg_queue); if (alloc_stepcompress) - se->sc = stepcompress_alloc(name); + se->sc = stepcompress_alloc(name, &se->msg_queue); return se; } @@ -137,10 +151,9 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock) struct queue_message *qm = NULL; struct syncemitter *se; list_for_each_entry(se, &ss->se_list, ss_node) { - struct list_head *sc_mq = stepcompress_get_msg_queue(se->sc); - if (!list_empty(sc_mq)) { + if (!list_empty(&se->msg_queue)) { struct queue_message *m = list_first_entry( - sc_mq, struct queue_message, node); + &se->msg_queue, struct queue_message, node); if (m->req_clock < req_clock) { qm = m; req_clock = m->req_clock; @@ -205,6 +218,7 @@ steppersyncmgr_free(struct steppersyncmgr *ssm) &ss->se_list, struct syncemitter, ss_node); list_del(&se->ss_node); stepcompress_free(se->sc); + message_queue_free(&se->msg_queue); free(se); } free(ss); diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index f42d3c85d..6acbef28c 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -5,6 +5,8 @@ struct syncemitter; struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); +void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock + , uint32_t *data, int len); struct steppersync; struct syncemitter *steppersync_alloc_syncemitter( diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index da1a5a33b..459b6fa72 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -77,13 +77,13 @@ class PrinterMotionQueuing: ss = ffi_lib.steppersyncmgr_alloc_steppersync(self.steppersyncmgr) self.steppersyncs.append((mcu, ss)) return ss - def allocate_stepcompress(self, mcu, name): + def allocate_syncemitter(self, mcu, name, alloc_stepcompress=True): name = name.encode("utf-8")[:15] ss = self._lookup_steppersync(mcu) ffi_main, ffi_lib = chelper.get_ffi() - se = ffi_lib.steppersync_alloc_syncemitter(ss, name, True) + se = ffi_lib.steppersync_alloc_syncemitter(ss, name, alloc_stepcompress) self.syncemitters.append(se) - return ffi_lib.syncemitter_get_stepcompress(se) + return se def setup_mcu_movequeue(self, mcu, serialqueue, move_count): # Setup steppersync object for the mcu's main movequeue ffi_main, ffi_lib = chelper.get_ffi() diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index dfa5c682b..a069dd2d8 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -18,9 +18,10 @@ class MCU_queued_pwm: printer = mcu.get_printer() sname = config.get_name().split()[-1] self._motion_queuing = printer.load_object(config, 'motion_queuing') - self._stepqueue = self._motion_queuing.allocate_stepcompress(mcu, sname) + self._syncemitter = self._motion_queuing.allocate_syncemitter( + mcu, sname, alloc_stepcompress=False) ffi_main, ffi_lib = chelper.get_ffi() - self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg + self._syncemitter_queue_msg = ffi_lib.syncemitter_queue_msg mcu.register_config_callback(self._build_config) self._pin = pin_params['pin'] self._invert = pin_params['invert'] @@ -107,10 +108,7 @@ class MCU_queued_pwm: self._last_clock = clock = max(self._last_clock, clock) self._last_value = val data = (self._set_cmd_tag, self._oid, clock & 0xffffffff, val) - ret = self._stepcompress_queue_mq_msg(self._stepqueue, clock, - data, len(data)) - if ret: - raise error("Internal error in stepcompress") + self._syncemitter_queue_msg(self._syncemitter, clock, data, len(data)) # Notify toolhead so that it will flush this update wakeclock = clock if self._last_value != self._default_value: diff --git a/klippy/stepper.py b/klippy/stepper.py index deb73769e..5fc20d0fa 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -45,8 +45,9 @@ class MCU_stepper: self._active_callbacks = [] motion_queuing = printer.load_object(config, 'motion_queuing') sname = self._name.split()[-1] - self._stepqueue = motion_queuing.allocate_stepcompress(mcu, sname) + syncemitter = motion_queuing.allocate_syncemitter(mcu, sname) ffi_main, ffi_lib = chelper.get_ffi() + self._stepqueue = ffi_lib.syncemitter_get_stepcompress(syncemitter) ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir) self._stepper_kinematics = None self._itersolve_check_active = ffi_lib.itersolve_check_active From 3ef4702e06882a5a53f99ba4cf860f958fad6725 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 21:49:12 -0400 Subject: [PATCH 132/411] steppersync: Move step generation thread from stepcompress.c to steppersync.c Implement step generation from 'struct syncemitter' instead of in the stepcompress code. This simplifies the stepcompress code and simplifies the overall interface. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 8 +- klippy/chelper/stepcompress.c | 152 +--------------------------- klippy/chelper/stepcompress.h | 12 +-- klippy/chelper/steppersync.c | 173 +++++++++++++++++++++++++++++--- klippy/chelper/steppersync.h | 4 + klippy/extras/motion_queuing.py | 5 +- klippy/stepper.py | 7 +- 7 files changed, 174 insertions(+), 187 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index c0324a07c..1c1f73b99 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -51,14 +51,14 @@ defs_stepcompress = """ int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); - void stepcompress_set_stepper_kinematics(struct stepcompress *sc - , struct stepper_kinematics *sk); - struct stepper_kinematics *stepcompress_get_stepper_kinematics( - struct stepcompress *sc); """ defs_steppersync = """ struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); + void syncemitter_set_stepper_kinematics(struct syncemitter *se + , struct stepper_kinematics *sk); + struct stepper_kinematics *syncemitter_get_stepper_kinematics( + struct syncemitter *se); void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock , uint32_t *data, int len); struct syncemitter *steppersync_alloc_syncemitter(struct steppersync *ss diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 1426d963e..0f2c4ef74 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -15,14 +15,12 @@ // efficiency - the repetitive integer math is vastly faster in C. #include // sqrt -#include // pthread_mutex_lock #include // offsetof #include // uint32_t #include // fprintf #include // malloc #include // memset #include "compiler.h" // DIV_ROUND_UP -#include "itersolve.h" // itersolve_generate_steps #include "pyhelper.h" // errorf #include "serialqueue.h" // struct queue_message #include "stepcompress.h" // stepcompress_alloc @@ -48,16 +46,6 @@ struct stepcompress { // History tracking int64_t last_position; struct list_head history_list; - // Thread for step generation - struct stepper_kinematics *sk; - char name[16]; - pthread_t tid; - pthread_mutex_t lock; // protects variables below - pthread_cond_t cond; - int have_work; - double bg_gen_steps_time; - uint64_t bg_flush_clock; - int32_t bg_result; }; struct step_move { @@ -253,22 +241,15 @@ check_line(struct stepcompress *sc, struct step_move move) * Step compress interface ****************************************************************/ -static int sc_thread_alloc(struct stepcompress *sc, char name[16]); -static void sc_thread_free(struct stepcompress *sc); - // Allocate a new 'stepcompress' object struct stepcompress * -stepcompress_alloc(char name[16], struct list_head *msg_queue) +stepcompress_alloc(struct list_head *msg_queue) { struct stepcompress *sc = malloc(sizeof(*sc)); memset(sc, 0, sizeof(*sc)); list_init(&sc->history_list); sc->sdir = -1; sc->msg_queue = msg_queue; - - int ret = sc_thread_alloc(sc, name); - if (ret) - return NULL; return sc; } @@ -315,7 +296,6 @@ stepcompress_free(struct stepcompress *sc) { if (!sc) return; - sc_thread_free(sc); free(sc->queue); stepcompress_history_expire(sc, UINT64_MAX); free(sc); @@ -551,7 +531,7 @@ stepcompress_commit(struct stepcompress *sc) } // Flush pending steps -static int +int stepcompress_flush(struct stepcompress *sc, uint64_t move_clock) { if (sc->next_step_clock && move_clock >= sc->next_step_clock) { @@ -660,131 +640,3 @@ stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p } return res; } - - -/**************************************************************** - * Step generation thread - ****************************************************************/ - -// Store a reference to stepper_kinematics -void __visible -stepcompress_set_stepper_kinematics(struct stepcompress *sc - , struct stepper_kinematics *sk) -{ - sc->sk = sk; -} - -// Report current stepper_kinematics -struct stepper_kinematics * __visible -stepcompress_get_stepper_kinematics(struct stepcompress *sc) -{ - return sc->sk; -} - -// Generate steps (via itersolve) and flush -static int32_t -stepcompress_generate_steps(struct stepcompress *sc, double gen_steps_time - , uint64_t flush_clock) -{ - if (!sc->sk) - return 0; - // Generate steps - int32_t ret = itersolve_generate_steps(sc->sk, sc, gen_steps_time); - if (ret) - return ret; - // Flush steps - return stepcompress_flush(sc, flush_clock); -} - -// Main background thread for generating steps -static void * -sc_background_thread(void *data) -{ - struct stepcompress *sc = data; - set_thread_name(sc->name); - - pthread_mutex_lock(&sc->lock); - for (;;) { - if (!sc->have_work) { - pthread_cond_wait(&sc->cond, &sc->lock); - continue; - } - if (sc->have_work < 0) - // Exit request - break; - - // Request to generate steps - sc->bg_result = stepcompress_generate_steps(sc, sc->bg_gen_steps_time - , sc->bg_flush_clock); - sc->have_work = 0; - pthread_cond_signal(&sc->cond); - } - pthread_mutex_unlock(&sc->lock); - - return NULL; -} - -// Signal background thread to start step generation -void -stepcompress_start_gen_steps(struct stepcompress *sc, double gen_steps_time - , uint64_t flush_clock) -{ - if (!sc->sk) - return; - pthread_mutex_lock(&sc->lock); - while (sc->have_work) - pthread_cond_wait(&sc->cond, &sc->lock); - sc->bg_gen_steps_time = gen_steps_time; - sc->bg_flush_clock = flush_clock; - sc->have_work = 1; - pthread_mutex_unlock(&sc->lock); - pthread_cond_signal(&sc->cond); -} - -// Wait for background thread to complete last step generation request -int32_t -stepcompress_finalize_gen_steps(struct stepcompress *sc) -{ - pthread_mutex_lock(&sc->lock); - while (sc->have_work) - pthread_cond_wait(&sc->cond, &sc->lock); - int32_t res = sc->bg_result; - pthread_mutex_unlock(&sc->lock); - return res; -} - -// Internal helper to start thread -static int -sc_thread_alloc(struct stepcompress *sc, char name[16]) -{ - strncpy(sc->name, name, sizeof(sc->name)); - sc->name[sizeof(sc->name)-1] = '\0'; - int ret = pthread_mutex_init(&sc->lock, NULL); - if (ret) - goto fail; - ret = pthread_cond_init(&sc->cond, NULL); - if (ret) - goto fail; - ret = pthread_create(&sc->tid, NULL, sc_background_thread, sc); - if (ret) - goto fail; - return 0; -fail: - report_errno("sc init", ret); - return -1; -} - -// Request background thread to exit -static void -sc_thread_free(struct stepcompress *sc) -{ - pthread_mutex_lock(&sc->lock); - while (sc->have_work) - pthread_cond_wait(&sc->cond, &sc->lock); - sc->have_work = -1; - pthread_cond_signal(&sc->cond); - pthread_mutex_unlock(&sc->lock); - int ret = pthread_join(sc->tid, NULL); - if (ret) - report_errno("sc pthread_join", ret); -} diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 25521fa0c..91d1bc11d 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -12,8 +12,7 @@ struct pull_history_steps { }; struct list_head; -struct stepcompress *stepcompress_alloc(char name[16] - , struct list_head *msg_queue); +struct stepcompress *stepcompress_alloc(struct list_head *msg_queue); void stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error , int32_t queue_step_msgtag , int32_t set_next_step_dir_msgtag); @@ -28,6 +27,7 @@ void stepcompress_set_time(struct stepcompress *sc int stepcompress_append(struct stepcompress *sc, int sdir , double print_time, double step_time); int stepcompress_commit(struct stepcompress *sc); +int stepcompress_flush(struct stepcompress *sc, uint64_t move_clock); int stepcompress_reset(struct stepcompress *sc, uint64_t last_step_clock); int stepcompress_set_last_position(struct stepcompress *sc, uint64_t clock , int64_t last_position); @@ -37,13 +37,5 @@ int stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len); int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); -struct stepper_kinematics; -void stepcompress_set_stepper_kinematics(struct stepcompress *sc - , struct stepper_kinematics *sk); -struct stepper_kinematics *stepcompress_get_stepper_kinematics( - struct stepcompress *sc); -void stepcompress_start_gen_steps(struct stepcompress *sc, double gen_steps_time - , uint64_t flush_clock); -int32_t stepcompress_finalize_gen_steps(struct stepcompress *sc); #endif // stepcompress.h diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index bf3a55393..1c2a4a73d 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -11,10 +11,13 @@ // mcu step queue is ordered between steppers so that no stepper // starves the other steppers of space in the mcu step queue. +#include // pthread_mutex_lock #include // offsetof #include // malloc #include // memset #include "compiler.h" // __visible +#include "pyhelper.h" // set_thread_name +#include "itersolve.h" // itersolve_generate_steps #include "serialqueue.h" // struct queue_message #include "stepcompress.h" // stepcompress_flush #include "steppersync.h" // steppersync_alloc @@ -29,8 +32,17 @@ struct syncemitter { struct list_node ss_node; // Transmit message queue struct list_head msg_queue; - // Step compression and generation + // Thread for step generation struct stepcompress *sc; + struct stepper_kinematics *sk; + char name[16]; + pthread_t tid; + pthread_mutex_t lock; // protects variables below + pthread_cond_t cond; + int have_work; + double bg_gen_steps_time; + uint64_t bg_flush_clock; + int32_t bg_result; }; // Return this emitters 'struct stepcompress' (or NULL if not allocated) @@ -40,6 +52,21 @@ syncemitter_get_stepcompress(struct syncemitter *se) return se->sc; } +// Store a reference to stepper_kinematics +void __visible +syncemitter_set_stepper_kinematics(struct syncemitter *se + , struct stepper_kinematics *sk) +{ + se->sk = sk; +} + +// Report current stepper_kinematics +struct stepper_kinematics * __visible +syncemitter_get_stepper_kinematics(struct syncemitter *se) +{ + return se->sk; +} + // Queue an mcu command that will consume space in the mcu move queue void __visible syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock @@ -50,6 +77,129 @@ syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock list_add_tail(&qm->node, &se->msg_queue); } +// Generate steps (via itersolve) and flush +static int32_t +se_generate_steps(struct syncemitter *se, double gen_steps_time + , uint64_t flush_clock) +{ + if (!se->sc || !se->sk) + return 0; + // Generate steps + int32_t ret = itersolve_generate_steps(se->sk, se->sc, gen_steps_time); + if (ret) + return ret; + // Flush steps + return stepcompress_flush(se->sc, flush_clock); +} + +// Main background thread for generating steps +static void * +se_background_thread(void *data) +{ + struct syncemitter *se = data; + set_thread_name(se->name); + + pthread_mutex_lock(&se->lock); + for (;;) { + if (!se->have_work) { + pthread_cond_wait(&se->cond, &se->lock); + continue; + } + if (se->have_work < 0) + // Exit request + break; + + // Request to generate steps + se->bg_result = se_generate_steps(se, se->bg_gen_steps_time + , se->bg_flush_clock); + se->have_work = 0; + pthread_cond_signal(&se->cond); + } + pthread_mutex_unlock(&se->lock); + + return NULL; +} + +// Signal background thread to start step generation +static void +se_start_gen_steps(struct syncemitter *se, double gen_steps_time + , uint64_t flush_clock) +{ + if (!se->sc || !se->sk) + return; + pthread_mutex_lock(&se->lock); + while (se->have_work) + pthread_cond_wait(&se->cond, &se->lock); + se->bg_gen_steps_time = gen_steps_time; + se->bg_flush_clock = flush_clock; + se->have_work = 1; + pthread_mutex_unlock(&se->lock); + pthread_cond_signal(&se->cond); +} + +// Wait for background thread to complete last step generation request +static int32_t +se_finalize_gen_steps(struct syncemitter *se) +{ + if (!se->sc || !se->sk) + return 0; + pthread_mutex_lock(&se->lock); + while (se->have_work) + pthread_cond_wait(&se->cond, &se->lock); + int32_t res = se->bg_result; + pthread_mutex_unlock(&se->lock); + return res; +} + +// Allocate syncemitter and start thread +static struct syncemitter * +syncemitter_alloc(char name[16], int alloc_stepcompress) +{ + struct syncemitter *se = malloc(sizeof(*se)); + memset(se, 0, sizeof(*se)); + list_init(&se->msg_queue); + strncpy(se->name, name, sizeof(se->name)); + se->name[sizeof(se->name)-1] = '\0'; + if (!alloc_stepcompress) + return se; + se->sc = stepcompress_alloc(&se->msg_queue); + int ret = pthread_mutex_init(&se->lock, NULL); + if (ret) + goto fail; + ret = pthread_cond_init(&se->cond, NULL); + if (ret) + goto fail; + ret = pthread_create(&se->tid, NULL, se_background_thread, se); + if (ret) + goto fail; + return se; +fail: + report_errno("se alloc", ret); + return NULL; +} + +// Free syncemitter and exit background thread +static void +syncemitter_free(struct syncemitter *se) +{ + if (!se) + return; + if (se->sc) { + pthread_mutex_lock(&se->lock); + while (se->have_work) + pthread_cond_wait(&se->cond, &se->lock); + se->have_work = -1; + pthread_cond_signal(&se->cond); + pthread_mutex_unlock(&se->lock); + int ret = pthread_join(se->tid, NULL); + if (ret) + report_errno("se pthread_join", ret); + stepcompress_free(se->sc); + } + message_queue_free(&se->msg_queue); + free(se); +} + /**************************************************************** * StepperSync - sort move queue for a micro-controller @@ -75,12 +225,9 @@ struct syncemitter * __visible steppersync_alloc_syncemitter(struct steppersync *ss, char name[16] , int alloc_stepcompress) { - struct syncemitter *se = malloc(sizeof(*se)); - memset(se, 0, sizeof(*se)); - list_add_tail(&se->ss_node, &ss->se_list); - list_init(&se->msg_queue); - if (alloc_stepcompress) - se->sc = stepcompress_alloc(name, &se->msg_queue); + struct syncemitter *se = syncemitter_alloc(name, alloc_stepcompress); + if (se) + list_add_tail(&se->ss_node, &ss->se_list); return se; } @@ -217,9 +364,7 @@ steppersyncmgr_free(struct steppersyncmgr *ssm) struct syncemitter *se = list_first_entry( &ss->se_list, struct syncemitter, ss_node); list_del(&se->ss_node); - stepcompress_free(se->sc); - message_queue_free(&se->msg_queue); - free(se); + syncemitter_free(se); } free(ss); } @@ -248,9 +393,7 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); struct syncemitter *se; list_for_each_entry(se, &ss->se_list, ss_node) { - if (!se->sc) - continue; - stepcompress_start_gen_steps(se->sc, gen_steps_time, flush_clock); + se_start_gen_steps(se, gen_steps_time, flush_clock); } } // Wait for step generation threads to complete @@ -258,9 +401,7 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time list_for_each_entry(ss, &ssm->ss_list, ssm_node) { struct syncemitter *se; list_for_each_entry(se, &ss->se_list, ss_node) { - if (!se->sc) - continue; - int32_t ret = stepcompress_finalize_gen_steps(se->sc); + int32_t ret = se_finalize_gen_steps(se); if (ret) res = ret; } diff --git a/klippy/chelper/steppersync.h b/klippy/chelper/steppersync.h index 6acbef28c..7ad6fcae6 100644 --- a/klippy/chelper/steppersync.h +++ b/klippy/chelper/steppersync.h @@ -5,6 +5,10 @@ struct syncemitter; struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se); +void syncemitter_set_stepper_kinematics(struct syncemitter *se + , struct stepper_kinematics *sk); +struct stepper_kinematics *syncemitter_get_stepper_kinematics( + struct syncemitter *se); void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock , uint32_t *data, int len); diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 459b6fa72..3491545a8 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -120,10 +120,7 @@ class PrinterMotionQueuing: ffi_main, ffi_lib = chelper.get_ffi() kin_flush_delay = SDS_CHECK_TIME for se in self.syncemitters: - sc = ffi_lib.syncemitter_get_stepcompress(se) - if sc == ffi_main.NULL: - continue - sk = ffi_lib.stepcompress_get_stepper_kinematics(sc) + sk = ffi_lib.syncemitter_get_stepper_kinematics(se) if sk == ffi_main.NULL: continue trapq = ffi_lib.itersolve_get_trapq(sk) diff --git a/klippy/stepper.py b/klippy/stepper.py index 5fc20d0fa..3bcca565c 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -45,9 +45,10 @@ class MCU_stepper: self._active_callbacks = [] motion_queuing = printer.load_object(config, 'motion_queuing') sname = self._name.split()[-1] - syncemitter = motion_queuing.allocate_syncemitter(mcu, sname) + self._syncemitter = motion_queuing.allocate_syncemitter(mcu, sname) ffi_main, ffi_lib = chelper.get_ffi() - self._stepqueue = ffi_lib.syncemitter_get_stepcompress(syncemitter) + self._stepqueue = ffi_lib.syncemitter_get_stepcompress( + self._syncemitter) ffi_lib.stepcompress_set_invert_sdir(self._stepqueue, self._invert_dir) self._stepper_kinematics = None self._itersolve_check_active = ffi_lib.itersolve_check_active @@ -194,7 +195,7 @@ class MCU_stepper: mcu_pos = self.get_mcu_position() self._stepper_kinematics = sk ffi_main, ffi_lib = chelper.get_ffi() - ffi_lib.stepcompress_set_stepper_kinematics(self._stepqueue, sk); + ffi_lib.syncemitter_set_stepper_kinematics(self._syncemitter, sk); self.set_trapq(self._trapq) self._set_mcu_position(mcu_pos) return old_sk From 414679ac99192371c95cd223b3393f0a15f3b949 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 22:03:29 -0400 Subject: [PATCH 133/411] steppersync: Move history clearing to background thread Signed-off-by: Kevin O'Connor --- klippy/chelper/steppersync.c | 38 ++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 1c2a4a73d..6a01083f1 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -41,7 +41,7 @@ struct syncemitter { pthread_cond_t cond; int have_work; double bg_gen_steps_time; - uint64_t bg_flush_clock; + uint64_t bg_flush_clock, bg_clear_history_clock; int32_t bg_result; }; @@ -79,17 +79,24 @@ syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock // Generate steps (via itersolve) and flush static int32_t -se_generate_steps(struct syncemitter *se, double gen_steps_time - , uint64_t flush_clock) +se_generate_steps(struct syncemitter *se) { if (!se->sc || !se->sk) return 0; + double gen_steps_time = se->bg_gen_steps_time; + uint64_t flush_clock = se->bg_flush_clock; + uint64_t clear_history_clock = se->bg_clear_history_clock; // Generate steps int32_t ret = itersolve_generate_steps(se->sk, se->sc, gen_steps_time); if (ret) return ret; // Flush steps - return stepcompress_flush(se->sc, flush_clock); + ret = stepcompress_flush(se->sc, flush_clock); + if (ret) + return ret; + // Clear history + stepcompress_history_expire(se->sc, clear_history_clock); + return 0; } // Main background thread for generating steps @@ -110,8 +117,7 @@ se_background_thread(void *data) break; // Request to generate steps - se->bg_result = se_generate_steps(se, se->bg_gen_steps_time - , se->bg_flush_clock); + se->bg_result = se_generate_steps(se); se->have_work = 0; pthread_cond_signal(&se->cond); } @@ -123,7 +129,7 @@ se_background_thread(void *data) // Signal background thread to start step generation static void se_start_gen_steps(struct syncemitter *se, double gen_steps_time - , uint64_t flush_clock) + , uint64_t flush_clock, uint64_t clear_history_clock) { if (!se->sc || !se->sk) return; @@ -132,6 +138,7 @@ se_start_gen_steps(struct syncemitter *se, double gen_steps_time pthread_cond_wait(&se->cond, &se->lock); se->bg_gen_steps_time = gen_steps_time; se->bg_flush_clock = flush_clock; + se->bg_clear_history_clock = clear_history_clock; se->have_work = 1; pthread_mutex_unlock(&se->lock); pthread_cond_signal(&se->cond); @@ -391,9 +398,10 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time // Start step generation threads list_for_each_entry(ss, &ssm->ss_list, ssm_node) { uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); + uint64_t clear_clock = clock_from_time(&ss->ce, clear_history_time); struct syncemitter *se; list_for_each_entry(se, &ss->se_list, ss_node) { - se_start_gen_steps(se, gen_steps_time, flush_clock); + se_start_gen_steps(se, gen_steps_time, flush_clock, clear_clock); } } // Wait for step generation threads to complete @@ -410,17 +418,5 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); steppersync_flush(ss, flush_clock); } - if (res) - return res; - // Clear history - list_for_each_entry(ss, &ssm->ss_list, ssm_node) { - uint64_t end_clock = clock_from_time(&ss->ce, clear_history_time); - struct syncemitter *se; - list_for_each_entry(se, &ss->se_list, ss_node) { - if (!se->sc) - continue; - stepcompress_history_expire(se->sc, end_clock); - } - } - return 0; + return res; } From 546976b1fe2eb3a7fa46042f1f0495d374e36174 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 22:08:53 -0400 Subject: [PATCH 134/411] steppersync: Print the thread name on a stepcompress error Signed-off-by: Kevin O'Connor --- klippy/chelper/steppersync.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 6a01083f1..4dede38fb 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -118,6 +118,8 @@ se_background_thread(void *data) // Request to generate steps se->bg_result = se_generate_steps(se); + if (se->bg_result) + errorf("Error in syncemitter '%s' step generation", se->name); se->have_work = 0; pthread_cond_signal(&se->cond); } From 56fb4d2b04242bb4e9f84f6cecb97a49cdbaca62 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 22:38:59 -0400 Subject: [PATCH 135/411] docs: Update Code_Overview.md to reflect recent steppersync changes Signed-off-by: Kevin O'Connor --- docs/Code_Overview.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index 6b5df5595..e8a61068c 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -190,18 +190,19 @@ provides further information on the mechanics of moves. stepper movements produced by the extruder class will be in sync with head movement even though the code is kept separate. -* Klipper uses an - [iterative solver](https://en.wikipedia.org/wiki/Root-finding_algorithm) - to generate the step times for each stepper. For efficiency reasons, - the stepper pulse times are generated in C code in a thread per - stepper motor. The threads are notified of new activity by the - motion_queuing module (klippy/extras/motion_queuing.py): +* For efficiency reasons, stepper motion is generated in the C code in + a thread per stepper motor. The threads are notified when steps + should be generated by the motion_queuing module + (klippy/extras/motion_queuing.py): `PrinterMotionQueuing._flush_handler() -> PrinterMotionQueuing._advance_move_time() -> - steppersync_start_gen_steps() -> - stepcompress_start_gen_steps()`. The step times are then generated - from that thread (klippy/chelper/stepcompress.c): - `sc_background_thread() -> stepcompress_generate_steps() -> + steppersyncmgr_gen_steps() -> se_start_gen_steps()`. + +* Klipper uses an + [iterative solver](https://en.wikipedia.org/wiki/Root-finding_algorithm) + to generate the step times for each stepper. The step times are + generated from the background thread (klippy/chelper/steppersync.c): + `se_background_thread() -> se_generate_steps() -> itersolve_generate_steps() -> itersolve_gen_steps_range()` (in klippy/chelper/itersolve.c). The goal of the iterative solver is to find step times given a function that calculates a stepper position @@ -228,7 +229,7 @@ provides further information on the mechanics of moves. commands that correspond to the list of stepper step times built in the previous stage. These "queue_step" commands are then queued, prioritized, and sent to the micro-controller (via - stepcompress.c:steppersync and serialqueue.c:serialqueue). + steppersync.c:steppersync and serialqueue.c:serialqueue). * Processing of the queue_step commands on the micro-controller starts in src/command.c which parses the command and calls From dfa666d9c1ef2cb9ce9545ba778bc71245fe2b95 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 00:01:24 -0400 Subject: [PATCH 136/411] stepcompress: Remove stepcompress_queue_msg() Callers can use syncemitter_queue_msg() instead. Signed-off-by: Kevin O'Connor --- klippy/chelper/__init__.py | 2 -- klippy/chelper/stepcompress.c | 14 -------------- klippy/chelper/stepcompress.h | 1 - klippy/stepper.py | 4 +--- 4 files changed, 1 insertion(+), 20 deletions(-) diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 1c1f73b99..c26196e66 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -46,8 +46,6 @@ defs_stepcompress = """ , uint64_t clock, int64_t last_position); int64_t stepcompress_find_past_position(struct stepcompress *sc , uint64_t clock); - int stepcompress_queue_msg(struct stepcompress *sc - , uint32_t *data, int len); int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 0f2c4ef74..141c8ac35 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -603,20 +603,6 @@ stepcompress_find_past_position(struct stepcompress *sc, uint64_t clock) return last_position; } -// Queue an mcu command to go out in order with stepper commands -int __visible -stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len) -{ - int ret = stepcompress_flush(sc, UINT64_MAX); - if (ret) - return ret; - - struct queue_message *qm = message_alloc_and_encode(data, len); - qm->req_clock = sc->last_step_clock; - list_add_tail(&qm->node, sc->msg_queue); - return 0; -} - // Return history of queue_step commands int __visible stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index 91d1bc11d..affc1e23c 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -33,7 +33,6 @@ int stepcompress_set_last_position(struct stepcompress *sc, uint64_t clock , int64_t last_position); int64_t stepcompress_find_past_position(struct stepcompress *sc , uint64_t clock); -int stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len); int stepcompress_extract_old(struct stepcompress *sc , struct pull_history_steps *p, int max , uint64_t start_clock, uint64_t end_clock); diff --git a/klippy/stepper.py b/klippy/stepper.py index 3bcca565c..248e487d2 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -205,9 +205,7 @@ class MCU_stepper: if ret: raise error("Internal error in stepcompress") data = (self._reset_cmd_tag, self._oid, 0) - ret = ffi_lib.stepcompress_queue_msg(self._stepqueue, data, len(data)) - if ret: - raise error("Internal error in stepcompress") + ffi_lib.syncemitter_queue_msg(self._syncemitter, 0, data, len(data)) self._query_mcu_position() def _query_mcu_position(self): if self._mcu.is_fileoutput(): From 1da2e39b856eb4a334a59791936af9efab5b625f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 24 Sep 2025 15:30:46 -0400 Subject: [PATCH 137/411] docs: Update Code_Overview.md with recent motion generation changes Signed-off-by: Kevin O'Connor --- docs/Code_Overview.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index e8a61068c..56f98d600 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -129,7 +129,7 @@ There are several threads in the Klipper host code: the main Python thread. * A thread per stepper motor that calculates the timing of stepper motor step pulses and compresses those times. This thread resides in - the **klippy/chelper/stepcompress.c** C code and its operation is + the **klippy/chelper/steppersync.c** C code and its operation is generally not exposed to the Python code. ## Code flow of a move command @@ -151,9 +151,10 @@ provides further information on the mechanics of moves. * The ToolHead class (in toolhead.py) handles "look-ahead" and tracks the timing of printing actions. The main codepath for a move is: - `ToolHead.move() -> LookAheadQueue.add_move() -> - LookAheadQueue.flush() -> Move.set_junction() -> - ToolHead._process_moves() -> trapq_append()`. + `ToolHead.move() -> LookAheadQueue.add_move()`, then + `ToolHead.move() -> ToolHead._process_lookahead() -> + LookAheadQueue.flush() -> Move.set_junction()`, and then + `ToolHead._process_lookahead() -> trapq_append()`. * ToolHead.move() creates a Move() object with the parameters of the move (in cartesian space and in units of seconds and millimeters). * The kinematics class is given the opportunity to audit each move @@ -172,7 +173,7 @@ provides further information on the mechanics of moves. phase, followed by a constant deceleration phase. Every move contains these three phases in this order, but some phases may be of zero duration. - * When ToolHead._process_moves() is called, everything about the + * When ToolHead._process_lookahead() resumes, everything about the move is known - its start location, its end location, its acceleration, its start/cruising/end velocity, and distance traveled during acceleration/cruising/deceleration. All the information is @@ -184,9 +185,9 @@ provides further information on the mechanics of moves. C code. * Note that the extruder is handled in its own kinematic class: - `ToolHead._process_moves() -> PrinterExtruder.process_move()`. Since - the Move() class specifies the exact movement time and since step - pulses are sent to the micro-controller with specific timing, + `ToolHead._process_lookahead() -> PrinterExtruder.process_move()`. + Since the Move() class specifies the exact movement time and since + step pulses are sent to the micro-controller with specific timing, stepper movements produced by the extruder class will be in sync with head movement even though the code is kept separate. From 3c01f71d9e2b760cbbe567da90684af486731f95 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 24 Sep 2025 18:52:10 -0400 Subject: [PATCH 138/411] itersolve: Don't call trapq_check_sentinels() from itersolve_generate_steps() Commit a89694ac changed the code to run itersolve_generate_steps() from multiple threads simultaneously. However, trapq_check_sentinels() can modify the shared trapq object. So, calling it from multiple threads could introduce a race condition. Move the call to trapq_check_sentinels() to steppersyncmgr_gen_steps() to avoid the issue. Signed-off-by: Kevin O'Connor --- klippy/chelper/itersolve.c | 1 - klippy/chelper/steppersync.c | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/klippy/chelper/itersolve.c b/klippy/chelper/itersolve.c index 9b1206249..5b1683606 100644 --- a/klippy/chelper/itersolve.c +++ b/klippy/chelper/itersolve.c @@ -151,7 +151,6 @@ itersolve_generate_steps(struct stepper_kinematics *sk, struct stepcompress *sc sk->last_flush_time = flush_time; if (!sk->tq) return 0; - trapq_check_sentinels(sk->tq); struct move *m = list_first_entry(&sk->tq->moves, struct move, node); while (last_flush_time >= m->print_time + m->move_t) m = list_next_entry(m, node); diff --git a/klippy/chelper/steppersync.c b/klippy/chelper/steppersync.c index 4dede38fb..6b571f4b6 100644 --- a/klippy/chelper/steppersync.c +++ b/klippy/chelper/steppersync.c @@ -21,6 +21,7 @@ #include "serialqueue.h" // struct queue_message #include "stepcompress.h" // stepcompress_flush #include "steppersync.h" // steppersync_alloc +#include "trapq.h" // trapq_check_sentinels /**************************************************************** @@ -397,6 +398,17 @@ steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time , double gen_steps_time, double clear_history_time) { struct steppersync *ss; + // Prepare trapqs for step generation + list_for_each_entry(ss, &ssm->ss_list, ssm_node) { + struct syncemitter *se; + list_for_each_entry(se, &ss->se_list, ss_node) { + if (!se->sc || !se->sk) + continue; + struct trapq *tq = itersolve_get_trapq(se->sk); + if (tq) + trapq_check_sentinels(tq); + } + } // Start step generation threads list_for_each_entry(ss, &ssm->ss_list, ssm_node) { uint64_t flush_clock = clock_from_time(&ss->ce, flush_time); From 8c7693c0482ce7da0cd2334017664b4bc44996fc Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 19 Sep 2025 22:29:57 +0200 Subject: [PATCH 139/411] uc1701: allow non blocking i2c writes Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/bus.py | 3 +++ klippy/extras/display/uc1701.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 9fb466390..b04fbe764 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -212,6 +212,9 @@ class MCU_I2C: "i2c_read oid=%c reg=%*s read_len=%u", "i2c_read_response oid=%c response=%*s", oid=self.oid, cq=self.cmd_queue) + def i2c_write_noack(self, data, minclock=0, reqclock=0): + self.i2c_write_cmd.send([self.oid, data], + minclock=minclock, reqclock=reqclock) def i2c_write(self, data, minclock=0, reqclock=0): if self.i2c_write_cmd is None: self._to_write.append(data) diff --git a/klippy/extras/display/uc1701.py b/klippy/extras/display/uc1701.py index 8e877cf2e..85b74decd 100644 --- a/klippy/extras/display/uc1701.py +++ b/klippy/extras/display/uc1701.py @@ -138,7 +138,7 @@ class I2C: hdr = 0x00 cmds = bytearray(cmds) cmds.insert(0, hdr) - self.i2c.i2c_write(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK) + self.i2c.i2c_write_noack(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK) # Helper code for toggling a reset pin on startup class ResetHelper: From 30a1f22e1d8feac8c7d3dd541105cdd4af255c5d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 26 Sep 2025 11:59:07 -0400 Subject: [PATCH 140/411] motion_queuing: Improve run to run stability of flushing when in debug mode Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 3491545a8..93f5594cd 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -224,8 +224,8 @@ class PrinterMotionQueuing: flush_count = 0 while self.last_step_gen_time < faux_time: target = self.last_step_gen_time + batch_time - if flush_count > 100.: - target = faux_time + if flush_count > 100. and faux_time > target: + target += int((faux_time-target) / batch_time) * batch_time self._advance_flush_time(0., target) flush_count += 1 if flush_count: From 870c0437e92e1de152078ba49d53420ec72d2f11 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 24 Sep 2025 14:22:24 -0400 Subject: [PATCH 141/411] stm32: Verify pin is valid in gpio_peripheral() Convert direct lookup of digital_regs[] to a new gpio_pin_to_regs() function that first validates the pin. This should help prevent invalid memory accesses if an invalid pin is provided. Signed-off-by: Kevin O'Connor --- src/stm32/gpio.c | 20 +++++++++----------- src/stm32/gpioperiph.c | 2 +- src/stm32/internal.h | 2 +- src/stm32/stm32f1.c | 2 +- src/stm32/stm32h7_gpio.c | 21 +++++++++------------ 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/stm32/gpio.c b/src/stm32/gpio.c index d19437cbe..0ef11822e 100644 --- a/src/stm32/gpio.c +++ b/src/stm32/gpio.c @@ -33,7 +33,7 @@ DECL_ENUMERATION_RANGE("pin", "PH0", GPIO('H', 0), 16); DECL_ENUMERATION_RANGE("pin", "PI0", GPIO('I', 0), 16); #endif -GPIO_TypeDef * const digital_regs[] = { +static GPIO_TypeDef * const digital_regs[] = { ['A' - 'A'] = GPIOA, GPIOB, GPIOC, #ifdef GPIOD ['D' - 'A'] = GPIOD, @@ -66,20 +66,20 @@ regs_to_pin(GPIO_TypeDef *regs, uint32_t bit) return 0; } -// Verify that a gpio is a valid pin -static int -gpio_valid(uint32_t pin) +// Verify that a gpio is a valid pin and return its hardware register +GPIO_TypeDef * +gpio_pin_to_regs(uint32_t pin) { uint32_t port = GPIO2PORT(pin); - return port < ARRAY_SIZE(digital_regs) && digital_regs[port]; + if (port >= ARRAY_SIZE(digital_regs) || !digital_regs[port]) + shutdown("Not a valid pin"); + return digital_regs[port]; } struct gpio_out gpio_out_setup(uint32_t pin, uint32_t val) { - if (!gpio_valid(pin)) - shutdown("Not an output pin"); - GPIO_TypeDef *regs = digital_regs[GPIO2PORT(pin)]; + GPIO_TypeDef *regs = gpio_pin_to_regs(pin); gpio_clock_enable(regs); struct gpio_out g = { .regs=regs, .bit=GPIO2BIT(pin) }; gpio_out_reset(g, val); @@ -129,9 +129,7 @@ gpio_out_write(struct gpio_out g, uint32_t val) struct gpio_in gpio_in_setup(uint32_t pin, int32_t pull_up) { - if (!gpio_valid(pin)) - shutdown("Not a valid input pin"); - GPIO_TypeDef *regs = digital_regs[GPIO2PORT(pin)]; + GPIO_TypeDef *regs = gpio_pin_to_regs(pin); struct gpio_in g = { .regs=regs, .bit=GPIO2BIT(pin) }; gpio_in_reset(g, pull_up); return g; diff --git a/src/stm32/gpioperiph.c b/src/stm32/gpioperiph.c index 10d03738e..84fbebf5c 100644 --- a/src/stm32/gpioperiph.c +++ b/src/stm32/gpioperiph.c @@ -10,7 +10,7 @@ void gpio_peripheral(uint32_t gpio, uint32_t mode, int pullup) { - GPIO_TypeDef *regs = digital_regs[GPIO2PORT(gpio)]; + GPIO_TypeDef *regs = gpio_pin_to_regs(gpio); // Enable GPIO clock gpio_clock_enable(regs); diff --git a/src/stm32/internal.h b/src/stm32/internal.h index 1d1e0a96b..a30bb6219 100644 --- a/src/stm32/internal.h +++ b/src/stm32/internal.h @@ -25,7 +25,7 @@ #endif // gpio.c -extern GPIO_TypeDef * const digital_regs[]; +GPIO_TypeDef *gpio_pin_to_regs(uint32_t pin); #define GPIO(PORT, NUM) (((PORT)-'A') * 16 + (NUM)) #define GPIO2PORT(PIN) ((PIN) / 16) #define GPIO2BIT(PIN) (1<<((PIN) % 16)) diff --git a/src/stm32/stm32f1.c b/src/stm32/stm32f1.c index 0e4cb7821..94cfe1390 100644 --- a/src/stm32/stm32f1.c +++ b/src/stm32/stm32f1.c @@ -115,7 +115,7 @@ stm32f1_alternative_remap(uint32_t mapr_mask, uint32_t mapr_value) void gpio_peripheral(uint32_t gpio, uint32_t mode, int pullup) { - GPIO_TypeDef *regs = digital_regs[GPIO2PORT(gpio)]; + GPIO_TypeDef *regs = gpio_pin_to_regs(gpio); // Enable GPIO clock gpio_clock_enable(regs); diff --git a/src/stm32/stm32h7_gpio.c b/src/stm32/stm32h7_gpio.c index f653c5708..78a1b4a45 100644 --- a/src/stm32/stm32h7_gpio.c +++ b/src/stm32/stm32h7_gpio.c @@ -33,7 +33,7 @@ DECL_ENUMERATION_RANGE("pin", "PH0", GPIO('H', 0), 16); DECL_ENUMERATION_RANGE("pin", "PI0", GPIO('I', 0), 16); #endif -GPIO_TypeDef * const digital_regs[] = { +static GPIO_TypeDef * const digital_regs[] = { ['A' - 'A'] = GPIOA, GPIOB, GPIOC, #ifdef GPIOD ['D' - 'A'] = GPIOD, @@ -66,12 +66,14 @@ regs_to_pin(GPIO_TypeDef *regs, uint32_t bit) return 0; } -// Verify that a gpio is a valid pin -static int -gpio_valid(uint32_t pin) +// Verify that a gpio is a valid pin and return its hardware register +GPIO_TypeDef * +gpio_pin_to_regs(uint32_t pin) { uint32_t port = GPIO2PORT(pin); - return port < ARRAY_SIZE(digital_regs) && digital_regs[port]; + if (port >= ARRAY_SIZE(digital_regs) || !digital_regs[port]) + shutdown("Not a valid pin"); + return digital_regs[port]; } // The stm32h7 has very slow read access to the gpio registers. Cache @@ -83,10 +85,7 @@ static struct odr_cache { struct gpio_out gpio_out_setup(uint32_t pin, uint32_t val) { - if (!gpio_valid(pin)) - shutdown("Not an output pin"); - uint32_t port = GPIO2PORT(pin); - GPIO_TypeDef *regs = digital_regs[port]; + GPIO_TypeDef *regs = gpio_pin_to_regs(pin); gpio_clock_enable(regs); struct gpio_out g = { .regs=regs, .oc=&ODR_CACHE[pin] }; @@ -141,9 +140,7 @@ gpio_out_write(struct gpio_out g, uint32_t val) struct gpio_in gpio_in_setup(uint32_t pin, int32_t pull_up) { - if (!gpio_valid(pin)) - shutdown("Not a valid input pin"); - GPIO_TypeDef *regs = digital_regs[GPIO2PORT(pin)]; + GPIO_TypeDef *regs = gpio_pin_to_regs(pin); struct gpio_in g = { .regs=regs, .bit=GPIO2BIT(pin) }; gpio_in_reset(g, pull_up); return g; From 6e73181c473b6738f78bb0e2de7a2f7579493d1d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 12:26:34 -0400 Subject: [PATCH 142/411] stm32: No need to special case GPIOI in stm32h7_spi.c Signed-off-by: Kevin O'Connor --- src/stm32/stm32h7_spi.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/stm32/stm32h7_spi.c b/src/stm32/stm32h7_spi.c index 1d8c2afdf..82a5ea0eb 100644 --- a/src/stm32/stm32h7_spi.c +++ b/src/stm32/stm32h7_spi.c @@ -34,10 +34,8 @@ DECL_ENUMERATION("spi_bus", "spi5a", 7); DECL_CONSTANT_STR("BUS_PINS_spi5a", "PH7,PF11,PH6"); DECL_ENUMERATION("spi_bus", "spi6", 8); DECL_CONSTANT_STR("BUS_PINS_spi6", "PG12,PG14,PG13"); -#ifdef GPIOI DECL_ENUMERATION("spi_bus", "spi2b", 9); DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); -#endif static const struct spi_info spi_bus[] = { { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), GPIO_FUNCTION(5) }, @@ -49,9 +47,7 @@ static const struct spi_info spi_bus[] = { { SPI5, GPIO('F', 8), GPIO('F', 9), GPIO('F', 7), GPIO_FUNCTION(5) }, { SPI5, GPIO('H', 7), GPIO('F', 11), GPIO('H', 6), GPIO_FUNCTION(5) }, { SPI6, GPIO('G', 12), GPIO('G', 14), GPIO('G', 13), GPIO_FUNCTION(5)}, -#ifdef GPIOI { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), GPIO_FUNCTION(5) }, -#endif }; struct spi_config From af17c8c238ba8cdeaaabb87b25a3ca2375b47c11 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 12:31:27 -0400 Subject: [PATCH 143/411] stm32: No need to special case GPIOI in spi.c Signed-off-by: Kevin O'Connor --- src/stm32/spi.c | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/stm32/spi.c b/src/stm32/spi.c index 5e3e428be..62c5eed18 100644 --- a/src/stm32/spi.c +++ b/src/stm32/spi.c @@ -86,19 +86,14 @@ struct spi_info { DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 5); DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10"); - #ifdef GPIOI - DECL_ENUMERATION("spi_bus", "spi2_PI2_PI3_PI1", 6); - DECL_CONSTANT_STR("BUS_PINS_spi2_PI2_PI3_PI1", "PI2,PI3,PI1"); - #define SPI4_INDEX (1 + 6) - #else - #define SPI4_INDEX (0 + 6) - #endif + DECL_ENUMERATION("spi_bus", "spi2_PI2_PI3_PI1", 6); + DECL_CONSTANT_STR("BUS_PINS_spi2_PI2_PI3_PI1", "PI2,PI3,PI1"); #ifdef SPI4 - DECL_ENUMERATION("spi_bus", "spi4_PE13_PE14_PE12", SPI4_INDEX); + DECL_ENUMERATION("spi_bus", "spi4_PE13_PE14_PE12", 7); DECL_CONSTANT_STR("BUS_PINS_spi4_PE13_PE14_PE12", "PE13,PE14,PE12"); - #define SPI6_INDEX (1 + SPI4_INDEX) + #define SPI6_INDEX (1 + 7) #else - #define SPI6_INDEX (0 + SPI4_INDEX) + #define SPI6_INDEX (0 + 7) #endif #ifdef SPI6 DECL_ENUMERATION("spi_bus", "spi6_PG12_PG14_PG13", SPI6_INDEX); @@ -117,12 +112,10 @@ struct spi_info { DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); DECL_ENUMERATION("spi_bus", "spi3a", 5); DECL_CONSTANT_STR("BUS_PINS_spi3a", "PC11,PC12,PC10"); - #ifdef GPIOI - DECL_ENUMERATION("spi_bus", "spi2b", 6); - DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); - #endif + DECL_ENUMERATION("spi_bus", "spi2b", 6); + DECL_CONSTANT_STR("BUS_PINS_spi2b", "PI2,PI3,PI1"); #ifdef SPI4 - DECL_ENUMERATION("spi_bus", "spi4", SPI4_INDEX); + DECL_ENUMERATION("spi_bus", "spi4", 7); DECL_CONSTANT_STR("BUS_PINS_spi4", "PE13,PE14,PE12"); #endif #elif CONFIG_MACH_STM32F7 @@ -254,9 +247,7 @@ static const struct spi_info spi_bus[] = { { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(5, 5, 5) }, { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(6, 6, 6) }, { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(6, 6, 6) }, - #ifdef GPIOI { SPI2, GPIO('I', 2), GPIO('I', 3), GPIO('I', 1), SPI_FUNCTION(5, 5, 5) }, - #endif #ifdef SPI4 { SPI4, GPIO('E', 13), GPIO('E', 14), GPIO('E', 12), SPI_FUNCTION(5, 5, 5) }, #endif From 366fb423c5efbee418c109724b9f53bb99cf448a Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Sat, 27 Sep 2025 16:32:29 +0800 Subject: [PATCH 144/411] stm32: Add spi2_PB6_PB7_PB8 and spi3_PC11_PC12_PC10 for stm32g0 Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- src/stm32/spi.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/stm32/spi.c b/src/stm32/spi.c index 62c5eed18..358d21a26 100644 --- a/src/stm32/spi.c +++ b/src/stm32/spi.c @@ -151,9 +151,13 @@ struct spi_info { DECL_CONSTANT_STR("BUS_PINS_spi2_PC2_PC3_PB10", "PC2,PC3,PB10"); DECL_ENUMERATION("spi_bus", "spi2_PB2_PB11_PB10", 4); DECL_CONSTANT_STR("BUS_PINS_spi2_PB2_PB11_PB10", "PB2,PB11,PB10"); + DECL_ENUMERATION("spi_bus", "spi2_PB6_PB7_PB8", 5); + DECL_CONSTANT_STR("BUS_PINS_spi2_PB6_PB7_PB8", "PB6,PB7,PB8"); #ifdef SPI3 - DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 5); + DECL_ENUMERATION("spi_bus", "spi3_PB4_PB5_PB3", 6); DECL_CONSTANT_STR("BUS_PINS_spi3_PB4_PB5_PB3", "PB4,PB5,PB3"); + DECL_ENUMERATION("spi_bus", "spi3_PC11_PC12_PC10", 7); + DECL_CONSTANT_STR("BUS_PINS_spi3_PC11_PC12_PC10", "PC11,PC12,PC10"); #endif // Deprecated "spi1" style mappings DECL_ENUMERATION("spi_bus", "spi2", 0); @@ -165,7 +169,7 @@ struct spi_info { DECL_ENUMERATION("spi_bus", "spi2a", 3); DECL_CONSTANT_STR("BUS_PINS_spi2a", "PC2,PC3,PB10"); #ifdef SPI3 - DECL_ENUMERATION("spi_bus", "spi3", 5); + DECL_ENUMERATION("spi_bus", "spi3", 6); DECL_CONSTANT_STR("BUS_PINS_spi3", "PB4,PB5,PB3"); #endif #elif CONFIG_MACH_STM32G4 @@ -266,8 +270,10 @@ static const struct spi_info spi_bus[] = { { SPI1, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(0, 0, 0) }, { SPI2, GPIO('C', 2), GPIO('C', 3), GPIO('B', 10), SPI_FUNCTION(1, 1, 5) }, { SPI2, GPIO('B', 2), GPIO('B', 11), GPIO('B', 10), SPI_FUNCTION(1, 0, 5) }, + { SPI2, GPIO('B', 6), GPIO('B', 7), GPIO('B', 8), SPI_FUNCTION(4, 1, 1) }, #ifdef SPI3 { SPI3, GPIO('B', 4), GPIO('B', 5), GPIO('B', 3), SPI_FUNCTION(9, 9, 9) }, + { SPI3, GPIO('C', 11), GPIO('C', 12), GPIO('C', 10), SPI_FUNCTION(4, 4, 4) }, #endif #elif CONFIG_MACH_STM32G4 { SPI2, GPIO('B', 14), GPIO('B', 15), GPIO('B', 13), SPI_FUNCTION(5, 5, 5) }, From 9e6430aa600b2532926285c15c4a80135d2ce37a Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Sat, 27 Sep 2025 16:49:16 +0800 Subject: [PATCH 145/411] config: Modify software SPI to hardware SPI for BIGTREETECH boards Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- config/generic-bigtreetech-manta-e3ez.cfg | 24 +++++-------------- config/sample-bigtreetech-ebb-canbus-v1.1.cfg | 4 +--- config/sample-bigtreetech-ebb-canbus-v1.2.cfg | 4 +--- .../sample-bigtreetech-ebb-sb-canbus-v1.0.cfg | 4 +--- 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/config/generic-bigtreetech-manta-e3ez.cfg b/config/generic-bigtreetech-manta-e3ez.cfg index 199eae708..39f12fd2a 100644 --- a/config/generic-bigtreetech-manta-e3ez.cfg +++ b/config/generic-bigtreetech-manta-e3ez.cfg @@ -133,44 +133,34 @@ max_z_accel: 100 #[tmc2130 stepper_x] #cs_pin: PB8 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 ##diag1_pin: PF3 #run_current: 0.800 #stealthchop_threshold: 999999 #[tmc2130 stepper_y] #cs_pin: PC9 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 ##diag1_pin: PF4 #run_current: 0.800 #stealthchop_threshold: 999999 #[tmc2130 stepper_z] #cs_pin: PD0 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 ##diag1_pin: PF5 #run_current: 0.650 #stealthchop_threshold: 999999 #[tmc2130 extruder] #cs_pin: PD1 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 #run_current: 0.800 #stealthchop_threshold: 999999 #[tmc2130 extruder1] #cs_pin: PB5 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 #run_current: 0.800 #stealthchop_threshold: 999999 @@ -195,6 +185,4 @@ aliases: #[adxl345] #cs_pin: PC15 -#spi_software_miso_pin: PC11 -#spi_software_mosi_pin: PC12 -#spi_software_sclk_pin: PC10 +#spi_bus: spi3_PC11_PC12_PC10 diff --git a/config/sample-bigtreetech-ebb-canbus-v1.1.cfg b/config/sample-bigtreetech-ebb-canbus-v1.1.cfg index c84abf173..8da7cccd4 100644 --- a/config/sample-bigtreetech-ebb-canbus-v1.1.cfg +++ b/config/sample-bigtreetech-ebb-canbus-v1.1.cfg @@ -11,9 +11,7 @@ serial: /dev/serial/by-id/usb-Klipper_Klipper_firmware_12345-if00 [adxl345] cs_pin: EBBCan:PB12 -spi_software_sclk_pin: EBBCan:PB10 -spi_software_mosi_pin: EBBCan:PB11 -spi_software_miso_pin: EBBCan:PB2 +spi_bus: spi2_PB2_PB11_PB10 axes_map: x,y,z [extruder] diff --git a/config/sample-bigtreetech-ebb-canbus-v1.2.cfg b/config/sample-bigtreetech-ebb-canbus-v1.2.cfg index 053b783c3..7b55a91cf 100644 --- a/config/sample-bigtreetech-ebb-canbus-v1.2.cfg +++ b/config/sample-bigtreetech-ebb-canbus-v1.2.cfg @@ -11,9 +11,7 @@ serial: /dev/serial/by-id/usb-Klipper_Klipper_firmware_12345-if00 [adxl345] cs_pin: EBBCan:PB12 -spi_software_sclk_pin: EBBCan:PB10 -spi_software_mosi_pin: EBBCan:PB11 -spi_software_miso_pin: EBBCan:PB2 +spi_bus: spi2_PB2_PB11_PB10 axes_map: x,y,z [extruder] diff --git a/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg b/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg index 192d385e5..1e60467dd 100644 --- a/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg +++ b/config/sample-bigtreetech-ebb-sb-canbus-v1.0.cfg @@ -15,9 +15,7 @@ sensor_pin: EBBCan:PA2 [adxl345] cs_pin: EBBCan:PB12 -spi_software_sclk_pin: EBBCan:PB10 -spi_software_mosi_pin: EBBCan:PB11 -spi_software_miso_pin: EBBCan:PB2 +spi_bus: spi2_PB2_PB11_PB10 axes_map: x,y,z [extruder] From 184ba4080ce8e7bbbd7f055d55622c92e22f6e3d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 29 Sep 2025 11:31:31 -0400 Subject: [PATCH 146/411] toolhead: Flush lookahead on dwell - fix flushing bug after long delays Commit 7ea5f5d2 changed how the lookahead queue is flushed. Previously, the main flush timer would always run while the toolhead was considered in an active state (print_time). After that commit, the flush timer could sleep if there were no steps generated (no call to note_mcu_movequeue_activity() ). This could lead to a situation where a G4 command (or series of commands) could cause the toolhead to be considered in an active state while the flush timer was disabled. The result was that a future command may not be properly flushed (the toolhead would fail to transition to a "Priming" state). Fix by ensuring that all dwell() requests fully flush the lookahead queue. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 1 + 1 file changed, 1 insertion(+) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index f54743b0a..a17750d74 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -398,6 +398,7 @@ class ToolHead: self.move(curpos, speed) self.printer.send_event("toolhead:manual_move") def dwell(self, delay): + self._flush_lookahead() next_print_time = self.get_last_move_time() + max(0., delay) self._advance_move_time(next_print_time) self._check_pause() From a683ef3503f462b1131932349df1dbb0acebbb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sezgin=20A=C3=87IKG=C3=96Z?= Date: Tue, 30 Sep 2025 03:24:13 +0300 Subject: [PATCH 147/411] spi_flash: add timestamp to firmware filenames on sdcard upload (#7063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some Creality bootloaders skip flashing if the firmware filename is unchanged. By appending a timestamp to the firmware filename during sdcard upload, each update generates a unique name, ensuring that the bootloader always accepts and flashes the new firmware. Signed-off-by: Sezgin AÇIKGÖZ --- scripts/spi_flash/board_defs.py | 3 ++- scripts/spi_flash/spi_flash.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index 4fdba64cc..0a403d7da 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -123,7 +123,8 @@ BOARD_DEFS = { 'spi_bus': "swspi", 'spi_pins': "PC8,PD2,PC12", 'cs_pin': "PC11", - 'skip_verify': True + 'skip_verify': True, + 'requires_unique_fw_name': True }, 'monster8': { 'mcu': "stm32f407xx", diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index 729dd2bbc..e9394dbe5 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -1380,7 +1380,32 @@ class MCUConnection: input_sha = hashlib.sha1() sd_sha = hashlib.sha1() klipper_bin_path = self.board_config['klipper_bin_path'] + add_ts = self.board_config.get('requires_unique_fw_name', False) fw_path = self.board_config.get('firmware_path', "firmware.bin") + if add_ts: + fw_dir = os.path.dirname(fw_path) + fw_name, fw_ext = os.path.splitext(os.path.basename(fw_path)) + ts = time.strftime("%Y%m%d%H%M%S") + fw_name_ts = f"{ts}{fw_name}{fw_ext}" + if fw_dir: + fw_path = os.path.join(fw_dir, fw_name_ts) + else: + fw_path = fw_name_ts + list_dir = fw_dir if fw_dir else "" + try: + output_line("\nSD Card FW Directory Contents:") + for f in self.fatfs.list_sd_directory(list_dir): + fname = f['name'].decode('utf-8') + if fname.endswith(fw_ext): + self.fatfs.remove_item( + os.path.join(list_dir, fname) + ) + output_line( + "Old firmware file %s found and deleted" + % (fname,) + ) + except Exception: + logging.exception("Error cleaning old firmware files") try: with open(klipper_bin_path, 'rb') as local_f: with self.fatfs.open_file(fw_path, "wb") as sd_f: From ff8c8eab5516242a654feee0f6d88700ae7ae4cc Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 26 Sep 2025 01:22:57 -0400 Subject: [PATCH 148/411] toolhead: Clarify internal minimum_cruise_ratio variable names Avoid using "smoothed" and "accel_to_decel" for variables associated with minimum_cruise_ratio. Instead introduce the prefix "mcr" for use with these variables. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index a17750d74..a65d37f18 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -45,9 +45,10 @@ class Move: self.max_start_v2 = 0. self.max_cruise_v2 = velocity**2 self.delta_v2 = 2.0 * move_d * self.accel - self.max_smoothed_v2 = 0. - self.smooth_delta_v2 = 2.0 * move_d * toolhead.max_accel_to_decel self.next_junction_v2 = 999999999.9 + # Setup for minimum_cruise_ratio checks + self.max_mcr_start_v2 = 0. + self.mcr_delta_v2 = 2.0 * move_d * toolhead.mcr_pseudo_accel def limit_speed(self, speed, accel): speed2 = speed**2 if speed2 < self.max_cruise_v2: @@ -55,7 +56,7 @@ class Move: self.min_move_t = self.move_d / speed self.accel = min(self.accel, accel) self.delta_v2 = 2.0 * self.move_d * self.accel - self.smooth_delta_v2 = min(self.smooth_delta_v2, self.delta_v2) + self.mcr_delta_v2 = min(self.mcr_delta_v2, self.delta_v2) def limit_next_junction_speed(self, speed): self.next_junction_v2 = min(self.next_junction_v2, speed**2) def move_error(self, msg="Move out of range"): @@ -94,8 +95,8 @@ class Move: move_centripetal_v2, pmove_centripetal_v2) # Apply limits self.max_start_v2 = max_start_v2 - self.max_smoothed_v2 = min( - max_start_v2, prev_move.max_smoothed_v2 + prev_move.smooth_delta_v2) + self.max_mcr_start_v2 = min( + max_start_v2, prev_move.max_mcr_start_v2 + prev_move.mcr_delta_v2) def set_junction(self, start_v2, cruise_v2, end_v2): # Determine accel, cruise, and decel portions of the move distance half_inv_accel = .5 / self.accel @@ -138,16 +139,16 @@ class LookAheadQueue: # junction speed assuming the robot comes to a complete stop # after the last move. delayed = [] - next_end_v2 = next_smoothed_v2 = peak_cruise_v2 = 0. + next_start_v2 = next_mcr_start_v2 = peak_cruise_v2 = 0. for i in range(flush_count-1, -1, -1): move = queue[i] - reachable_start_v2 = next_end_v2 + move.delta_v2 + reachable_start_v2 = next_start_v2 + move.delta_v2 start_v2 = min(move.max_start_v2, reachable_start_v2) - reachable_smoothed_v2 = next_smoothed_v2 + move.smooth_delta_v2 - smoothed_v2 = min(move.max_smoothed_v2, reachable_smoothed_v2) - if smoothed_v2 < reachable_smoothed_v2: + reach_mcr_start_v2 = next_mcr_start_v2 + move.mcr_delta_v2 + mcr_start_v2 = min(move.max_mcr_start_v2, reach_mcr_start_v2) + if mcr_start_v2 < reach_mcr_start_v2: # It's possible for this move to accelerate - if (smoothed_v2 + move.smooth_delta_v2 > next_smoothed_v2 + if (mcr_start_v2 + move.mcr_delta_v2 > next_mcr_start_v2 or delayed): # This move can decelerate or this is a full accel # move after a full decel move @@ -155,7 +156,7 @@ class LookAheadQueue: flush_count = i update_flush_count = False peak_cruise_v2 = min(move.max_cruise_v2, ( - smoothed_v2 + reachable_smoothed_v2) * .5) + mcr_start_v2 + reach_mcr_start_v2) * .5) if delayed: # Propagate peak_cruise_v2 to any delayed moves if not update_flush_count and i < flush_count: @@ -169,12 +170,12 @@ class LookAheadQueue: cruise_v2 = min((start_v2 + reachable_start_v2) * .5 , move.max_cruise_v2, peak_cruise_v2) move.set_junction(min(start_v2, cruise_v2), cruise_v2 - , min(next_end_v2, cruise_v2)) + , min(next_start_v2, cruise_v2)) else: # Delay calculating this move until peak_cruise_v2 is known - delayed.append((move, start_v2, next_end_v2)) - next_end_v2 = start_v2 - next_smoothed_v2 = smoothed_v2 + delayed.append((move, start_v2, next_start_v2)) + next_start_v2 = start_v2 + next_mcr_start_v2 = mcr_start_v2 if update_flush_count or not flush_count: return [] # Remove processed moves from the queue @@ -209,7 +210,7 @@ class ToolHead: 0.5, below=1., minval=0.) self.square_corner_velocity = config.getfloat( 'square_corner_velocity', 5., minval=0.) - self.junction_deviation = self.max_accel_to_decel = 0. + self.junction_deviation = self.mcr_pseudo_accel = 0. self._calc_junction_deviation() # Input stall detection self.check_stall_time = 0. @@ -510,7 +511,7 @@ class ToolHead: def _calc_junction_deviation(self): scv2 = self.square_corner_velocity**2 self.junction_deviation = scv2 * (math.sqrt(2.) - 1.) / self.max_accel - self.max_accel_to_decel = self.max_accel * (1. - self.min_cruise_ratio) + self.mcr_pseudo_accel = self.max_accel * (1. - self.min_cruise_ratio) def set_max_velocities(self, max_velocity, max_accel, square_corner_velocity, min_cruise_ratio): if max_velocity is not None: From 41901ec382ab3b87ee7a5ca6411a6e4bc2c0fe3f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 25 Sep 2025 14:32:05 -0400 Subject: [PATCH 149/411] toolhead: Simplify LookAheadQueue.flush() code Replace "delayed" storage with a full pass through the queue. This simplifies the lookahead processing logic. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index a65d37f18..6ebacd0a9 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -138,46 +138,45 @@ class LookAheadQueue: # Traverse queue from last to first move and determine maximum # junction speed assuming the robot comes to a complete stop # after the last move. - delayed = [] + junction_info = [None] * flush_count next_start_v2 = next_mcr_start_v2 = peak_cruise_v2 = 0. + pending_cv2_assign = 0 for i in range(flush_count-1, -1, -1): move = queue[i] reachable_start_v2 = next_start_v2 + move.delta_v2 start_v2 = min(move.max_start_v2, reachable_start_v2) + cruise_v2 = None + pending_cv2_assign += 1 reach_mcr_start_v2 = next_mcr_start_v2 + move.mcr_delta_v2 mcr_start_v2 = min(move.max_mcr_start_v2, reach_mcr_start_v2) if mcr_start_v2 < reach_mcr_start_v2: # It's possible for this move to accelerate if (mcr_start_v2 + move.mcr_delta_v2 > next_mcr_start_v2 - or delayed): - # This move can decelerate or this is a full accel - # move after a full decel move + or pending_cv2_assign > 1): + # This move can both accel and decel, or this is a + # full accel move followed by a full decel move if update_flush_count and peak_cruise_v2: flush_count = i update_flush_count = False - peak_cruise_v2 = min(move.max_cruise_v2, ( - mcr_start_v2 + reach_mcr_start_v2) * .5) - if delayed: - # Propagate peak_cruise_v2 to any delayed moves - if not update_flush_count and i < flush_count: - mc_v2 = peak_cruise_v2 - for m, ms_v2, me_v2 in reversed(delayed): - mc_v2 = min(mc_v2, ms_v2) - m.set_junction(min(ms_v2, mc_v2), mc_v2 - , min(me_v2, mc_v2)) - del delayed[:] - if not update_flush_count and i < flush_count: - cruise_v2 = min((start_v2 + reachable_start_v2) * .5 - , move.max_cruise_v2, peak_cruise_v2) - move.set_junction(min(start_v2, cruise_v2), cruise_v2 - , min(next_start_v2, cruise_v2)) - else: - # Delay calculating this move until peak_cruise_v2 is known - delayed.append((move, start_v2, next_start_v2)) + peak_cruise_v2 = (mcr_start_v2 + reach_mcr_start_v2) * .5 + cruise_v2 = min((start_v2 + reachable_start_v2) * .5 + , move.max_cruise_v2, peak_cruise_v2) + pending_cv2_assign = 0 + junction_info[i] = (move, start_v2, cruise_v2, next_start_v2) next_start_v2 = start_v2 next_mcr_start_v2 = mcr_start_v2 if update_flush_count or not flush_count: return [] + # Traverse queue in forward direction to propagate cruise_v2 + prev_cruise_v2 = 0. + for i in range(flush_count): + move, start_v2, cruise_v2, next_start_v2 = junction_info[i] + if cruise_v2 is None: + # This move can't accelerate - propagate cruise_v2 from previous + cruise_v2 = min(prev_cruise_v2, start_v2) + move.set_junction(min(start_v2, cruise_v2), cruise_v2 + , min(next_start_v2, cruise_v2)) + prev_cruise_v2 = cruise_v2 # Remove processed moves from the queue res = queue[:flush_count] del queue[:flush_count] From 6118525c19fdc7403817782591e584278866939f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 26 Sep 2025 11:43:19 -0400 Subject: [PATCH 150/411] toolhead: Allow more entries to flush from "lazy" lookahead flush Previously the code would always keep at least 2 items on the lookahead queue after a "lazy" flush. In most cases it's okay to leave only a single item. Update the code to better handle flushing of items that are fully ready. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 6ebacd0a9..6326e607f 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -156,7 +156,7 @@ class LookAheadQueue: # This move can both accel and decel, or this is a # full accel move followed by a full decel move if update_flush_count and peak_cruise_v2: - flush_count = i + flush_count = i + pending_cv2_assign update_flush_count = False peak_cruise_v2 = (mcr_start_v2 + reach_mcr_start_v2) * .5 cruise_v2 = min((start_v2 + reachable_start_v2) * .5 From fe09e2e6bfe677959315446086bfb9c4de9536b2 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 16 Sep 2025 00:23:01 +0200 Subject: [PATCH 151/411] klippy: track the device model Signed-off-by: Timofey Titovets --- klippy/klippy.py | 2 ++ klippy/util.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/klippy/klippy.py b/klippy/klippy.py index 316343cbd..df86f49fa 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -327,12 +327,14 @@ def main(): extra_git_desc += "\nTracked URL: %s" % (git_info["url"]) start_args['software_version'] = git_vers start_args['cpu_info'] = util.get_cpu_info() + start_args['device'] = util.get_device_info() if bglogger is not None: versions = "\n".join([ "Args: %s" % (sys.argv,), "Git version: %s%s" % (repr(start_args['software_version']), extra_git_desc), "CPU: %s" % (start_args['cpu_info'],), + "Device: %s" % (start_args['device']), "Python: %s" % (repr(sys.version),)]) logging.info(versions) elif not options.debugoutput: diff --git a/klippy/util.py b/klippy/util.py index 6a8baee7f..9b64ed8f2 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -125,6 +125,17 @@ def get_cpu_info(): model_name = dict(lines).get("model name", "?") return "%d core %s" % (core_count, model_name) +def get_device_info(): + try: + f = open('/proc/device-tree/model', 'r') + data = f.read() + f.close() + except (IOError, OSError) as e: + logging.debug("Exception on read /proc/device-tree/model: %s", + traceback.format_exc()) + return "?" + return data.rstrip(' \0') + def get_version_from_file(klippy_src): try: with open(os.path.join(klippy_src, '.version')) as h: From ac6f059cb9235eb1a51ae9768edaaf3a922538c3 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 25 Sep 2025 00:32:50 +0200 Subject: [PATCH 152/411] util: use dmi data on x86 instead of device-tree Signed-off-by: Timofey Titovets --- klippy/util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/klippy/util.py b/klippy/util.py index 9b64ed8f2..d4de87788 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -127,7 +127,12 @@ def get_cpu_info(): def get_device_info(): try: - f = open('/proc/device-tree/model', 'r') + path = '/proc/device-tree/model' + if not os.access(path, os.F_OK): + path = "/sys/class/dmi/id/product_name" + if not os.access(path, os.F_OK): + return "?" + f = open(path, 'r') data = f.read() f.close() except (IOError, OSError) as e: From 07c5973142533d13393a7a16b390a3553bc859f5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 21:21:00 -0400 Subject: [PATCH 153/411] util: Introduce _try_read_file() helper Introduce a helper function to read the contents of a file and use that helper throughout util.py . Signed-off-by: Kevin O'Connor --- klippy/util.py | 58 ++++++++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/klippy/util.py b/klippy/util.py index d4de87788..68a999bcd 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -51,6 +51,15 @@ def create_pty(ptyname): # Helper code for extracting mcu build info ###################################################################### +def _try_read_file(filename, maxsize=32*1024): + try: + with open(filename, 'r') as f: + return f.read(maxsize) + except (IOError, OSError) as e: + logging.debug("Exception on read %s: %s", filename, + traceback.format_exc()) + return None + def dump_file_stats(build_dir, filename): fname = os.path.join(build_dir, filename) try: @@ -66,20 +75,14 @@ def dump_mcu_build(): build_dir = os.path.join(os.path.dirname(__file__), '..') # Try to log last mcu config dump_file_stats(build_dir, '.config') - try: - f = open(os.path.join(build_dir, '.config'), 'r') - data = f.read(32*1024) - f.close() + data = _try_read_file(os.path.join(build_dir, '.config')) + if data is not None: logging.info("========= Last MCU build config =========\n%s" "=======================", data) - except: - pass # Try to log last mcu build version dump_file_stats(build_dir, 'out/klipper.dict') try: - f = open(os.path.join(build_dir, 'out/klipper.dict'), 'r') - data = f.read(32*1024) - f.close() + data = _try_read_file(os.path.join(build_dir, 'out/klipper.dict')) data = json.loads(data) logging.info("Last MCU build version: %s", data.get('version', '')) logging.info("Last MCU build tools: %s", data.get('build_versions', '')) @@ -111,13 +114,8 @@ setup_python2_wrappers() ###################################################################### def get_cpu_info(): - try: - f = open('/proc/cpuinfo', 'r') - data = f.read() - f.close() - except (IOError, OSError) as e: - logging.debug("Exception on read /proc/cpuinfo: %s", - traceback.format_exc()) + data = _try_read_file('/proc/cpuinfo', maxsize=1024*1024) + if data is None: return "?" lines = [l.split(':', 1) for l in data.split('\n')] lines = [(l[0].strip(), l[1].strip()) for l in lines if len(l) == 2] @@ -126,28 +124,18 @@ def get_cpu_info(): return "%d core %s" % (core_count, model_name) def get_device_info(): - try: - path = '/proc/device-tree/model' - if not os.access(path, os.F_OK): - path = "/sys/class/dmi/id/product_name" - if not os.access(path, os.F_OK): - return "?" - f = open(path, 'r') - data = f.read() - f.close() - except (IOError, OSError) as e: - logging.debug("Exception on read /proc/device-tree/model: %s", - traceback.format_exc()) - return "?" + data = _try_read_file('/proc/device-tree/model') + if data is None: + data = _try_read_file("/sys/class/dmi/id/product_name") + if data is None: + return "?" return data.rstrip(' \0') def get_version_from_file(klippy_src): - try: - with open(os.path.join(klippy_src, '.version')) as h: - return h.read().rstrip() - except IOError: - pass - return "?" + data = _try_read_file(os.path.join(klippy_src, '.version')) + if data is None: + return "?" + return data.rstrip() def _get_repo_info(gitdir): repo_info = {"branch": "?", "remote": "?", "url": "?"} From 35aeb78088f2a5c76c1945266e3458e097dc0c69 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 21:22:53 -0400 Subject: [PATCH 154/411] util: Strip all leading/trailing whitespace in get_device_info() Signed-off-by: Kevin O'Connor --- klippy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/util.py b/klippy/util.py index 68a999bcd..8e08e0438 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -129,7 +129,7 @@ def get_device_info(): data = _try_read_file("/sys/class/dmi/id/product_name") if data is None: return "?" - return data.rstrip(' \0') + return data.rstrip(' \0').strip() def get_version_from_file(klippy_src): data = _try_read_file(os.path.join(klippy_src, '.version')) From ea9c88526b390032c8f43a71332ab7c3ee8ee403 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 21:27:36 -0400 Subject: [PATCH 155/411] klippy: Report Linux version in log Signed-off-by: Kevin O'Connor --- klippy/klippy.py | 2 ++ klippy/util.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/klippy/klippy.py b/klippy/klippy.py index df86f49fa..1d3ffbf06 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -328,6 +328,7 @@ def main(): start_args['software_version'] = git_vers start_args['cpu_info'] = util.get_cpu_info() start_args['device'] = util.get_device_info() + start_args['linux_version'] = util.get_linux_version() if bglogger is not None: versions = "\n".join([ "Args: %s" % (sys.argv,), @@ -335,6 +336,7 @@ def main(): extra_git_desc), "CPU: %s" % (start_args['cpu_info'],), "Device: %s" % (start_args['device']), + "Linux: %s" % (start_args['linux_version']), "Python: %s" % (repr(sys.version),)]) logging.info(versions) elif not options.debugoutput: diff --git a/klippy/util.py b/klippy/util.py index 8e08e0438..9abb94df6 100644 --- a/klippy/util.py +++ b/klippy/util.py @@ -131,6 +131,12 @@ def get_device_info(): return "?" return data.rstrip(' \0').strip() +def get_linux_version(): + data = _try_read_file('/proc/version') + if data is None: + return "?" + return data.strip() + def get_version_from_file(klippy_src): data = _try_read_file(os.path.join(klippy_src, '.version')) if data is None: From 07466411acc398e6132887f544d7415ab628a40f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 13:57:34 -0400 Subject: [PATCH 156/411] mcu: Remove max_stepper_error config parameter Use a regular code constant - MAX_STEPCOMPRESS_ERROR in stepper.py. Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 3 +++ klippy/mcu.py | 5 ----- klippy/stepper.py | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 296343b12..3e7f0daf7 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,9 @@ All dates in this document are approximate. ## Changes +20251003: Support for the undocumented `max_stepper_error` option in +the `[printer]` config section has been removed. + 20250916: The definitions of EI, 2HUMP_EI, and 3HUMP_EI input shapers were updated. For best performance it is recommended to recalibrate input shapers, especially if some of these shapers are currently used. diff --git a/klippy/mcu.py b/klippy/mcu.py index 74e2164e7..6a7e84b66 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -603,9 +603,6 @@ class MCU: self._restart_cmds = [] self._init_cmds = [] self._mcu_freq = 0. - # Move command queuing - self._max_stepper_error = config.getfloat('max_stepper_error', 0.000025, - minval=0.) self._reserved_move_slots = 0 # Stats self._get_status_info = {} @@ -871,8 +868,6 @@ class MCU: return self.print_time_to_clock(t) + slot def seconds_to_clock(self, time): return int(time * self._mcu_freq) - def get_max_stepper_error(self): - return self._max_stepper_error def min_schedule_time(self): return MIN_SCHEDULE_TIME def max_nominal_duration(self): diff --git a/klippy/stepper.py b/klippy/stepper.py index 248e487d2..8d93d6c16 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -16,6 +16,7 @@ class error(Exception): MIN_BOTH_EDGE_DURATION = 0.000000500 MIN_OPTIMIZED_BOTH_EDGE_DURATION = 0.000000150 +MAX_STEPCOMPRESS_ERROR = 0.000025 # Interface to low-level mcu and chelper code class MCU_stepper: @@ -122,8 +123,7 @@ class MCU_stepper: self._get_position_cmd = self._mcu.lookup_query_command( "stepper_get_position oid=%c", "stepper_position oid=%c pos=%i", oid=self._oid) - max_error = self._mcu.get_max_stepper_error() - max_error_ticks = self._mcu.seconds_to_clock(max_error) + max_error_ticks = self._mcu.seconds_to_clock(MAX_STEPCOMPRESS_ERROR) ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.stepcompress_fill(self._stepqueue, self._oid, max_error_ticks, step_cmd_tag, dir_cmd_tag) From ce55d4116624177e8f6ced9a5320953ebfd024d5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 20:34:51 -0400 Subject: [PATCH 157/411] mcu: Split _mcu_identify() into separate methods Split up the _mcu_identify() into several internal methods separated by functionality. Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 83 +++++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 6a7e84b66..3903e8b48 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -659,7 +659,7 @@ class MCU: self._printer.request_exit('firmware_restart') self._reactor.pause(self._reactor.monotonic() + 2.000) raise error("Attempt MCU '%s' restart failed" % (self._name,)) - def _connect_file(self, pace=False): + def _attach_file(self, pace=False): # In a debugging mode. Open debug output file and read data dictionary start_args = self._printer.get_start_args() if self._name == 'mcu': @@ -775,40 +775,32 @@ class MCU: logging.info(move_msg) log_info = self._log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) - def _mcu_identify(self): - if self.is_fileoutput(): - self._connect_file() - else: - resmeth = self._restart_method - if resmeth == 'rpi_usb' and not os.path.exists(self._serialport): - # Try toggling usb power - self._check_restart("enable power") - try: - if self._canbus_iface is not None: - cbid = self._printer.lookup_object('canbus_ids') - nodeid = cbid.get_nodeid(self._serialport) - self._serial.connect_canbus(self._serialport, nodeid, - self._canbus_iface) - elif self._baud: - # Cheetah boards require RTS to be deasserted - # else a reset will trigger the built-in bootloader. - rts = (resmeth != "cheetah") - self._serial.connect_uart(self._serialport, self._baud, rts) - else: - self._serial.connect_pipe(self._serialport) - self._clocksync.connect(self._serial) - except serialhdl.error as e: - raise error(str(e)) - logging.info(self._log_info()) - ppins = self._printer.lookup_object('pins') - pin_resolver = ppins.get_pin_resolver(self._name) - for cname, value in self.get_constants().items(): - if cname.startswith("RESERVE_PINS_"): - for pin in value.split(','): - pin_resolver.reserve_pin(pin, cname[13:]) - self._mcu_freq = self.get_constant_float('CLOCK_FREQ') - self._stats_sumsq_base = self.get_constant_float('STATS_SUMSQ_BASE') + def _attach(self): + resmeth = self._restart_method + if resmeth == 'rpi_usb' and not os.path.exists(self._serialport): + # Try toggling usb power + self._check_restart("enable power") + try: + if self._canbus_iface is not None: + cbid = self._printer.lookup_object('canbus_ids') + nodeid = cbid.get_nodeid(self._serialport) + self._serial.connect_canbus(self._serialport, nodeid, + self._canbus_iface) + elif self._baud: + # Cheetah boards require RTS to be deasserted + # else a reset will trigger the built-in bootloader. + rts = (resmeth != "cheetah") + self._serial.connect_uart(self._serialport, self._baud, rts) + else: + self._serial.connect_pipe(self._serialport) + self._clocksync.connect(self._serial) + except serialhdl.error as e: + raise error(str(e)) + def _post_attach_setup_shutdown(self): self._emergency_stop_cmd = self.lookup_command("emergency_stop") + self.register_response(self._handle_shutdown, 'shutdown') + self.register_response(self._handle_shutdown, 'is_shutdown') + def _post_attach_setup_restart(self): self._reset_cmd = self.try_lookup_command("reset") self._config_reset_cmd = self.try_lookup_command("config_reset") ext_only = self._reset_cmd is None and self._config_reset_cmd is None @@ -820,13 +812,32 @@ class MCU: self._is_mcu_bridge = True self._printer.register_event_handler("klippy:firmware_restart", self._firmware_restart_bridge) + def _post_attach_setup_stats(self): + self._stats_sumsq_base = self.get_constant_float('STATS_SUMSQ_BASE') + msgparser = self._serial.get_msgparser() version, build_versions = msgparser.get_version_info() self._get_status_info['mcu_version'] = version self._get_status_info['mcu_build_versions'] = build_versions self._get_status_info['mcu_constants'] = msgparser.get_constants() - self.register_response(self._handle_shutdown, 'shutdown') - self.register_response(self._handle_shutdown, 'is_shutdown') self.register_response(self._handle_mcu_stats, 'stats') + def _post_attach_setup_for_config(self): + self._mcu_freq = self.get_constant_float('CLOCK_FREQ') + ppins = self._printer.lookup_object('pins') + pin_resolver = ppins.get_pin_resolver(self._name) + for cname, value in self.get_constants().items(): + if cname.startswith("RESERVE_PINS_"): + for pin in value.split(','): + pin_resolver.reserve_pin(pin, cname[13:]) + def _mcu_identify(self): + if self.is_fileoutput(): + self._attach_file() + else: + self._attach() + logging.info(self._log_info()) + self._post_attach_setup_shutdown() + self._post_attach_setup_restart() + self._post_attach_setup_for_config() + self._post_attach_setup_stats() def _ready(self): if self.is_fileoutput(): return From fcd9cefb3fb4b1325515acc35de150dd1aba1333 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 20:46:22 -0400 Subject: [PATCH 158/411] mcu: Add _check_restart_on_xxx() helper methods Breakout each of the possible restart cases into its own internal method. Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 3903e8b48..6af8fe430 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -659,6 +659,21 @@ class MCU: self._printer.request_exit('firmware_restart') self._reactor.pause(self._reactor.monotonic() + 2.000) raise error("Attempt MCU '%s' restart failed" % (self._name,)) + def _check_restart_on_crc_mismatch(self): + self._check_restart("CRC mismatch") + def _check_restart_on_send_config(self): + if self._restart_method == 'rpi_usb': + # Only configure mcu after usb power reset + self._check_restart("full reset before config") + def _check_restart_on_attach(self): + resmeth = self._restart_method + if resmeth == 'rpi_usb' and not os.path.exists(self._serialport): + # Try toggling usb power + self._check_restart("enable power") + def _lookup_attach_uart_rts(self): + # Cheetah boards require RTS to be deasserted + # else a reset will trigger the built-in bootloader. + return (self._restart_method != "cheetah") def _attach_file(self, pace=False): # In a debugging mode. Open debug output file and read data dictionary start_args = self._printer.get_start_args() @@ -696,7 +711,7 @@ class MCU: config_crc = zlib.crc32(encoded_config) & 0xffffffff self.add_config_cmd("finalize_config crc=%d" % (config_crc,)) if prev_crc is not None and config_crc != prev_crc: - self._check_restart("CRC mismatch") + self._check_restart_on_crc_mismatch() raise error("MCU '%s' CRC does not match config" % (self._name,)) # Transmit config messages (if needed) self.register_response(self._handle_starting, 'starting') @@ -747,9 +762,7 @@ class MCU: def _connect(self): config_params = self._send_get_config() if not config_params['is_config']: - if self._restart_method == 'rpi_usb': - # Only configure mcu after usb power reset - self._check_restart("full reset before config") + self._check_restart_on_send_config() # Not configured - send config and issue get_config again self._send_config(None) config_params = self._send_get_config() @@ -776,10 +789,7 @@ class MCU: log_info = self._log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) def _attach(self): - resmeth = self._restart_method - if resmeth == 'rpi_usb' and not os.path.exists(self._serialport): - # Try toggling usb power - self._check_restart("enable power") + self._check_restart_on_attach() try: if self._canbus_iface is not None: cbid = self._printer.lookup_object('canbus_ids') @@ -787,9 +797,7 @@ class MCU: self._serial.connect_canbus(self._serialport, nodeid, self._canbus_iface) elif self._baud: - # Cheetah boards require RTS to be deasserted - # else a reset will trigger the built-in bootloader. - rts = (resmeth != "cheetah") + rts = self._lookup_attach_uart_rts() self._serial.connect_uart(self._serialport, self._baud, rts) else: self._serial.connect_pipe(self._serialport) From 1e18f32914d8559f5f932287df5bd9dac619452b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 20:47:40 -0400 Subject: [PATCH 159/411] mcu: Move registration of "starting" message to _post_attach_setup_shutdown() Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 6af8fe430..9bfdc978d 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -714,7 +714,6 @@ class MCU: self._check_restart_on_crc_mismatch() raise error("MCU '%s' CRC does not match config" % (self._name,)) # Transmit config messages (if needed) - self.register_response(self._handle_starting, 'starting') try: if prev_crc is None: logging.info("Sending MCU '%s' printer configuration...", @@ -808,6 +807,7 @@ class MCU: self._emergency_stop_cmd = self.lookup_command("emergency_stop") self.register_response(self._handle_shutdown, 'shutdown') self.register_response(self._handle_shutdown, 'is_shutdown') + self.register_response(self._handle_starting, 'starting') def _post_attach_setup_restart(self): self._reset_cmd = self.try_lookup_command("reset") self._config_reset_cmd = self.try_lookup_command("config_reset") From b086349a9f1c313b0f7f8b5f45380fd59f1eb3c3 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 21:22:39 -0400 Subject: [PATCH 160/411] mcu: Setup debugging estimated_print_time() in constructor Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 9bfdc978d..9e269fd73 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -610,6 +610,11 @@ class MCU: self._mcu_tick_avg = 0. self._mcu_tick_stddev = 0. self._mcu_tick_awake = 0. + # Alter time reporting when debugging + if self.is_fileoutput(): + def dummy_estimated_print_time(eventtime): + return 0. + self.estimated_print_time = dummy_estimated_print_time # Register handlers printer.load_object(config, "error_mcu") printer.register_event_handler("klippy:firmware_restart", @@ -674,7 +679,7 @@ class MCU: # Cheetah boards require RTS to be deasserted # else a reset will trigger the built-in bootloader. return (self._restart_method != "cheetah") - def _attach_file(self, pace=False): + def _attach_file(self): # In a debugging mode. Open debug output file and read data dictionary start_args = self._printer.get_start_args() if self._name == 'mcu': @@ -688,12 +693,7 @@ class MCU: dict_data = dfile.read() dfile.close() self._serial.connect_file(outfile, dict_data) - self._clocksync.connect_file(self._serial, pace) - # Handle pacing - if not pace: - def dummy_estimated_print_time(eventtime): - return 0. - self.estimated_print_time = dummy_estimated_print_time + self._clocksync.connect_file(self._serial) def _send_config(self, prev_crc): # Build config commands for cb in self._config_callbacks: From 1668d6d7c65e05601d7ecc5e2c9733e35746e55b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 21:18:46 -0400 Subject: [PATCH 161/411] mcu: Separate low-level connection handling to new MCUConnectHelper class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 364 +++++++++++++++++++++++++++----------------------- 1 file changed, 194 insertions(+), 170 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 9e269fd73..f97d4954c 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -549,7 +549,7 @@ class MCU_adc: ###################################################################### -# Main MCU class +# Main MCU class (and its helper classes) ###################################################################### # Minimum time host needs to get scheduled events queued into mcu @@ -557,18 +557,16 @@ MIN_SCHEDULE_TIME = 0.100 # Maximum time all MCUs can internally schedule into the future MAX_NOMINAL_DURATION = 3.0 -class MCU: - error = error - def __init__(self, config, clocksync): - self._printer = printer = config.get_printer() +# Low-level mcu connection management helper +class MCUConnectHelper: + def __init__(self, config, mcu, clocksync): + self._mcu = mcu self._clocksync = clocksync + self._printer = printer = config.get_printer() self._reactor = printer.get_reactor() - self._name = config.get_name() - if self._name.startswith('mcu '): - self._name = self._name[4:] + self._name = name = mcu.get_name() # Serial port - name = self._name - self._serial = serialhdl.SerialReader(self._reactor, mcu_name = name) + self._serial = serialhdl.SerialReader(self._reactor, mcu_name=name) self._baud = 0 self._canbus_iface = None canbus_uuid = config.get('canbus_uuid', None) @@ -577,7 +575,7 @@ class MCU: self._canbus_iface = config.get('canbus_interface', 'can0') cbid = self._printer.load_object(config, 'canbus_ids') cbid.add_uuid(config, canbus_uuid, self._canbus_iface) - self._printer.load_object(config, 'canbus_stats %s' % (self._name,)) + self._printer.load_object(config, 'canbus_stats %s' % (name,)) else: self._serialport = config.get('serial') if not (self._serialport.startswith("/dev/rpmsg_") @@ -595,53 +593,20 @@ class MCU: self._is_shutdown = self._is_timeout = False self._shutdown_clock = 0 self._shutdown_msg = "" - # Config building - printer.lookup_object('pins').register_chip(self._name, self) - self._oid_count = 0 - self._config_callbacks = [] - self._config_cmds = [] - self._restart_cmds = [] - self._init_cmds = [] - self._mcu_freq = 0. - self._reserved_move_slots = 0 - # Stats - self._get_status_info = {} - self._stats_sumsq_base = 0. - self._mcu_tick_avg = 0. - self._mcu_tick_stddev = 0. - self._mcu_tick_awake = 0. - # Alter time reporting when debugging - if self.is_fileoutput(): - def dummy_estimated_print_time(eventtime): - return 0. - self.estimated_print_time = dummy_estimated_print_time # Register handlers - printer.load_object(config, "error_mcu") printer.register_event_handler("klippy:firmware_restart", self._firmware_restart) - printer.register_event_handler("klippy:mcu_identify", - self._mcu_identify) - printer.register_event_handler("klippy:connect", self._connect) printer.register_event_handler("klippy:shutdown", self._shutdown) printer.register_event_handler("klippy:disconnect", self._disconnect) - printer.register_event_handler("klippy:ready", self._ready) - # Serial callbacks - def _handle_mcu_stats(self, params): - count = params['count'] - tick_sum = params['sum'] - c = 1.0 / (count * self._mcu_freq) - self._mcu_tick_avg = tick_sum * c - tick_sumsq = params['sumsq'] * self._stats_sumsq_base - diff = count*tick_sumsq - tick_sum**2 - self._mcu_tick_stddev = c * math.sqrt(max(0., diff)) - self._mcu_tick_awake = tick_sum / self._mcu_freq + def get_serial(self): + return self._serial def _handle_shutdown(self, params): if self._is_shutdown: return self._is_shutdown = True clock = params.get("clock") if clock is not None: - self._shutdown_clock = self.clock32_to_clock64(clock) + self._shutdown_clock = self._mcu.clock32_to_clock64(clock) self._shutdown_msg = msg = params['static_string_id'] event_type = params['#name'] self._printer.invoke_async_shutdown( @@ -664,9 +629,9 @@ class MCU: self._printer.request_exit('firmware_restart') self._reactor.pause(self._reactor.monotonic() + 2.000) raise error("Attempt MCU '%s' restart failed" % (self._name,)) - def _check_restart_on_crc_mismatch(self): + def check_restart_on_crc_mismatch(self): self._check_restart("CRC mismatch") - def _check_restart_on_send_config(self): + def check_restart_on_send_config(self): if self._restart_method == 'rpi_usb': # Only configure mcu after usb power reset self._check_restart("full reset before config") @@ -679,6 +644,17 @@ class MCU: # Cheetah boards require RTS to be deasserted # else a reset will trigger the built-in bootloader. return (self._restart_method != "cheetah") + def log_info(self): + msgparser = self._serial.get_msgparser() + message_count = len(msgparser.get_messages()) + version, build_versions = msgparser.get_version_info() + log_info = [ + "Loaded MCU '%s' %d commands (%s / %s)" + % (self._name, message_count, version, build_versions), + "MCU '%s' config: %s" % (self._name, " ".join( + ["%s=%s" % (k, v) + for k, v in msgparser.get_constants().items()]))] + return "\n".join(log_info) def _attach_file(self): # In a debugging mode. Open debug output file and read data dictionary start_args = self._printer.get_start_args() @@ -694,6 +670,166 @@ class MCU: dfile.close() self._serial.connect_file(outfile, dict_data) self._clocksync.connect_file(self._serial) + def _attach(self): + self._check_restart_on_attach() + try: + if self._canbus_iface is not None: + cbid = self._printer.lookup_object('canbus_ids') + nodeid = cbid.get_nodeid(self._serialport) + self._serial.connect_canbus(self._serialport, nodeid, + self._canbus_iface) + elif self._baud: + rts = self._lookup_attach_uart_rts() + self._serial.connect_uart(self._serialport, self._baud, rts) + else: + self._serial.connect_pipe(self._serialport) + self._clocksync.connect(self._serial) + except serialhdl.error as e: + raise error(str(e)) + def _post_attach_setup_shutdown(self): + self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop") + self._mcu.register_response(self._handle_shutdown, 'shutdown') + self._mcu.register_response(self._handle_shutdown, 'is_shutdown') + self._mcu.register_response(self._handle_starting, 'starting') + def _post_attach_setup_restart(self): + self._reset_cmd = self._mcu.try_lookup_command("reset") + self._config_reset_cmd = self._mcu.try_lookup_command("config_reset") + ext_only = self._reset_cmd is None and self._config_reset_cmd is None + msgparser = self._serial.get_msgparser() + mbaud = msgparser.get_constant('SERIAL_BAUD', None) + if self._restart_method is None and mbaud is None and not ext_only: + self._restart_method = 'command' + if msgparser.get_constant('CANBUS_BRIDGE', 0): + self._is_mcu_bridge = True + self._printer.register_event_handler("klippy:firmware_restart", + self._firmware_restart_bridge) + def start_attach(self): + if self._mcu.is_fileoutput(): + self._attach_file() + else: + self._attach() + logging.info(self.log_info()) + self._post_attach_setup_shutdown() + self._post_attach_setup_restart() + # Restarts + def _disconnect(self): + self._serial.disconnect() + def _shutdown(self, force=False): + if (self._emergency_stop_cmd is None + or (self._is_shutdown and not force)): + return + self._emergency_stop_cmd.send() + def _restart_arduino(self): + logging.info("Attempting MCU '%s' reset", self._name) + self._disconnect() + serialhdl.arduino_reset(self._serialport, self._reactor) + def _restart_cheetah(self): + logging.info("Attempting MCU '%s' Cheetah-style reset", self._name) + self._disconnect() + serialhdl.cheetah_reset(self._serialport, self._reactor) + def _restart_via_command(self): + if ((self._reset_cmd is None and self._config_reset_cmd is None) + or not self._clocksync.is_active()): + logging.info("Unable to issue reset command on MCU '%s'", + self._name) + return + if self._reset_cmd is None: + # Attempt reset via config_reset command + logging.info("Attempting MCU '%s' config_reset command", self._name) + self._is_shutdown = True + self._shutdown(force=True) + self._reactor.pause(self._reactor.monotonic() + 0.015) + self._config_reset_cmd.send() + else: + # Attempt reset via reset command + logging.info("Attempting MCU '%s' reset command", self._name) + self._reset_cmd.send() + self._reactor.pause(self._reactor.monotonic() + 0.015) + self._disconnect() + def _restart_rpi_usb(self): + logging.info("Attempting MCU '%s' reset via rpi usb power", self._name) + self._disconnect() + chelper.run_hub_ctrl(0) + self._reactor.pause(self._reactor.monotonic() + 2.) + chelper.run_hub_ctrl(1) + def _firmware_restart(self, force=False): + if self._is_mcu_bridge and not force: + return + if self._restart_method == 'rpi_usb': + self._restart_rpi_usb() + elif self._restart_method == 'command': + self._restart_via_command() + elif self._restart_method == 'cheetah': + self._restart_cheetah() + else: + self._restart_arduino() + def _firmware_restart_bridge(self): + self._firmware_restart(True) + def check_timeout(self, eventtime): + if (self._clocksync.is_active() or self._mcu.is_fileoutput() + or self._is_timeout): + return + self._is_timeout = True + logging.info("Timeout with MCU '%s' (eventtime=%f)", + self._name, eventtime) + self._printer.invoke_shutdown("Lost communication with MCU '%s'" % ( + self._name,)) + def is_shutdown(self): + return self._is_shutdown + def get_shutdown_clock(self): + return self._shutdown_clock + def get_shutdown_msg(self): + return self._shutdown_msg + +# Main MCU class +class MCU: + error = error + def __init__(self, config, clocksync): + self._printer = printer = config.get_printer() + self._clocksync = clocksync + self._reactor = printer.get_reactor() + self._name = config.get_name() + if self._name.startswith('mcu '): + self._name = self._name[4:] + # Low-level connection + self._conn_helper = MCUConnectHelper(config, self, clocksync) + self._serial = self._conn_helper.get_serial() + # Config building + printer.lookup_object('pins').register_chip(self._name, self) + self._oid_count = 0 + self._config_callbacks = [] + self._config_cmds = [] + self._restart_cmds = [] + self._init_cmds = [] + self._mcu_freq = 0. + self._reserved_move_slots = 0 + # Stats + self._get_status_info = {} + self._stats_sumsq_base = 0. + self._mcu_tick_avg = 0. + self._mcu_tick_stddev = 0. + self._mcu_tick_awake = 0. + # Alter time reporting when debugging + if self.is_fileoutput(): + def dummy_estimated_print_time(eventtime): + return 0. + self.estimated_print_time = dummy_estimated_print_time + # Register handlers + printer.load_object(config, "error_mcu") + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) + printer.register_event_handler("klippy:connect", self._connect) + printer.register_event_handler("klippy:ready", self._ready) + # Serial callbacks + def _handle_mcu_stats(self, params): + count = params['count'] + tick_sum = params['sum'] + c = 1.0 / (count * self._mcu_freq) + self._mcu_tick_avg = tick_sum * c + tick_sumsq = params['sumsq'] * self._stats_sumsq_base + diff = count*tick_sumsq - tick_sum**2 + self._mcu_tick_stddev = c * math.sqrt(max(0., diff)) + self._mcu_tick_awake = tick_sum / self._mcu_freq def _send_config(self, prev_crc): # Build config commands for cb in self._config_callbacks: @@ -711,7 +847,7 @@ class MCU: config_crc = zlib.crc32(encoded_config) & 0xffffffff self.add_config_cmd("finalize_config crc=%d" % (config_crc,)) if prev_crc is not None and config_crc != prev_crc: - self._check_restart_on_crc_mismatch() + self._conn_helper.check_restart_on_crc_mismatch() raise error("MCU '%s' CRC does not match config" % (self._name,)) # Transmit config messages (if needed) try: @@ -741,27 +877,17 @@ class MCU: if self.is_fileoutput(): return { 'is_config': 0, 'move_count': 500, 'crc': 0 } config_params = get_config_cmd.send() - if self._is_shutdown: + if self._conn_helper.is_shutdown(): raise error("MCU '%s' error during config: %s" % ( - self._name, self._shutdown_msg)) + self._name, self._conn_helper.get_shutdown_msg())) if config_params['is_shutdown']: raise error("Can not update MCU '%s' config as it is shutdown" % ( self._name,)) return config_params - def _log_info(self): - msgparser = self._serial.get_msgparser() - message_count = len(msgparser.get_messages()) - version, build_versions = msgparser.get_version_info() - log_info = [ - "Loaded MCU '%s' %d commands (%s / %s)" - % (self._name, message_count, version, build_versions), - "MCU '%s' config: %s" % (self._name, " ".join( - ["%s=%s" % (k, v) for k, v in self.get_constants().items()]))] - return "\n".join(log_info) def _connect(self): config_params = self._send_get_config() if not config_params['is_config']: - self._check_restart_on_send_config() + self._conn_helper.check_restart_on_send_config() # Not configured - send config and issue get_config again self._send_config(None) config_params = self._send_get_config() @@ -785,41 +911,8 @@ class MCU: # Log config information move_msg = "Configured MCU '%s' (%d moves)" % (self._name, move_count) logging.info(move_msg) - log_info = self._log_info() + "\n" + move_msg + log_info = self._conn_helper.log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) - def _attach(self): - self._check_restart_on_attach() - try: - if self._canbus_iface is not None: - cbid = self._printer.lookup_object('canbus_ids') - nodeid = cbid.get_nodeid(self._serialport) - self._serial.connect_canbus(self._serialport, nodeid, - self._canbus_iface) - elif self._baud: - rts = self._lookup_attach_uart_rts() - self._serial.connect_uart(self._serialport, self._baud, rts) - else: - self._serial.connect_pipe(self._serialport) - self._clocksync.connect(self._serial) - except serialhdl.error as e: - raise error(str(e)) - def _post_attach_setup_shutdown(self): - self._emergency_stop_cmd = self.lookup_command("emergency_stop") - self.register_response(self._handle_shutdown, 'shutdown') - self.register_response(self._handle_shutdown, 'is_shutdown') - self.register_response(self._handle_starting, 'starting') - def _post_attach_setup_restart(self): - self._reset_cmd = self.try_lookup_command("reset") - self._config_reset_cmd = self.try_lookup_command("config_reset") - ext_only = self._reset_cmd is None and self._config_reset_cmd is None - msgparser = self._serial.get_msgparser() - mbaud = msgparser.get_constant('SERIAL_BAUD', None) - if self._restart_method is None and mbaud is None and not ext_only: - self._restart_method = 'command' - if msgparser.get_constant('CANBUS_BRIDGE', 0): - self._is_mcu_bridge = True - self._printer.register_event_handler("klippy:firmware_restart", - self._firmware_restart_bridge) def _post_attach_setup_stats(self): self._stats_sumsq_base = self.get_constant_float('STATS_SUMSQ_BASE') msgparser = self._serial.get_msgparser() @@ -837,13 +930,7 @@ class MCU: for pin in value.split(','): pin_resolver.reserve_pin(pin, cname[13:]) def _mcu_identify(self): - if self.is_fileoutput(): - self._attach_file() - else: - self._attach() - logging.info(self._log_info()) - self._post_attach_setup_shutdown() - self._post_attach_setup_restart() + self._conn_helper.start_attach() self._post_attach_setup_for_config() self._post_attach_setup_stats() def _ready(self): @@ -926,83 +1013,20 @@ class MCU: return self._clocksync.estimated_print_time(eventtime) def clock32_to_clock64(self, clock32): return self._clocksync.clock32_to_clock64(clock32) - # Restarts - def _disconnect(self): - self._serial.disconnect() - def _shutdown(self, force=False): - if (self._emergency_stop_cmd is None - or (self._is_shutdown and not force)): - return - self._emergency_stop_cmd.send() - def _restart_arduino(self): - logging.info("Attempting MCU '%s' reset", self._name) - self._disconnect() - serialhdl.arduino_reset(self._serialport, self._reactor) - def _restart_cheetah(self): - logging.info("Attempting MCU '%s' Cheetah-style reset", self._name) - self._disconnect() - serialhdl.cheetah_reset(self._serialport, self._reactor) - def _restart_via_command(self): - if ((self._reset_cmd is None and self._config_reset_cmd is None) - or not self._clocksync.is_active()): - logging.info("Unable to issue reset command on MCU '%s'", - self._name) - return - if self._reset_cmd is None: - # Attempt reset via config_reset command - logging.info("Attempting MCU '%s' config_reset command", self._name) - self._is_shutdown = True - self._shutdown(force=True) - self._reactor.pause(self._reactor.monotonic() + 0.015) - self._config_reset_cmd.send() - else: - # Attempt reset via reset command - logging.info("Attempting MCU '%s' reset command", self._name) - self._reset_cmd.send() - self._reactor.pause(self._reactor.monotonic() + 0.015) - self._disconnect() - def _restart_rpi_usb(self): - logging.info("Attempting MCU '%s' reset via rpi usb power", self._name) - self._disconnect() - chelper.run_hub_ctrl(0) - self._reactor.pause(self._reactor.monotonic() + 2.) - chelper.run_hub_ctrl(1) - def _firmware_restart(self, force=False): - if self._is_mcu_bridge and not force: - return - if self._restart_method == 'rpi_usb': - self._restart_rpi_usb() - elif self._restart_method == 'command': - self._restart_via_command() - elif self._restart_method == 'cheetah': - self._restart_cheetah() - else: - self._restart_arduino() - def _firmware_restart_bridge(self): - self._firmware_restart(True) # Move queue tracking def request_move_queue_slot(self): self._reserved_move_slots += 1 - def _check_timeout(self, eventtime): - if (self._clocksync.is_active() or self.is_fileoutput() - or self._is_timeout): - return - self._is_timeout = True - logging.info("Timeout with MCU '%s' (eventtime=%f)", - self._name, eventtime) - self._printer.invoke_shutdown("Lost communication with MCU '%s'" % ( - self._name,)) def calibrate_clock(self, print_time, eventtime): offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) - self._check_timeout(eventtime) + self._conn_helper.check_timeout(eventtime) return offset, freq # Misc external commands def is_fileoutput(self): return self._printer.get_start_args().get('debugoutput') is not None def is_shutdown(self): - return self._is_shutdown + return self._conn_helper.is_shutdown() def get_shutdown_clock(self): - return self._shutdown_clock + return self._conn_helper.get_shutdown_clock() def get_status(self, eventtime=None): return dict(self._get_status_info) def stats(self, eventtime): From 1bb674d0b8127f07db19c5e37b8db63ae6d9fc32 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 22:23:15 -0400 Subject: [PATCH 162/411] mcu: Add new MCUStatsHelper() helper class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 121 +++++++++++++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 50 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index f97d4954c..cb749b082 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -598,8 +598,12 @@ class MCUConnectHelper: self._firmware_restart) printer.register_event_handler("klippy:shutdown", self._shutdown) printer.register_event_handler("klippy:disconnect", self._disconnect) + def get_mcu(self): + return self._mcu def get_serial(self): return self._serial + def get_clocksync(self): + return self._clocksync def _handle_shutdown(self, params): if self._is_shutdown: return @@ -781,6 +785,69 @@ class MCUConnectHelper: def get_shutdown_msg(self): return self._shutdown_msg +# Handle statistics reporting +class MCUStatsHelper: + def __init__(self, config, conn_helper): + self._printer = printer = config.get_printer() + self._mcu = mcu = conn_helper.get_mcu() + self._serial = conn_helper.get_serial() + self._clocksync = conn_helper.get_clocksync() + self._reactor = printer.get_reactor() + self._name = mcu.get_name() + self._mcu_freq = 0. + self._get_status_info = {} + self._stats_sumsq_base = 0. + self._mcu_tick_avg = 0. + self._mcu_tick_stddev = 0. + self._mcu_tick_awake = 0. + printer.register_event_handler("klippy:ready", self._ready) + def _handle_mcu_stats(self, params): + count = params['count'] + tick_sum = params['sum'] + c = 1.0 / (count * self._mcu_freq) + self._mcu_tick_avg = tick_sum * c + tick_sumsq = params['sumsq'] * self._stats_sumsq_base + diff = count*tick_sumsq - tick_sum**2 + self._mcu_tick_stddev = c * math.sqrt(max(0., diff)) + self._mcu_tick_awake = tick_sum / self._mcu_freq + def post_attach_setup_stats(self): + self._mcu_freq = self._mcu.get_constant_float('CLOCK_FREQ') + self._stats_sumsq_base = self._mcu.get_constant_float( + 'STATS_SUMSQ_BASE') + msgparser = self._serial.get_msgparser() + version, build_versions = msgparser.get_version_info() + self._get_status_info['mcu_version'] = version + self._get_status_info['mcu_build_versions'] = build_versions + self._get_status_info['mcu_constants'] = msgparser.get_constants() + self._mcu.register_response(self._handle_mcu_stats, 'stats') + def _ready(self): + if self._mcu.is_fileoutput(): + return + # Check that reported mcu frequency is in range + mcu_freq = self._mcu_freq + systime = self._reactor.monotonic() + get_clock = self._clocksync.get_clock + calc_freq = get_clock(systime + 1) - get_clock(systime) + freq_diff = abs(mcu_freq - calc_freq) + mcu_freq_mhz = int(mcu_freq / 1000000. + 0.5) + calc_freq_mhz = int(calc_freq / 1000000. + 0.5) + if freq_diff > mcu_freq*0.01 and mcu_freq_mhz != calc_freq_mhz: + pconfig = self._printer.lookup_object('configfile') + msg = ("MCU '%s' configured for %dMhz but running at %dMhz!" + % (self._name, mcu_freq_mhz, calc_freq_mhz)) + pconfig.runtime_warning(msg) + def get_status(self, eventtime=None): + return dict(self._get_status_info) + def stats(self, eventtime): + load = "mcu_awake=%.03f mcu_task_avg=%.06f mcu_task_stddev=%.06f" % ( + self._mcu_tick_awake, self._mcu_tick_avg, self._mcu_tick_stddev) + stats = ' '.join([load, self._serial.stats(eventtime), + self._clocksync.stats(eventtime)]) + parts = [s.split('=', 1) for s in stats.split()] + last_stats = {k:(float(v) if '.' in v else int(v)) for k, v in parts} + self._get_status_info['last_stats'] = last_stats + return False, '%s: %s' % (self._name, stats) + # Main MCU class class MCU: error = error @@ -803,33 +870,18 @@ class MCU: self._init_cmds = [] self._mcu_freq = 0. self._reserved_move_slots = 0 - # Stats - self._get_status_info = {} - self._stats_sumsq_base = 0. - self._mcu_tick_avg = 0. - self._mcu_tick_stddev = 0. - self._mcu_tick_awake = 0. # Alter time reporting when debugging if self.is_fileoutput(): def dummy_estimated_print_time(eventtime): return 0. self.estimated_print_time = dummy_estimated_print_time # Register handlers + self._stats_helper = MCUStatsHelper(self, self._conn_helper) printer.load_object(config, "error_mcu") printer.register_event_handler("klippy:mcu_identify", self._mcu_identify) printer.register_event_handler("klippy:connect", self._connect) - printer.register_event_handler("klippy:ready", self._ready) # Serial callbacks - def _handle_mcu_stats(self, params): - count = params['count'] - tick_sum = params['sum'] - c = 1.0 / (count * self._mcu_freq) - self._mcu_tick_avg = tick_sum * c - tick_sumsq = params['sumsq'] * self._stats_sumsq_base - diff = count*tick_sumsq - tick_sum**2 - self._mcu_tick_stddev = c * math.sqrt(max(0., diff)) - self._mcu_tick_awake = tick_sum / self._mcu_freq def _send_config(self, prev_crc): # Build config commands for cb in self._config_callbacks: @@ -913,14 +965,6 @@ class MCU: logging.info(move_msg) log_info = self._conn_helper.log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) - def _post_attach_setup_stats(self): - self._stats_sumsq_base = self.get_constant_float('STATS_SUMSQ_BASE') - msgparser = self._serial.get_msgparser() - version, build_versions = msgparser.get_version_info() - self._get_status_info['mcu_version'] = version - self._get_status_info['mcu_build_versions'] = build_versions - self._get_status_info['mcu_constants'] = msgparser.get_constants() - self.register_response(self._handle_mcu_stats, 'stats') def _post_attach_setup_for_config(self): self._mcu_freq = self.get_constant_float('CLOCK_FREQ') ppins = self._printer.lookup_object('pins') @@ -932,23 +976,7 @@ class MCU: def _mcu_identify(self): self._conn_helper.start_attach() self._post_attach_setup_for_config() - self._post_attach_setup_stats() - def _ready(self): - if self.is_fileoutput(): - return - # Check that reported mcu frequency is in range - mcu_freq = self._mcu_freq - systime = self._reactor.monotonic() - get_clock = self._clocksync.get_clock - calc_freq = get_clock(systime + 1) - get_clock(systime) - freq_diff = abs(mcu_freq - calc_freq) - mcu_freq_mhz = int(mcu_freq / 1000000. + 0.5) - calc_freq_mhz = int(calc_freq / 1000000. + 0.5) - if freq_diff > mcu_freq*0.01 and mcu_freq_mhz != calc_freq_mhz: - pconfig = self._printer.lookup_object('configfile') - msg = ("MCU '%s' configured for %dMhz but running at %dMhz!" - % (self._name, mcu_freq_mhz, calc_freq_mhz)) - pconfig.runtime_warning(msg) + self._stats_helper.post_attach_setup_stats() # Config creation helpers def setup_pin(self, pin_type, pin_params): pcs = {'endstop': MCU_endstop, @@ -1028,16 +1056,9 @@ class MCU: def get_shutdown_clock(self): return self._conn_helper.get_shutdown_clock() def get_status(self, eventtime=None): - return dict(self._get_status_info) + return self._stats_helper.get_status(eventtime) def stats(self, eventtime): - load = "mcu_awake=%.03f mcu_task_avg=%.06f mcu_task_stddev=%.06f" % ( - self._mcu_tick_awake, self._mcu_tick_avg, self._mcu_tick_stddev) - stats = ' '.join([load, self._serial.stats(eventtime), - self._clocksync.stats(eventtime)]) - parts = [s.split('=', 1) for s in stats.split()] - last_stats = {k:(float(v) if '.' in v else int(v)) for k, v in parts} - self._get_status_info['last_stats'] = last_stats - return False, '%s: %s' % (self._name, stats) + return self._stats_helper.stats(eventtime) def add_printer_objects(config): printer = config.get_printer() From 64c155121e01bcfcb5bcace0f050eea9af94b5d7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 19 Sep 2025 22:23:40 -0400 Subject: [PATCH 163/411] mcu: Add new MCUConfigHelper helper class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 104 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 41 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index cb749b082..fa1ac6eb0 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -848,21 +848,17 @@ class MCUStatsHelper: self._get_status_info['last_stats'] = last_stats return False, '%s: %s' % (self._name, stats) -# Main MCU class -class MCU: - error = error - def __init__(self, config, clocksync): +# Handle process of configuring an mcu +class MCUConfigHelper: + def __init__(self, config, conn_helper): self._printer = printer = config.get_printer() - self._clocksync = clocksync + self._conn_helper = conn_helper + self._mcu = mcu = conn_helper.get_mcu() + self._serial = conn_helper.get_serial() + self._clocksync = conn_helper.get_clocksync() self._reactor = printer.get_reactor() - self._name = config.get_name() - if self._name.startswith('mcu '): - self._name = self._name[4:] - # Low-level connection - self._conn_helper = MCUConnectHelper(config, self, clocksync) - self._serial = self._conn_helper.get_serial() - # Config building - printer.lookup_object('pins').register_chip(self._name, self) + self._name = mcu.get_name() + printer.lookup_object('pins').register_chip(self._name, mcu) self._oid_count = 0 self._config_callbacks = [] self._config_cmds = [] @@ -870,18 +866,7 @@ class MCU: self._init_cmds = [] self._mcu_freq = 0. self._reserved_move_slots = 0 - # Alter time reporting when debugging - if self.is_fileoutput(): - def dummy_estimated_print_time(eventtime): - return 0. - self.estimated_print_time = dummy_estimated_print_time - # Register handlers - self._stats_helper = MCUStatsHelper(self, self._conn_helper) - printer.load_object(config, "error_mcu") - printer.register_event_handler("klippy:mcu_identify", - self._mcu_identify) printer.register_event_handler("klippy:connect", self._connect) - # Serial callbacks def _send_config(self, prev_crc): # Build config commands for cb in self._config_callbacks: @@ -923,10 +908,10 @@ class MCU: % (enum_value, self._name)) raise def _send_get_config(self): - get_config_cmd = self.lookup_query_command( + get_config_cmd = self._mcu.lookup_query_command( "get_config", "config is_config=%c crc=%u is_shutdown=%c move_count=%hu") - if self.is_fileoutput(): + if self._mcu.is_fileoutput(): return { 'is_config': 0, 'move_count': 500, 'crc': 0 } config_params = get_config_cmd.send() if self._conn_helper.is_shutdown(): @@ -943,7 +928,7 @@ class MCU: # Not configured - send config and issue get_config again self._send_config(None) config_params = self._send_get_config() - if not config_params['is_config'] and not self.is_fileoutput(): + if not config_params['is_config'] and not self._mcu.is_fileoutput(): raise error("Unable to configure MCU '%s'" % (self._name,)) else: start_reason = self._printer.get_start_args().get("start_reason") @@ -959,31 +944,27 @@ class MCU: ss_move_count = move_count - self._reserved_move_slots motion_queuing = self._printer.lookup_object('motion_queuing') motion_queuing.setup_mcu_movequeue( - self, self._serial.get_serialqueue(), ss_move_count) + self._mcu, self._serial.get_serialqueue(), ss_move_count) # Log config information move_msg = "Configured MCU '%s' (%d moves)" % (self._name, move_count) logging.info(move_msg) log_info = self._conn_helper.log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) - def _post_attach_setup_for_config(self): - self._mcu_freq = self.get_constant_float('CLOCK_FREQ') + def post_attach_setup_for_config(self): + self._mcu_freq = self._mcu.get_constant_float('CLOCK_FREQ') ppins = self._printer.lookup_object('pins') pin_resolver = ppins.get_pin_resolver(self._name) - for cname, value in self.get_constants().items(): + for cname, value in self._mcu.get_constants().items(): if cname.startswith("RESERVE_PINS_"): for pin in value.split(','): pin_resolver.reserve_pin(pin, cname[13:]) - def _mcu_identify(self): - self._conn_helper.start_attach() - self._post_attach_setup_for_config() - self._stats_helper.post_attach_setup_stats() # Config creation helpers def setup_pin(self, pin_type, pin_params): pcs = {'endstop': MCU_endstop, 'digital_out': MCU_digital_out, 'pwm': MCU_pwm, 'adc': MCU_adc} if pin_type not in pcs: raise pins.error("pin type %s not supported on mcu" % (pin_type,)) - return pcs[pin_type](self, pin_params) + return pcs[pin_type](self._mcu, pin_params) def create_oid(self): self._oid_count += 1 return self._oid_count - 1 @@ -998,10 +979,54 @@ class MCU: self._config_cmds.append(cmd) def get_query_slot(self, oid): slot = self.seconds_to_clock(oid * .01) - t = int(self.estimated_print_time(self._reactor.monotonic()) + 1.5) - return self.print_time_to_clock(t) + slot + t = int(self._mcu.estimated_print_time(self._reactor.monotonic()) + 1.5) + return self._mcu.print_time_to_clock(t) + slot def seconds_to_clock(self, time): return int(time * self._mcu_freq) + def request_move_queue_slot(self): + self._reserved_move_slots += 1 + +# Main MCU class +class MCU: + error = error + def __init__(self, config, clocksync): + self._printer = printer = config.get_printer() + self._clocksync = clocksync + self._name = config.get_name() + if self._name.startswith('mcu '): + self._name = self._name[4:] + # Low-level connection + self._conn_helper = MCUConnectHelper(config, self, clocksync) + self._serial = self._conn_helper.get_serial() + self._config_helper = MCUConfigHelper(self, self._conn_helper) + # Alter time reporting when debugging + if self.is_fileoutput(): + def dummy_estimated_print_time(eventtime): + return 0. + self.estimated_print_time = dummy_estimated_print_time + # Register handlers + self._stats_helper = MCUStatsHelper(self, self._conn_helper) + printer.load_object(config, "error_mcu") + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) + def _mcu_identify(self): + self._conn_helper.start_attach() + self._config_helper.post_attach_setup_for_config() + self._stats_helper.post_attach_setup_stats() + def setup_pin(self, pin_type, pin_params): + return self._config_helper.setup_pin(pin_type, pin_params) + def create_oid(self): + return self._config_helper.create_oid() + def register_config_callback(self, cb): + self._config_helper.register_config_callback(cb) + def add_config_cmd(self, cmd, is_init=False, on_restart=False): + self._config_helper.add_config_cmd(cmd, is_init, on_restart) + def request_move_queue_slot(self): + self._config_helper.request_move_queue_slot() + def get_query_slot(self, oid): + return self._config_helper.get_query_slot(oid) + def seconds_to_clock(self, time): + return self._config_helper.seconds_to_clock(time) def min_schedule_time(self): return MIN_SCHEDULE_TIME def max_nominal_duration(self): @@ -1041,9 +1066,6 @@ class MCU: return self._clocksync.estimated_print_time(eventtime) def clock32_to_clock64(self, clock32): return self._clocksync.clock32_to_clock64(clock32) - # Move queue tracking - def request_move_queue_slot(self): - self._reserved_move_slots += 1 def calibrate_clock(self, print_time, eventtime): offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) self._conn_helper.check_timeout(eventtime) From de73e41d0f1b1af35a9faa9c01523a8a79912929 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 22 Sep 2025 15:47:57 -0400 Subject: [PATCH 164/411] mcu: Add new MCURestartHelper helper class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 272 +++++++++++++++++++++++++++----------------------- 1 file changed, 147 insertions(+), 125 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index fa1ac6eb0..9375f20e1 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -557,72 +557,28 @@ MIN_SCHEDULE_TIME = 0.100 # Maximum time all MCUs can internally schedule into the future MAX_NOMINAL_DURATION = 3.0 -# Low-level mcu connection management helper -class MCUConnectHelper: - def __init__(self, config, mcu, clocksync): - self._mcu = mcu - self._clocksync = clocksync +# Support for restarting a micro-controller +class MCURestartHelper: + def __init__(self, config, conn_helper): self._printer = printer = config.get_printer() + self._conn_helper = conn_helper + self._mcu = mcu = conn_helper.get_mcu() + self._serial = conn_helper.get_serial() + self._clocksync = conn_helper.get_clocksync() self._reactor = printer.get_reactor() - self._name = name = mcu.get_name() - # Serial port - self._serial = serialhdl.SerialReader(self._reactor, mcu_name=name) - self._baud = 0 - self._canbus_iface = None - canbus_uuid = config.get('canbus_uuid', None) - if canbus_uuid is not None: - self._serialport = canbus_uuid - self._canbus_iface = config.get('canbus_interface', 'can0') - cbid = self._printer.load_object(config, 'canbus_ids') - cbid.add_uuid(config, canbus_uuid, self._canbus_iface) - self._printer.load_object(config, 'canbus_stats %s' % (name,)) - else: - self._serialport = config.get('serial') - if not (self._serialport.startswith("/dev/rpmsg_") - or self._serialport.startswith("/tmp/klipper_host_")): - self._baud = config.getint('baud', 250000, minval=2400) - # Restarts + self._name = mcu.get_name() restart_methods = [None, 'arduino', 'cheetah', 'command', 'rpi_usb'] self._restart_method = 'command' - if self._baud: + serialport, baud = conn_helper.get_serialport() + if baud: self._restart_method = config.getchoice('restart_method', restart_methods, None) self._reset_cmd = self._config_reset_cmd = None self._is_mcu_bridge = False - self._emergency_stop_cmd = None - self._is_shutdown = self._is_timeout = False - self._shutdown_clock = 0 - self._shutdown_msg = "" # Register handlers printer.register_event_handler("klippy:firmware_restart", self._firmware_restart) - printer.register_event_handler("klippy:shutdown", self._shutdown) printer.register_event_handler("klippy:disconnect", self._disconnect) - def get_mcu(self): - return self._mcu - def get_serial(self): - return self._serial - def get_clocksync(self): - return self._clocksync - def _handle_shutdown(self, params): - if self._is_shutdown: - return - self._is_shutdown = True - clock = params.get("clock") - if clock is not None: - self._shutdown_clock = self._mcu.clock32_to_clock64(clock) - self._shutdown_msg = msg = params['static_string_id'] - event_type = params['#name'] - self._printer.invoke_async_shutdown( - "MCU shutdown", {"reason": msg, "mcu": self._name, - "event_type": event_type}) - logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, event_type, - self._shutdown_msg, self._clocksync.dump_debug(), - self._serial.dump_debug()) - def _handle_starting(self, params): - if not self._is_shutdown: - self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart" - % (self._name,)) # Connection phase def _check_restart(self, reason): start_reason = self._printer.get_start_args().get("start_reason") @@ -639,63 +595,17 @@ class MCUConnectHelper: if self._restart_method == 'rpi_usb': # Only configure mcu after usb power reset self._check_restart("full reset before config") - def _check_restart_on_attach(self): + def check_restart_on_attach(self): resmeth = self._restart_method - if resmeth == 'rpi_usb' and not os.path.exists(self._serialport): + serialport, baud = self._conn_helper.get_serialport() + if resmeth == 'rpi_usb' and not os.path.exists(serialport): # Try toggling usb power self._check_restart("enable power") - def _lookup_attach_uart_rts(self): + def lookup_attach_uart_rts(self): # Cheetah boards require RTS to be deasserted # else a reset will trigger the built-in bootloader. return (self._restart_method != "cheetah") - def log_info(self): - msgparser = self._serial.get_msgparser() - message_count = len(msgparser.get_messages()) - version, build_versions = msgparser.get_version_info() - log_info = [ - "Loaded MCU '%s' %d commands (%s / %s)" - % (self._name, message_count, version, build_versions), - "MCU '%s' config: %s" % (self._name, " ".join( - ["%s=%s" % (k, v) - for k, v in msgparser.get_constants().items()]))] - return "\n".join(log_info) - def _attach_file(self): - # In a debugging mode. Open debug output file and read data dictionary - start_args = self._printer.get_start_args() - if self._name == 'mcu': - out_fname = start_args.get('debugoutput') - dict_fname = start_args.get('dictionary') - else: - out_fname = start_args.get('debugoutput') + "-" + self._name - dict_fname = start_args.get('dictionary_' + self._name) - outfile = open(out_fname, 'wb') - dfile = open(dict_fname, 'rb') - dict_data = dfile.read() - dfile.close() - self._serial.connect_file(outfile, dict_data) - self._clocksync.connect_file(self._serial) - def _attach(self): - self._check_restart_on_attach() - try: - if self._canbus_iface is not None: - cbid = self._printer.lookup_object('canbus_ids') - nodeid = cbid.get_nodeid(self._serialport) - self._serial.connect_canbus(self._serialport, nodeid, - self._canbus_iface) - elif self._baud: - rts = self._lookup_attach_uart_rts() - self._serial.connect_uart(self._serialport, self._baud, rts) - else: - self._serial.connect_pipe(self._serialport) - self._clocksync.connect(self._serial) - except serialhdl.error as e: - raise error(str(e)) - def _post_attach_setup_shutdown(self): - self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop") - self._mcu.register_response(self._handle_shutdown, 'shutdown') - self._mcu.register_response(self._handle_shutdown, 'is_shutdown') - self._mcu.register_response(self._handle_starting, 'starting') - def _post_attach_setup_restart(self): + def post_attach_setup_restart(self): self._reset_cmd = self._mcu.try_lookup_command("reset") self._config_reset_cmd = self._mcu.try_lookup_command("config_reset") ext_only = self._reset_cmd is None and self._config_reset_cmd is None @@ -707,30 +617,18 @@ class MCUConnectHelper: self._is_mcu_bridge = True self._printer.register_event_handler("klippy:firmware_restart", self._firmware_restart_bridge) - def start_attach(self): - if self._mcu.is_fileoutput(): - self._attach_file() - else: - self._attach() - logging.info(self.log_info()) - self._post_attach_setup_shutdown() - self._post_attach_setup_restart() - # Restarts def _disconnect(self): self._serial.disconnect() - def _shutdown(self, force=False): - if (self._emergency_stop_cmd is None - or (self._is_shutdown and not force)): - return - self._emergency_stop_cmd.send() def _restart_arduino(self): logging.info("Attempting MCU '%s' reset", self._name) self._disconnect() - serialhdl.arduino_reset(self._serialport, self._reactor) + serialport, baud = self._conn_helper.get_serialport() + serialhdl.arduino_reset(serialport, self._reactor) def _restart_cheetah(self): logging.info("Attempting MCU '%s' Cheetah-style reset", self._name) self._disconnect() - serialhdl.cheetah_reset(self._serialport, self._reactor) + serialport, baud = self._conn_helper.get_serialport() + serialhdl.cheetah_reset(serialport, self._reactor) def _restart_via_command(self): if ((self._reset_cmd is None and self._config_reset_cmd is None) or not self._clocksync.is_active()): @@ -740,8 +638,7 @@ class MCUConnectHelper: if self._reset_cmd is None: # Attempt reset via config_reset command logging.info("Attempting MCU '%s' config_reset command", self._name) - self._is_shutdown = True - self._shutdown(force=True) + self._conn_helper.force_local_shutdown() self._reactor.pause(self._reactor.monotonic() + 0.015) self._config_reset_cmd.send() else: @@ -769,6 +666,129 @@ class MCUConnectHelper: self._restart_arduino() def _firmware_restart_bridge(self): self._firmware_restart(True) + +# Low-level mcu connection management helper +class MCUConnectHelper: + def __init__(self, config, mcu, clocksync): + self._mcu = mcu + self._clocksync = clocksync + self._printer = printer = config.get_printer() + self._reactor = printer.get_reactor() + self._name = name = mcu.get_name() + # Serial port + self._serial = serialhdl.SerialReader(self._reactor, mcu_name=name) + self._baud = 0 + self._canbus_iface = None + canbus_uuid = config.get('canbus_uuid', None) + if canbus_uuid is not None: + self._serialport = canbus_uuid + self._canbus_iface = config.get('canbus_interface', 'can0') + cbid = self._printer.load_object(config, 'canbus_ids') + cbid.add_uuid(config, canbus_uuid, self._canbus_iface) + self._printer.load_object(config, 'canbus_stats %s' % (name,)) + else: + self._serialport = config.get('serial') + if not (self._serialport.startswith("/dev/rpmsg_") + or self._serialport.startswith("/tmp/klipper_host_")): + self._baud = config.getint('baud', 250000, minval=2400) + self._emergency_stop_cmd = None + self._is_shutdown = self._is_timeout = False + self._shutdown_clock = 0 + self._shutdown_msg = "" + self._restart_helper = MCURestartHelper(config, self) + printer.register_event_handler("klippy:shutdown", self._shutdown) + def get_mcu(self): + return self._mcu + def get_serial(self): + return self._serial + def get_clocksync(self): + return self._clocksync + def get_serialport(self): + return self._serialport, self._baud + def get_restart_helper(self): + return self._restart_helper + def _handle_shutdown(self, params): + if self._is_shutdown: + return + self._is_shutdown = True + clock = params.get("clock") + if clock is not None: + self._shutdown_clock = self._mcu.clock32_to_clock64(clock) + self._shutdown_msg = msg = params['static_string_id'] + event_type = params['#name'] + self._printer.invoke_async_shutdown( + "MCU shutdown", {"reason": msg, "mcu": self._name, + "event_type": event_type}) + logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, event_type, + self._shutdown_msg, self._clocksync.dump_debug(), + self._serial.dump_debug()) + def _handle_starting(self, params): + if not self._is_shutdown: + self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart" + % (self._name,)) + def log_info(self): + msgparser = self._serial.get_msgparser() + message_count = len(msgparser.get_messages()) + version, build_versions = msgparser.get_version_info() + log_info = [ + "Loaded MCU '%s' %d commands (%s / %s)" + % (self._name, message_count, version, build_versions), + "MCU '%s' config: %s" % (self._name, " ".join( + ["%s=%s" % (k, v) + for k, v in msgparser.get_constants().items()]))] + return "\n".join(log_info) + def _attach_file(self): + # In a debugging mode. Open debug output file and read data dictionary + start_args = self._printer.get_start_args() + if self._name == 'mcu': + out_fname = start_args.get('debugoutput') + dict_fname = start_args.get('dictionary') + else: + out_fname = start_args.get('debugoutput') + "-" + self._name + dict_fname = start_args.get('dictionary_' + self._name) + outfile = open(out_fname, 'wb') + dfile = open(dict_fname, 'rb') + dict_data = dfile.read() + dfile.close() + self._serial.connect_file(outfile, dict_data) + self._clocksync.connect_file(self._serial) + def _attach(self): + self._restart_helper.check_restart_on_attach() + try: + if self._canbus_iface is not None: + cbid = self._printer.lookup_object('canbus_ids') + nodeid = cbid.get_nodeid(self._serialport) + self._serial.connect_canbus(self._serialport, nodeid, + self._canbus_iface) + elif self._baud: + rts = self._restart_helper.lookup_attach_uart_rts() + self._serial.connect_uart(self._serialport, self._baud, rts) + else: + self._serial.connect_pipe(self._serialport) + self._clocksync.connect(self._serial) + except serialhdl.error as e: + raise error(str(e)) + def _post_attach_setup_shutdown(self): + self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop") + self._mcu.register_response(self._handle_shutdown, 'shutdown') + self._mcu.register_response(self._handle_shutdown, 'is_shutdown') + self._mcu.register_response(self._handle_starting, 'starting') + def start_attach(self): + if self._mcu.is_fileoutput(): + self._attach_file() + else: + self._attach() + logging.info(self.log_info()) + self._post_attach_setup_shutdown() + self._restart_helper.post_attach_setup_restart() + def _shutdown(self, force=False): + if (self._emergency_stop_cmd is None + or (self._is_shutdown and not force)): + return + self._emergency_stop_cmd.send() + def force_local_shutdown(self): + self._is_shutdown = True + self._shutdown(force=True) def check_timeout(self, eventtime): if (self._clocksync.is_active() or self._mcu.is_fileoutput() or self._is_timeout): @@ -884,7 +904,8 @@ class MCUConfigHelper: config_crc = zlib.crc32(encoded_config) & 0xffffffff self.add_config_cmd("finalize_config crc=%d" % (config_crc,)) if prev_crc is not None and config_crc != prev_crc: - self._conn_helper.check_restart_on_crc_mismatch() + restart_helper = self._conn_helper.get_restart_helper() + restart_helper.check_restart_on_crc_mismatch() raise error("MCU '%s' CRC does not match config" % (self._name,)) # Transmit config messages (if needed) try: @@ -924,7 +945,8 @@ class MCUConfigHelper: def _connect(self): config_params = self._send_get_config() if not config_params['is_config']: - self._conn_helper.check_restart_on_send_config() + restart_helper = self._conn_helper.get_restart_helper() + restart_helper.check_restart_on_send_config() # Not configured - send config and issue get_config again self._send_config(None) config_params = self._send_get_config() From 95f263fa5951e719b2a644a31dd7fd7c2734615d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 22 Sep 2025 16:33:21 -0400 Subject: [PATCH 165/411] mcu: Directly register "mcu_identify" handler in each helper class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 9375f20e1..c4e44e18e 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -567,6 +567,7 @@ class MCURestartHelper: self._clocksync = conn_helper.get_clocksync() self._reactor = printer.get_reactor() self._name = mcu.get_name() + # Restart tracking restart_methods = [None, 'arduino', 'cheetah', 'command', 'rpi_usb'] self._restart_method = 'command' serialport, baud = conn_helper.get_serialport() @@ -579,6 +580,8 @@ class MCURestartHelper: printer.register_event_handler("klippy:firmware_restart", self._firmware_restart) printer.register_event_handler("klippy:disconnect", self._disconnect) + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) # Connection phase def _check_restart(self, reason): start_reason = self._printer.get_start_args().get("start_reason") @@ -605,7 +608,7 @@ class MCURestartHelper: # Cheetah boards require RTS to be deasserted # else a reset will trigger the built-in bootloader. return (self._restart_method != "cheetah") - def post_attach_setup_restart(self): + def _mcu_identify(self): self._reset_cmd = self._mcu.try_lookup_command("reset") self._config_reset_cmd = self._mcu.try_lookup_command("config_reset") ext_only = self._reset_cmd is None and self._config_reset_cmd is None @@ -691,10 +694,14 @@ class MCUConnectHelper: if not (self._serialport.startswith("/dev/rpmsg_") or self._serialport.startswith("/tmp/klipper_host_")): self._baud = config.getint('baud', 250000, minval=2400) + # Shutdown tracking self._emergency_stop_cmd = None self._is_shutdown = self._is_timeout = False self._shutdown_clock = 0 self._shutdown_msg = "" + # Register handlers + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) self._restart_helper = MCURestartHelper(config, self) printer.register_event_handler("klippy:shutdown", self._shutdown) def get_mcu(self): @@ -768,19 +775,17 @@ class MCUConnectHelper: self._clocksync.connect(self._serial) except serialhdl.error as e: raise error(str(e)) - def _post_attach_setup_shutdown(self): - self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop") - self._mcu.register_response(self._handle_shutdown, 'shutdown') - self._mcu.register_response(self._handle_shutdown, 'is_shutdown') - self._mcu.register_response(self._handle_starting, 'starting') - def start_attach(self): + def _mcu_identify(self): if self._mcu.is_fileoutput(): self._attach_file() else: self._attach() logging.info(self.log_info()) - self._post_attach_setup_shutdown() - self._restart_helper.post_attach_setup_restart() + # Setup shutdown handling + self._emergency_stop_cmd = self._mcu.lookup_command("emergency_stop") + self._mcu.register_response(self._handle_shutdown, 'shutdown') + self._mcu.register_response(self._handle_shutdown, 'is_shutdown') + self._mcu.register_response(self._handle_starting, 'starting') def _shutdown(self, force=False): if (self._emergency_stop_cmd is None or (self._is_shutdown and not force)): @@ -814,13 +819,17 @@ class MCUStatsHelper: self._clocksync = conn_helper.get_clocksync() self._reactor = printer.get_reactor() self._name = mcu.get_name() + # Statistics tracking self._mcu_freq = 0. self._get_status_info = {} self._stats_sumsq_base = 0. self._mcu_tick_avg = 0. self._mcu_tick_stddev = 0. self._mcu_tick_awake = 0. + # Register handlers printer.register_event_handler("klippy:ready", self._ready) + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) def _handle_mcu_stats(self, params): count = params['count'] tick_sum = params['sum'] @@ -830,7 +839,7 @@ class MCUStatsHelper: diff = count*tick_sumsq - tick_sum**2 self._mcu_tick_stddev = c * math.sqrt(max(0., diff)) self._mcu_tick_awake = tick_sum / self._mcu_freq - def post_attach_setup_stats(self): + def _mcu_identify(self): self._mcu_freq = self._mcu.get_constant_float('CLOCK_FREQ') self._stats_sumsq_base = self._mcu.get_constant_float( 'STATS_SUMSQ_BASE') @@ -878,7 +887,7 @@ class MCUConfigHelper: self._clocksync = conn_helper.get_clocksync() self._reactor = printer.get_reactor() self._name = mcu.get_name() - printer.lookup_object('pins').register_chip(self._name, mcu) + # Configuration tracking self._oid_count = 0 self._config_callbacks = [] self._config_cmds = [] @@ -886,6 +895,10 @@ class MCUConfigHelper: self._init_cmds = [] self._mcu_freq = 0. self._reserved_move_slots = 0 + # Register handlers + printer.lookup_object('pins').register_chip(self._name, mcu) + printer.register_event_handler("klippy:mcu_identify", + self._mcu_identify) printer.register_event_handler("klippy:connect", self._connect) def _send_config(self, prev_crc): # Build config commands @@ -972,7 +985,7 @@ class MCUConfigHelper: logging.info(move_msg) log_info = self._conn_helper.log_info() + "\n" + move_msg self._printer.set_rollover_info(self._name, log_info, log=False) - def post_attach_setup_for_config(self): + def _mcu_identify(self): self._mcu_freq = self._mcu.get_constant_float('CLOCK_FREQ') ppins = self._printer.lookup_object('pins') pin_resolver = ppins.get_pin_resolver(self._name) @@ -1017,24 +1030,17 @@ class MCU: self._name = config.get_name() if self._name.startswith('mcu '): self._name = self._name[4:] - # Low-level connection + # Low-level connection and helpers self._conn_helper = MCUConnectHelper(config, self, clocksync) self._serial = self._conn_helper.get_serial() self._config_helper = MCUConfigHelper(self, self._conn_helper) + self._stats_helper = MCUStatsHelper(self, self._conn_helper) + printer.load_object(config, "error_mcu") # Alter time reporting when debugging if self.is_fileoutput(): def dummy_estimated_print_time(eventtime): return 0. self.estimated_print_time = dummy_estimated_print_time - # Register handlers - self._stats_helper = MCUStatsHelper(self, self._conn_helper) - printer.load_object(config, "error_mcu") - printer.register_event_handler("klippy:mcu_identify", - self._mcu_identify) - def _mcu_identify(self): - self._conn_helper.start_attach() - self._config_helper.post_attach_setup_for_config() - self._stats_helper.post_attach_setup_stats() def setup_pin(self, pin_type, pin_params): return self._config_helper.setup_pin(pin_type, pin_params) def create_oid(self): From a43846c277058c931d9ceb6b328d6d8fefdd5df7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 22 Sep 2025 16:40:36 -0400 Subject: [PATCH 166/411] mcu: Reorganize wrapper methods in main mcu class Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index c4e44e18e..eeec7c0b4 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -1041,6 +1041,13 @@ class MCU: def dummy_estimated_print_time(eventtime): return 0. self.estimated_print_time = dummy_estimated_print_time + def get_name(self): + return self._name + def get_printer(self): + return self._printer + def is_fileoutput(self): + return self._printer.get_start_args().get('debugoutput') is not None + # MCU Configuration wrappers def setup_pin(self, pin_type, pin_params): return self._config_helper.setup_pin(pin_type, pin_params) def create_oid(self): @@ -1055,19 +1062,11 @@ class MCU: return self._config_helper.get_query_slot(oid) def seconds_to_clock(self, time): return self._config_helper.seconds_to_clock(time) + # Command Handler helpers def min_schedule_time(self): return MIN_SCHEDULE_TIME def max_nominal_duration(self): return MAX_NOMINAL_DURATION - # Wrapper functions - def get_printer(self): - return self._printer - def get_name(self): - return self._name - def register_response(self, cb, msg, oid=None): - self._serial.register_response(cb, msg, oid) - def alloc_command_queue(self): - return self._serial.alloc_command_queue() def lookup_command(self, msgformat, cq=None): return CommandWrapper(self._serial, msgformat, cq, debugoutput=self.is_fileoutput()) @@ -1080,12 +1079,19 @@ class MCU: return self.lookup_command(msgformat) except self._serial.get_msgparser().error as e: return None + # SerialHdl wrappers + def register_response(self, cb, msg, oid=None): + self._serial.register_response(cb, msg, oid) + def alloc_command_queue(self): + return self._serial.alloc_command_queue() + # MsgParser wrappers def get_enumerations(self): return self._serial.get_msgparser().get_enumerations() def get_constants(self): return self._serial.get_msgparser().get_constants() def get_constant_float(self, name): return self._serial.get_msgparser().get_constant_float(name) + # ClockSync wrappers def print_time_to_clock(self, print_time): return self._clocksync.print_time_to_clock(print_time) def clock_to_print_time(self, clock): @@ -1098,13 +1104,12 @@ class MCU: offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) self._conn_helper.check_timeout(eventtime) return offset, freq - # Misc external commands - def is_fileoutput(self): - return self._printer.get_start_args().get('debugoutput') is not None + # Low-level connection wrappers def is_shutdown(self): return self._conn_helper.is_shutdown() def get_shutdown_clock(self): return self._conn_helper.get_shutdown_clock() + # Statistics wrappers def get_status(self, eventtime=None): return self._stats_helper.get_status(eventtime) def stats(self, eventtime): From 4cd786fe087580d6370f9b8bccebd6c39de84bca Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 20:16:27 -0400 Subject: [PATCH 167/411] toolhead: Avoid pausing an infinitesimal amount of time Due to differences in mcu clock vs system clock it's possible to repeatedly underestimate a system delay needed to bring about a sufficient mcu time - which just wastes cpu cycles retrying a pause. It's okay to sleep a slightly longer time to avoid the issue. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 6326e607f..cf545d127 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -337,7 +337,8 @@ class ToolHead: if not self.can_pause: self.need_check_pause = self.reactor.NEVER return - eventtime = self.reactor.pause(eventtime + min(1., pause_time)) + eventtime = self.reactor.pause( + eventtime + max(.005, min(1., pause_time))) est_print_time = self.mcu.estimated_print_time(eventtime) buffer_time = self.print_time - est_print_time if not self.special_queuing_state: From 0c86b388a905b56cd511986eb22842152801f696 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 20:21:09 -0400 Subject: [PATCH 168/411] toolhead: Remove extra batching time added in _check_pause() The code currently adds an additional 100ms to BUFFER_TIME_HIGH in _check_pause() to reduce the number of calls to _check_pause(). However, LOOKAHEAD_FLUSH_TIME should already provide sufficient batching so adding more is not necessary. This change should hopefully make configuring BUFFER_TIME_HIGH a little more transparent. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index cf545d127..386f0620e 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -343,7 +343,7 @@ class ToolHead: buffer_time = self.print_time - est_print_time if not self.special_queuing_state: # In main state - defer pause checking until needed - self.need_check_pause = est_print_time + BUFFER_TIME_HIGH + 0.100 + self.need_check_pause = est_print_time + BUFFER_TIME_HIGH def _priming_handler(self, eventtime): self.reactor.unregister_timer(self.priming_timer) self.priming_timer = None From c803249467a84fe1e37e01bc02f88abb29384bab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 Oct 2025 14:09:24 -0400 Subject: [PATCH 169/411] docs: Minor wording change in Code_Overview.md thread description Signed-off-by: Kevin O'Connor --- docs/Code_Overview.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index 56f98d600..50666da04 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -115,13 +115,13 @@ There are several threads in the Klipper host code: (**klippy/reactor.py**) and most high-level actions originate from IO and timer event callbacks from that reactor. * A thread for writing messages to the log so that the other threads - do not block on log writes. This thread resides entirely in the - **klippy/queuelogger.py** code and its operation is generally not + do not block on log writes. This thread resides in the + **klippy/queuelogger.py** code and its multi-threaded nature is not exposed to the main Python thread. * A thread per micro-controller that performs the low-level reading and writing of messages to that micro-controller. It resides in the - **klippy/chelper/serialqueue.c** C code and its operation is - generally not exposed to the Python code. + **klippy/chelper/serialqueue.c** C code and its multi-threaded + nature is not exposed to the Python code. * A thread per micro-controller for processing messages received from that micro-controller in the Python code. This thread is created in **klippy/serialhdl.py**. Care must be taken in Python callbacks @@ -129,8 +129,8 @@ There are several threads in the Klipper host code: the main Python thread. * A thread per stepper motor that calculates the timing of stepper motor step pulses and compresses those times. This thread resides in - the **klippy/chelper/steppersync.c** C code and its operation is - generally not exposed to the Python code. + the **klippy/chelper/steppersync.c** C code and its multi-threaded + nature is not exposed to the Python code. ## Code flow of a move command From d825d43108288d702f9eefcde0cb3041e24948a9 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Wed, 1 Oct 2025 00:27:52 +0200 Subject: [PATCH 170/411] scripts: Updated graph_shaper.py script The change removes the shapers defined there in favor of the standard ones and makes the script a lot more configurable via command-line arguments. Signed-off-by: Dmitry Butyugin --- scripts/graph_shaper.py | 193 ++++++++++++++++------------------------ 1 file changed, 77 insertions(+), 116 deletions(-) diff --git a/scripts/graph_shaper.py b/scripts/graph_shaper.py index b9a6627c8..e97361693 100755 --- a/scripts/graph_shaper.py +++ b/scripts/graph_shaper.py @@ -5,20 +5,15 @@ # Copyright (C) 2020 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. -import optparse, math +import importlib, math, optparse, os, sys import matplotlib +sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), + '..', 'klippy')) +shaper_defs = importlib.import_module('.shaper_defs', 'extras') + # A set of damping ratios to calculate shaper response for -DAMPING_RATIOS=[0.05, 0.1, 0.2] - -# Parameters of the input shaper -SHAPER_FREQ=50.0 -SHAPER_DAMPING_RATIO=0.1 - -# Simulate input shaping of step function for these true resonance frequency -# and damping ratio -STEP_SIMULATION_RESONANCE_FREQ=60. -STEP_SIMULATION_DAMPING_RATIO=0.15 +DEFAULT_DAMPING_RATIOS=[0.075, 0.1, 0.15] # If set, defines which range of frequencies to plot shaper frequency response PLOT_FREQ_RANGE = [] # If empty, will be automatically determined @@ -30,86 +25,8 @@ PLOT_FREQ_STEP = .01 # Input shapers ###################################################################### -def get_zv_shaper(): - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - A = [1., K] - T = [0., .5*t_d] - return (A, T, "ZV") - -def get_zvd_shaper(): - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - A = [1., 2.*K, K**2] - T = [0., .5*t_d, t_d] - return (A, T, "ZVD") - -def get_mzv_shaper(): - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-.75 * SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - - a1 = 1. - 1. / math.sqrt(2.) - a2 = (math.sqrt(2.) - 1.) * K - a3 = a1 * K * K - - A = [a1, a2, a3] - T = [0., .375*t_d, .75*t_d] - return (A, T, "MZV") - -def get_ei_shaper(): - v_tol = 0.05 # vibration tolerance - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - - a1 = .25 * (1. + v_tol) - a2 = .5 * (1. - v_tol) * K - a3 = a1 * K * K - - A = [a1, a2, a3] - T = [0., .5*t_d, t_d] - return (A, T, "EI") - -def get_2hump_ei_shaper(): - v_tol = 0.05 # vibration tolerance - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - - V2 = v_tol**2 - X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.) - a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X) - a2 = (.5 - a1) * K - a3 = a2 * K - a4 = a1 * K * K * K - - A = [a1, a2, a3, a4] - T = [0., .5*t_d, t_d, 1.5*t_d] - return (A, T, "2-hump EI") - -def get_3hump_ei_shaper(): - v_tol = 0.05 # vibration tolerance - df = math.sqrt(1. - SHAPER_DAMPING_RATIO**2) - K = math.exp(-SHAPER_DAMPING_RATIO * math.pi / df) - t_d = 1. / (SHAPER_FREQ * df) - - K2 = K*K - a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol)) - a2 = 0.25 * (1. - v_tol) * K - a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2 - a4 = a2 * K2 - a5 = a1 * K2 * K2 - - A = [a1, a2, a3, a4, a5] - T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d] - return (A, T, "3-hump EI") - - def estimate_shaper(shaper, freq, damping_ratio): - A, T, _ = shaper + A, T = shaper n = len(T) inv_D = 1. / sum(A) omega = 2. * math.pi * freq @@ -123,14 +40,18 @@ def estimate_shaper(shaper, freq, damping_ratio): return math.sqrt(S*S + C*C) * inv_D def shift_pulses(shaper): - A, T, name = shaper + A, T = shaper n = len(T) ts = sum([A[i] * T[i] for i in range(n)]) / sum(A) for i in range(n): T[i] -= ts # Shaper selection -get_shaper = get_ei_shaper +def get_shaper(shaper_name, shaper_freq, damping_ratio): + for s in shaper_defs.INPUT_SHAPERS: + if shaper_name.lower() == s.name: + return s.init_func(shaper_freq, damping_ratio) + return shaper_defs.get_none_shaper() ###################################################################### @@ -148,44 +69,46 @@ def bisect(func, left, right): right = mid return .5 * (left + right) -def find_shaper_plot_range(shaper, vib_tol): +def find_shaper_plot_range(shaper, shaper_freq, test_damping_ratios, vib_tol): def eval_shaper(freq): - return estimate_shaper(shaper, freq, DAMPING_RATIOS[0]) - vib_tol + return estimate_shaper(shaper, freq, test_damping_ratios[0]) - vib_tol if not PLOT_FREQ_RANGE: - left = bisect(eval_shaper, 0., SHAPER_FREQ) - right = bisect(eval_shaper, SHAPER_FREQ, 2.4 * SHAPER_FREQ) + left = bisect(eval_shaper, 0., shaper_freq) + right = bisect(eval_shaper, shaper_freq, 2.4 * shaper_freq) else: left, right = PLOT_FREQ_RANGE return (left, right) -def gen_shaper_response(shaper): +def gen_shaper_response(shaper, shaper_freq, test_damping_ratios): # Calculate shaper vibration response on a range of frequencies response = [] freqs = [] - freq, freq_end = find_shaper_plot_range(shaper, vib_tol=0.25) + freq, freq_end = find_shaper_plot_range(shaper, shaper_freq, + test_damping_ratios, vib_tol=0.25) while freq <= freq_end: vals = [] - for damping_ratio in DAMPING_RATIOS: + for damping_ratio in test_damping_ratios: vals.append(estimate_shaper(shaper, freq, damping_ratio)) response.append(vals) freqs.append(freq) freq += PLOT_FREQ_STEP - legend = ['damping ratio = %.3f' % d_r for d_r in DAMPING_RATIOS] + legend = ['damping ratio = %.3f' % d_r for d_r in test_damping_ratios] return freqs, response, legend -def gen_shaped_step_function(shaper): +def gen_shaped_step_function(shaper, shaper_freq, + system_freq, system_damping_ratio): # Calculate shaping of a step function - A, T, _ = shaper + A, T = shaper inv_D = 1. / sum(A) n = len(T) - omega = 2. * math.pi * STEP_SIMULATION_RESONANCE_FREQ - damping = STEP_SIMULATION_DAMPING_RATIO * omega - omega_d = omega * math.sqrt(1. - STEP_SIMULATION_DAMPING_RATIO**2) - phase = math.acos(STEP_SIMULATION_DAMPING_RATIO) + omega = 2. * math.pi * system_freq + damping = system_damping_ratio * omega + omega_d = omega * math.sqrt(1. - system_damping_ratio**2) + phase = math.acos(system_damping_ratio) - t_start = T[0] - .5 / SHAPER_FREQ - t_end = T[-1] + 1.5 / STEP_SIMULATION_RESONANCE_FREQ + t_start = T[0] - .5 / shaper_freq + t_end = T[-1] + 1.5 / system_freq result = [] time = [] t = t_start @@ -214,20 +137,24 @@ def gen_shaped_step_function(shaper): result.append(val) time.append(t) - t += .01 / SHAPER_FREQ + t += .01 / shaper_freq legend = ['step', 'shaper commanded', 'system response'] return time, result, legend -def plot_shaper(shaper): +def plot_shaper(shaper_name, shaper_freq, damping_ratio, test_damping_ratios, + system_freq, system_damping_ratio): + shaper = get_shaper(shaper_name, shaper_freq, damping_ratio) shift_pulses(shaper) - freqs, response, response_legend = gen_shaper_response(shaper) - time, step_vals, step_legend = gen_shaped_step_function(shaper) + freqs, response, response_legend = gen_shaper_response( + shaper, shaper_freq, test_damping_ratios) + time, step_vals, step_legend = gen_shaped_step_function( + shaper, shaper_freq, system_freq, system_damping_ratio) fig, (ax1, ax2) = matplotlib.pyplot.subplots(nrows=2, figsize=(10,9)) ax1.set_title("Vibration response simulation for shaper '%s',\n" "shaper_freq=%.1f Hz, damping_ratio=%.3f" - % (shaper[-1], SHAPER_FREQ, SHAPER_DAMPING_RATIO)) + % (shaper_name, shaper_freq, damping_ratio)) ax1.plot(freqs, response) ax1.set_ylim(bottom=0.) fontP = matplotlib.font_manager.FontProperties() @@ -241,8 +168,7 @@ def plot_shaper(shaper): ax1.grid(which='minor', color='lightgrey') ax2.set_title("Unit step input, resonance frequency=%.1f Hz, " - "damping ratio=%.3f" % (STEP_SIMULATION_RESONANCE_FREQ, - STEP_SIMULATION_DAMPING_RATIO)) + "damping ratio=%.3f" % (system_freq, system_damping_ratio)) ax2.plot(time, step_vals) ax2.legend(step_legend, loc='best', prop=fontP) ax2.set_xlabel('Time, sec') @@ -264,13 +190,48 @@ def main(): opts = optparse.OptionParser(usage) opts.add_option("-o", "--output", type="string", dest="output", default=None, help="filename of output graph") + opts.add_option("--shaper", type="string", dest="shaper", default="mzv", + help="a shaper to plot") + opts.add_option("--shaper_freq", type="float", dest="shaper_freq", + default=50.0, help="shaper frequency") + opts.add_option("--damping_ratio", type="float", dest="damping_ratio", + default=shaper_defs.DEFAULT_DAMPING_RATIO, + help="shaper damping_ratio parameter") + opts.add_option("--test_damping_ratios", type="string", + dest="test_damping_ratios", + default=",".join(["%.3f" % dr + for dr in DEFAULT_DAMPING_RATIOS]), + help="a comma-separated list of damping ratios to test " + + "input shaper for") + opts.add_option("--system_freq", type="float", dest="system_freq", + default=60.0, + help="natural frequency of a system for step simulation") + opts.add_option("--system_damping_ratio", type="float", + dest="system_damping_ratio", default=0.15, + help="damping_ratio of a system for step simulation") options, args = opts.parse_args() if len(args) != 0: opts.error("Incorrect number of arguments") + if options.shaper.lower() not in [ + s.name for s in shaper_defs.INPUT_SHAPERS]: + opts.error("Invalid --shaper=%s specified" % options.shaper) + + if options.test_damping_ratios: + try: + test_damping_ratios = [float(s) for s in + options.test_damping_ratios.split(',')] + except ValueError: + opts.error("invalid floating point value in " + + "--test_damping_ratios param") + else: + test_damping_ratios = None + # Draw graph setup_matplotlib(options.output is not None) - fig = plot_shaper(get_shaper()) + fig = plot_shaper(options.shaper, options.shaper_freq, + options.damping_ratio, test_damping_ratios, + options.system_freq, options.system_damping_ratio) # Show graph if options.output is None: From c98527ff0089a7c850d1f994ae83c765f4644b6b Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Wed, 1 Oct 2025 00:30:36 +0200 Subject: [PATCH 171/411] docs: Updated the docs to the latest shaper changes and fixed typos Signed-off-by: Dmitry Butyugin --- docs/Config_Reference.md | 18 ++++++++---------- docs/Resonance_Compensation.md | 10 +++++----- scripts/calibrate_shaper.py | 2 +- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index ca21bcf21..6f76c5567 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1955,11 +1955,10 @@ section of the measuring resonances guide for more information on # are reachable by the toolhead. #accel_chip: # A name of the accelerometer chip to use for measurements. If -# adxl345 chip was defined without an explicit name, this parameter -# can simply reference it as "accel_chip: adxl345", otherwise an -# explicit name must be supplied as well, e.g. "accel_chip: adxl345 -# my_chip_name". Either this, or the next two parameters must be -# set. +# an accelerometer was defined without an explicit name, this parameter +# can simply reference it by type, e.g. "accel_chip: adxl345", otherwise +# a full name must be supplied, e.g. "accel_chip: adxl345 my_chip_name". +# Either this, or the next two parameters must be set. #accel_chip_x: #accel_chip_y: # Names of the accelerometer chips to use for measurements for each @@ -1978,16 +1977,15 @@ section of the measuring resonances guide for more information on # during the calibration. The default is 50. #min_freq: 5 # Minimum frequency to test for resonances. The default is 5 Hz. -#max_freq: 133.33 -# Maximum frequency to test for resonances. The default is 133.33 Hz. +#max_freq: 135 +# Maximum frequency to test for resonances. The default is 135 Hz. #accel_per_hz: 60 # This parameter is used to determine which acceleration to use to # test a specific frequency: accel = accel_per_hz * freq. Higher the # value, the higher is the energy of the oscillations. Can be set to # a lower than the default value if the resonances get too strong on -# the printer. However, lower values make measurements of -# high-frequency resonances less precise. The default value is 75 -# (mm/sec). +# the printer. However, lower values make measurements of high-frequency +# resonances less precise. The default value is 60 (mm/sec). #hz_per_sec: 1 # Determines the speed of the test. When testing all frequencies in # range [min_freq, max_freq], each second the frequency increases by diff --git a/docs/Resonance_Compensation.md b/docs/Resonance_Compensation.md index 8f2d2b643..436c762b4 100644 --- a/docs/Resonance_Compensation.md +++ b/docs/Resonance_Compensation.md @@ -469,8 +469,8 @@ parameters of each shaper. | MZV | 0.75 / shaper_freq | ± 4% shaper_freq | -10%...+15% shaper_freq | | ZVD | 1 / shaper_freq | ± 15% shaper_freq | ± 22% shaper_freq | | EI | 1 / shaper_freq | ± 20% shaper_freq | ± 25% shaper_freq | -| 2HUMP_EI | 1.5 / shaper_freq | ± 35% shaper_freq | ± 40 shaper_freq | -| 3HUMP_EI | 2 / shaper_freq | -45...+50% shaper_freq | -50%...+55% shaper_freq | +| 2HUMP_EI | 1.5 / shaper_freq | -40...+45% shaper_freq | -45..+50% shaper_freq | +| 3HUMP_EI | 2 / shaper_freq | -50...+60% shaper_freq | -55%...+65% shaper_freq | A note on vibration reduction: the values in the table above are approximate. If the damping ratio of the printer is known for each axis, the shaper can be @@ -502,11 +502,11 @@ so the values for 10% vibration tolerance are provided only for the reference. resonances at 35 Hz and 60 Hz on the same axis: a) EI shaper needs to have shaper_freq = 35 / (1 - 0.2) = 43.75 Hz, and it will reduce resonances until 43.75 * (1 + 0.2) = 52.5 Hz, so it is not sufficient; b) 2HUMP_EI - shaper needs to have shaper_freq = 35 / (1 - 0.35) = 53.85 Hz and will - reduce vibrations until 53.85 * (1 + 0.35) = 72.7 Hz - so this is an + shaper needs to have shaper_freq = 35 / (1 - 0.4) = 58.3 Hz and will + reduce vibrations until 58.3 * (1 + 0.45) = 84.5 Hz - so this is an acceptable configuration. Always try to use as high shaper_freq as possible for a given shaper (perhaps with some safety margin, so in this example - shaper_freq ≈ 50-52 Hz would work best), and try to use a shaper with as + shaper_freq ≈ 55 Hz would work best), and try to use a shaper with as small shaper duration as possible. * If one needs to reduce vibrations at several very different frequencies (say, 30 Hz and 100 Hz), they may see that the table above does not provide diff --git a/scripts/calibrate_shaper.py b/scripts/calibrate_shaper.py index b56ce5daa..cda0276f3 100755 --- a/scripts/calibrate_shaper.py +++ b/scripts/calibrate_shaper.py @@ -166,7 +166,7 @@ def main(): default=None, help="shaper damping_ratio parameter") opts.add_option("--test_damping_ratios", type="string", dest="test_damping_ratios", default=None, - help="a comma-separated liat of damping ratios to test " + + help="a comma-separated list of damping ratios to test " + "input shaper for") options, args = opts.parse_args() if len(args) < 1: From c570f4e095a424c61e0ee7974f4d8788841d3058 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Thu, 2 Oct 2025 01:28:43 +0200 Subject: [PATCH 172/411] docs: Added description of MZV input shaper Signed-off-by: Dmitry Butyugin --- docs/Resonance_Compensation.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/Resonance_Compensation.md b/docs/Resonance_Compensation.md index 436c762b4..43ceac8f0 100644 --- a/docs/Resonance_Compensation.md +++ b/docs/Resonance_Compensation.md @@ -457,11 +457,24 @@ parameter described in [this section](#selecting-max_accel)). ### Input shapers -Input shapers used in Klipper are rather standard, and one can find more -in-depth overview in the articles describing the corresponding shapers. This section contains a brief overview of some technical aspects of the -supported input shapers. The table below shows some (usually approximate) -parameters of each shaper. +supported input shapers. Input shapers used in Klipper are rather standard, +with the exception of MZV, and one can find more in-depth overview in +the articles describing the corresponding shapers. + +MZV stands for a Modified-ZV input shaper. The classic definition of ZV shaper +assumes the duration Ts equal to 1/2 of the damped period of oscillations Td and +has two pulses. However, ZV input shaper has a generalized form for an arbitrary +duration in the range (0, Td] with three pulses (Specified-Duration ZV, see also +SNA-ZV), with a negative middle pulse if Ts < Td and a positive one if Ts > Td. +The MZV shaper was designed as an intermediate shaper between ZV and ZVD, +offering better vibrations suppression than ZV when the determined (measured) +shaper parameters deviate from the ones actually required by the printer, +and smaller smoothing than ZVD. Effectively, it is a SD-ZV shaper with the +specific duration Ts = 3/4 Td, exactly between ZV (Ts = 1/2 Td) and +ZVD (Ts = Td), and it happens to work well for many real-life 3D printers. + +The table below shows some (usually approximate) parameters of each shaper. | Input
shaper | Shaper
duration | Vibration reduction 20x
(5% vibration tolerance) | Vibration reduction 10x
(10% vibration tolerance) | |:--:|:--:|:--:|:--:| From 768b19e8be6458f43615423f48b74cf1e3ad1a9d Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sat, 4 Oct 2025 00:18:21 +0200 Subject: [PATCH 173/411] input_shaper: Limit maximum damping_ratio of the shapers Numerically optimized versions of *EI shapers have been tuned for specific ranges of damping ratios and will show poor stability outside of these designated ranges. Signed-off-by: Dmitry Butyugin --- klippy/extras/input_shaper.py | 25 +++++++++++++++---------- klippy/extras/shaper_defs.py | 21 ++++++++++++++------- test/klippy/input_shaper.cfg | 3 ++- test/klippy/input_shaper.test | 2 +- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py index c79cdcf2c..ae8ffe815 100644 --- a/klippy/extras/input_shaper.py +++ b/klippy/extras/input_shaper.py @@ -11,34 +11,39 @@ from . import shaper_defs class InputShaperParams: def __init__(self, axis, config): self.axis = axis - self.shapers = {s.name : s.init_func for s in shaper_defs.INPUT_SHAPERS} + self.shapers = {s.name : s for s in shaper_defs.INPUT_SHAPERS} shaper_type = config.get('shaper_type', 'mzv') self.shaper_type = config.get('shaper_type_' + axis, shaper_type) if self.shaper_type not in self.shapers: raise config.error( 'Unsupported shaper type: %s' % (self.shaper_type,)) - self.damping_ratio = config.getfloat('damping_ratio_' + axis, - shaper_defs.DEFAULT_DAMPING_RATIO, - minval=0., maxval=1.) + self.damping_ratio = config.getfloat( + 'damping_ratio_' + axis, + shaper_defs.DEFAULT_DAMPING_RATIO, minval=0., + maxval=self.shapers[self.shaper_type].max_damping_ratio) self.shaper_freq = config.getfloat('shaper_freq_' + axis, 0., minval=0.) def update(self, gcmd): axis = self.axis.upper() - self.damping_ratio = gcmd.get_float('DAMPING_RATIO_' + axis, - self.damping_ratio, - minval=0., maxval=1.) - self.shaper_freq = gcmd.get_float('SHAPER_FREQ_' + axis, - self.shaper_freq, minval=0.) shaper_type = gcmd.get('SHAPER_TYPE', None) if shaper_type is None: shaper_type = gcmd.get('SHAPER_TYPE_' + axis, self.shaper_type) if shaper_type.lower() not in self.shapers: raise gcmd.error('Unsupported shaper type: %s' % (shaper_type,)) + damping_ratio = gcmd.get_float('DAMPING_RATIO_' + axis, + self.damping_ratio, minval=0.) + if damping_ratio > self.shapers[shaper_type.lower()].max_damping_ratio: + raise gcmd.error( + 'Too high value of damping_ratio=%.3f for shaper %s' + ' on axis %c' % (damping_ratio, shaper_type, axis)) + self.shaper_freq = gcmd.get_float('SHAPER_FREQ_' + axis, + self.shaper_freq, minval=0.) + self.damping_ratio = damping_ratio self.shaper_type = shaper_type.lower() def get_shaper(self): if not self.shaper_freq: A, T = shaper_defs.get_none_shaper() else: - A, T = self.shapers[self.shaper_type]( + A, T = self.shapers[self.shaper_type].init_func( self.shaper_freq, self.damping_ratio) return len(A), A, T def get_status(self): diff --git a/klippy/extras/shaper_defs.py b/klippy/extras/shaper_defs.py index 8a0d93962..60fb1aa66 100644 --- a/klippy/extras/shaper_defs.py +++ b/klippy/extras/shaper_defs.py @@ -9,7 +9,8 @@ SHAPER_VIBRATION_REDUCTION=20. DEFAULT_DAMPING_RATIO = 0.1 InputShaperCfg = collections.namedtuple( - 'InputShaperCfg', ('name', 'init_func', 'min_freq')) + 'InputShaperCfg', + ('name', 'init_func', 'min_freq', 'max_damping_ratio')) def get_none_shaper(): return ([], []) @@ -105,10 +106,16 @@ def get_3hump_ei_shaper(shaper_freq, damping_ratio): # min_freq for each shaper is chosen to have projected max_accel ~= 1500 INPUT_SHAPERS = [ - InputShaperCfg('zv', get_zv_shaper, min_freq=21.), - InputShaperCfg('mzv', get_mzv_shaper, min_freq=23.), - InputShaperCfg('zvd', get_zvd_shaper, min_freq=29.), - InputShaperCfg('ei', get_ei_shaper, min_freq=29.), - InputShaperCfg('2hump_ei', get_2hump_ei_shaper, min_freq=39.), - InputShaperCfg('3hump_ei', get_3hump_ei_shaper, min_freq=48.), + InputShaperCfg(name='zv', init_func=get_zv_shaper, + min_freq=21., max_damping_ratio=0.99), + InputShaperCfg(name='mzv', init_func=get_mzv_shaper, + min_freq=23., max_damping_ratio=0.99), + InputShaperCfg(name='zvd', init_func=get_zvd_shaper, + min_freq=29., max_damping_ratio=0.99), + InputShaperCfg(name='ei', init_func=get_ei_shaper, + min_freq=29., max_damping_ratio=0.4), + InputShaperCfg(name='2hump_ei', init_func=get_2hump_ei_shaper, + min_freq=39., max_damping_ratio=0.3), + InputShaperCfg(name='3hump_ei', init_func=get_3hump_ei_shaper, + min_freq=48., max_damping_ratio=0.2), ] diff --git a/test/klippy/input_shaper.cfg b/test/klippy/input_shaper.cfg index bca94eb77..4491f1e5f 100644 --- a/test/klippy/input_shaper.cfg +++ b/test/klippy/input_shaper.cfg @@ -70,8 +70,9 @@ max_z_accel: 100 [input_shaper] shaper_type_x: mzv shaper_freq_x: 33.2 -shaper_type_x: ei +shaper_type_y: ei shaper_freq_x: 39.3 +damping_ratio_y: 0.4 [adxl345] cs_pin: PK7 diff --git a/test/klippy/input_shaper.test b/test/klippy/input_shaper.test index 8714ec5e2..bc993ce27 100644 --- a/test/klippy/input_shaper.test +++ b/test/klippy/input_shaper.test @@ -4,4 +4,4 @@ DICTIONARY atmega2560.dict # Simple command test SET_INPUT_SHAPER SHAPER_FREQ_X=22.2 DAMPING_RATIO_X=.1 SHAPER_TYPE_X=zv -SET_INPUT_SHAPER SHAPER_FREQ_Y=33.3 DAMPING_RATIO_X=.11 SHAPER_TYPE_X=2hump_ei +SET_INPUT_SHAPER SHAPER_FREQ_Y=33.3 DAMPING_RATIO_Y=.11 SHAPER_TYPE_Y=2hump_ei From c49dbb5a879df16ebf3014ef0901eb9dd61e6225 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 4 Oct 2025 19:35:41 -0400 Subject: [PATCH 174/411] trapq: Fix bug that broke numerical stability workaround for initial movement Commit b60804bb changed the trapq head sentinel to store print_time=-1. However, it failed to update trapq_add_move() that relied on that value to detect the head sentinel. As a result, numerical stability issues could lead to stepcompress errors. Fix by changing trapq_add_move() to detect the head sentinel even with a negative print_time. Signed-off-by: Kevin O'Connor --- klippy/chelper/trapq.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/chelper/trapq.c b/klippy/chelper/trapq.c index a21969414..c238a3818 100644 --- a/klippy/chelper/trapq.c +++ b/klippy/chelper/trapq.c @@ -104,7 +104,7 @@ trapq_add_move(struct trapq *tq, struct move *m) // Add a null move to fill time gap struct move *null_move = move_alloc(); null_move->start_pos = m->start_pos; - if (!prev->print_time && m->print_time > MAX_NULL_MOVE) + if (prev->print_time <= 0. && m->print_time > MAX_NULL_MOVE) // Limit the first null move to improve numerical stability null_move->print_time = m->print_time - MAX_NULL_MOVE; else From caf7accf2d7b4d3c810bbb16f4ff7d4be80b043c Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Fri, 19 Sep 2025 21:46:56 +0200 Subject: [PATCH 175/411] input_shaper: Z-axis input shaper Signed-off-by: Dmitry Butyugin --- klippy/chelper/kin_shaper.c | 96 ++++++++++++++++++++++++----------- klippy/extras/input_shaper.py | 10 ++-- test/klippy/input_shaper.cfg | 3 +- 3 files changed, 73 insertions(+), 36 deletions(-) diff --git a/klippy/chelper/kin_shaper.c b/klippy/chelper/kin_shaper.c index d5138ff04..1c4724360 100644 --- a/klippy/chelper/kin_shaper.c +++ b/klippy/chelper/kin_shaper.c @@ -1,7 +1,7 @@ // Kinematic input shapers to minimize motion vibrations in XY plane // // Copyright (C) 2019-2020 Kevin O'Connor -// Copyright (C) 2020 Dmitry Butyugin +// Copyright (C) 2020-2025 Dmitry Butyugin // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -18,6 +18,8 @@ * Shaper initialization ****************************************************************/ +static const int KIN_FLAGS[3] = { AF_X, AF_Y, AF_Z }; + struct shaper_pulses { int num_pulses; struct { @@ -113,7 +115,7 @@ struct input_shaper { struct stepper_kinematics sk; struct stepper_kinematics *orig_sk; struct move m; - struct shaper_pulses sx, sy; + struct shaper_pulses sp[3]; }; // Optimized calc_position when only x axis is needed @@ -122,9 +124,10 @@ shaper_x_calc_position(struct stepper_kinematics *sk, struct move *m , double move_time) { struct input_shaper *is = container_of(sk, struct input_shaper, sk); - if (!is->sx.num_pulses) + struct shaper_pulses *sx = &is->sp[0]; + if (!sx->num_pulses) return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time); - is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx); + is->m.start_pos.x = calc_position(m, 'x', move_time, sx); return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T); } @@ -134,25 +137,41 @@ shaper_y_calc_position(struct stepper_kinematics *sk, struct move *m , double move_time) { struct input_shaper *is = container_of(sk, struct input_shaper, sk); - if (!is->sy.num_pulses) + struct shaper_pulses *sy = &is->sp[1]; + if (!sy->num_pulses) return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time); - is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy); + is->m.start_pos.y = calc_position(m, 'y', move_time, sy); return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T); } -// General calc_position for both x and y axes +// Optimized calc_position when only z axis is needed static double -shaper_xy_calc_position(struct stepper_kinematics *sk, struct move *m - , double move_time) +shaper_z_calc_position(struct stepper_kinematics *sk, struct move *m + , double move_time) { struct input_shaper *is = container_of(sk, struct input_shaper, sk); - if (!is->sx.num_pulses && !is->sy.num_pulses) + struct shaper_pulses *sz = &is->sp[2]; + if (!sz->num_pulses) + return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time); + is->m.start_pos.z = calc_position(m, 'z', move_time, sz); + return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T); +} + +// General calc_position for all x, y, and z axes +static double +shaper_xyz_calc_position(struct stepper_kinematics *sk, struct move *m + , double move_time) +{ + struct input_shaper *is = container_of(sk, struct input_shaper, sk); + if (!is->sp[0].num_pulses && !is->sp[1].num_pulses && !is->sp[2].num_pulses) return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time); is->m.start_pos = move_get_coord(m, move_time); - if (is->sx.num_pulses) - is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx); - if (is->sy.num_pulses) - is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy); + if (is->sp[0].num_pulses) + is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sp[0]); + if (is->sp[1].num_pulses) + is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sp[1]); + if (is->sp[2].num_pulses) + is->m.start_pos.z = calc_position(m, 'z', move_time, &is->sp[2]); return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T); } @@ -170,15 +189,24 @@ static void shaper_note_generation_time(struct input_shaper *is) { double pre_active = 0., post_active = 0.; - if ((is->sk.active_flags & AF_X) && is->sx.num_pulses) { - pre_active = is->sx.pulses[is->sx.num_pulses-1].t; - post_active = -is->sx.pulses[0].t; + struct shaper_pulses *sx = &is->sp[0]; + if ((is->sk.active_flags & AF_X) && sx->num_pulses) { + pre_active = sx->pulses[sx->num_pulses-1].t; + post_active = -sx->pulses[0].t; } - if ((is->sk.active_flags & AF_Y) && is->sy.num_pulses) { - pre_active = is->sy.pulses[is->sy.num_pulses-1].t > pre_active - ? is->sy.pulses[is->sy.num_pulses-1].t : pre_active; - post_active = -is->sy.pulses[0].t > post_active - ? -is->sy.pulses[0].t : post_active; + struct shaper_pulses *sy = &is->sp[1]; + if ((is->sk.active_flags & AF_Y) && sy->num_pulses) { + pre_active = sy->pulses[sy->num_pulses-1].t > pre_active + ? sy->pulses[sy->num_pulses-1].t : pre_active; + post_active = -sy->pulses[0].t > post_active + ? -sy->pulses[0].t : post_active; + } + struct shaper_pulses *sz = &is->sp[2]; + if ((is->sk.active_flags & AF_Z) && sz->num_pulses) { + pre_active = sz->pulses[sz->num_pulses-1].t > pre_active + ? sz->pulses[sz->num_pulses-1].t : pre_active; + post_active = -sz->pulses[0].t > post_active + ? -sz->pulses[0].t : post_active; } is->sk.gen_steps_pre_active = pre_active; is->sk.gen_steps_post_active = post_active; @@ -188,12 +216,15 @@ void __visible input_shaper_update_sk(struct stepper_kinematics *sk) { struct input_shaper *is = container_of(sk, struct input_shaper, sk); - if ((is->orig_sk->active_flags & (AF_X | AF_Y)) == (AF_X | AF_Y)) - is->sk.calc_position_cb = shaper_xy_calc_position; - else if (is->orig_sk->active_flags & AF_X) + int kin_flags = is->orig_sk->active_flags & (AF_X | AF_Y | AF_Z); + if ((kin_flags & AF_X) == AF_X) is->sk.calc_position_cb = shaper_x_calc_position; - else if (is->orig_sk->active_flags & AF_Y) + else if ((kin_flags & AF_Y) == AF_Y) is->sk.calc_position_cb = shaper_y_calc_position; + else if ((kin_flags & AF_Z) == AF_Z) + is->sk.calc_position_cb = shaper_z_calc_position; + else + is->sk.calc_position_cb = shaper_xyz_calc_position; is->sk.active_flags = is->orig_sk->active_flags; shaper_note_generation_time(is); } @@ -207,8 +238,10 @@ input_shaper_set_sk(struct stepper_kinematics *sk is->sk.calc_position_cb = shaper_x_calc_position; else if (orig_sk->active_flags == AF_Y) is->sk.calc_position_cb = shaper_y_calc_position; - else if (orig_sk->active_flags & (AF_X | AF_Y)) - is->sk.calc_position_cb = shaper_xy_calc_position; + else if (orig_sk->active_flags == AF_Z) + is->sk.calc_position_cb = shaper_z_calc_position; + else if (orig_sk->active_flags & (AF_X | AF_Y | AF_Z)) + is->sk.calc_position_cb = shaper_xyz_calc_position; else return -1; is->sk.active_flags = orig_sk->active_flags; @@ -226,13 +259,14 @@ int __visible input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis , int n, double a[], double t[]) { - if (axis != 'x' && axis != 'y') + int axis_ind = axis-'x'; + if (axis_ind < 0 || axis_ind >= ARRAY_SIZE(KIN_FLAGS)) return -1; struct input_shaper *is = container_of(sk, struct input_shaper, sk); - struct shaper_pulses *sp = axis == 'x' ? &is->sx : &is->sy; + struct shaper_pulses *sp = &is->sp[axis_ind]; int status = 0; // Ignore input shaper update if the axis is not active - if (is->orig_sk->active_flags & (axis == 'x' ? AF_X : AF_Y)) { + if (is->orig_sk->active_flags & KIN_FLAGS[axis_ind]) { status = init_shaper(n, a, t, sp); shaper_note_generation_time(is); } diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py index ae8ffe815..39b3f5a85 100644 --- a/klippy/extras/input_shaper.py +++ b/klippy/extras/input_shaper.py @@ -1,7 +1,7 @@ # Kinematic input shaper to minimize motion vibrations in XY plane # # Copyright (C) 2019-2020 Kevin O'Connor -# Copyright (C) 2020 Dmitry Butyugin +# Copyright (C) 2020-2025 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. import collections @@ -100,7 +100,8 @@ class InputShaper: self._update_kinematics) self.toolhead = None self.shapers = [AxisInputShaper('x', config), - AxisInputShaper('y', config)] + AxisInputShaper('y', config), + AxisInputShaper('z', config)] self.input_shaper_stepper_kinematics = [] self.orig_stepper_kinematics = [] # Register gcode commands @@ -191,8 +192,9 @@ class InputShaper: for shaper in self.shapers: shaper.update(gcmd) self._update_input_shaping() - for shaper in self.shapers: - shaper.report(gcmd) + for ind, shaper in enumerate(self.shapers): + if ind < 2 or shaper.is_enabled(): + shaper.report(gcmd) def load_config(config): return InputShaper(config) diff --git a/test/klippy/input_shaper.cfg b/test/klippy/input_shaper.cfg index 4491f1e5f..c6c47c1ad 100644 --- a/test/klippy/input_shaper.cfg +++ b/test/klippy/input_shaper.cfg @@ -71,8 +71,9 @@ max_z_accel: 100 shaper_type_x: mzv shaper_freq_x: 33.2 shaper_type_y: ei -shaper_freq_x: 39.3 +shaper_freq_y: 39.3 damping_ratio_y: 0.4 +shaper_freq_z: 42 [adxl345] cs_pin: PK7 From ec82cee7fc6653304b766b15d4d31dbd59698e21 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Fri, 19 Sep 2025 21:48:36 +0200 Subject: [PATCH 176/411] resonance_tester: Support testing resonances over Z axis Signed-off-by: Dmitry Butyugin --- klippy/extras/resonance_tester.py | 149 ++++++++++++++++++++++-------- scripts/calibrate_shaper.py | 3 + test/klippy/input_shaper.cfg | 1 + 3 files changed, 117 insertions(+), 36 deletions(-) diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index 4f4ef7bb4..b92a163bc 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -1,19 +1,23 @@ # A utility class to test resonances of the printer # -# Copyright (C) 2020-2024 Dmitry Butyugin +# Copyright (C) 2020-2025 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. -import logging, math, os, time +import itertools, logging, math, os, time from . import shaper_calibrate class TestAxis: def __init__(self, axis=None, vib_dir=None): if axis is None: - self._name = "axis=%.3f,%.3f" % (vib_dir[0], vib_dir[1]) + self._name = "axis=%.3f,%.3f,%.3f" % ( + vib_dir[0], vib_dir[1], + (vib_dir[2] if len(vib_dir) == 3 else 0.)) else: self._name = axis if vib_dir is None: - self._vib_dir = (1., 0.) if axis == 'x' else (0., 1.) + self._vib_dir = [(1., 0., 0.), + (0., 1., 0.), + (0., 0., 1.)][ord(axis)-ord('x')] else: s = math.sqrt(sum([d*d for d in vib_dir])) self._vib_dir = [d / s for d in vib_dir] @@ -22,43 +26,54 @@ class TestAxis: return True if self._vib_dir[1] and 'y' in chip_axis: return True + if self._vib_dir[2] and 'z' in chip_axis: + return True return False + def get_dir(self): + return self._vib_dir def get_name(self): return self._name def get_point(self, l): - return (self._vib_dir[0] * l, self._vib_dir[1] * l) + return tuple(d * l for d in self._vib_dir) def _parse_axis(gcmd, raw_axis): if raw_axis is None: return None raw_axis = raw_axis.lower() - if raw_axis in ['x', 'y']: + if raw_axis in ['x', 'y', 'z']: return TestAxis(axis=raw_axis) dirs = raw_axis.split(',') - if len(dirs) != 2: + if len(dirs) not in (2, 3): raise gcmd.error("Invalid format of axis '%s'" % (raw_axis,)) try: dir_x = float(dirs[0].strip()) dir_y = float(dirs[1].strip()) + dir_z = float(dirs[2].strip()) if len(dirs) == 3 else 0. except: raise gcmd.error( "Unable to parse axis direction '%s'" % (raw_axis,)) - return TestAxis(vib_dir=(dir_x, dir_y)) + return TestAxis(vib_dir=(dir_x, dir_y, dir_z)) class VibrationPulseTestGenerator: def __init__(self, config): self.min_freq = config.getfloat('min_freq', 5., minval=1.) self.max_freq = config.getfloat('max_freq', 135., minval=self.min_freq, maxval=300.) + self.max_freq_z = config.getfloat('max_freq_z', 100., + minval=self.min_freq, maxval=300.) self.accel_per_hz = config.getfloat('accel_per_hz', 60., above=0.) + self.accel_per_hz_z = config.getfloat('accel_per_hz_z', 15., above=0.) self.hz_per_sec = config.getfloat('hz_per_sec', 1., minval=0.1, maxval=2.) - def prepare_test(self, gcmd): + def prepare_test(self, gcmd, is_z): self.freq_start = gcmd.get_float("FREQ_START", self.min_freq, minval=1.) - self.freq_end = gcmd.get_float("FREQ_END", self.max_freq, + self.freq_end = gcmd.get_float("FREQ_END", (self.max_freq_z if is_z + else self.max_freq), minval=self.freq_start, maxval=300.) self.test_accel_per_hz = gcmd.get_float("ACCEL_PER_HZ", - self.accel_per_hz, above=0.) + (self.accel_per_hz_z if is_z + else self.accel_per_hz), + above=0.) self.test_hz_per_sec = gcmd.get_float("HZ_PER_SEC", self.hz_per_sec, above=0., maxval=2.) def gen_test(self): @@ -83,12 +98,15 @@ class SweepingVibrationsTestGenerator: def __init__(self, config): self.vibration_generator = VibrationPulseTestGenerator(config) self.sweeping_accel = config.getfloat('sweeping_accel', 400., above=0.) + self.sweeping_accel_z = config.getfloat('sweeping_accel_z', 50., + above=0.) self.sweeping_period = config.getfloat('sweeping_period', 1.2, minval=0.) - def prepare_test(self, gcmd): - self.vibration_generator.prepare_test(gcmd) + def prepare_test(self, gcmd, is_z): + self.vibration_generator.prepare_test(gcmd, is_z) self.test_sweeping_accel = gcmd.get_float( - "SWEEPING_ACCEL", self.sweeping_accel, above=0.) + "SWEEPING_ACCEL", (self.sweeping_accel_z if is_z + else self.sweeping_accel), above=0.) self.test_sweeping_period = gcmd.get_float( "SWEEPING_PERIOD", self.sweeping_period, minval=0.) def gen_test(self): @@ -120,25 +138,62 @@ class SweepingVibrationsTestGenerator: def get_max_freq(self): return self.vibration_generator.get_max_freq() +# Helper to lookup Z kinematics limits +def lookup_z_limits(configfile): + sconfig = configfile.get_status(None)['settings'] + printer_config = sconfig.get('printer') + max_z_velocity = printer_config.get('max_z_velocity') + if max_z_velocity is None: + max_z_velocity = printer_config.get('max_velocity') + max_z_accel = printer_config.get('max_z_accel') + if max_z_accel is None: + max_z_accel = printer_config.get('max_accel') + return max_z_velocity, max_z_accel + class ResonanceTestExecutor: def __init__(self, config): self.printer = config.get_printer() self.gcode = self.printer.lookup_object('gcode') def run_test(self, test_seq, axis, gcmd): reactor = self.printer.get_reactor() + configfile = self.printer.lookup_object('configfile') toolhead = self.printer.lookup_object('toolhead') tpos = toolhead.get_position() - X, Y = tpos[:2] + X, Y, Z = tpos[:3] # Override maximum acceleration and acceleration to # deceleration based on the maximum test frequency systime = reactor.monotonic() toolhead_info = toolhead.get_status(systime) + old_max_velocity = toolhead_info['max_velocity'] old_max_accel = toolhead_info['max_accel'] old_minimum_cruise_ratio = toolhead_info['minimum_cruise_ratio'] max_accel = max([abs(a) for _, a, _ in test_seq]) + max_velocity = 0. + last_v = last_t = 0. + for next_t, accel, freq in test_seq: + v = last_v + accel * (next_t - last_t) + max_velocity = max(max_velocity, abs(v)) + last_t, last_v = next_t, v + if axis.get_dir()[2]: + max_z_velocity, max_z_accel = lookup_z_limits(configfile) + error_msg = "" + if max_velocity > max_z_velocity: + error_msg = ( + "Insufficient maximum Z velocity for these" + " test parameters, increase at least to %.f mm/s" + " for the resonance test." % (max_velocity+0.5)) + if max_accel > max_z_accel: + if error_msg: + error_msg += "\n" + error_msg += ( + "Insufficient maximum Z acceleration for these" + " test parameters, increase at least to %.f mm/s^2" + " for the resonance test." % (max_accel+0.5)) + if error_msg: + raise gcmd.error(error_msg) self.gcode.run_script_from_command( - "SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=0" - % (max_accel,)) + "SET_VELOCITY_LIMIT VELOCITY=%.f ACCEL=%.f MINIMUM_CRUISE_RATIO=0" + % (max_velocity+0.5, max_accel+0.5,)) input_shaper = self.printer.lookup_object('input_shaper', None) if input_shaper is not None and not gcmd.get_int('INPUT_SHAPING', 0): input_shaper.disable_shaping() @@ -166,34 +221,38 @@ class ResonanceTestExecutor: v = abs_v = 0. half_inv_accel = .5 / accel d = (v * v - last_v2) * half_inv_accel - dX, dY = axis.get_point(d) + dX, dY, dZ = axis.get_point(d) nX = X + dX nY = Y + dY + nZ = Z + dZ toolhead.limit_next_junction_speed(abs_last_v) if v * last_v < 0: # The move first goes to a complete stop, then changes direction d_decel = -last_v2 * half_inv_accel - decel_X, decel_Y = axis.get_point(d_decel) - toolhead.move([X + decel_X, Y + decel_Y] + tpos[2:], abs_last_v) - toolhead.move([nX, nY] + tpos[2:], abs_v) + decel_X, decel_Y, decel_Z = axis.get_point(d_decel) + toolhead.move([X + decel_X, Y + decel_Y, Z + decel_Z] + + tpos[3:], abs_last_v) + toolhead.move([nX, nY, nZ] + tpos[3:], abs_v) else: - toolhead.move([nX, nY] + tpos[2:], max(abs_v, abs_last_v)) + toolhead.move([nX, nY, nZ] + tpos[3:], max(abs_v, abs_last_v)) if math.floor(freq) > math.floor(last_freq): gcmd.respond_info("Testing frequency %.0f Hz" % (freq,)) reactor.pause(reactor.monotonic() + 0.01) - X, Y = nX, nY + X, Y, Z = nX, nY, nZ last_t = next_t last_v = v last_freq = freq if last_v: d_decel = -.5 * last_v2 / old_max_accel - decel_X, decel_Y = axis.get_point(d_decel) + decel_X, decel_Y, decel_Z = axis.get_point(d_decel) toolhead.set_max_velocities(None, old_max_accel, None, None) - toolhead.move([X + decel_X, Y + decel_Y] + tpos[2:], abs(last_v)) + toolhead.move([X + decel_X, Y + decel_Y, Z + decel_Z] + tpos[3:], + abs(last_v)) # Restore the original acceleration values self.gcode.run_script_from_command( - "SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=%.3f" - % (old_max_accel, old_minimum_cruise_ratio)) + ("SET_VELOCITY_LIMIT VELOCITY=%.3f ACCEL=%.3f" + + " MINIMUM_CRUISE_RATIO=%.3f") % (old_max_velocity, old_max_accel, + old_minimum_cruise_ratio)) # Restore input shaper if it was disabled for resonance testing if input_shaper is not None: input_shaper.enable_shaping() @@ -206,13 +265,21 @@ class ResonanceTester: self.generator = SweepingVibrationsTestGenerator(config) self.executor = ResonanceTestExecutor(config) if not config.get('accel_chip_x', None): - self.accel_chip_names = [('xy', config.get('accel_chip').strip())] + accel_chip_names = [ + ('xy', config.get('accel_chip').strip()), + ('z', config.get('accel_chip_z', '').strip())] else: - self.accel_chip_names = [ - ('x', config.get('accel_chip_x').strip()), - ('y', config.get('accel_chip_y').strip())] - if self.accel_chip_names[0][1] == self.accel_chip_names[1][1]: - self.accel_chip_names = [('xy', self.accel_chip_names[0][1])] + accel_chip_names = [ + ('x', config.get('accel_chip_x').strip()), + ('y', config.get('accel_chip_y').strip()), + ('z', config.get('accel_chip_z', '').strip())] + get_chip_name = lambda t: t[1] + # Group chips by their axes + self.accel_chip_names = [ + (''.join(sorted(axis for axis, _ in vals)), chip_name) + for chip_name, vals in itertools.groupby( + sorted(accel_chip_names, key=get_chip_name), + key=get_chip_name)] self.max_smoothing = config.getfloat('max_smoothing', None, minval=0.05) self.probe_points = config.getlists('probe_points', seps=(',', '\n'), parser=float, count=3) @@ -232,6 +299,8 @@ class ResonanceTester: def connect(self): self.accel_chips = [] for chip_axis, chip_name in self.accel_chip_names: + if not chip_name: + continue chip = self.printer.lookup_object(chip_name) if not hasattr(chip, 'start_internal_client'): raise self.printer.config_error( @@ -243,7 +312,10 @@ class ResonanceTester: toolhead = self.printer.lookup_object('toolhead') calibration_data = {axis: None for axis in axes} - self.generator.prepare_test(gcmd) + has_z = [axis.get_dir()[2] for axis in axes] + if all(has_z) != any(has_z): + raise gcmd.error("Cannot test Z axis together with other axes") + self.generator.prepare_test(gcmd, is_z=all(has_z)) test_points = [test_point] if test_point else self.probe_points @@ -268,6 +340,10 @@ class ResonanceTester: for chip in accel_chips: aclient = chip.start_internal_client() raw_values.append((axis, aclient, chip.name)) + if not raw_values: + raise gcmd.error( + "No accelerometers specified that can measure" + " resonances over axis '%s'" % axis.get_name()) # Generate moves test_seq = self.generator.gen_test() @@ -278,7 +354,8 @@ class ResonanceTester: raw_name = self.get_filename( 'raw_data', raw_name_suffix, axis, point if len(test_points) > 1 else None, - chip_name if accel_chips is not None else None,) + chip_name if (accel_chips is not None + or len(raw_values) > 1) else None) aclient.write_to_file(raw_name) gcmd.respond_info( "Writing raw accelerometer data to " @@ -366,7 +443,7 @@ class ResonanceTester: axis = gcmd.get("AXIS", None) if not axis: calibrate_axes = [TestAxis('x'), TestAxis('y')] - elif axis.lower() not in 'xy': + elif axis.lower() not in 'xyz': raise gcmd.error("Unsupported axis '%s'" % (axis,)) else: calibrate_axes = [TestAxis(axis.lower())] diff --git a/scripts/calibrate_shaper.py b/scripts/calibrate_shaper.py index cda0276f3..ebf5c8aa1 100755 --- a/scripts/calibrate_shaper.py +++ b/scripts/calibrate_shaper.py @@ -77,6 +77,9 @@ def calibrate_shaper(datas, csv_output, *, shapers, damping_ratio, scv, def plot_freq_response(lognames, calibration_data, shapers, selected_shaper, max_freq): + max_freq_bin = calibration_data.freq_bins.max() + if max_freq > max_freq_bin: + max_freq = max_freq_bin freqs = calibration_data.freq_bins psd = calibration_data.psd_sum[freqs <= max_freq] px = calibration_data.psd_x[freqs <= max_freq] diff --git a/test/klippy/input_shaper.cfg b/test/klippy/input_shaper.cfg index c6c47c1ad..33ec4e213 100644 --- a/test/klippy/input_shaper.cfg +++ b/test/klippy/input_shaper.cfg @@ -85,3 +85,4 @@ axes_map: -x,-y,z probe_points: 20,20,20 accel_chip_x: adxl345 accel_chip_y: mpu9250 my_mpu +accel_chip_z: adxl345 From 470803853e4d6b547e933a979ff3736cb95c7c2a Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Tue, 23 Sep 2025 22:23:33 +0200 Subject: [PATCH 177/411] docs: Documentation for Z axis input shaper and resonance measurements Signed-off-by: Dmitry Butyugin --- docs/Config_Reference.md | 22 ++++++-- docs/G-Codes.md | 29 ++++++----- docs/Measuring_Resonances.md | 89 +++++++++++++++++++++++++++++++++ docs/Resonance_Compensation.md | 21 ++++++-- docs/img/calibrate-z.png | Bin 0 -> 161737 bytes 5 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 docs/img/calibrate-z.png diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 6f76c5567..94d3a9730 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1780,17 +1780,22 @@ the [command reference](G-Codes.md#input_shaper). # input shapers, this parameter can be set from different # considerations. The default value is 0, which disables input # shaping for Y axis. +#shaper_freq_z: 0 +# A frequency (in Hz) of the input shaper for Z axis. The default +# value is 0, which disables input shaping for Z axis. #shaper_type: mzv -# A type of the input shaper to use for both X and Y axes. Supported +# A type of the input shaper to use for all axes. Supported # shapers are zv, mzv, zvd, ei, 2hump_ei, and 3hump_ei. The default # is mzv input shaper. #shaper_type_x: #shaper_type_y: -# If shaper_type is not set, these two parameters can be used to -# configure different input shapers for X and Y axes. The same +#shaper_type_z: +# If shaper_type is not set, these parameters can be used to +# configure different input shapers for X, Y, and Z axes. The same # values are supported as for shaper_type parameter. #damping_ratio_x: 0.1 #damping_ratio_y: 0.1 +#damping_ratio_z: 0.1 # Damping ratios of vibrations of X and Y axes used by input shapers # to improve vibration suppression. Default value is 0.1 which is a # good all-round value for most printers. In most circumstances this @@ -1967,6 +1972,10 @@ section of the measuring resonances guide for more information on # and on the toolhead (for X axis). These parameters have the same # format as 'accel_chip' parameter. Only 'accel_chip' or these two # parameters must be provided. +#accel_chip_z: +# A name of the accelerometer chip to use for measurements of Z axis. +# This parameter has the same format as 'accel_chip'. The default is +# not to configure an accelerometer for Z axis. #max_smoothing: # Maximum input shaper smoothing to allow for each axis during shaper # auto-calibration (with 'SHAPER_CALIBRATE' command). By default no @@ -1979,6 +1988,8 @@ section of the measuring resonances guide for more information on # Minimum frequency to test for resonances. The default is 5 Hz. #max_freq: 135 # Maximum frequency to test for resonances. The default is 135 Hz. +#max_freq_z: 100 +# Maximum frequency to test Z axis for resonances. The default is 100 Hz. #accel_per_hz: 60 # This parameter is used to determine which acceleration to use to # test a specific frequency: accel = accel_per_hz * freq. Higher the @@ -1986,6 +1997,9 @@ section of the measuring resonances guide for more information on # a lower than the default value if the resonances get too strong on # the printer. However, lower values make measurements of high-frequency # resonances less precise. The default value is 60 (mm/sec). +#accel_per_hz_z: 15 +# This parameter has the same meaning as accel_per_hz, but applies to +# Z axis specifically. The default is 15 (mm/sec). #hz_per_sec: 1 # Determines the speed of the test. When testing all frequencies in # range [min_freq, max_freq], each second the frequency increases by @@ -1994,6 +2008,8 @@ section of the measuring resonances guide for more information on # (Hz/sec == sec^-2). #sweeping_accel: 400 # An acceleration of slow sweeping moves. The default is 400 mm/sec^2. +#sweeping_accel_z: 50 +# Same as sweeping_accel above, but for Z axis. The default is 50 mm/sec^2. #sweeping_period: 1.2 # A period of slow sweeping moves. Setting this parameter to 0 # disables slow sweeping moves. Avoid setting it to a too small diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 7a60aa8a0..caad39bec 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -828,15 +828,17 @@ been enabled (also see the #### SET_INPUT_SHAPER `SET_INPUT_SHAPER [SHAPER_FREQ_X=] -[SHAPER_FREQ_Y=] [DAMPING_RATIO_X=] -[DAMPING_RATIO_Y=] [SHAPER_TYPE=] -[SHAPER_TYPE_X=] [SHAPER_TYPE_Y=]`: +[SHAPER_FREQ_Y=] [SHAPER_FREQ_Y=] +[DAMPING_RATIO_X=] [DAMPING_RATIO_Y=] +[DAMPING_RATIO_Z=] [SHAPER_TYPE=] +[SHAPER_TYPE_X=] [SHAPER_TYPE_Y=] +[SHAPER_TYPE_Z=]`: Modify input shaper parameters. Note that SHAPER_TYPE parameter resets -input shaper for both X and Y axes even if different shaper types have +input shaper for all axes even if different shaper types have been configured in [input_shaper] section. SHAPER_TYPE cannot be used -together with either of SHAPER_TYPE_X and SHAPER_TYPE_Y parameters. -See [config reference](Config_Reference.md#input_shaper) for more -details on each of these parameters. +together with any of SHAPER_TYPE_X, SHAPER_TYPE_Y, and SHAPER_TYPE_Z +parameters. See [config reference](Config_Reference.md#input_shaper) +for more details on each of these parameters. ### [led] @@ -1284,13 +1286,14 @@ all enabled accelerometer chips. [POINT=x,y,z] [INPUT_SHAPING=<0:1>]`: Runs the resonance test in all configured probe points for the requested "axis" and measures the acceleration using the accelerometer chips configured for -the respective axis. "axis" can either be X or Y, or specify an -arbitrary direction as `AXIS=dx,dy`, where dx and dy are floating +the respective axis. "axis" can either be X, Y or Z, or specify an +arbitrary direction as `AXIS=dx,dy[,dz]`, where dx, dy, dz are floating point numbers defining a direction vector (e.g. `AXIS=X`, `AXIS=Y`, or -`AXIS=1,-1` to define a diagonal direction). Note that `AXIS=dx,dy` -and `AXIS=-dx,-dy` is equivalent. `chip_name` can be one or -more configured accel chips, delimited with comma, for example -`CHIPS="adxl345, adxl345 rpi"`. If POINT is specified it will override the point(s) +`AXIS=1,-1` to define a diagonal direction in XY plane, or `AXIS=0,1,1` +to define a direction in YZ plane). Note that `AXIS=dx,dy` and `AXIS=-dx,-dy` +is equivalent. `chip_name` can be one or more configured accel chips, +delimited with comma, for example `CHIPS="adxl345, adxl345 rpi"`. +If POINT is specified it will override the point(s) configured in `[resonance_tester]`. If `INPUT_SHAPING=0` or not set(default), disables input shaping for the resonance testing, because it is not valid to run the resonance testing with the input shaper diff --git a/docs/Measuring_Resonances.md b/docs/Measuring_Resonances.md index 49c050e0c..fe140d93d 100644 --- a/docs/Measuring_Resonances.md +++ b/docs/Measuring_Resonances.md @@ -697,6 +697,95 @@ If you are doing a shaper re-calibration and the reported smoothing for the suggested shaper configuration is almost the same as what you got during the previous calibration, this step can be skipped. +### Measuring the resonances of Z axis + +Measuring the resonances of Z axis is similar in many aspects to measuring +resonances of X and Y axes, with some subtle differences. Similarly to other +axes measurements, you will need to have an accelerometer mounted on the +moving parts of Z axis - either the bed itself (if the bed moves over Z axis), +or the toolhead (if the toolhead/gantry moves over Z). You will need to +add the appropriate chip configuration to `printer.cfg` and also add it to +`[resonance_tester]` section, e.g. +``` +[resonance_tester] +accel_chip_z: +``` +Also make sure that `probe_points` configured in `[resonance_tester]` allow +sufficient clearance for Z axis movements (20 mm above bed surface should +provide enough clearance with the default test parameters). + +The next consideration is that Z axis can typically reach lower maximum +speeds and accelerations that X and Y axes. Default parameters of the test +take that into consideration and are much less agressive, but it may still +be necessary to increase `max_z_accel` and `max_z_velocity`. If you have +them configured in `[printer]` section, make sure to set them to at least +``` +[printer] +max_z_velocity: 20 +max_z_accel: 1550 +``` +but only for the duration of the test, afterwards you can revert them back +to their original values if necessary. And if you use custom test parameters +for Z axis, `TEST_RESONANCES` and `SHAPER_CALIBRATE` will provide the minimum +required limits if necessary for your specific case. + +After all changes to `printer.cfg` have been made, restart Klipper and run +either +``` +TEST_RESONANCES AXIS=Z +``` +or +``` +SHAPER_CALIBRATE AXIS=Z +``` +and proceed from there accordingly how you would for other axes. +For example, after `TEST_RESONANCES` command you can run +`calibrate_shaper.py` script and get shaper recommendations and +the chart of resonance response: + +![Resonances](img/calibrate-z.png) + +After the calibration, the shaper parameters can be stored in the +`printer.cfg`, e.g. from the example above: +``` +[input_shaper] +... +shaper_type_z: mzv +shaper_freq_z: 42.6 +``` + +Also, given the movements of Z axis are slow, you can easily consider +more aggressive input shapers, e.g. +``` +[input_shaper] +... +shaper_type_z: 2hump_ei +shaper_freq_z: 63.0 +``` + +If the test produces bogus results, you may try to increase +`accel_per_hz_z` parameter in `[resonance_tester]` from its +default value 15 to a larger value in the range of 20-30, e.g. +``` +[resonance_tester] +accel_per_hz_z: 25 +``` +and repeat the test. Increasing this value will likely require +increasing `max_z_accel` and `max_z_velocity` parameters as well. +You can run `TEST_RESONANCES AXIS=Z` command to get the required +minimum values. + +However, if you are unable to measure the resonances of Z axis, +you can consider just using +``` +[input_shaper] +... +shaper_type_z: 3hump_ei +shaper_freq_z: 65 +``` +as an acceptable all-round choice, given that the smoothing of +Z axis movements is not of particular concerns. + ### Unreliable measurements of resonance frequencies Sometimes the resonance measurements can produce bogus results, leading to diff --git a/docs/Resonance_Compensation.md b/docs/Resonance_Compensation.md index 43ceac8f0..dcfb4ed78 100644 --- a/docs/Resonance_Compensation.md +++ b/docs/Resonance_Compensation.md @@ -439,9 +439,12 @@ gcode: SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= ``` Note that `SHAPER_TYPE_Y` and `SHAPER_FREQ_Y` should be the same in both -commands. It is also possible to put a similar snippet into the start g-code -in the slicer, however then the shaper will not be enabled until any print -is started. +commands. If you need to configure an input shaper for Z axis, include +its parameters in both `SET_INPUT_SHAPER` commands. + +Besides `delayed_gcode`, it is also possible to put a similar snippet into +the start g-code in the slicer, however then the shaper will not be enabled +until any print is started. Note that the input shaper only needs to be configured once. Subsequent changes of the carriages or their modes via `SET_DUAL_CARRIAGE` command will preserve @@ -453,6 +456,18 @@ No, `input_shaper` feature has pretty much no impact on the print times by itself. However, the value of `max_accel` certainly does (tuning of this parameter described in [this section](#selecting-max_accel)). +### Should I enable and tune input shaper for Z axis? + +Most of the users are not likely to see improvements in the quality of +the prints directly, much unlike X and Y shapers. However, users of +delta printers, printers with flying gantry, or printers with heavy +moving beds may be able to increase the `max_z_accel` and `max_z_velocity` +kinematics limits and thus get faster Z movements. This can be especially +useful e.g. for toolchangers, but also when Z-hops are enabled in slicer. +And in general, after enabling Z input shaper many users will hear that +Z axis operates more smoothly, which may increase the comfort of printer +operation, and may somewhat extend lifespan of Z axis parts. + ## Technical details ### Input shapers diff --git a/docs/img/calibrate-z.png b/docs/img/calibrate-z.png new file mode 100644 index 0000000000000000000000000000000000000000..b75a65c33902d7c304c3b8f0117b8be1182bc270 GIT binary patch literal 161737 zcmdSBhdY<=|39p)p_CRXL{=mtLPnICEm@I~l~HD7v?OJZ>{Ld13!z9>ii|||3?VyY zWb=ER@6UJK_wW7-ZpZOHKA)rVdR^Cfo#*rUSkKE>Raus58{IYv3JNNDxpV3i6dM>R zDAsFJZpNQPJKw#I{}FaPukEN|XXfZ)WPg=H$;k1#wVk82r7@%PReJ|ZJ6rw}f+zU7 z7%dzfuRDnF@Ywv%KR97$Z_ZOYTQrLg*?L`0$AN-kml64I-7ATgmK5tKDCEzb)w~%y z7U!a^S>AHHJ1Je0W7|>Zb6f)3TSfnNoyz>O{}i3SZ()mtG1Ze-!L7G6$5yR<3twoz zFqZwe{?Xe_&Mrj~^Y4v*Y&x6C(lzHgzCm=ZI?ueOP*l`mu-Z?Gk|`+oe}5Jz4tiK` zF#X>jafbe#5B>heSa?P=H@)n zUQMA+q%McY;BoTWfo}v60bP z{9+WZ$!3bwOBpiHRj-dUaum9UvZ;J&Yz%w&@ZklID1)xY2YBk!FN=axDRMf+QhpFe+!6wwJ-P%B29 zDEsl_u!Dm`RT#(T2UPSk)*swvIkD`eH8nev)P3<`#cQiedP^ro|6a{~{eW`&_5}0V zr(s+=d$w=izHL93-1NY=tyEMV*J8Y>tD{dnK6?JqGfhqo!=f{{=Egk|xQDH~3TVB& zyoRGV3<^#cy3N*yDk>@AFShJDl6dv&llLD!ywK#l;oy*9SnPSmYgJ$N#EQ-b$KgFT z?QcuIeA%q0r&qK*rTA3rRATfCbz!Um*QqQK=dp*`+1UncvT0h`j>GD@xwdg>Z{7*o zwD$}Qd>Qx_qmyO2m11Rawy(pkI0^g3y1OXu{Nw#Mez%|cM=Pxg+k!$)PL7e0@iv>v zQ|f~UD@yKdi4w9q;=TO<>xmP}gLI-U z;n!NyGq04a&#~!{iQ>CDRF|+N+w5Ca=C15(hR#*dw7YljK7IQ1Nr!<8e}8w#Ub(`^ z%E~&wv_wCqYBpG;5-s4nn^}gMn))ODc42WeW4PgE;8~9h2kcqgn%tk>vbHxy8}eNy z%$Dc>xOK;mwc8KY9R5c$@J%A8J3-Sy@rx{)Lhjg~K|_{+GhKgJ;w=G|GaRWj8WfHNQ>?a zcuj!r=kMR#%gf7&0@$-ZdwY+DgoGr%y_8|F_13Lh!I@PbTqd4UlvafwpB>v+dVecT zVl5TD;NJA~^rM1;TC%p^pPiLT(R>scd64OT(3o9)qHjPacFaab;RiJ(q9B&1k$--YU7gJY6d? z9C$=*4-WQjiRH?5*3FJ#MwpVF@pJl@o8b4Jc%Uu|`=AM2*==iuOYVOkktJoNp!x|_3&=bv70 zRnG7iPNU5t_H4?r)VE(T6?NOYUrujz)8v$t_U$@yhWd7EriqUu z&TN&o8Pk33eCITZ(&$sy?o&(Ql&MBWMn1GFT9A=Ffjd7fD*AZmVG%jYANei^ZqNVD z?RGTHUi$Y5byYAkq81CAq0YvNx+o7;oGwqW>Bte8!hU99WqmOIBY)H{+Txuq-6^Zm zJcmKD+nJc|I1JX*B`eW~DI|q;x3@>*Ar^xdIPSd1*w9PdVd~!}QB-l58i(bO#s^1BPOaCk9841>@2nSz zZ;-y){_dJjN=i!2pT0neqxVq)9vt8?c-CZf!`ZnxKl%CFG3vvI{dLpKetzkhk$ z2r0_g*w~pWhs7Do6t0|{92|)q3=9l1fk%|uvaa5^al`1-{jF|4g>@E0MMQKLmp*V$ z){dAD+Yc91`m|;8fht|Mr+j&~|oz?m4*H4<)#QyBebGUiymZEci>A(zM(eG(g zeI?YD6Q`ywU%t$7DgDrBOGa%bw{F^#%~W)6Y&rxz=YMX&ZY^!gHczT~`+Ta&~4!)${6$UxssDte>94Bbr^dF*3U2@9(dytNc_HD_48KRfi7$t3NoawoX=7mSX=2 zeYsy%;Zy@R?CnFesvkXiR4=;brn^WfZ*QNayf{i_Bq}QE=kGt<{_gmOEjtBn{x&Xq zbXAScW9rw}EkSAW@1!5>Fv89nYt6jpvGg}s$j+cps7}YIA_4%Zcy&?HD$I<Lq` zHhMv8Ny?J&RR*nG(DkYmH6~fbVG+J93qoj4IaT!#gF!LFUlN75hjhAg^i6o5wq&Zvw#!qx^LMp z{-ar$!;RYb3s<^v)26z@TP_@89wl3(&z(Q7_U6jj;cVSBRy-okON(L`;Vk_g?GnX-XSVAq&@c8B`&u=(2UrFNg z7ygtp42tBI{d*BJ(=%b8-|yA6jrO4Q}V*|TT$3vQ|nEW8_dTP!h&R6QMk()yq3?NJ9odPe*s@;WMmX{d)xQTp7h zauq6->+(D=ii2?Tr9AtqVG764T${FviMUR&pkh{NZDApR%c|+0^IZk5ZN**^SkEf8 zEi5Zhw+h{X$i4~G;;5ylKEFHTiqrk7$;xs=8L*kLvGJ!kQI{F1)UF3YVlD&}^Vn}W zCnGaZXma&x*(dMq!(D}Ft(jju{@ zMwRMbocDeFn3;>quczkwq|pdC16@7a z`L`|r`?uFR*>B#x`=%zGWn$?3HJbKY^eJX`cFJA5b`=y9yubc)J(kGRWJuqP)N}Xw z>&eP-3KLz0mguhNy2ibwzHQq|=p;PP05~_MUfOl((j|i;_e!iW@2OKsT4}n%S!JI; z-`jIc;iP@P+1lNlDp*r%Eo`&dx5y;nG_RHCC0U)seg= z?{CeW@TWWV35Xrt?mCtp0GBGAE+RPg%)d{Wm*Lvqe1O^Fz?qA0YT`uAC*2pfiN^@p z3Eo^q`+Z^Ako>8u%OEm3`qAA@+aCdNj8sl^=4sq?b=}I(&#(6Wx)Cp+o8_^1ztS&X z9(5Jo+RnztW@Kr}0Z4jcyt_CfciR(c%TzN|8O6O^+N$J}+!v;N(QY2$C>7PMR%>;( zXPXBC3p{!A_U+#N`}e;=m744Z|Hz@V#FM3=H}Y9za4g;y1Nav zb#)(|ah-aI)p>}==1YJJ)BX9SiQ@CXa_4>b96R^)jO+chw6w|TX&M|r5jP6n>pha0 z7IjC}UYy%%WM##9=FFK=tln%oEgc;vxoB$}8}F#7sL8*7o2TWSJt8kJWoa~5-voG> zA4$zPheGC!pH~KjALfC+&!y?UbJTiWRq4@U_0IT4x3Z z*y5%*PzCM_>J$3l`$_(F;-|0&1Erobva_?7yJi>`*OdCw2bKZ04KWAa+ovjEyVnGz0zjRi`{3|PFETQ`iS zX#Q?TuHEQx>g9aFZQfE77LBihpIT#Ifrh=XsCy9^6VvzeX9+4OReK(4hP#J{8fW-v z5fOaX0YJ7yW*S=BN*uKm^`vb90Rh19*g%^+S7t8>(D?^g%gIrK`z2lf`3Ws|3z!0- zHbB8y_L*l1{L3&XoWzCJ*N5PI)?*<~=)Zr)9MQ1EZ`XCE{jH@m^a@q>y-}XIckkZa zv8exCb?Eo9I1~b0NM>%XY>K9O!Oa_se}8|yw`G@OKlVptin6!V^X%H{my#{%mo1zA zX(|_#mKn&FG}xplGr&xH2u5@RxwYU(r|>W?Kx8<)K&Z24cpElLiTNof+Cqpe{;F zthh&X*43{gt!lWj=Jd;{p??>%*IUx808V@h;{f7K*#kkvydsDR6pzcNHi;Mac<(S6P~yN43jL@Tn-4VYnAg{eiUKzAkrOOv?!7(? zp47O?y>$Kqg+pjZ`Ru^{Px$QGB`1|wSXlbyJaPQjQ_qe6xRpBU!Nu1RbW#**iw;F^ zMKE)o?Iqh6TB&8IwzKsOzYPoB-i7@IynC+6dA_^I16*1uh861Dd*|QLVP+Pe^UDJn zwrgl;{As#mIGot>&qc*BZoR+$IgnB=qIdIpEIP)`hGh6{u)g!{3 zE8@<$s#6$4^HA5laKTSbUcN5B`Qx1pn;4IXyU)wyFR!ee$GUg1N9>kf1%RMYR#tw2 zk5<=J9uK6JdTGF+mt&nAbF9cRv`#3~y!Obthx?9enR2*HbcsDzWiJKyTF`27?H0J! zd;u81-J0Ljbanb)0goX#ENm~4RDhx!Wh6pEL!-{P@+B!o$fRiY0`h_epLU%}WP$P~ zhrVAKFCl?vchk*{?^-ig;r9d51xN0N2Ph;Nx4tm~Lj@B*0)SG64{$Vfn;E(j9=`AS z^XCE<-ziYsf&aFAL|eCP%Q^v$qIm6yUP9%iavZSttG&97q|aOrgNE9kYqyKsW+LB9 zAN#B2c5Tm5cB3BTUx+=;t0w|xG23->17P*Kq5E7ABzJ`?oX56yGQIV8%$NEu{zTCd z-L9o!xVQIiYHF&%K}}~gFHtGVdo3!c0oW}R`h{-ct*zRHN{p$RDR&DC#Xuj!>!CZI zJawumU(0Rb^XJdq77+;v%&e@!+fSbCXE=WRI3&e{zWCTqhMs%={|tPM*wy3zYZa`Y zTIxHbAnV~3K!9PKMC=6=U=5%Y94}UnIT{)oT5WKd z>-Oyp4PqeO0R4_d=4h$E&{7#(#ycyYc;MLB`5d}?jCp#uu*1NPy1F{BXIUbPpbbWX zEIqa>UU`DA!O;-*SQOaBAfAY)O#yc3bK>#`)z{j*_}KUE3w$VSeR!75TWKn{qJbr& zW^ox7#bOaqedW-%@}Nt*i*H^!4>MLNT-M$njslX&Z$q8s|O16iv>N#+1(h_>rj2kSLZ>K>-i%1qD$9 z!=^D8-UT`5P4f%<9O{95O&?T=`nz>zSf<6zgX42#8~)8SV(xVX6BSCq)MF`eix zRzceuYD}T|`}Z$kcGk^5#zeRTOt=9g4e)p&UOcWYL6)45g{fcbpe3KnS$T38>{fPd zb;YTE{e}&ME+?;kJK?j7Ve|K`;Y1{+c>46INY16ixQ< zstYz^?(-)hF}}~wAB}6UdI#Odpx|Z+Dh=q8SNa$i13mp$EPnRBou)&p1ynfKeZZWk zM(X*_Hh|R^Akprq`lrbXh+qNRYynvGhKmboo?z5Sv(-DW)t>_cGJu6$7GJ?DQVa@3 z(ASAP8gfi=51KtzT3%kh1lR&vLZ;iS6*$48j0~QwTep(KfC@-KbZHsCgJ)G$sZrs> z9iblLp-4jzcXIa00MwA8M1lS+)a@R6ITx6!KP*s`0^HYi8r^uCBuKKnWvW)%p4-|v z);c+}06M|Z(VXaD$8cuPql`M%s8VVLA3c8#B|fQkm`NK~dO$Z#OQG0ntq3P?U*WB} z=DIBpyFHh<6=VLtqFgHVYbYSY_kFo^vd&Q$V@r$J4;P7E@3|eenGy`(&q&j84sPg% z2We@2Y5C1eE$w9X^BBA~&!)C~E>^&_Q87*H@%;Qv!W0H-<9U+Ig*F~PUi>Qz%eWQr zgT{D9@V<`^Aro+1&c_8kMT4LOx4q}u`QyiVi1B<)QMe_mmUM1NVl$tj0R)(VH&_6Q zxUfUHff-mpj?Uo$I;ArJU@L@kUR5myvfoR$|L|dM*j+PHg`Yls+Q>+Oy&asA22gN7 zMn;Cnm78nlMq59Lb#2Mj4uK5hX2bN%AWm&gdcNZAa)PzIWH?)vpN zr=7V7$K24}EdIDJ7S| zvxTeH=DNKegTy=rZQPDUQbV!f1;)E~qnvNe=D`lyqiDyfsZHv-zzW{#Deir-K`?ko zf87p98TWvKQ8`|myYg4!hN%V>iQ&sHu~lAxuZt{F?53wrWn1d6q^Zj=l=wL^!ij}* zw2l^bNM?x$-6;nf<9Ziyw9Xp7n;zsGVi%`!FD2e*}-={?*mhda#V7gr_JGXSXyx&lKrmZRz5T$6YV{MTOVc7cPLexr08bcQy?RXd zt;KVf6Q@r7D>fgJ@YE;5FH9|UR2=iRUlxM9hYe8 z#-$H;6OeV3msd@N_4H|N3S$&XbzK;E-tcjhf5Zf2WMqJSF4EhCfooyL_Cw#@5zcNn*GD?l^EDg3BbA zLEK#q1p6kWB|y4CpxRzEt&N*EE4Xz(Xpz>bZ>axFTc7#n&3C=?im6TY&(3ZD5&E)A zV)-MatkbvtazOz1WQ%!`kN~|FhT>4-U~N1D4vSdFjksBOq%Z4r=fa-m9Gkp# zT5!vjEs)*O>)c#TjE&#q=C&vvSYuc#Hv2{gmSX`aH^+Jb645%A;zYx>*`-~UPL2u+ zk7{P2Diigb_T&``$gT5>i-5;xQM`DCgoNfhA^G{u~%_T$&W{ zMT%A$9jfWTEYg|{N2_|54WX91U;$2;Zm)l{B=27_ogsL}eDFK378y}7cES;p@Q z9ykC7VSFgGN!bMseP8@JRG(2m(hhS(8w`cuG2B=}8-n)RdExP4?T5f6 z!fY+3%$2WSzup7ijgrYReF@${qt!d*WoO%;Jv|@MPG{>4=Txu@K?seZxP#-VE2t} znwooolUrg$gSr-Aq8>Z0^*TsopD z>lwEh$^!=u96EgX&*bDY9DTRYmpqq0oWnP9s3%fx*?H(=bF*NF;3A}B)5<;LU4_S$ zYHCh^+!F!|7_4Sa`?IC8W~GwqK6P7k#rVj`qsNc8;|TKI^n~V>n3!mKYbpBy9C+n8Cx{#S*CGiRh_-sx`5~(g<%1197igw+^Nr zkpzjIgmQ>;J%BngVOon1u>A4il*i%>lc3-e6o14fP`4WCr`>MeRE6#-6L83vdOb?Z z-Fx?hZ%#gYYP}dU4WdT0!wX7E92fuD+uYpjmi6O<6Z6TFL0B$_wN(!Y=)$lAiOd2q zr!m8DEw0L%=&rmjzU*1|aBvQws>5o>lLB;~Eht=W_<_5j)7nc9EcBYhFL%~e$ zAJC=a_y+6(toak>;zD0gr0Y;ZiS9*Iv1}H=s8RaQI*e)e8s8X6x}Y5MqbZC&Ck`5XR2rZ-obYfS#Tnp8s`w`(CUs zB$Ox==c$#s?m>$gqW+N7f`C=yLn5quizT_Nx3~9Vw22x*W85$a41J&4x-DzdZR|36 z=2tkdFM<59{}COMM(IP4Lh8BdNo;WWZk(TkB981R_X*Id0lN`esX`yWasB!gq@dic zih{MGx0*mn*)08(f;a@=d%})GK{I?OJdT}4DuL{am>7CenE(_~3iblFL13|LeoYJ7 z2qo-B-xn&a3Iz(l+=UB`I#H@npT#DF2c9mQ z_q>xILabuXZ6M=DVFJrh!yTs};HVPm>NRQ!F$b0*CNi|TMXB6@7ypn|DS9aJ*s){2 zfBMQFgF9^ljzj-o!~;97{PYY*<;E>rw!`rtx)Vxy87e4G8eiy*7Y@p-&%qFNA?a!N ze~nOq1n?LiIF$DBzr=CNH(V_lh6iwjKDD>&;Cs$T`RW#K*Yq;&Nd7ey;DKOKAT{a0L=_Lc$&ZSIC*Y{;t=_=pkKH9pE>& z3YU%sb%Gjv^;2o-x&Ci4mtf*6+VOuJQ$-8l%7{@`o={nQvD=RGEB-B!VVH?;0VIaP zxf@Da(tFqGf15stW5qQ+213V`XAc!Ak^+#daz0LdRSNSR2)XDd9 zpRuve6Mzdxf5v@ws7~5#Kei$VLIUg%g6`+#<*^$SyvO!_Rq~AvfaTIUq7@$0KPOGe z1Sx?H?VJDdbx9=eTvRE8nYyG{%h5Gf=lgbXG+sd~a{p6yfb6pgrNHp;i^-uAgiHg> zK`#6{Lls=@6^c|)eWr=*kux_Xv6Uopgi*%%;*TNP1!RcK*e8?@aq_2YmH^y&exU1! z7XbJ64h$qan7*6xp}vbuME`2fpe!~z(U=j#n{Q1fZijmg1oH=AyG6*m1=>9FtM)qL zy0b!$nt>rBTk0FYA?4PsC0$)dAg+)w2C1dB5^Lm30BQ!&IiM*L3<@>-aY_mYsv5Sm z12k!DLP8H`wT$7~AR-95PfHqy{jcLuolG7rf2+Y-7Fj0F+P0?ES!B6aLhRXmmwoLz>oC;(iU-K}9IoB5p;|S{ycWbD<@0rnLhS3W*A-!A~SkS!`U~d0>(< z9B^cCtXtol4dXF1Al?+bQZ*<|M9GC@IosLQM{~z&td49BN>Cax z&5~X0eDHdwG`)Ql7G^mjc#tGb-35V@t|Q67brr$I#C>Dp`|Q7tfB&uu$(3Ks&c;Rw zGE-NT4DIK0=gv`stS9+JL>z$T$xv~W7*m0P|Ce>-?slh!Ji-6^S<~r{vTHk+rsXhP ze+(|Nc;(iA{`>Fv;!hx%0qY3ZWT}lu{Eusd_!eUROGp&|o9X;dF1ETj668-E*AZy* zMfUvoZBrU(6BGdq63UQK?3FTP5}3HSb_faO>R(=+>P{8{md1JTK6`KTuMyK~uNVf? zwN=sCs#T9-x2tPKhJ{&P-5CaL#S@n%iYtr32BD(L+f>!k=4}X3s{fomHCU^1XZ@!C zd@wK<*na=wftBt=Az@)!@E<%3ke1q0g=>Nm6{{;QBjGDcp8wa!cX@?+ouZ?rudU8WReaJ3SW9J zr{+VDY~rWHIas}0^v~aG{y-+-=jzH5JTQTlZUJ|$LO^%nr~2WsvDIigv?7ic>BS}Q zoksVMkB{e#`*?-^d#@Q|-9n=t5pH}~9g>IyvKVxGSmu?s>OpV%->cH4(p3DQU5b1$ zx31>+ZPUAg@<1P=-#CpPraULBmN2d-bbB$(+Q*RGBZoc%8qrk2nq_y zq-l-L3kfZB{`Y#Ar3>Yb$fLm>WoBN}nC*X=cwjbu&g3e?e1NLRp8MtBbL%+<+{2o zjIxeaoUJXw6jTCc{)iwYSeHhFzM2B zRfLc4r-OzsRs`{RnySbF8of zp7bbyn5|CYfI2jL_Iv_9It{1+x1G40uw3U<`N$#dWIA~g6^~)~x`V@Kh@YQ*b`g$u zxDARuXtx6=(^p1`muSBI;hxP{eNy48c@^erz+!uaD+ywT3 z6P9jJ_zxRDvYd#@9re&k(ZsHqv3X<{1To`ZH@C%2k7(GJGLC3zX@!hC;@;#T;<6)| zg<2^PxdxgNcKp&#RC<6St|S+z%Am_6w91hN)3CG1V=2zYGoC2`0!;Tn(Hwkf-2)ZTmP$9+vo1@GYT^JuwCCxB!?o<4CF@!qOx(mU!y6*Wxf6tfVhE z{m1g)V>|^6Cjn`F{X>v9n;K5JPMKCk@YLdbf5Tp|cQWZ1Q-_8-Tz1h}t~;OcIN;ww z1YS^1o8@;=QC&snM=&ZFLN~9|h$cLbnWycxTKxAM zZZ1Rp*rCv%prEwD4MbW3b{$CyWRQ3Y9)>+Eg%%!($QA`E__0gr`b;b=_n`xygsn$` z|xHh3S&Ucyxp>R z9hxIFAtXt0>R@7y4!0w+*6lt`6|`T^*2X3-fe+ROt)ZbIws8dj zrygPTDq#SDv4)0u!ox>&K?V%=m=}B!pfbbn?{D;37m5k~C+q}5BFnY{$wxC?pB8W* zMVLeD^+AM%zX!v&l%zZhZI4$_aNsdd5##C82jME?Xsf2KxoRa}i28)j>KbFOcfSlU zg47)eSp^C9Hl$wS8EMH-K^$m)x>jPL_c0GZu{mg7Tm2I2ed<`TTUvBRqsT}iGT#{5 zft;72`=3n|V80En`(^#<%AnX}oto>xHbgb`!8y=CM1xR}xEL71fMcL!nF!ZYK+_|0 z4bWl{Tcf{?%|}B`E$O{&ABlwsvB+i^M5g{~fRotp6VMUD%Oz$c(_i`7<+fE};(DnXQrlw{1kH7z24qT z9h(=yrDV)eytV20>pz7G)#4hOpq1*x8RH0}gurv<_73HT0_UuZIr(4Pdx@96npca| zF)K`;y|ZBpR69l_@E~GlISeX)31kS=O8tehF+V>~@f`_gON)F^KoEln6h#F$QgW%v ze7wEo7f3i5WO&_aNCb#+;6(FAC<0zg=c$^xx4$a!&YZpRMnUiJK#Ro0WWyaj$A2^* zM%fgyv~AN^l~w=eL7G@q&(vBleX6q9m8$g7w=*}4JlPDzRvus?e`~3MRzHkc1EgA@NMml{AyTZ!ZIH+l z*!1v_>!03DI8*M^--OWPHls3w&f8u0j)j~nKYQa#K>Pfkz9?}I5vZJqV+eh%ISWQ- z+5R@A@yf29JI!FoqN1Od9mlCAE)0|^xt1!2kI;(B?U*rr|VSJ6K(>t z`QJT9w2fF4FhPKnkws#&=qktyxBy)NWG+lR9}3pccP0=kX`nwsBZvcsqE%8`yGvHK z-1?LoYCD|`%$*`wf$>>aF$gzci7(2x6xwPNhMiBQ3Kr|i? zwvH)|ALdB5qFuwQC_x*ZP0w>0)q8O{il%1RQ)>u+jTO#LPQO6DCEKTyo2-(Q z$73o&*><64mLd=jpn*FoM@uG&N{GdS$T&ff+Ko^GdKS?l@P{vv@GdParLbi7M$vo; z&lcVM4m5CL6~~PtADVTePlm`a2(yA08UdQtPsfC-KtTykS&>+sXT|_c4-z(IB_((G z&1>Wa2WsNJLJ9(n?FUKP=oo*%Tk6iedmD&LaP+9u;4g;{D%~8F zn(r53B&v8_j1w9e?lh?uXOqb8Gh=NUvdYg@54zLO+6_XaLgGCV9kw2i{DeV)9HOFl zLc$3CgJ{wpn%W3LRjG`oxp7Yhjjt5KyBpBfi5Oe<(>xHt(8$Smk})RGhma%h zbiaLz5h594Xne_b-9-Qnkw_b8O+=e~@e3X?30Fws7doG&)sUjW=D9fhl56k*

TI zC7{lohdBfR?$klSj(;v5hr9=q<_jh{+;mzXtfkx;>+6%o%tLZZl^@bhXfB4BUhxeL zr2%@Z>xljKUCwJUW^E>q0fZNOJ==BK436R_>>CoLKuxTjX@_5vgdr=!vymLoRj`8# z0V@@#R2}5e(%~lx3R2@VAnLw;e*LJT#OcP?qS?RyOHau?Y~*znjwiM=e%r^V`Sf)? z!ieZQkV`Ug0~cnC1E}x9GQ5NEmkIiow)Q~^h&9Q;X7vN^h#ri=4uNrF83q#l^!@wO z&;ptKrz{2N?$w4vjo-(qc?ELoX6c@+DS0iD9VU4T2)O(JvZQZ-+*6PZi{f&XSZ4a~ zgP?F;rK1gLasbB_zZWMN6twIE72lCC10P$g{OtMGzQMpfTgU4)m!MKv)f2n&-~U2ZhZU=pVG zpJpqm)>W7+)+XzFTzm5D(yZ}8`r+n#2fj4Hh?(MBjF-8b%Xxli>=I>y^$Gtxgjgvo z_c9*_dy5vZ`~(RLt$-ghb@?UB3P~aT{idXSA{+#D9CWme3`#VVIme{95aRJjs1%|;| z^Jlvh)4v5)By#w7NJ|?a4ASsPc;1c^U2*8MBYralzbPwl=ZOJkxWPp8W6Wr{NswE~ z%$=J7&<(W|nL>$+yZoe9&EDQVU8b<2Ky)o-ryMFULB6*fdf+A^vH?H<68Q)aP$NzE z6ms&h!{>7FIG|=AYw^dIwgDPl;(J4;jjXQr0M5GGb{=6SvMMkdj zZ+qLY__v|yPRWG2py+4?a>3)0i*^mi|3fEZ^1A$HP2J*N2*{^O@HN}9%3PURn^pMo zLDtgvKJ_*Oj=dpAOg0^i8Tv|h6o3E`4l*Vxro*%mQkfb0F(ngnrzc-!Xr`!xAko82 zz*Wn^heI7Eaa0;Mlpqv-!Y3|_;(`JNqAPHDst##vB0_izOXVm zHB}`rBbabFV|2gNB#I3rlv)rFOg1ONTo!S~rNEOSg$I+KC@do67*?VMP2nTo0PX<2 zajc|+#=X_$zH{e}r0zjA890855T^c2Pn*CbK*oJ!{;$)T+448{2D?jI(;D8?nygV+y@yxgrKr#1&Q?>k&78s@C@S+5skj2?^e6VkAd0GYes-$h!sI^f$MWIcFpjFbHfw3wQ=Z@AQ zVz-}f>VxMyT_xDaz()YqOl;>vw2FgVp@(@CSFAR%35`|?Dfyi$1*-rme~ASY8OAJD zd0pLZ=m{@gyntJME7|EP>LUqx2xO56p@QA3m{ZqS(=;)S!9xyWW+pG}&6ki`(Ynb9 z#|P)%k8*Q`&;ZRp-o~^XC#3BfsP(hy^b8DO@ve!E;R@a;`Z&DRF+wVCOWQ5&9^gY| ztgQYyyQFlX-oUmEo)sSu0e&0Tfx}+@<3}7kzCW4`zRo;62Ic}X{iUrf3fP=j5F|H(|He zUaL{~Wq!vYRebAv$KjAh-s%b>^MJvw8*0sDhW;f|KYNg6-P7$ z6#0`^K2qQ!WQb5W)xVW|ZAV88vR2=*TBzgYP`YuX_mbq#@q5rjIXSl_smsHwlA;ug z!aryLu7bp&0#3xz9S;3C+S(Q^9gzyc%MOY5LUg+rQ5?tCgHi) z+A86n`VxmRF?q?*gCtFb?fWUAowm_kj>JeZguwy)9-vQR91W7!^cmLER@~?(k^pDPIEmczSvwu7mj37Zr&WmIW(#1@&`( zq>Wy2hut_)KNuk8{z*@O^FJ0t;ZAQPmhsOUaPQWq*2CSEzIpR~$<;k)y*Bm(jjc(nO#iX1)_?l*8%W93FR0O}tV|AX#I7Ij20IGruG!dA@E#N;!!h^(XI z+qVbgY=^#a-^%7cz;awX$E*9|M2E7cXJ}XF(cI;w#iy6o%Km=tm}Ff;0fBsyc*sSy zyU9S>=6P&;P(!#80`#(wFNPXk?z-0e`X|^K;p!xqbbQUHKXmkVX?eNg@zavg{L)fV zNFBX|kVHTdcuuxe6K9+|?6i;F-Q7KV9+Bw}kbLC6LV!|3NT%s1UMfzolP^j70zi$i@3m(QlSS zY7^muqkMeocH!R0zQLM~9;U11F)T_1JwTl@x_0d`)LyUE1@oMi)+ab4|M^)66LuO2 ztEsvbloR_GVkTy-%Nhly!!Jq*#OrTK1f|g#Up>5cZ(@WQ!r_RWB)fud#as)O&b<&9 zFxWF;Ii|sRX@q6S?2DFNznLN$k$iZQ9lu+PC4h z+wu1cnM(g&^@0~M8f_A7u3bC+TQrouK-}=yN|L8-O@1X+|4zA`nQ{dR4l$qKBvU1e zF5Z|fVHUjn@X9aWTO#%xBV!q-R&qM?T`-Whi_8hXxBs;b_zWnxXzm(jBHS0xh^$iH zj@V`3sW1KSD^XZ5@vm%c{o2uwE_~YmH5JJe=Qd8FU87#twFyjVNRHp`587-(XQZ1Z z!S46nprgj$C&6N(=)vE8Q;K{L-!7UTc~M@KUCL5WtYnT=mO)pG6n0<@${ARI*dU&A zPy)LkP`Y>e+|#v{L$_PEWa>P~S#1G4*+I*9Zqa?TnHv-cyMgJ*7lF8n$W_x9&!pT4m%E{~_s438W+0`gUlq&TLC&fdCr_b&V8 z4+2-K!i*r~yA2|_ospfpPZC-}R_DGmFG-&V#<~Zo9JNOpOohBQ0~L33d?h3#U^Q7N z=0hF18E-$;p{J0k=F1-(gH12J);2oHwWNW>BxyXPHK2gpTwTt4S`F9wm3HP)GAz@P ztD@jFKgk3zfZ#o3+mKzBIjGMw-!p$-j>Pq%9fTCw0SIilf~r9hK47KDPa)wGAKZaP z+J{Uvv_Iine_w|cxi8p5!#R)46B)`366ARgx|U}=x&%9)4!Xv0MOqUL#tA+FyExS& z#la|M7{3P&jL$)xcBur~?}L;jyFF5_|GH(+bxZR1_jN%BH+)>!!h0rA<)?1x@u`7@ z!3VPCY=dshSCE!?ZCLyiOWlX)fz1g_yCG#g)XzM;7B;_ElSh-GE;>*VR|MVbz2=&2 z`&)V42S4DGfM4u}l8pH+?3|ZKHbIbkg4|>lq!xJO4l#gNOv1fWTfvGt>3+q>y9)B2R#|Cw#nhiy6KFUCaTwvi2OC zLA$PMc-v~JO(b2984W0^QA?XvK9Vx) zdDlzpfs&!0B|b`|`wsNJ>7J4e7*z&35+W~n(9Sg81PHcvaKPNRbDgWA=Kk?gcB>H6 zVTDjO+tcEwslIMt>}99{P_ZpLa?%XAW^rmU4pCZFrJz91Z?*#rp&>X` zFE@Q=e?7nxgfuaF$OWH#K(BQ2lf^sMwGV!+;<;3 z^qTF!?%j9L8-fNh$biluq7c=|-zr`dKp{@HaBI@bB=#Ft>*cbVvZ(Km#3+g2%?{x(Qb3=l-$@8AAd+P50+0Qv|yivWmimR;i81KSs( z5i*)RP#N2D8mJ+*)w>)Q7dLP-A}`xPrWic`OG5*VzYNMa3-VJhA>=}KK=~#k8%S~= z3d*tmiyegMqskpp)nG1qu;C9LU^O5uZ27K7n}cnI_40usgzfzq%8vG%DD<7-(a{xFNCSA$AW{lJ|?ujJE7T z7K#Fn5P3%cv;m;8H`s`X;H5=OAYn18TkuvHf2dk3KD!p4RmY4wC7&D z{(zEDqrSC6dg2|dALZV-`D+$`7->2K)e-z@}y&%>~1G>`~0Hm-kh3M``v z_#b20JE3(TWY1^xX(JKSNPZA^kIm?BP%f3)WZ&~~9j;CRA}0D@54b68^7W=8p(sIQ zewPB0QyJt^a>zliFlxBovO5k_P9b4oEZ%!%&YdGeUGgf6*`ebdEe-34)rxIuUzy4f zn5#pZ0}}w=%O5e_aM6mxL5J{+6W?F{x~%m+XIg4hdq>?>HlCN7#`?UMq&{pn)0X`% zFz+2!du&|Te0~1zUiEE0iC#|(=-cM$ji*}qdRr&L+*Tzg+@v0I$({{3EcykvjH39^-yMM%R6rgp$%y7FpMxNGA?cn_iYMO9bh2Ut5_&E{{YJh`+B9 z57miSmzn5?af@kCx{!t-!=#8KljL8$ig1*bn~5`gQRG-V;Q=he`Uu z8;>Ptv3zf8gbsRs{n{i6(|Q4NJ!m!+N)cu_B|}tpMJUKpp0&5zcEt1b@S{+v4aVQed zc@GMLh_~M!*hJnYgcXDNGHWXeHty0xv&mHab zLRhAs3Iiy6FgB>5&~7>fwLc^_cE|GcBtloAN2lyhJDs57DNUHtyIVUX%(9Z4?~u-+ zx>ar$-OZ>(v%!$Zi544ISZ7#LZtrtDNPDO1`dE%kf)OUZ;hY>q8$~2n`~7t$WP;zZ zgxX%S4Gr%CKZoLk_dhA;JScAKjTqK-zKwa!Y=Axy@-l +TBt?At86s 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%0D4T=cn+HU2kxv+cHsplE~>OA(BBB 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 S-e|%CUvdy7>4N7*0sjTvE(aX| literal 0 HcmV?d00001 From b7602ff969bf4e1534ed30bfa51caa6613ca0e8d Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Mon, 6 Oct 2025 18:40:57 +0200 Subject: [PATCH 178/411] input_shaper: Fixed initialization for dual_carriage Signed-off-by: Dmitry Butyugin --- klippy/chelper/kin_shaper.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/klippy/chelper/kin_shaper.c b/klippy/chelper/kin_shaper.c index 1c4724360..682d7fa49 100644 --- a/klippy/chelper/kin_shaper.c +++ b/klippy/chelper/kin_shaper.c @@ -217,11 +217,11 @@ input_shaper_update_sk(struct stepper_kinematics *sk) { struct input_shaper *is = container_of(sk, struct input_shaper, sk); int kin_flags = is->orig_sk->active_flags & (AF_X | AF_Y | AF_Z); - if ((kin_flags & AF_X) == AF_X) + if (kin_flags == AF_X) is->sk.calc_position_cb = shaper_x_calc_position; - else if ((kin_flags & AF_Y) == AF_Y) + else if (kin_flags == AF_Y) is->sk.calc_position_cb = shaper_y_calc_position; - else if ((kin_flags & AF_Z) == AF_Z) + else if (kin_flags == AF_Z) is->sk.calc_position_cb = shaper_z_calc_position; else is->sk.calc_position_cb = shaper_xyz_calc_position; From d55baaf2654e989aac570d47910ca9434ec5814a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 28 Sep 2025 14:05:09 -0400 Subject: [PATCH 179/411] bus: Additional devices require i2c_write_noack() Currently, the LEDHelper() and GCodeRequestQueue() helper classes require that their callbacks do not block. As a result, the pca9533, pca9632, and sx1509 devices need to use non-blocking i2c write calls. Signed-off-by: Kevin O'Connor --- klippy/extras/bus.py | 3 +++ klippy/extras/pca9533.py | 4 ++-- klippy/extras/pca9632.py | 4 ++-- klippy/extras/sx1509.py | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index b04fbe764..676ff3b93 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -213,6 +213,9 @@ class MCU_I2C: "i2c_read_response oid=%c response=%*s", oid=self.oid, cq=self.cmd_queue) def i2c_write_noack(self, data, minclock=0, reqclock=0): + if self.i2c_write_cmd is None: + self._to_write.append(data) + return self.i2c_write_cmd.send([self.oid, data], minclock=minclock, reqclock=reqclock) def i2c_write(self, data, minclock=0, reqclock=0): diff --git a/klippy/extras/pca9533.py b/klippy/extras/pca9533.py index a94e1334e..20f1a6ca9 100644 --- a/klippy/extras/pca9533.py +++ b/klippy/extras/pca9533.py @@ -27,8 +27,8 @@ class PCA9533: minclock = 0 if print_time is not None: minclock = self.i2c.get_mcu().print_time_to_clock(print_time) - self.i2c.i2c_write([PCA9533_PLS0, ls0], minclock=minclock, - reqclock=BACKGROUND_PRIORITY_CLOCK) + self.i2c.i2c_write_noack([PCA9533_PLS0, ls0], minclock=minclock, + reqclock=BACKGROUND_PRIORITY_CLOCK) def get_status(self, eventtime): return self.led_helper.get_status(eventtime) diff --git a/klippy/extras/pca9632.py b/klippy/extras/pca9632.py index b8a813c33..099676e0f 100644 --- a/klippy/extras/pca9632.py +++ b/klippy/extras/pca9632.py @@ -37,8 +37,8 @@ class PCA9632: if self.prev_regs.get(reg) == val: return self.prev_regs[reg] = val - self.i2c.i2c_write([reg, val], minclock=minclock, - reqclock=BACKGROUND_PRIORITY_CLOCK) + self.i2c.i2c_write_noack([reg, val], minclock=minclock, + reqclock=BACKGROUND_PRIORITY_CLOCK) def handle_connect(self): #Configure MODE1 self.reg_write(PCA9632_MODE1, 0x00) diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index fd36c7fe1..99df55df3 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -92,7 +92,8 @@ class SX1509(object): # Byte data += [self.reg_i_on_dict[reg] & 0xFF] clock = self._mcu.print_time_to_clock(print_time) - self._i2c.i2c_write(data, minclock=self._last_clock, reqclock=clock) + self._i2c.i2c_write_noack(data, minclock=self._last_clock, + reqclock=clock) self._last_clock = clock class SX1509_digital_out(object): From e87de2ae493e694e6f2bdca5ba2410b3894a07d2 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 20:10:24 -0400 Subject: [PATCH 180/411] motion_queuing: Don't disable step+dir+step filter in drip_update_time() Allow the step compress code to perform regular step+dir+step filtering even during probing and homing actions. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 93f5594cd..20473503c 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -250,7 +250,7 @@ class PrinterMotionQueuing: # Disable background flushing from timer self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) self.do_kick_flush_timer = False - self._advance_flush_time(start_time) + self._advance_flush_time(start_time - SDS_CHECK_TIME, start_time) # Flush in segments until drip_completion signal flush_time = start_time while flush_time < end_time: @@ -265,7 +265,7 @@ class PrinterMotionQueuing: continue flush_time = min(flush_time + DRIP_SEGMENT_TIME, end_time) self.note_mcu_movequeue_activity(flush_time) - self._advance_flush_time(flush_time) + self._advance_flush_time(flush_time - SDS_CHECK_TIME, flush_time) # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) self._advance_flush_time(flush_time + self.kin_flush_delay) From 8fd263ca691cb4f91b5a721901687b2f3cc8340d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 Oct 2025 17:29:30 -0400 Subject: [PATCH 181/411] toolhead: Add a lookahead.is_empty() helper function Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 386f0620e..f56f0f114 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -126,6 +126,8 @@ class LookAheadQueue: self.junction_flush = LOOKAHEAD_FLUSH_TIME def set_flush_time(self, flush_time): self.junction_flush = flush_time + def is_empty(self): + return not self.queue def get_last(self): if self.queue: return self.queue[-1] @@ -476,8 +478,7 @@ class ToolHead: self.print_time, max(buffer_time, 0.), self.print_stall) def check_busy(self, eventtime): est_print_time = self.mcu.estimated_print_time(eventtime) - lookahead_empty = not self.lookahead.queue - return self.print_time, est_print_time, lookahead_empty + return self.print_time, est_print_time, self.lookahead.is_empty() def get_status(self, eventtime): print_time = self.print_time estimated_print_time = self.mcu.estimated_print_time(eventtime) From 3f9733d04dc4eb3245352139995f485cafe776e1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 Oct 2025 17:40:23 -0400 Subject: [PATCH 182/411] toolhead: Move priming logic from _check_pause() to new _check_priming_state() Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index f56f0f114..aa31dbc8e 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -313,36 +313,39 @@ class ToolHead: else: self._process_lookahead() return self.print_time + def _check_priming_state(self, eventtime): + est_print_time = self.mcu.estimated_print_time(eventtime) + if self.check_stall_time: + # Was in "NeedPrime" state and got there from idle input + if est_print_time < self.check_stall_time: + self.print_stall += 1 + self.check_stall_time = 0. + # Transition from "NeedPrime"/"Priming" state to "Priming" state + self.special_queuing_state = "Priming" + self.need_check_pause = -1. + if self.priming_timer is None: + self.priming_timer = self.reactor.register_timer( + self._priming_handler) + buffer_time = self.print_time - est_print_time + wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_HIGH) + self.reactor.update_timer(self.priming_timer, wtime) def _check_pause(self): eventtime = self.reactor.monotonic() - est_print_time = self.mcu.estimated_print_time(eventtime) - buffer_time = self.print_time - est_print_time if self.special_queuing_state: - if self.check_stall_time: - # Was in "NeedPrime" state and got there from idle input - if est_print_time < self.check_stall_time: - self.print_stall += 1 - self.check_stall_time = 0. - # Transition from "NeedPrime"/"Priming" state to "Priming" state - self.special_queuing_state = "Priming" - self.need_check_pause = -1. - if self.priming_timer is None: - self.priming_timer = self.reactor.register_timer( - self._priming_handler) - wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_HIGH) - self.reactor.update_timer(self.priming_timer, wtime) + # In "NeedPrime"/"Priming" state - update priming expiration timer + self._check_priming_state(eventtime) # Check if there are lots of queued moves and pause if so while 1: + est_print_time = self.mcu.estimated_print_time(eventtime) + buffer_time = self.print_time - est_print_time pause_time = buffer_time - BUFFER_TIME_HIGH if pause_time <= 0.: break if not self.can_pause: self.need_check_pause = self.reactor.NEVER return - eventtime = self.reactor.pause( - eventtime + max(.005, min(1., pause_time))) - est_print_time = self.mcu.estimated_print_time(eventtime) - buffer_time = self.print_time - est_print_time + pause_time = max(.005, min(1., pause_time)) + eventtime = self.reactor.pause(eventtime + pause_time) if not self.special_queuing_state: # In main state - defer pause checking until needed self.need_check_pause = est_print_time + BUFFER_TIME_HIGH From 7f177aad1a1a8687737d98c0eb647528e0bdbf49 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 Oct 2025 17:49:06 -0400 Subject: [PATCH 183/411] toolhead: Minor code movement Move flushing and priming code together. No code changes - only code movement. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index aa31dbc8e..192793463 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -303,6 +303,13 @@ class ToolHead: self.check_stall_time = 0. if is_runout and prev_print_time != self.print_time: self.check_stall_time = self.print_time + def _handle_step_flush(self, flush_time, step_gen_time): + if self.special_queuing_state: + return + # In "main" state - flush lookahead if buffer runs low + kin_flush_delay = self.motion_queuing.get_kin_flush_delay() + if step_gen_time >= self.print_time - kin_flush_delay - 0.001: + self._flush_lookahead(is_runout=True) def flush_step_generation(self): self._flush_lookahead() self.motion_queuing.flush_all_steps() @@ -313,6 +320,16 @@ class ToolHead: else: self._process_lookahead() return self.print_time + def _priming_handler(self, eventtime): + self.reactor.unregister_timer(self.priming_timer) + self.priming_timer = None + try: + if self.special_queuing_state == "Priming": + self._flush_lookahead(is_runout=True) + except: + logging.exception("Exception in priming_handler") + self.printer.invoke_shutdown("Exception in priming_handler") + return self.reactor.NEVER def _check_priming_state(self, eventtime): est_print_time = self.mcu.estimated_print_time(eventtime) if self.check_stall_time: @@ -349,23 +366,6 @@ class ToolHead: if not self.special_queuing_state: # In main state - defer pause checking until needed self.need_check_pause = est_print_time + BUFFER_TIME_HIGH - def _priming_handler(self, eventtime): - self.reactor.unregister_timer(self.priming_timer) - self.priming_timer = None - try: - if self.special_queuing_state == "Priming": - self._flush_lookahead(is_runout=True) - except: - logging.exception("Exception in priming_handler") - self.printer.invoke_shutdown("Exception in priming_handler") - return self.reactor.NEVER - def _handle_step_flush(self, flush_time, step_gen_time): - if self.special_queuing_state: - return - # In "main" state - flush lookahead if buffer runs low - kin_flush_delay = self.motion_queuing.get_kin_flush_delay() - if step_gen_time >= self.print_time - kin_flush_delay - 0.001: - self._flush_lookahead(is_runout=True) # Movement commands def get_position(self): return list(self.commanded_pos) From b85b92fdfb8496123671eccc696c856e3ca18b39 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 Oct 2025 17:53:09 -0400 Subject: [PATCH 184/411] toolhead: Don't enter "Priming" state on a dwell() After a toolhead dwell, there is no reason to enter the priming state and to create the priming exiration timer. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 192793463..996c34a37 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -331,6 +331,9 @@ class ToolHead: self.printer.invoke_shutdown("Exception in priming_handler") return self.reactor.NEVER def _check_priming_state(self, eventtime): + if self.lookahead.is_empty(): + # In "NeedPrime" state and can remain there + return est_print_time = self.mcu.estimated_print_time(eventtime) if self.check_stall_time: # Was in "NeedPrime" state and got there from idle input From cba7a285e4693fbca736f76df9ff407892676cdf Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 15:45:00 -0400 Subject: [PATCH 185/411] trapq: Extend trapq_extract_old() to also check active moves Support extracting moves from both the "live" trapq->moves as well as the "history" from trapq->history storage. Now that moves are flushed separately from the lookahead queue, there is a good chance that the current move being processed on the mcus will still be in the active trapq list. Signed-off-by: Kevin O'Connor --- klippy/chelper/list.h | 5 +++++ klippy/chelper/trapq.c | 36 ++++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/klippy/chelper/list.h b/klippy/chelper/list.h index 12fe2b038..f38fca6cc 100644 --- a/klippy/chelper/list.h +++ b/klippy/chelper/list.h @@ -116,6 +116,11 @@ list_join_tail(struct list_head *add, struct list_head *h) ; &pos->member != &(head)->root \ ; pos = list_next_entry(pos, member)) +#define list_for_each_entry_reverse(pos, head, member) \ + for (pos = list_last_entry((head), typeof(*pos), member) \ + ; &pos->member != &(head)->root \ + ; pos = list_prev_entry(pos, member)) + #define list_for_each_entry_safe(pos, n, head, member) \ for (pos = list_first_entry((head), typeof(*pos), member) \ , n = list_next_entry(pos, member) \ diff --git a/klippy/chelper/trapq.c b/klippy/chelper/trapq.c index c238a3818..a2a5f37f3 100644 --- a/klippy/chelper/trapq.c +++ b/klippy/chelper/trapq.c @@ -228,6 +228,22 @@ trapq_set_position(struct trapq *tq, double print_time list_add_head(&m->node, &tq->history); } +// Copy the info in a 'struct move' to a 'struct pull_move' +static void +copy_pull_move(struct pull_move *p, struct move *m) +{ + p->print_time = m->print_time; + p->move_t = m->move_t; + p->start_v = m->start_v; + p->accel = 2. * m->half_accel; + p->start_x = m->start_pos.x; + p->start_y = m->start_pos.y; + p->start_z = m->start_pos.z; + p->x_r = m->axes_r.x; + p->y_r = m->axes_r.y; + p->z_r = m->axes_r.z; +} + // Return history of movement queue int __visible trapq_extract_old(struct trapq *tq, struct pull_move *p, int max @@ -235,21 +251,21 @@ trapq_extract_old(struct trapq *tq, struct pull_move *p, int max { int res = 0; struct move *m; + list_for_each_entry_reverse(m, &tq->moves, node) { + if (start_time >= m->print_time + m->move_t || res >= max) + break; + if (end_time <= m->print_time || (!m->start_v && !m->half_accel)) + continue; + copy_pull_move(p, m); + p++; + res++; + } list_for_each_entry(m, &tq->history, node) { if (start_time >= m->print_time + m->move_t || res >= max) break; if (end_time <= m->print_time) continue; - p->print_time = m->print_time; - p->move_t = m->move_t; - p->start_v = m->start_v; - p->accel = 2. * m->half_accel; - p->start_x = m->start_pos.x; - p->start_y = m->start_pos.y; - p->start_z = m->start_pos.z; - p->x_r = m->axes_r.x; - p->y_r = m->axes_r.y; - p->z_r = m->axes_r.z; + copy_pull_move(p, m); p++; res++; } From 6269dda56bb7c08263153b18715d08e2d964bcab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 6 Oct 2025 13:32:01 -0400 Subject: [PATCH 186/411] sensor_lis2dw: Fix fifo_empty check on lis2dw chips Fix inverted check for fifo empty. The fifo is empty when the number of entries in the fifo is zero. Signed-off-by: Kevin O'Connor --- src/sensor_lis2dw.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sensor_lis2dw.c b/src/sensor_lis2dw.c index bf4beba1d..26452a0ad 100644 --- a/src/sensor_lis2dw.c +++ b/src/sensor_lis2dw.c @@ -141,7 +141,7 @@ lis2dw_query(struct lis2dw *ax, uint8_t oid) if (ax->model == LIS3DH) fifo_empty = fifo[1] & 0x20; else - fifo_empty = fifo[1] & 0x3F; + fifo_empty = ((fifo[1] & 0x3F) == 0); fifo_ovrn = fifo[1] & 0x40; @@ -167,7 +167,7 @@ lis2dw_query(struct lis2dw *ax, uint8_t oid) if (ax->model == LIS3DH) fifo_empty = fifo[0] & 0x20; else - fifo_empty = fifo[0] & 0x3F; + fifo_empty = ((fifo[0] & 0x3F) == 0); fifo_ovrn = fifo[0] & 0x40; From 4415b988c1b719ef3c56bb338d696310f849a121 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 9 Oct 2025 01:14:53 -0400 Subject: [PATCH 187/411] toolhead: Clarify priming timer scheduling Make sure each command gets an additional 100ms before flushing via the priming timer. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 996c34a37..895014cdb 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -194,6 +194,7 @@ class LookAheadQueue: BUFFER_TIME_HIGH = 2.0 BUFFER_TIME_START = 0.250 +PRIMING_CMD_TIME = 0.100 # Main code to track events (and their timing) on the printer toolhead class ToolHead: @@ -346,8 +347,8 @@ class ToolHead: if self.priming_timer is None: self.priming_timer = self.reactor.register_timer( self._priming_handler) - buffer_time = self.print_time - est_print_time - wtime = eventtime + max(0.100, buffer_time - BUFFER_TIME_HIGH) + will_pause_time = self.print_time - est_print_time - BUFFER_TIME_HIGH + wtime = eventtime + max(0., will_pause_time) + PRIMING_CMD_TIME self.reactor.update_timer(self.priming_timer, wtime) def _check_pause(self): eventtime = self.reactor.monotonic() @@ -357,8 +358,7 @@ class ToolHead: # Check if there are lots of queued moves and pause if so while 1: est_print_time = self.mcu.estimated_print_time(eventtime) - buffer_time = self.print_time - est_print_time - pause_time = buffer_time - BUFFER_TIME_HIGH + pause_time = self.print_time - est_print_time - BUFFER_TIME_HIGH if pause_time <= 0.: break if not self.can_pause: From 50cb362234f277a4923f1d59d21473d1e0317f62 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 23:15:09 -0400 Subject: [PATCH 188/411] toolhead: Make sure to periodically yield to other tasks when buffering moves Normally the toolhead code will flush the lookahead buffer every ~250ms and will briefly pause to avoid buffering too much data. That pause allows other tasks to run. Make sure to periodically yield to other tasks on each lookahead buffer flush even if a delay isn't needed. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 895014cdb..9163e82cc 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -356,6 +356,7 @@ class ToolHead: # In "NeedPrime"/"Priming" state - update priming expiration timer self._check_priming_state(eventtime) # Check if there are lots of queued moves and pause if so + did_pause = False while 1: est_print_time = self.mcu.estimated_print_time(eventtime) pause_time = self.print_time - est_print_time - BUFFER_TIME_HIGH @@ -366,9 +367,13 @@ class ToolHead: return pause_time = max(.005, min(1., pause_time)) eventtime = self.reactor.pause(eventtime + pause_time) + did_pause = True if not self.special_queuing_state: - # In main state - defer pause checking until needed - self.need_check_pause = est_print_time + BUFFER_TIME_HIGH + # In main state - defer pause checking + self.need_check_pause = self.print_time + if not did_pause: + # May be falling behind - yield to avoid starving other tasks + self.reactor.pause(self.reactor.NOW) # Movement commands def get_position(self): return list(self.commanded_pos) From 16fc46fe5ff0dbbc5188ee6a7829eee5976c1eb9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Sep 2025 19:32:49 -0400 Subject: [PATCH 189/411] toolhead: Reduce LOOKAHEAD_FLUSH_TIME to 0.150 seconds The current code is likely to perform a lazy flush of the lookahead queue around 4 times a second. Increase that to around 6-7 times a second. This change may slightly improve the responsiveness to user requests mid-print (eg, changing extrusion ratio) and may make a "print stall" less likely in some corner cases. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 9163e82cc..da297e966 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -113,7 +113,7 @@ class Move: self.cruise_t = cruise_d / cruise_v self.decel_t = decel_d / ((end_v + cruise_v) * 0.5) -LOOKAHEAD_FLUSH_TIME = 0.250 +LOOKAHEAD_FLUSH_TIME = 0.150 # Class to track a list of pending move requests and to facilitate # "look-ahead" across moves to reduce acceleration between moves. From 8de426d2446345ad38e3a19ad04e5572525707ff Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 7 Sep 2025 00:08:24 -0400 Subject: [PATCH 190/411] toolhead: Reduce target buffer time to 1 second from 2 seconds During normal printing the host software would attempt to stay ahead of the micro-controller by 2 full seconds. Change that time to 1 second. This should make the software more responsive to user requests (such as pause requests). Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 4 ++++ klippy/toolhead.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 3e7f0daf7..cb43d9564 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,10 @@ All dates in this document are approximate. ## Changes +20251010: During normal printing the command processing will now +attempt to stay one second ahead of printer movement (reduced from two +seconds previously). + 20251003: Support for the undocumented `max_stepper_error` option in the `[printer]` config section has been removed. diff --git a/klippy/toolhead.py b/klippy/toolhead.py index da297e966..d7ce66b5c 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -192,7 +192,7 @@ class LookAheadQueue: # Check if enough moves have been queued to reach the target flush time. return self.junction_flush <= 0. -BUFFER_TIME_HIGH = 2.0 +BUFFER_TIME_HIGH = 1.0 BUFFER_TIME_START = 0.250 PRIMING_CMD_TIME = 0.100 From 84e9a281416216bbef6cc200c77eea4ace5baf71 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 22:02:08 -0400 Subject: [PATCH 191/411] virtual_sdcard: Reduce pause time on busy detection If there are other users of the gcode mutex then pause for 50ms (instead of 100ms). Signed-off-by: Kevin O'Connor --- klippy/extras/virtual_sdcard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/virtual_sdcard.py b/klippy/extras/virtual_sdcard.py index 6dc49e2f5..b17004562 100644 --- a/klippy/extras/virtual_sdcard.py +++ b/klippy/extras/virtual_sdcard.py @@ -259,7 +259,7 @@ class VirtualSD: continue # Pause if any other request is pending in the gcode class if gcode_mutex.test(): - self.reactor.pause(self.reactor.monotonic() + 0.100) + self.reactor.pause(self.reactor.monotonic() + 0.050) continue # Dispatch command self.cmd_from_sd = True From 9117c090376533848f820ee5b76f7fcd07cfb0ab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 9 Oct 2025 17:56:25 -0400 Subject: [PATCH 192/411] graphstats: Set MAXBUFFER=1 Signed-off-by: Kevin O'Connor --- scripts/graphstats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/graphstats.py b/scripts/graphstats.py index 5cd2ad34f..c49223705 100755 --- a/scripts/graphstats.py +++ b/scripts/graphstats.py @@ -1,14 +1,14 @@ #!/usr/bin/env python # Script to parse a logging file, extract the stats, and graph them # -# Copyright (C) 2016-2021 Kevin O'Connor +# Copyright (C) 2016-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import optparse, datetime import matplotlib MAXBANDWIDTH=25000. -MAXBUFFER=2. +MAXBUFFER=1. STATS_INTERVAL=5. TASK_MAX=0.0025 From a5c764bbe93f5ffcffa8067d706dec4304afbdf7 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Sun, 12 Oct 2025 18:02:34 +0100 Subject: [PATCH 193/411] docs: Command parameter fix Signed-off-by: Pedro Lamas --- docs/G-Codes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/G-Codes.md b/docs/G-Codes.md index caad39bec..eb38e0134 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -493,7 +493,7 @@ enabled. `SET_FAN_SPEED FAN=config_name SPEED=` This command sets the speed of a fan. "speed" must be between 0.0 and 1.0. -`SET_FAN_SPEED PIN=config_name TEMPLATE= +`SET_FAN_SPEED FAN=config_name TEMPLATE= [=]`: If `TEMPLATE` is specified then it assigns a [display_template](Config_Reference.md#display_template) to the given fan. For example, if one defined a `[display_template From e3909fb2053657eb44a300d86e5fa7d541cab676 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 11 Oct 2025 01:33:38 -0400 Subject: [PATCH 194/411] manual_stepper: Remove some unused code Signed-off-by: Kevin O'Connor --- klippy/extras/manual_stepper.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 9c775567f..8a8911e18 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -4,7 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import logging -import stepper, chelper +import stepper from . import force_move class ManualStepper: @@ -203,7 +203,6 @@ class ManualStepper: self.motion_queuing.drip_update_time(start_time, end_time, drip_completion) # Clear trapq of any remaining parts of movement - reactor = self.printer.get_reactor() self.motion_queuing.wipe_trapq(self.trapq) self.rail.set_position([self.commanded_pos, 0., 0.]) self.sync_print_time() From b1dd6a73f78c8cdae2b835c7c2af2db297813097 Mon Sep 17 00:00:00 2001 From: JamesH1978 <87171443+JamesH1978@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:53:13 +0100 Subject: [PATCH 195/411] docs: Update FAQ.md - Typo (#7089) Kliper is not Klipper! Signed-off-by: James Hartley --- docs/FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 7c8214b32..dbea920f0 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -106,7 +106,7 @@ Klipper will run on a Raspberry Pi 1, 2 and on the Raspberry Pi Zero1, but these boards don't have enough processing power to run Klipper well. It is common for print stalls to occur on these slower machines when printing (The printer may move faster than Klipper can send -movement commands.) It is not reccomended to run Kliper on these older +movement commands.) It is not reccomended to run Klipper on these older machines. For running on the Beaglebone, see the From 3fe594ef20d5b33ede62822e68ea7f96415b466e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 6 Oct 2025 14:03:27 -0400 Subject: [PATCH 196/411] sensor_lis2dw: Read 8 samples at a time from sensor fifo Batch reading of 8 samples (48 bytes) at a time from the sensor. This reduces the number of transactions - which can notably improve performance on i2c. Signed-off-by: Kevin O'Connor --- src/sensor_lis2dw.c | 164 +++++++++++++++++++++++++------------------- 1 file changed, 92 insertions(+), 72 deletions(-) diff --git a/src/sensor_lis2dw.c b/src/sensor_lis2dw.c index 26452a0ad..1dd06418d 100644 --- a/src/sensor_lis2dw.c +++ b/src/sensor_lis2dw.c @@ -1,7 +1,7 @@ // Support for gathering acceleration data from LIS2DW chip // // Copyright (C) 2023 Zhou.XianMing -// Copyright (C) 2020-2023 Kevin O'Connor +// Copyright (C) 2020-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -24,6 +24,7 @@ #define LIS_FIFO_SAMPLES 0x2F #define BYTES_PER_SAMPLE 6 +#define BYTES_PER_BLOCK 48 struct lis2dw { struct timer timer; @@ -35,6 +36,7 @@ struct lis2dw { uint8_t bus_type; uint8_t flags; uint8_t model; + uint8_t fifo_bytes_pending; struct sensor_bulk sb; }; @@ -118,73 +120,94 @@ lis2dw_reschedule_timer(struct lis2dw *ax) irq_enable(); } +// Update local status tracking from newly read fifo status register +static void +update_fifo_status(struct lis2dw *ax, uint8_t fifo_status) +{ + if (fifo_status & 0x40) + ax->sb.possible_overflows++; + + uint_fast8_t pending; + if (ax->model == LIS3DH) { + if (fifo_status & 0x20) + pending = 0; + else + pending = fifo_status & 0x1F; + } else { + pending = fifo_status & 0x3F; + } + ax->fifo_bytes_pending = pending * BYTES_PER_SAMPLE; +} + +// Query fifo status register +static void +query_fifo_status(struct lis2dw *ax) +{ + uint8_t fifo_status = 0; + if (CONFIG_WANT_SPI && ax->bus_type == SPI_SERIAL) { + uint8_t fifo[2] = { LIS_FIFO_SAMPLES | LIS_AM_READ, 0x00 }; + spidev_transfer(ax->spi, 1, sizeof(fifo), fifo); + fifo_status = fifo[1]; + } else if (CONFIG_WANT_I2C && ax->bus_type == I2C_SERIAL) { + uint8_t fifo_reg[1] = {LIS_FIFO_SAMPLES}; + int ret = i2c_dev_read(ax->i2c, sizeof(fifo_reg), fifo_reg + , sizeof(fifo_status), &fifo_status); + i2c_shutdown_on_err(ret); + } + update_fifo_status(ax, fifo_status); +} + +// Read 8 samples from FIFO via SPI +static void +read_fifo_block_spi(struct lis2dw *ax) +{ + uint8_t msg[BYTES_PER_BLOCK + 1] = {0}; + msg[0] = LIS_AR_DATAX0 | LIS_AM_READ; + if (ax->model == LIS3DH) + msg[0] |= LIS_MS_SPI; + + spidev_transfer(ax->spi, 1, sizeof(msg), msg); + memcpy(ax->sb.data, &msg[1], BYTES_PER_BLOCK); +} + +// Read 8 samples from FIFO via i2c +static void +read_fifo_block_i2c(struct lis2dw *ax) +{ + uint8_t msg_reg[] = {LIS_AR_DATAX0}; + if (ax->model == LIS3DH) + msg_reg[0] |= LIS_MS_I2C; + + int ret = i2c_dev_read(ax->i2c, sizeof(msg_reg), msg_reg + , BYTES_PER_BLOCK, ax->sb.data); + i2c_shutdown_on_err(ret); +} + +// Read from fifo and transmit data to host +static void +read_fifo_block(struct lis2dw *ax, uint8_t oid) +{ + if (CONFIG_WANT_SPI && ax->bus_type == SPI_SERIAL) + read_fifo_block_spi(ax); + else if (CONFIG_WANT_I2C && ax->bus_type == I2C_SERIAL) + read_fifo_block_i2c(ax); + ax->sb.data_count = BYTES_PER_BLOCK; + sensor_bulk_report(&ax->sb, oid); + ax->fifo_bytes_pending -= BYTES_PER_BLOCK; +} + // Query accelerometer data static void lis2dw_query(struct lis2dw *ax, uint8_t oid) { - uint8_t fifo_empty = 0; - uint8_t fifo_ovrn = 0; - uint8_t *d = &ax->sb.data[ax->sb.data_count]; + if (ax->fifo_bytes_pending < BYTES_PER_BLOCK) + query_fifo_status(ax); - if (CONFIG_WANT_SPI && ax->bus_type == SPI_SERIAL) { - uint8_t msg[7] = {0}; - uint8_t fifo[2] = {LIS_FIFO_SAMPLES | LIS_AM_READ , 0}; - - msg[0] = LIS_AR_DATAX0 | LIS_AM_READ; - if (ax->model == LIS3DH) - msg[0] |= LIS_MS_SPI; - - spidev_transfer(ax->spi, 1, sizeof(msg), msg); - - spidev_transfer(ax->spi, 1, sizeof(fifo), fifo); - - if (ax->model == LIS3DH) - fifo_empty = fifo[1] & 0x20; - else - fifo_empty = ((fifo[1] & 0x3F) == 0); - - fifo_ovrn = fifo[1] & 0x40; - - for (uint32_t i = 0; i < BYTES_PER_SAMPLE; i++) - d[i] = msg[i + 1]; - } else if (CONFIG_WANT_I2C && ax->bus_type == I2C_SERIAL) { - uint8_t msg_reg[] = {LIS_AR_DATAX0}; - if (ax->model == LIS3DH) - msg_reg[0] |= LIS_MS_I2C; - uint8_t msg[6]; - uint8_t fifo_reg[1] = {LIS_FIFO_SAMPLES}; - uint8_t fifo[1]; - - int ret; - ret = i2c_dev_read(ax->i2c, sizeof(msg_reg), msg_reg - , sizeof(msg), msg); - i2c_shutdown_on_err(ret); - - ret = i2c_dev_read(ax->i2c, sizeof(fifo_reg), fifo_reg - , sizeof(fifo), fifo); - i2c_shutdown_on_err(ret); - - if (ax->model == LIS3DH) - fifo_empty = fifo[0] & 0x20; - else - fifo_empty = ((fifo[0] & 0x3F) == 0); - - fifo_ovrn = fifo[0] & 0x40; - - for (uint32_t i = 0; i < BYTES_PER_SAMPLE; i++) - d[i] = msg[i]; - } - - ax->sb.data_count += BYTES_PER_SAMPLE; - if (ax->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(ax->sb.data)) - sensor_bulk_report(&ax->sb, oid); - - // Check fifo status - if (fifo_ovrn) - ax->sb.possible_overflows++; + if (ax->fifo_bytes_pending >= BYTES_PER_BLOCK) + read_fifo_block(ax, oid); // check if we need to run the task again (more packets in fifo?) - if (!fifo_empty) { + if (ax->fifo_bytes_pending >= BYTES_PER_BLOCK) { // More data in fifo - wake this task again sched_wake_task(&lis2dw_wake); } else { @@ -207,6 +230,7 @@ command_query_lis2dw(uint32_t *args) // Start new measurements query ax->rest_ticks = args[1]; + ax->fifo_bytes_pending = 0; sensor_bulk_reset(&ax->sb); lis2dw_reschedule_timer(ax); } @@ -218,30 +242,26 @@ command_query_lis2dw_status(uint32_t *args) struct lis2dw *ax = oid_lookup(args[0], command_config_lis2dw); uint32_t time1 = 0; uint32_t time2 = 0; - uint8_t status = 0; + uint8_t fifo_status = 0; if (CONFIG_WANT_SPI && ax->bus_type == SPI_SERIAL) { - uint8_t msg[2] = { LIS_FIFO_SAMPLES | LIS_AM_READ, 0x00 }; + uint8_t fifo[2] = { LIS_FIFO_SAMPLES | LIS_AM_READ, 0x00 }; time1 = timer_read_time(); - spidev_transfer(ax->spi, 1, sizeof(msg), msg); + spidev_transfer(ax->spi, 1, sizeof(fifo), fifo); time2 = timer_read_time(); - status = msg[1]; + fifo_status = fifo[1]; } else if (CONFIG_WANT_I2C && ax->bus_type == I2C_SERIAL) { uint8_t fifo_reg[1] = {LIS_FIFO_SAMPLES}; - uint8_t fifo[1]; - time1 = timer_read_time(); int ret = i2c_dev_read(ax->i2c, sizeof(fifo_reg), fifo_reg - , sizeof(fifo), fifo); + , sizeof(fifo_status), &fifo_status); time2 = timer_read_time(); - i2c_shutdown_on_err(ret); - - status = fifo[0]; } + update_fifo_status(ax, fifo_status); sensor_bulk_status(&ax->sb, args[0], time1, time2-time1 - , (status & 0x1f) * BYTES_PER_SAMPLE); + , ax->fifo_bytes_pending); } DECL_COMMAND(command_query_lis2dw_status, "query_lis2dw_status oid=%c"); From 95b0ebf4c72bbaed156f60848f7ec5055d01ece9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 9 Oct 2025 21:05:34 -0400 Subject: [PATCH 197/411] motion_report: Improve "motion_report/dump_trapq" during homing Now that trapq_extract_old() can return upcoming moves, it's possible for homing to cause confusing results (as these moves can end early). Avoid this by delaying query responses until after homing completes. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 9 +++++++++ klippy/extras/motion_report.py | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index 20473503c..e1a0d7fcc 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -54,6 +54,8 @@ class PrinterMotionQueuing: self.do_kick_flush_timer = True self.last_flush_time = self.last_step_gen_time = 0. self.need_flush_time = self.need_step_gen_time = 0. + # "Drip" timing (for homing and probing moves) + self.drip_start_times = [] # Register handlers printer.register_event_handler("klippy:shutdown", self._handle_shutdown) # C trapq tracking @@ -245,7 +247,9 @@ class PrinterMotionQueuing: if self.do_kick_flush_timer: self.do_kick_flush_timer = False self.reactor.update_timer(self.flush_timer, self.reactor.NOW) + # "Drip" timing (for homing and probing moves) def drip_update_time(self, start_time, end_time, drip_completion): + self.drip_start_times.append(start_time) self._await_flush_time(start_time) # Disable background flushing from timer self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) @@ -269,6 +273,11 @@ class PrinterMotionQueuing: # Restore background flushing self.reactor.update_timer(self.flush_timer, self.reactor.NOW) self._advance_flush_time(flush_time + self.kin_flush_delay) + self.drip_start_times.remove(start_time) + def check_drip_timing(self): + if not self.drip_start_times: + return None + return min(self.drip_start_times) def load_config(config): return PrinterMotionQueuing(config) diff --git a/klippy/extras/motion_report.py b/klippy/extras/motion_report.py index c142fb393..0595bbb94 100644 --- a/klippy/extras/motion_report.py +++ b/klippy/extras/motion_report.py @@ -71,6 +71,7 @@ class DumpTrapQ: self.name = name self.trapq = trapq self.last_batch_msg = (0., 0.) + self.motion_queuing = printer.lookup_object("motion_queuing") self.batch_bulk = bulk_sensor.BatchBulkHelper(printer, self._process_batch) api_resp = {'header': ('time', 'duration', 'start_velocity', @@ -121,6 +122,12 @@ class DumpTrapQ: d = [(m.print_time, m.move_t, m.start_v, m.accel, (m.start_x, m.start_y, m.start_z), (m.x_r, m.y_r, m.z_r)) for m in data] + if d: + start_drip_time = self.motion_queuing.check_drip_timing() + if start_drip_time is not None: + # If homing, delay sending trapq entries that may change + while d and d[-1][0] + d[-1][1] >= start_drip_time: + d.pop() if d and d[0] == self.last_batch_msg: d.pop(0) if not d: From 3215e3a2aae73a6641596d6d2b9cdbf92f615faa Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 13 Oct 2025 13:29:24 -0400 Subject: [PATCH 198/411] motion_report: Improve "motion_report/dump_stepper" during homing Step timing is reset when stepper.set_position() is called. Detect that case and ensure future steps after set_position start on a new block. This simplifies the timing for users of the data. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_report.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/klippy/extras/motion_report.py b/klippy/extras/motion_report.py index 0595bbb94..aabf91417 100644 --- a/klippy/extras/motion_report.py +++ b/klippy/extras/motion_report.py @@ -47,6 +47,11 @@ class DumpStepper: data, cdata = self.get_step_queue(self.last_batch_clock, 1<<63) if not data: return {} + for i, d in enumerate(data): + if not d.step_count: + # End block on a set_position marker to simplify timing + del data[i+1:] + break clock_to_print_time = self.mcu_stepper.get_mcu().clock_to_print_time first = data[0] first_clock = first.first_clock @@ -56,8 +61,8 @@ class DumpStepper: mcu_pos = first.start_position start_position = self.mcu_stepper.mcu_to_commanded_position(mcu_pos) step_dist = self.mcu_stepper.get_step_dist() - d = [(s.interval, s.step_count, s.add) for s in data] - return {"data": d, "start_position": start_position, + tdata = [(s.interval, s.step_count, s.add) for s in data] + return {"data": tdata, "start_position": start_position, "start_mcu_position": mcu_pos, "step_distance": step_dist, "first_clock": first_clock, "first_step_time": first_time, "last_clock": last_clock, "last_step_time": last_time} From f147804d97180d486fc6591c2663f3d5562fd499 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 13:15:07 -0400 Subject: [PATCH 199/411] error_mcu: Rename "klippy:notify_mcu_shutdown" to "klippy:analyze_shutdown" Rename the event to make it a little more clear what it is intended for. Also, check for an exception in each event handler. Signed-off-by: Kevin O'Connor --- klippy/extras/error_mcu.py | 6 +++--- klippy/klippy.py | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/klippy/extras/error_mcu.py b/klippy/extras/error_mcu.py index dc91c33a9..d30fcf2cf 100644 --- a/klippy/extras/error_mcu.py +++ b/klippy/extras/error_mcu.py @@ -62,8 +62,8 @@ class PrinterMCUError: def __init__(self, config): self.printer = config.get_printer() self.clarify_callbacks = {} - self.printer.register_event_handler("klippy:notify_mcu_shutdown", - self._handle_notify_mcu_shutdown) + self.printer.register_event_handler("klippy:analyze_shutdown", + self._handle_analyze_shutdown) self.printer.register_event_handler("klippy:notify_mcu_error", self._handle_notify_mcu_error) def add_clarify(self, msg, callback): @@ -88,7 +88,7 @@ class PrinterMCUError: newmsg = "%s%s%s%s%s" % (prefix, mcu_msg, clarify_msg, hint, message_shutdown) self.printer.update_error_msg(msg, newmsg) - def _handle_notify_mcu_shutdown(self, msg, details): + def _handle_analyze_shutdown(self, msg, details): if msg == "MCU shutdown": self._check_mcu_shutdown(msg, details) else: diff --git a/klippy/klippy.py b/klippy/klippy.py index 1d3ffbf06..a4bfd08c0 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -213,7 +213,11 @@ class Printer: logging.exception("Exception during shutdown handler") logging.info("Reactor garbage collection: %s", self.reactor.get_gc_stats()) - self.send_event("klippy:notify_mcu_shutdown", msg, details) + for cb in self.event_handlers.get("klippy:analyze_shutdown", []): + try: + cb(msg, details) + except: + logging.exception("Exception during analyze_shutdown handler") def invoke_async_shutdown(self, msg, details={}): self.reactor.register_async_callback( (lambda e: self.invoke_shutdown(msg, details))) From 38b286db4b44def662c21507ce82e3bdc060fd42 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 19:54:22 -0400 Subject: [PATCH 200/411] error_mcu: Report reactor stats in error_mcu module Move reactor debugging info from main klippy.py code to error_mcu code for improved exception handling. Signed-off-by: Kevin O'Connor --- klippy/extras/error_mcu.py | 3 +++ klippy/klippy.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/klippy/extras/error_mcu.py b/klippy/extras/error_mcu.py index d30fcf2cf..ae1bbcecc 100644 --- a/klippy/extras/error_mcu.py +++ b/klippy/extras/error_mcu.py @@ -93,6 +93,9 @@ class PrinterMCUError: self._check_mcu_shutdown(msg, details) else: self.printer.update_error_msg(msg, "%s%s" % (msg, message_shutdown)) + # Report reactor info (no good place to do this, so done here) + logging.info("Reactor garbage collection: %s", + self.printer.get_reactor().get_gc_stats()) def _check_protocol_error(self, msg, details): host_version = self.printer.start_args['software_version'] msg_update = [] diff --git a/klippy/klippy.py b/klippy/klippy.py index a4bfd08c0..5ee26abe1 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -211,8 +211,6 @@ class Printer: cb() except: logging.exception("Exception during shutdown handler") - logging.info("Reactor garbage collection: %s", - self.reactor.get_gc_stats()) for cb in self.event_handlers.get("klippy:analyze_shutdown", []): try: cb(msg, details) From e672391dbceabb30d2069a44e2d4479339cb70f0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 13:52:46 -0400 Subject: [PATCH 201/411] gcode_move: Move shutdown debugging to "klippy:analyze_shutdown" event Signed-off-by: Kevin O'Connor --- klippy/extras/gcode_move.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/klippy/extras/gcode_move.py b/klippy/extras/gcode_move.py index 655b8108f..929f7a807 100644 --- a/klippy/extras/gcode_move.py +++ b/klippy/extras/gcode_move.py @@ -8,20 +8,6 @@ import logging class GCodeMove: def __init__(self, config): self.printer = printer = config.get_printer() - printer.register_event_handler("klippy:ready", self._handle_ready) - printer.register_event_handler("klippy:shutdown", self._handle_shutdown) - printer.register_event_handler("toolhead:set_position", - self.reset_last_position) - printer.register_event_handler("toolhead:manual_move", - self.reset_last_position) - printer.register_event_handler("toolhead:update_extra_axes", - self._update_extra_axes) - printer.register_event_handler("gcode:command_error", - self.reset_last_position) - printer.register_event_handler("extruder:activate_extruder", - self._handle_activate_extruder) - printer.register_event_handler("homing:home_rails_end", - self._handle_home_rails_end) self.is_printer_ready = False # Register g-code commands gcode = printer.lookup_object('gcode') @@ -52,6 +38,23 @@ class GCodeMove: self.saved_states = {} self.move_transform = self.move_with_transform = None self.position_with_transform = (lambda: [0., 0., 0., 0.]) + # Register callbacks + printer.register_event_handler("klippy:ready", self._handle_ready) + printer.register_event_handler("klippy:shutdown", self._handle_shutdown) + printer.register_event_handler("klippy:analyze_shutdown", + self._handle_analyze_shutdown) + printer.register_event_handler("toolhead:set_position", + self.reset_last_position) + printer.register_event_handler("toolhead:manual_move", + self.reset_last_position) + printer.register_event_handler("toolhead:update_extra_axes", + self._update_extra_axes) + printer.register_event_handler("gcode:command_error", + self.reset_last_position) + printer.register_event_handler("extruder:activate_extruder", + self._handle_activate_extruder) + printer.register_event_handler("homing:home_rails_end", + self._handle_home_rails_end) def _handle_ready(self): self.is_printer_ready = True if self.move_transform is None: @@ -60,9 +63,8 @@ class GCodeMove: self.position_with_transform = toolhead.get_position self.reset_last_position() def _handle_shutdown(self): - if not self.is_printer_ready: - return self.is_printer_ready = False + def _handle_analyze_shutdown(self, msg, details): logging.info("gcode state: absolute_coord=%s absolute_extrude=%s" " base_position=%s last_position=%s homing_position=%s" " speed_factor=%s extrude_factor=%s speed=%s", From c64c5e891c0d6014c0cfe6925d9c797b1afdbdb1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 14:03:33 -0400 Subject: [PATCH 202/411] virtual_sdcard: Move shutdown debugging to "klippy:analyze_shutdown" event Signed-off-by: Kevin O'Connor --- klippy/extras/virtual_sdcard.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/klippy/extras/virtual_sdcard.py b/klippy/extras/virtual_sdcard.py index b17004562..a2d6d0a0b 100644 --- a/klippy/extras/virtual_sdcard.py +++ b/klippy/extras/virtual_sdcard.py @@ -16,8 +16,6 @@ DEFAULT_ERROR_GCODE = """ class VirtualSD: def __init__(self, config): self.printer = config.get_printer() - self.printer.register_event_handler("klippy:shutdown", - self.handle_shutdown) # sdcard state sd = config.get('path') self.sdcard_dirname = os.path.normpath(os.path.expanduser(sd)) @@ -46,7 +44,9 @@ class VirtualSD: self.gcode.register_command( "SDCARD_PRINT_FILE", self.cmd_SDCARD_PRINT_FILE, desc=self.cmd_SDCARD_PRINT_FILE_help) - def handle_shutdown(self): + self.printer.register_event_handler("klippy:analyze_shutdown", + self._handle_analyze_shutdown) + def _handle_analyze_shutdown(self, msg, details): if self.work_timer is not None: self.must_pause_work = True try: From 6f5ab8d0e81cfca1b8d7e0db671ab65182a15481 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 14:06:12 -0400 Subject: [PATCH 203/411] gcode: Move shutdown debugging to "klippy:analyze_shutdown" event Signed-off-by: Kevin O'Connor --- klippy/gcode.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/klippy/gcode.py b/klippy/gcode.py index 1c50695d2..93ef38a1e 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -369,8 +369,6 @@ class GCodeDispatch: class GCodeIO: def __init__(self, printer): self.printer = printer - printer.register_event_handler("klippy:ready", self._handle_ready) - printer.register_event_handler("klippy:shutdown", self._handle_shutdown) self.gcode = printer.lookup_object('gcode') self.gcode_mutex = self.gcode.get_mutex() self.fd = printer.get_start_args().get("gcode_fd") @@ -388,12 +386,17 @@ class GCodeIO: self.pending_commands = [] self.bytes_read = 0 self.input_log = collections.deque([], 50) + # Register event handlers + printer.register_event_handler("klippy:ready", self._handle_ready) + printer.register_event_handler("klippy:shutdown", self._handle_shutdown) + printer.register_event_handler("klippy:analyze_shutdown", + self._handle_analyze_shutdown) def _handle_ready(self): self.is_printer_ready = True if self.is_fileinput and self.fd_handle is None: self.fd_handle = self.reactor.register_fd(self.fd, self._process_data) - def _dump_debug(self): + def _handle_analyze_shutdown(self, msg, details): out = [] out.append("Dumping gcode input %d blocks" % (len(self.input_log),)) for eventtime, data in self.input_log: @@ -403,7 +406,6 @@ class GCodeIO: if not self.is_printer_ready: return self.is_printer_ready = False - self._dump_debug() if self.is_fileinput: self.printer.request_exit('error_exit') m112_r = re.compile(r'^(?:[nN][0-9]+)?\s*[mM]112(?:\s|$)') From 54d2716bdd2b0d1765783ffb0f7450aff700f65d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 14:17:11 -0400 Subject: [PATCH 204/411] webhooks: Move shutdown debugging to "klippy:analyze_shutdown" event Signed-off-by: Kevin O'Connor --- klippy/webhooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klippy/webhooks.py b/klippy/webhooks.py index 9e17177af..78edc6e5b 100644 --- a/klippy/webhooks.py +++ b/klippy/webhooks.py @@ -136,7 +136,7 @@ class ServerSocket: printer.register_event_handler( 'klippy:disconnect', self._handle_disconnect) printer.register_event_handler( - "klippy:shutdown", self._handle_shutdown) + "klippy:analyze_shutdown", self._handle_analyze_shutdown) def _handle_accept(self, eventtime): try: @@ -157,7 +157,7 @@ class ServerSocket: except socket.error: pass - def _handle_shutdown(self): + def _handle_analyze_shutdown(self, msg, details): for client in self.clients.values(): client.dump_request_log() From bc27bd9a13ed0aaa7a5f1b6f569fca69768c2869 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 14:25:28 -0400 Subject: [PATCH 205/411] mcu: Move shutdown debugging to "klippy:analyze_shutdown" event Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index eeec7c0b4..97262dd50 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -704,6 +704,8 @@ class MCUConnectHelper: self._mcu_identify) self._restart_helper = MCURestartHelper(config, self) printer.register_event_handler("klippy:shutdown", self._shutdown) + printer.register_event_handler("klippy:analyze_shutdown", + self._analyze_shutdown) def get_mcu(self): return self._mcu def get_serial(self): @@ -726,9 +728,6 @@ class MCUConnectHelper: self._printer.invoke_async_shutdown( "MCU shutdown", {"reason": msg, "mcu": self._name, "event_type": event_type}) - logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, event_type, - self._shutdown_msg, self._clocksync.dump_debug(), - self._serial.dump_debug()) def _handle_starting(self, params): if not self._is_shutdown: self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart" @@ -786,6 +785,12 @@ class MCUConnectHelper: self._mcu.register_response(self._handle_shutdown, 'shutdown') self._mcu.register_response(self._handle_shutdown, 'is_shutdown') self._mcu.register_response(self._handle_starting, 'starting') + def _analyze_shutdown(self, msg, details): + if self._mcu.is_fileoutput(): + return + logging.info("MCU '%s' shutdown: %s\n%s\n%s", self._name, + self._shutdown_msg, self._clocksync.dump_debug(), + self._serial.dump_debug()) def _shutdown(self, force=False): if (self._emergency_stop_cmd is None or (self._is_shutdown and not force)): From 1bba59b7a0c64f9285b8616b3d23686673b7f48c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 13:56:21 -0400 Subject: [PATCH 206/411] motion_report: Move shutdown debugging to "klippy:analyze_shutdown" event Signed-off-by: Kevin O'Connor --- klippy/extras/motion_report.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/klippy/extras/motion_report.py b/klippy/extras/motion_report.py index aabf91417..a762bb12c 100644 --- a/klippy/extras/motion_report.py +++ b/klippy/extras/motion_report.py @@ -157,7 +157,8 @@ class PrinterMotionReport: } # Register handlers self.printer.register_event_handler("klippy:connect", self._connect) - self.printer.register_event_handler("klippy:shutdown", self._shutdown) + self.printer.register_event_handler("klippy:analyze_shutdown", + self._handle_analyze_shutdown) def register_stepper(self, config, mcu_stepper): ds = DumpStepper(self.printer, mcu_stepper) self.steppers[mcu_stepper.get_name()] = ds @@ -180,7 +181,7 @@ class PrinterMotionReport: self.last_status['steppers'] = list(sorted(self.steppers.keys())) self.last_status['trapq'] = list(sorted(self.trapqs.keys())) # Shutdown handling - def _dump_shutdown(self, eventtime): + def _handle_analyze_shutdown(self, msg, details): # Log stepper queue_steps on mcu that started shutdown (if any) shutdown_time = NEVER_TIME for dstepper in self.steppers.values(): @@ -209,8 +210,6 @@ class PrinterMotionReport: if pos is not None: logging.info("Requested toolhead position at shutdown time %.6f: %s" , shutdown_time, pos) - def _shutdown(self): - self.printer.get_reactor().register_callback(self._dump_shutdown) # Status reporting def get_status(self, eventtime): if eventtime < self.next_status_time or not self.trapqs: From 0d0d3917c9a6207b1bff8682732f4b3ab7501731 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 14:40:23 -0400 Subject: [PATCH 207/411] motion_report: Simplify shutdown_clock tracking Determine which mcu raised the shutdown from the shutdown details report. Also, pass shutdown_clock via that details report. Signed-off-by: Kevin O'Connor --- klippy/extras/motion_report.py | 39 +++++++++++++++++----------------- klippy/mcu.py | 14 +++++------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/klippy/extras/motion_report.py b/klippy/extras/motion_report.py index a762bb12c..9f3a0385b 100644 --- a/klippy/extras/motion_report.py +++ b/klippy/extras/motion_report.py @@ -146,7 +146,7 @@ class PrinterMotionReport: def __init__(self, config): self.printer = config.get_printer() self.steppers = {} - self.trapqs = {} + self.dtrapqs = {} # get_status information self.next_status_time = 0. gcode = self.printer.lookup_object('gcode') @@ -166,7 +166,7 @@ class PrinterMotionReport: # Lookup toolhead trapq toolhead = self.printer.lookup_object("toolhead") trapq = toolhead.get_trapq() - self.trapqs['toolhead'] = DumpTrapQ(self.printer, 'toolhead', trapq) + self.dtrapqs['toolhead'] = DumpTrapQ(self.printer, 'toolhead', trapq) # Lookup extruder trapqs for i in range(99): ename = "extruder%d" % (i,) @@ -176,34 +176,35 @@ class PrinterMotionReport: if extruder is None: break etrapq = extruder.get_trapq() - self.trapqs[ename] = DumpTrapQ(self.printer, ename, etrapq) + self.dtrapqs[ename] = DumpTrapQ(self.printer, ename, etrapq) # Populate 'trapq' and 'steppers' in get_status result self.last_status['steppers'] = list(sorted(self.steppers.keys())) - self.last_status['trapq'] = list(sorted(self.trapqs.keys())) + self.last_status['trapq'] = list(sorted(self.dtrapqs.keys())) # Shutdown handling def _handle_analyze_shutdown(self, msg, details): + if msg != "MCU shutdown": + return + mcu = self.printer.lookup_object(details.get("mcu"), None) + if mcu is None or details.get("shutdown_clock") is None: + return + shutdown_clock = details["shutdown_clock"] + shutdown_time = mcu.clock_to_print_time(shutdown_clock) + clock_100ms = mcu.seconds_to_clock(0.100) + start_clock = max(0, shutdown_clock - clock_100ms) + end_clock = shutdown_clock + clock_100ms # Log stepper queue_steps on mcu that started shutdown (if any) - shutdown_time = NEVER_TIME for dstepper in self.steppers.values(): - mcu = dstepper.mcu_stepper.get_mcu() - sc = mcu.get_shutdown_clock() - if not sc: + if dstepper.mcu_stepper.get_mcu() is not mcu: continue - shutdown_time = min(shutdown_time, mcu.clock_to_print_time(sc)) - clock_100ms = mcu.seconds_to_clock(0.100) - start_clock = max(0, sc - clock_100ms) - end_clock = sc + clock_100ms data, cdata = dstepper.get_step_queue(start_clock, end_clock) dstepper.log_steps(data) - if shutdown_time >= NEVER_TIME: - return # Log trapqs around time of shutdown - for dtrapq in self.trapqs.values(): + for dtrapq in self.dtrapqs.values(): data, cdata = dtrapq.extract_trapq(shutdown_time - .100, shutdown_time + .100) dtrapq.log_trapq(data) # Log estimated toolhead position at time of shutdown - dtrapq = self.trapqs.get('toolhead') + dtrapq = self.dtrapqs.get('toolhead') if dtrapq is None: return pos, velocity = dtrapq.get_trapq_position(shutdown_time) @@ -212,7 +213,7 @@ class PrinterMotionReport: , shutdown_time, pos) # Status reporting def get_status(self, eventtime): - if eventtime < self.next_status_time or not self.trapqs: + if eventtime < self.next_status_time or not self.dtrapqs: return self.last_status self.next_status_time = eventtime + STATUS_REFRESH_TIME xyzpos = (0., 0., 0.) @@ -221,13 +222,13 @@ class PrinterMotionReport: # Calculate current requested toolhead position mcu = self.printer.lookup_object('mcu') print_time = mcu.estimated_print_time(eventtime) - pos, velocity = self.trapqs['toolhead'].get_trapq_position(print_time) + pos, velocity = self.dtrapqs['toolhead'].get_trapq_position(print_time) if pos is not None: xyzpos = pos[:3] xyzvelocity = velocity # Calculate requested position of currently active extruder toolhead = self.printer.lookup_object('toolhead') - ehandler = self.trapqs.get(toolhead.get_extruder().get_name()) + ehandler = self.dtrapqs.get(toolhead.get_extruder().get_name()) if ehandler is not None: pos, velocity = ehandler.get_trapq_position(print_time) if pos is not None: diff --git a/klippy/mcu.py b/klippy/mcu.py index 97262dd50..d336cfa40 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -697,7 +697,6 @@ class MCUConnectHelper: # Shutdown tracking self._emergency_stop_cmd = None self._is_shutdown = self._is_timeout = False - self._shutdown_clock = 0 self._shutdown_msg = "" # Register handlers printer.register_event_handler("klippy:mcu_identify", @@ -720,14 +719,15 @@ class MCUConnectHelper: if self._is_shutdown: return self._is_shutdown = True - clock = params.get("clock") - if clock is not None: - self._shutdown_clock = self._mcu.clock32_to_clock64(clock) self._shutdown_msg = msg = params['static_string_id'] + shutdown_clock = params.get("clock") + if shutdown_clock is not None: + shutdown_clock = self._mcu.clock32_to_clock64(shutdown_clock) event_type = params['#name'] self._printer.invoke_async_shutdown( "MCU shutdown", {"reason": msg, "mcu": self._name, - "event_type": event_type}) + "event_type": event_type, + "shutdown_clock": shutdown_clock}) def _handle_starting(self, params): if not self._is_shutdown: self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart" @@ -810,8 +810,6 @@ class MCUConnectHelper: self._name,)) def is_shutdown(self): return self._is_shutdown - def get_shutdown_clock(self): - return self._shutdown_clock def get_shutdown_msg(self): return self._shutdown_msg @@ -1112,8 +1110,6 @@ class MCU: # Low-level connection wrappers def is_shutdown(self): return self._conn_helper.is_shutdown() - def get_shutdown_clock(self): - return self._conn_helper.get_shutdown_clock() # Statistics wrappers def get_status(self, eventtime=None): return self._stats_helper.get_status(eventtime) From e96a944f3388690f91a074549a99f31bfc610563 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 8 Oct 2025 16:24:27 -0400 Subject: [PATCH 208/411] mcu: It is not necessary to export an is_shutdown() method Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index d336cfa40..7798c7517 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -1107,9 +1107,6 @@ class MCU: offset, freq = self._clocksync.calibrate_clock(print_time, eventtime) self._conn_helper.check_timeout(eventtime) return offset, freq - # Low-level connection wrappers - def is_shutdown(self): - return self._conn_helper.is_shutdown() # Statistics wrappers def get_status(self, eventtime=None): return self._stats_helper.get_status(eventtime) From d1c0cbd63ab240ca0120434106b66dac91a03079 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 6 Oct 2025 21:32:59 -0400 Subject: [PATCH 209/411] reactor: Add support for temporarily disabling reactor pauses Add a new reactor.assert_no_pause() helper that can temporarily disable reactor pause requests. Signed-off-by: Kevin O'Connor --- klippy/reactor.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/klippy/reactor.py b/klippy/reactor.py index db6a089e3..03742b149 100644 --- a/klippy/reactor.py +++ b/klippy/reactor.py @@ -10,6 +10,9 @@ import chelper, util _NOW = 0. _NEVER = 9999999999999999. +class ReactorError(Exception): + pass + class ReactorTimer: def __init__(self, callback, waketime): self.callback = callback @@ -90,6 +93,14 @@ class ReactorMutex: self.next_pending = True self.reactor.update_timer(self.queue[0].timer, self.reactor.NOW) +class ReactorPreventPause: + def __init__(self, reactor): + self.reactor = reactor + def __enter__(self): + self.reactor._prevent_pause_count += 1 + def __exit__(self, type=None, value=None, tb=None): + self.reactor._prevent_pause_count -= 1 + class SelectReactor: NOW = _NOW NEVER = _NEVER @@ -118,6 +129,7 @@ class SelectReactor: self._g_dispatch = None self._greenlets = [] self._all_greenlets = [] + self._prevent_pause_count = 0 def get_gc_stats(self): return tuple(self._last_gc_times) # Timers @@ -219,8 +231,12 @@ class SelectReactor: if self._g_dispatch is None: return self._sys_pause(waketime) # Switch to _check_timers (via g.timer.callback return) + if self._prevent_pause_count: + self.verify_can_pause() return self._g_dispatch.switch(waketime) # Pausing the dispatch greenlet - prepare a new greenlet to do dispatch + if self._prevent_pause_count: + self.verify_can_pause() if self._greenlets: g_next = self._greenlets.pop() else: @@ -242,6 +258,12 @@ class SelectReactor: self._g_dispatch.switch(self.NEVER) # This greenlet reactivated from pause() - return to main dispatch loop self._g_dispatch = g_old + # Support for temporarily disabling pauses + def assert_no_pause(self): + return ReactorPreventPause(self) + def verify_can_pause(self): + if self._prevent_pause_count: + raise ReactorError("Internal error - reactor pause disabled") # Mutexes def mutex(self, is_locked=False): return ReactorMutex(self, is_locked) @@ -301,6 +323,7 @@ class SelectReactor: if self._pipe_fds is None: self._setup_async_callbacks() self._process = True + self._prevent_pause_count = 0 g_next = ReactorGreenlet(run=self._dispatch_loop) self._all_greenlets.append(g_next) g_next.switch() From 0e7e908af470d77a4a4284ae530f703cdd8f0825 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 6 Oct 2025 21:36:00 -0400 Subject: [PATCH 210/411] klippy: Verify nothing attempts to pause in the klippy:shutdown event Signed-off-by: Kevin O'Connor --- klippy/klippy.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/klippy/klippy.py b/klippy/klippy.py index 5ee26abe1..de24da2af 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -206,16 +206,17 @@ class Printer: logging.error("Transition to shutdown state: %s", msg) self.in_shutdown_state = True self._set_state(msg) - for cb in self.event_handlers.get("klippy:shutdown", []): - try: - cb() - except: - logging.exception("Exception during shutdown handler") - for cb in self.event_handlers.get("klippy:analyze_shutdown", []): - try: - cb(msg, details) - except: - logging.exception("Exception during analyze_shutdown handler") + with self.reactor.assert_no_pause(): + for cb in self.event_handlers.get("klippy:shutdown", []): + try: + cb() + except: + logging.exception("Exception during shutdown handler") + for cb in self.event_handlers.get("klippy:analyze_shutdown", []): + try: + cb(msg, details) + except: + logging.exception("Exception in analyze_shutdown handler") def invoke_async_shutdown(self, msg, details={}): self.reactor.register_async_callback( (lambda e: self.invoke_shutdown(msg, details))) From e549cc4143ac6fe23778d8e0fd5785d85ac066e1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 6 Oct 2025 21:48:44 -0400 Subject: [PATCH 211/411] load_cell: Only launch a callback in the klippy:ready event handler It is not valid to pause in the klippy:ready callback, so perform the sensor startup process in a separate reactor task. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py index 5ef2c5b77..f370cad90 100644 --- a/klippy/extras/load_cell.py +++ b/klippy/extras/load_cell.py @@ -383,7 +383,7 @@ class LoadCell: # startup, when klippy is ready, start capturing data printer.register_event_handler("klippy:ready", self._handle_ready) - def _handle_ready(self): + def _handle_do_ready(self, eventtime): self.sensor.add_client(self._sensor_data_event) self.add_client(self._track_force) # announce calibration status on ready @@ -391,6 +391,8 @@ class LoadCell: self.printer.send_event("load_cell:calibrate", self) if self.is_tared(): self.printer.send_event("load_cell:tare", self) + def _handle_ready(self): + self.printer.get_reactor().register_callback(self._handle_do_ready) # convert raw counts to grams and broadcast to clients def _sensor_data_event(self, msg): From 302df255db419eddcf65095fe4fa9fbbe8d6ec37 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 6 Oct 2025 21:37:31 -0400 Subject: [PATCH 212/411] klippy: Verify nothing attempts to pause in the klippy:ready event Signed-off-by: Kevin O'Connor --- klippy/klippy.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/klippy/klippy.py b/klippy/klippy.py index de24da2af..5f0ea4633 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -158,10 +158,11 @@ class Printer: return try: self._set_state(message_ready) - for cb in self.event_handlers.get("klippy:ready", []): - if self.state_message is not message_ready: - return - cb() + with self.reactor.assert_no_pause(): + for cb in self.event_handlers.get("klippy:ready", []): + if self.state_message is not message_ready: + return + cb() except Exception as e: logging.exception("Unhandled exception during ready callback") self.invoke_shutdown("Internal error during ready callback: %s" From 6557050968abf85f9a81ee1bfdf02109262bfce5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 6 Oct 2025 21:53:36 -0400 Subject: [PATCH 213/411] motion_queuing: Verify nothing attempts to pause in flush callbacks Signed-off-by: Kevin O'Connor --- klippy/extras/motion_queuing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/klippy/extras/motion_queuing.py b/klippy/extras/motion_queuing.py index e1a0d7fcc..c5fa73628 100644 --- a/klippy/extras/motion_queuing.py +++ b/klippy/extras/motion_queuing.py @@ -141,8 +141,9 @@ class PrinterMotionQueuing: step_gen_time = max(want_step_gen_time, self.last_step_gen_time, flush_time) # Invoke flush callbacks (if any) - for cb in self.flush_callbacks: - cb(flush_time, step_gen_time) + with self.reactor.assert_no_pause(): + for cb in self.flush_callbacks: + cb(flush_time, step_gen_time) # Determine maximum history to keep trapq_free_time = step_gen_time - self.kin_flush_delay clear_history_time = self.clear_history_time From 7a036a6ba7e0ddd09147a8d4804aa238c639af78 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 6 Oct 2025 21:55:11 -0400 Subject: [PATCH 214/411] toolhead: Verify nothing attempts to pause in lookahead callbacks Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index d7ce66b5c..8f16e75b6 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -276,21 +276,22 @@ class ToolHead: self._calc_print_time() # Queue moves into trapezoid motion queue (trapq) next_move_time = self.print_time - for move in moves: - if move.is_kinematic_move: - self.trapq_append( - self.trapq, next_move_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) - for e_index, ea in enumerate(self.extra_axes): - if move.axes_d[e_index + 3]: - ea.process_move(next_move_time, move, e_index + 3) - next_move_time = (next_move_time + move.accel_t - + move.cruise_t + move.decel_t) - for cb in move.timing_callbacks: - cb(next_move_time) + with self.reactor.assert_no_pause(): + for move in moves: + if move.is_kinematic_move: + self.trapq_append( + self.trapq, next_move_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) + for e_index, ea in enumerate(self.extra_axes): + if move.axes_d[e_index + 3]: + ea.process_move(next_move_time, move, e_index + 3) + next_move_time = (next_move_time + move.accel_t + + move.cruise_t + move.decel_t) + for cb in move.timing_callbacks: + cb(next_move_time) # Generate steps for moves self._advance_move_time(next_move_time) self.motion_queuing.note_mcu_movequeue_activity(next_move_time) From d7da45e152995e57123983e05f5faa9974e5df7a Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Mon, 22 Sep 2025 18:19:55 +0200 Subject: [PATCH 215/411] serialqueue: decouple pending & ready queues Simply describe how the cmdqueue is moved between states. Signed-off-by: Timofey Titovets --- klippy/chelper/serialqueue.c | 107 +++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 44 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 914d4c395..724064bae 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -29,11 +29,15 @@ #include "pyhelper.h" // get_monotonic #include "serialqueue.h" // struct queue_message -struct command_queue { - struct list_head upcoming_queue, ready_queue; +struct message_sub_queue { + struct list_head msg_queue; struct list_node node; }; +struct command_queue { + struct message_sub_queue ready, upcoming; +}; + struct serialqueue { // Input reading struct pollreactor *pr; @@ -59,8 +63,10 @@ struct serialqueue { struct list_head sent_queue; double srtt, rttvar, rto; // Pending transmission message queues - struct list_head pending_queues; - int ready_bytes, upcoming_bytes, need_ack_bytes, last_ack_bytes; + struct list_head upcoming_queues; + int upcoming_bytes; + struct list_head ready_queues; + int ready_bytes, need_ack_bytes, last_ack_bytes; uint64_t need_kick_clock; struct list_head notify_queue; double last_write_fail_time; @@ -452,23 +458,21 @@ build_and_send_command(struct serialqueue *sq, uint8_t *buf, int pending uint64_t min_clock = MAX_CLOCK; struct command_queue *q, *cq = NULL; struct queue_message *qm = NULL; - list_for_each_entry(q, &sq->pending_queues, node) { - if (!list_empty(&q->ready_queue)) { - struct queue_message *m = list_first_entry( - &q->ready_queue, struct queue_message, node); - if (m->req_clock < min_clock) { - min_clock = m->req_clock; - cq = q; - qm = m; - } + list_for_each_entry(q, &sq->ready_queues, ready.node) { + struct queue_message *m = list_first_entry( + &q->ready.msg_queue, struct queue_message, node); + if (m->req_clock < min_clock) { + min_clock = m->req_clock; + cq = q; + qm = m; } } // Append message to outgoing command if (len + qm->len > MESSAGE_MAX - MESSAGE_TRAILER_SIZE) break; list_del(&qm->node); - if (list_empty(&cq->ready_queue) && list_empty(&cq->upcoming_queue)) - list_del(&cq->node); + if (list_empty(&cq->ready.msg_queue)) + list_del(&cq->ready.node); memcpy(&buf[len], qm->msg, qm->len); len += qm->len; sq->ready_bytes -= qm->len; @@ -530,34 +534,42 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) idletime += calculate_bittime(sq, pending + MESSAGE_MIN); uint64_t ack_clock = clock_from_time(&sq->ce, idletime); uint64_t min_stalled_clock = MAX_CLOCK, min_ready_clock = MAX_CLOCK; - struct command_queue *cq; - list_for_each_entry(cq, &sq->pending_queues, node) { - // Move messages from the upcoming_queue to the ready_queue - while (!list_empty(&cq->upcoming_queue)) { + struct command_queue *cq, *_ncq; + list_for_each_entry_safe(cq, _ncq, &sq->upcoming_queues, upcoming.node) { + int not_in_ready_queues = list_empty(&cq->ready.msg_queue); + // Move messages from the upcoming.msg_queue to the ready.msg_queue + while (!list_empty(&cq->upcoming.msg_queue)) { struct queue_message *qm = list_first_entry( - &cq->upcoming_queue, struct queue_message, node); + &cq->upcoming.msg_queue, struct queue_message, node); if (ack_clock < qm->min_clock) { if (qm->min_clock < min_stalled_clock) min_stalled_clock = qm->min_clock; break; } list_del(&qm->node); - list_add_tail(&qm->node, &cq->ready_queue); + list_add_tail(&qm->node, &cq->ready.msg_queue); sq->upcoming_bytes -= qm->len; sq->ready_bytes += qm->len; } + // Remove cq from the list if it is now empty + if (list_empty(&cq->upcoming.msg_queue)) + list_del(&cq->upcoming.node); + // Add to ready queues + if (not_in_ready_queues && !list_empty(&cq->ready.msg_queue)) + list_add_tail(&cq->ready.node, &sq->ready_queues); + } + // Check if it is still needed to send messages from the ready_queues + list_for_each_entry(cq, &sq->ready_queues, ready.node) { // Update min_ready_clock - if (!list_empty(&cq->ready_queue)) { - struct queue_message *qm = list_first_entry( - &cq->ready_queue, struct queue_message, node); - uint64_t req_clock = qm->req_clock; - double bgtime = pending ? idletime : sq->idle_time; - double bgoffset = MIN_REQTIME_DELTA + MIN_BACKGROUND_DELTA; - if (req_clock == BACKGROUND_PRIORITY_CLOCK) - req_clock = clock_from_time(&sq->ce, bgtime + bgoffset); - if (req_clock < min_ready_clock) - min_ready_clock = req_clock; - } + struct queue_message *qm = list_first_entry( + &cq->ready.msg_queue, struct queue_message, node); + uint64_t req_clock = qm->req_clock; + double bgtime = pending ? idletime : sq->idle_time; + double bgoffset = MIN_REQTIME_DELTA + MIN_BACKGROUND_DELTA; + if (req_clock == BACKGROUND_PRIORITY_CLOCK) + req_clock = clock_from_time(&sq->ce, bgtime + bgoffset); + if (req_clock < min_ready_clock) + min_ready_clock = req_clock; } // Check for messages to send @@ -664,7 +676,8 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id // Queues sq->need_kick_clock = MAX_CLOCK; - list_init(&sq->pending_queues); + list_init(&sq->upcoming_queues); + list_init(&sq->ready_queues); list_init(&sq->sent_queue); list_init(&sq->receive_queue); list_init(&sq->notify_queue); @@ -722,12 +735,17 @@ serialqueue_free(struct serialqueue *sq) message_queue_free(&sq->notify_queue); message_queue_free(&sq->old_sent); message_queue_free(&sq->old_receive); - while (!list_empty(&sq->pending_queues)) { + while (!list_empty(&sq->ready_queues)) { + struct command_queue* cq = list_first_entry( + &sq->ready_queues, struct command_queue, ready.node); + list_del(&cq->ready.node); + message_queue_free(&cq->ready.msg_queue); + } + while (!list_empty(&sq->upcoming_queues)) { struct command_queue *cq = list_first_entry( - &sq->pending_queues, struct command_queue, node); - list_del(&cq->node); - message_queue_free(&cq->ready_queue); - message_queue_free(&cq->upcoming_queue); + &sq->upcoming_queues, struct command_queue, upcoming.node); + list_del(&cq->upcoming.node); + message_queue_free(&cq->upcoming.msg_queue); } pthread_mutex_unlock(&sq->lock); pollreactor_free(sq->pr); @@ -740,8 +758,8 @@ serialqueue_alloc_commandqueue(void) { struct command_queue *cq = malloc(sizeof(*cq)); memset(cq, 0, sizeof(*cq)); - list_init(&cq->ready_queue); - list_init(&cq->upcoming_queue); + list_init(&cq->ready.msg_queue); + list_init(&cq->upcoming.msg_queue); return cq; } @@ -751,7 +769,8 @@ serialqueue_free_commandqueue(struct command_queue *cq) { if (!cq) return; - if (!list_empty(&cq->ready_queue) || !list_empty(&cq->upcoming_queue)) { + if (!list_empty(&cq->ready.msg_queue) || + !list_empty(&cq->upcoming.msg_queue)) { errorf("Memory leak! Can't free non-empty commandqueue"); return; } @@ -799,9 +818,9 @@ serialqueue_send_batch(struct serialqueue *sq, struct command_queue *cq // Add list to cq->upcoming_queue pthread_mutex_lock(&sq->lock); - if (list_empty(&cq->ready_queue) && list_empty(&cq->upcoming_queue)) - list_add_tail(&cq->node, &sq->pending_queues); - list_join_tail(msgs, &cq->upcoming_queue); + if (list_empty(&cq->upcoming.msg_queue)) + list_add_tail(&cq->upcoming.node, &sq->upcoming_queues); + list_join_tail(msgs, &cq->upcoming.msg_queue); sq->upcoming_bytes += len; int mustwake = 0; if (qm->min_clock < sq->need_kick_clock) { From 493271697f2aff1b54bdae62c27588e2db614161 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 19 Sep 2025 01:28:14 +0200 Subject: [PATCH 216/411] serialqueue: decouple serialhdl receive lock Signed-off-by: Timofey Titovets --- klippy/chelper/serialqueue.c | 121 +++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 47 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 724064bae..0403b7a80 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -38,6 +38,14 @@ struct command_queue { struct message_sub_queue ready, upcoming; }; +struct receiver { + pthread_mutex_t lock; + pthread_cond_t cond; + int waiting; + struct list_head queue; + struct list_head old_receive; +}; + struct serialqueue { // Input reading struct pollreactor *pr; @@ -49,9 +57,9 @@ struct serialqueue { // Threading char name[16]; pthread_t tid; + // SerialHDL reader + struct receiver receiver; pthread_mutex_t lock; // protects variables below - pthread_cond_t cond; - int receive_waiting; // Baud / clock tracking int receive_window; double bittime_adjust, idle_time; @@ -70,13 +78,11 @@ struct serialqueue { uint64_t need_kick_clock; struct list_head notify_queue; double last_write_fail_time; - // Received messages - struct list_head receive_queue; // Fastreader support pthread_mutex_t fast_reader_dispatch_lock; struct list_head fast_readers; // Debugging - struct list_head old_sent, old_receive; + struct list_head old_sent; // Stats uint32_t bytes_write, bytes_read, bytes_retransmit, bytes_invalid; }; @@ -115,23 +121,30 @@ debug_queue_alloc(struct list_head *root, int count) } // Copy a message to a debug queue and free old debug messages -static void -debug_queue_add(struct list_head *root, struct queue_message *qm) +static struct queue_message * +_debug_queue_add(struct list_head *root, struct queue_message *qm) { list_add_tail(&qm->node, root); struct queue_message *old = list_first_entry( root, struct queue_message, node); list_del(&old->node); + return old; +} + +static void +debug_queue_add(struct list_head *root, struct queue_message *qm) +{ + struct queue_message *old = _debug_queue_add(root, qm); message_free(old); } // Wake up the receiver thread if it is waiting static void -check_wake_receive(struct serialqueue *sq) +check_wake_receive(struct receiver *receiver) { - if (sq->receive_waiting) { - sq->receive_waiting = 0; - pthread_cond_signal(&sq->cond); + if (receiver->waiting) { + receiver->waiting = 0; + pthread_cond_signal(&receiver->cond); } } @@ -245,7 +258,8 @@ handle_message(struct serialqueue *sq, double eventtime, int len) sq->bytes_read += len; // Check for pending messages on notify_queue - int must_wake = 0; + struct list_head received; + list_init(&received); while (!list_empty(&sq->notify_queue)) { struct queue_message *qm = list_first_entry( &sq->notify_queue, struct queue_message, node); @@ -257,8 +271,7 @@ handle_message(struct serialqueue *sq, double eventtime, int len) qm->len = 0; qm->sent_time = sq->last_receive_sent_time; qm->receive_time = eventtime; - list_add_tail(&qm->node, &sq->receive_queue); - must_wake = 1; + list_add_tail(&qm->node, &received); } // Process message @@ -276,8 +289,14 @@ handle_message(struct serialqueue *sq, double eventtime, int len) ? sq->last_receive_sent_time : 0.); qm->receive_time = get_monotonic(); // must be time post read() qm->receive_time -= calculate_bittime(sq, len); - list_add_tail(&qm->node, &sq->receive_queue); - must_wake = 1; + list_add_tail(&qm->node, &received); + } + + if (!list_empty(&received)) { + pthread_mutex_lock(&sq->receiver.lock); + list_join_tail(&received, &sq->receiver.queue); + check_wake_receive(&sq->receiver); + pthread_mutex_unlock(&sq->receiver.lock); } // Check fast readers @@ -289,16 +308,11 @@ handle_message(struct serialqueue *sq, double eventtime, int len) continue; // Release main lock and invoke callback pthread_mutex_lock(&sq->fast_reader_dispatch_lock); - if (must_wake) - check_wake_receive(sq); pthread_mutex_unlock(&sq->lock); fr->func(fr, sq->input_buf, len); pthread_mutex_unlock(&sq->fast_reader_dispatch_lock); return; } - - if (must_wake) - check_wake_receive(sq); pthread_mutex_unlock(&sq->lock); } @@ -628,9 +642,9 @@ background_thread(void *data) set_thread_name(sq->name); pollreactor_run(sq->pr); - pthread_mutex_lock(&sq->lock); - check_wake_receive(sq); - pthread_mutex_unlock(&sq->lock); + pthread_mutex_lock(&sq->receiver.lock); + check_wake_receive(&sq->receiver); + pthread_mutex_unlock(&sq->receiver.lock); return NULL; } @@ -679,21 +693,24 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id list_init(&sq->upcoming_queues); list_init(&sq->ready_queues); list_init(&sq->sent_queue); - list_init(&sq->receive_queue); + list_init(&sq->receiver.queue); list_init(&sq->notify_queue); list_init(&sq->fast_readers); // Debugging list_init(&sq->old_sent); - list_init(&sq->old_receive); + list_init(&sq->receiver.old_receive); debug_queue_alloc(&sq->old_sent, DEBUG_QUEUE_SENT); - debug_queue_alloc(&sq->old_receive, DEBUG_QUEUE_RECEIVE); + debug_queue_alloc(&sq->receiver.old_receive, DEBUG_QUEUE_RECEIVE); // Thread setup ret = pthread_mutex_init(&sq->lock, NULL); if (ret) goto fail; - ret = pthread_cond_init(&sq->cond, NULL); + ret = pthread_mutex_init(&sq->receiver.lock, NULL); + if (ret) + goto fail; + ret = pthread_cond_init(&sq->receiver.cond, NULL); if (ret) goto fail; ret = pthread_mutex_init(&sq->fast_reader_dispatch_lock, NULL); @@ -731,10 +748,12 @@ serialqueue_free(struct serialqueue *sq) serialqueue_exit(sq); pthread_mutex_lock(&sq->lock); message_queue_free(&sq->sent_queue); - message_queue_free(&sq->receive_queue); + pthread_mutex_lock(&sq->receiver.lock); + message_queue_free(&sq->receiver.queue); + message_queue_free(&sq->receiver.old_receive); + pthread_mutex_unlock(&sq->receiver.lock); message_queue_free(&sq->notify_queue); message_queue_free(&sq->old_sent); - message_queue_free(&sq->old_receive); while (!list_empty(&sq->ready_queues)) { struct command_queue* cq = list_first_entry( &sq->ready_queues, struct command_queue, ready.node); @@ -864,20 +883,21 @@ serialqueue_send(struct serialqueue *sq, struct command_queue *cq, uint8_t *msg void __visible serialqueue_pull(struct serialqueue *sq, struct pull_queue_message *pqm) { - pthread_mutex_lock(&sq->lock); + struct receiver *receiver = &sq->receiver; + pthread_mutex_lock(&receiver->lock); // Wait for message to be available - while (list_empty(&sq->receive_queue)) { + while (list_empty(&receiver->queue)) { if (pollreactor_is_exit(sq->pr)) goto exit; - sq->receive_waiting = 1; - int ret = pthread_cond_wait(&sq->cond, &sq->lock); + receiver->waiting = 1; + int ret = pthread_cond_wait(&receiver->cond, &receiver->lock); if (ret) report_errno("pthread_cond_wait", ret); } // Remove message from queue struct queue_message *qm = list_first_entry( - &sq->receive_queue, struct queue_message, node); + &receiver->queue, struct queue_message, node); list_del(&qm->node); // Copy message @@ -887,16 +907,14 @@ serialqueue_pull(struct serialqueue *sq, struct pull_queue_message *pqm) pqm->receive_time = qm->receive_time; pqm->notify_id = qm->notify_id; if (qm->len) - debug_queue_add(&sq->old_receive, qm); - else - message_free(qm); - - pthread_mutex_unlock(&sq->lock); + qm = _debug_queue_add(&receiver->old_receive, qm); + pthread_mutex_unlock(&receiver->lock); + message_free(qm); return; exit: pqm->len = -1; - pthread_mutex_unlock(&sq->lock); + pthread_mutex_unlock(&receiver->lock); } void __visible @@ -969,18 +987,27 @@ serialqueue_extract_old(struct serialqueue *sq, int sentq , struct pull_queue_message *q, int max) { int count = sentq ? DEBUG_QUEUE_SENT : DEBUG_QUEUE_RECEIVE; - struct list_head *rootp = sentq ? &sq->old_sent : &sq->old_receive; + struct list_head *rootp; + rootp = sentq ? &sq->old_sent : &sq->receiver.old_receive; struct list_head replacement, current; list_init(&replacement); debug_queue_alloc(&replacement, count); list_init(¤t); // Atomically replace existing debug list with new zero'd list - pthread_mutex_lock(&sq->lock); - list_join_tail(rootp, ¤t); - list_init(rootp); - list_join_tail(&replacement, rootp); - pthread_mutex_unlock(&sq->lock); + if (rootp == &sq->receiver.old_receive) { + pthread_mutex_lock(&sq->receiver.lock); + list_join_tail(rootp, ¤t); + list_init(rootp); + list_join_tail(&replacement, rootp); + pthread_mutex_unlock(&sq->receiver.lock); + } else { + pthread_mutex_lock(&sq->lock); + list_join_tail(rootp, ¤t); + list_init(rootp); + list_join_tail(&replacement, rootp); + pthread_mutex_unlock(&sq->lock); + } // Walk the debug list int pos = 0; From aea8d8e0a1f3a8655eab3ff0c51d5d7c7950f553 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 20 Sep 2025 02:42:40 +0200 Subject: [PATCH 217/411] serialqueue: decouple transmit requests Signed-off-by: Timofey Titovets --- klippy/chelper/serialqueue.c | 77 +++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 0403b7a80..b6d274a91 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -46,11 +46,18 @@ struct receiver { struct list_head old_receive; }; +struct transmit_requests { + int pipe_fds[2]; + pthread_mutex_t lock; // protects variables below + struct list_head upcoming_queues; + int upcoming_bytes; + uint64_t need_kick_clock; +}; + struct serialqueue { // Input reading struct pollreactor *pr; int serial_fd, serial_fd_type, client_id; - int pipe_fds[2]; uint8_t input_buf[4096]; uint8_t need_sync; int input_pos; @@ -71,11 +78,9 @@ struct serialqueue { struct list_head sent_queue; double srtt, rttvar, rto; // Pending transmission message queues - struct list_head upcoming_queues; - int upcoming_bytes; + struct transmit_requests transmit_requests; struct list_head ready_queues; int ready_bytes, need_ack_bytes, last_ack_bytes; - uint64_t need_kick_clock; struct list_head notify_queue; double last_write_fail_time; // Fastreader support @@ -152,7 +157,7 @@ check_wake_receive(struct receiver *receiver) static void kick_bg_thread(struct serialqueue *sq) { - int ret = write(sq->pipe_fds[1], ".", 1); + int ret = write(sq->transmit_requests.pipe_fds[1], ".", 1); if (ret < 0) report_errno("pipe write", ret); } @@ -371,7 +376,7 @@ static void kick_event(struct serialqueue *sq, double eventtime) { char dummy[4096]; - int ret = read(sq->pipe_fds[0], dummy, sizeof(dummy)); + int ret = read(sq->transmit_requests.pipe_fds[0], dummy, sizeof(dummy)); if (ret < 0) report_errno("pipe read", ret); pollreactor_update_timer(sq->pr, SQPT_COMMAND, PR_NOW); @@ -549,7 +554,9 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) uint64_t ack_clock = clock_from_time(&sq->ce, idletime); uint64_t min_stalled_clock = MAX_CLOCK, min_ready_clock = MAX_CLOCK; struct command_queue *cq, *_ncq; - list_for_each_entry_safe(cq, _ncq, &sq->upcoming_queues, upcoming.node) { + pthread_mutex_lock(&sq->transmit_requests.lock); + list_for_each_entry_safe(cq, _ncq, &sq->transmit_requests.upcoming_queues, + upcoming.node) { int not_in_ready_queues = list_empty(&cq->ready.msg_queue); // Move messages from the upcoming.msg_queue to the ready.msg_queue while (!list_empty(&cq->upcoming.msg_queue)) { @@ -562,7 +569,7 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) } list_del(&qm->node); list_add_tail(&qm->node, &cq->ready.msg_queue); - sq->upcoming_bytes -= qm->len; + sq->transmit_requests.upcoming_bytes -= qm->len; sq->ready_bytes += qm->len; } // Remove cq from the list if it is now empty @@ -588,21 +595,26 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) // Check for messages to send if (sq->ready_bytes >= MESSAGE_PAYLOAD_MAX) - return PR_NOW; + goto now; if (! sq->ce.est_freq) { if (sq->ready_bytes) - return PR_NOW; - sq->need_kick_clock = MAX_CLOCK; + goto now; + sq->transmit_requests.need_kick_clock = MAX_CLOCK; + pthread_mutex_unlock(&sq->transmit_requests.lock); return PR_NEVER; } uint64_t reqclock_delta = MIN_REQTIME_DELTA * sq->ce.est_freq; if (min_ready_clock <= ack_clock + reqclock_delta) - return PR_NOW; + goto now; uint64_t wantclock = min_ready_clock - reqclock_delta; if (min_stalled_clock < wantclock) wantclock = min_stalled_clock; - sq->need_kick_clock = wantclock; + sq->transmit_requests.need_kick_clock = wantclock; + pthread_mutex_unlock(&sq->transmit_requests.lock); return idletime + (wantclock - ack_clock) / sq->ce.est_freq; +now: + pthread_mutex_unlock(&sq->transmit_requests.lock); + return PR_NOW; } // Callback timer to send data to the serial port @@ -662,7 +674,7 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id strncpy(sq->name, name, sizeof(sq->name)); sq->name[sizeof(sq->name)-1] = '\0'; - int ret = pipe(sq->pipe_fds); + int ret = pipe(sq->transmit_requests.pipe_fds); if (ret) goto fail; @@ -670,12 +682,13 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id sq->pr = pollreactor_alloc(SQPF_NUM, SQPT_NUM, sq); pollreactor_add_fd(sq->pr, SQPF_SERIAL, serial_fd, input_event , serial_fd_type==SQT_DEBUGFILE); - pollreactor_add_fd(sq->pr, SQPF_PIPE, sq->pipe_fds[0], kick_event, 0); + pollreactor_add_fd(sq->pr, SQPF_PIPE, sq->transmit_requests.pipe_fds[0] + , kick_event, 0); pollreactor_add_timer(sq->pr, SQPT_RETRANSMIT, retransmit_event); pollreactor_add_timer(sq->pr, SQPT_COMMAND, command_event); fd_set_non_blocking(serial_fd); - fd_set_non_blocking(sq->pipe_fds[0]); - fd_set_non_blocking(sq->pipe_fds[1]); + fd_set_non_blocking(sq->transmit_requests.pipe_fds[0]); + fd_set_non_blocking(sq->transmit_requests.pipe_fds[1]); // Retransmit setup sq->send_seq = 1; @@ -689,8 +702,9 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id } // Queues - sq->need_kick_clock = MAX_CLOCK; - list_init(&sq->upcoming_queues); + sq->transmit_requests.need_kick_clock = MAX_CLOCK; + list_init(&sq->transmit_requests.upcoming_queues); + pthread_mutex_init(&sq->transmit_requests.lock, NULL); list_init(&sq->ready_queues); list_init(&sq->sent_queue); list_init(&sq->receiver.queue); @@ -760,12 +774,15 @@ serialqueue_free(struct serialqueue *sq) list_del(&cq->ready.node); message_queue_free(&cq->ready.msg_queue); } - while (!list_empty(&sq->upcoming_queues)) { + pthread_mutex_lock(&sq->transmit_requests.lock); + while (!list_empty(&sq->transmit_requests.upcoming_queues)) { struct command_queue *cq = list_first_entry( - &sq->upcoming_queues, struct command_queue, upcoming.node); + &sq->transmit_requests.upcoming_queues, + struct command_queue, upcoming.node); list_del(&cq->upcoming.node); message_queue_free(&cq->upcoming.msg_queue); } + pthread_mutex_unlock(&sq->transmit_requests.lock); pthread_mutex_unlock(&sq->lock); pollreactor_free(sq->pr); free(sq); @@ -836,17 +853,19 @@ serialqueue_send_batch(struct serialqueue *sq, struct command_queue *cq qm = list_first_entry(msgs, struct queue_message, node); // Add list to cq->upcoming_queue - pthread_mutex_lock(&sq->lock); + pthread_mutex_lock(&sq->transmit_requests.lock); if (list_empty(&cq->upcoming.msg_queue)) - list_add_tail(&cq->upcoming.node, &sq->upcoming_queues); + list_add_tail(&cq->upcoming.node, + &sq->transmit_requests.upcoming_queues); list_join_tail(msgs, &cq->upcoming.msg_queue); - sq->upcoming_bytes += len; + sq->transmit_requests.upcoming_bytes += len; + int mustwake = 0; - if (qm->min_clock < sq->need_kick_clock) { - sq->need_kick_clock = 0; + if (qm->min_clock < sq->transmit_requests.need_kick_clock) { + sq->transmit_requests.need_kick_clock = 0; mustwake = 1; } - pthread_mutex_unlock(&sq->lock); + pthread_mutex_unlock(&sq->transmit_requests.lock); // Wake the background thread if necessary if (mustwake) @@ -965,7 +984,9 @@ serialqueue_get_stats(struct serialqueue *sq, char *buf, int len) { struct serialqueue stats; pthread_mutex_lock(&sq->lock); + pthread_mutex_lock(&sq->transmit_requests.lock); memcpy(&stats, sq, sizeof(stats)); + pthread_mutex_unlock(&sq->transmit_requests.lock); pthread_mutex_unlock(&sq->lock); snprintf(buf, len, "bytes_write=%u bytes_read=%u" @@ -978,7 +999,7 @@ serialqueue_get_stats(struct serialqueue *sq, char *buf, int len) , (int)stats.send_seq, (int)stats.receive_seq , (int)stats.retransmit_seq , stats.srtt, stats.rttvar, stats.rto - , stats.ready_bytes, stats.upcoming_bytes); + , stats.ready_bytes, stats.transmit_requests.upcoming_bytes); } // Extract old messages stored in the debug queues From 6a8b823a4534f5a205f2ae6f0ca8a9a014b5da69 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 11 Oct 2025 15:10:13 +0200 Subject: [PATCH 218/411] led: run update function as reactor callback Update functions could be called within the flush/lookahead context If the update function internally does pause() That would lead to the unpredictable execution of time-critical functions Signed-off-by: Timofey Titovets --- klippy/extras/led.py | 14 ++++++++++---- klippy/extras/neopixel.py | 8 ++------ klippy/extras/pca9533.py | 2 +- klippy/extras/pca9632.py | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/klippy/extras/led.py b/klippy/extras/led.py index 37b58de36..4dc71f3cc 100644 --- a/klippy/extras/led.py +++ b/klippy/extras/led.py @@ -10,6 +10,7 @@ from . import output_pin class LEDHelper: def __init__(self, config, update_func, led_count=1): self.printer = config.get_printer() + self.mutex = self.printer.get_reactor().mutex() self.update_func = update_func self.led_count = led_count self.need_transmit = False @@ -59,11 +60,16 @@ class LEDHelper: def _check_transmit(self, print_time=None): if not self.need_transmit: return + # Just avoid any race conditions + led_state = self.led_state self.need_transmit = False - try: - self.update_func(self.led_state, print_time) - except self.printer.command_error as e: - logging.exception("led update transmit error") + def reactor_cb(eventtime): + try: + with self.mutex: + self.update_func(led_state, print_time) + except self.printer.command_error as e: + logging.exception("led update transmit error") + self.printer.get_reactor().register_callback(reactor_cb) cmd_SET_LED_help = "Set the color of an LED" def cmd_SET_LED(self, gcmd): # Parse parameters diff --git a/klippy/extras/neopixel.py b/klippy/extras/neopixel.py index e72b8a91a..b160fc2f4 100644 --- a/klippy/extras/neopixel.py +++ b/klippy/extras/neopixel.py @@ -16,7 +16,6 @@ MAX_MCU_SIZE = 500 # Sanity check on LED chain length class PrinterNeoPixel: def __init__(self, config): self.printer = printer = config.get_printer() - self.mutex = printer.get_reactor().mutex() # Configure neopixel ppins = printer.lookup_object('pins') pin_params = ppins.lookup_pin(config.get('pin')) @@ -99,11 +98,8 @@ class PrinterNeoPixel: else: logging.info("Neopixel update did not succeed") def update_leds(self, led_state, print_time): - def reactor_bgfunc(eventtime): - with self.mutex: - self.update_color_data(led_state) - self.send_data(print_time) - self.printer.get_reactor().register_callback(reactor_bgfunc) + self.update_color_data(led_state) + self.send_data(print_time) def get_status(self, eventtime=None): return self.led_helper.get_status(eventtime) diff --git a/klippy/extras/pca9533.py b/klippy/extras/pca9533.py index 20f1a6ca9..8f06e1c1a 100644 --- a/klippy/extras/pca9533.py +++ b/klippy/extras/pca9533.py @@ -27,7 +27,7 @@ class PCA9533: minclock = 0 if print_time is not None: minclock = self.i2c.get_mcu().print_time_to_clock(print_time) - self.i2c.i2c_write_noack([PCA9533_PLS0, ls0], minclock=minclock, + self.i2c.i2c_write([PCA9533_PLS0, ls0], minclock=minclock, reqclock=BACKGROUND_PRIORITY_CLOCK) def get_status(self, eventtime): return self.led_helper.get_status(eventtime) diff --git a/klippy/extras/pca9632.py b/klippy/extras/pca9632.py index 099676e0f..af7c241b1 100644 --- a/klippy/extras/pca9632.py +++ b/klippy/extras/pca9632.py @@ -37,7 +37,7 @@ class PCA9632: if self.prev_regs.get(reg) == val: return self.prev_regs[reg] = val - self.i2c.i2c_write_noack([reg, val], minclock=minclock, + self.i2c.i2c_write([reg, val], minclock=minclock, reqclock=BACKGROUND_PRIORITY_CLOCK) def handle_connect(self): #Configure MODE1 From bb04546e6e1a0c3c7239e629dbca36482d796d92 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 11 Oct 2025 15:20:35 +0200 Subject: [PATCH 219/411] pca9533: do write on connect like pca9632 does This is the only user of the i2c_write inside the init compatibility layer, so remove it as well. Signed-off-by: Timofey Titovets --- klippy/extras/bus.py | 3 --- klippy/extras/pca9533.py | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 676ff3b93..b04fbe764 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -213,9 +213,6 @@ class MCU_I2C: "i2c_read_response oid=%c response=%*s", oid=self.oid, cq=self.cmd_queue) def i2c_write_noack(self, data, minclock=0, reqclock=0): - if self.i2c_write_cmd is None: - self._to_write.append(data) - return self.i2c_write_cmd.send([self.oid, data], minclock=minclock, reqclock=reqclock) def i2c_write(self, data, minclock=0, reqclock=0): diff --git a/klippy/extras/pca9533.py b/klippy/extras/pca9533.py index 8f06e1c1a..0dfcb9217 100644 --- a/klippy/extras/pca9533.py +++ b/klippy/extras/pca9533.py @@ -14,9 +14,11 @@ PCA9533_PLS0=0b101 class PCA9533: def __init__(self, config): - self.printer = config.get_printer() + self.printer = printer = config.get_printer() self.i2c = bus.MCU_I2C_from_config(config, default_addr=98) self.led_helper = led.LEDHelper(config, self.update_leds, 1) + printer.register_event_handler("klippy:connect", self.handle_connect) + def handle_connect(self): self.i2c.i2c_write([PCA9533_PWM0, 85]) self.i2c.i2c_write([PCA9533_PWM1, 170]) self.update_leds(self.led_helper.get_status()['color_data'], None) From 7a723bdc1cb5e292ca0cd7b708035a7249b5e9a7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 18 Oct 2025 19:46:23 -0400 Subject: [PATCH 220/411] serialqueue: Revert recent serialqueue locking changes This reverts commit aea8d8e0a1f3a8655eab3ff0c51d5d7c7950f553. This reverts commit 493271697f2aff1b54bdae62c27588e2db614161. This reverts commit d7da45e152995e57123983e05f5faa9974e5df7a. There are reports of a regression since making this change. Revert for now until the root cause can be found. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 291 ++++++++++++++--------------------- 1 file changed, 112 insertions(+), 179 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index b6d274a91..914d4c395 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -29,44 +29,25 @@ #include "pyhelper.h" // get_monotonic #include "serialqueue.h" // struct queue_message -struct message_sub_queue { - struct list_head msg_queue; - struct list_node node; -}; - struct command_queue { - struct message_sub_queue ready, upcoming; -}; - -struct receiver { - pthread_mutex_t lock; - pthread_cond_t cond; - int waiting; - struct list_head queue; - struct list_head old_receive; -}; - -struct transmit_requests { - int pipe_fds[2]; - pthread_mutex_t lock; // protects variables below - struct list_head upcoming_queues; - int upcoming_bytes; - uint64_t need_kick_clock; + struct list_head upcoming_queue, ready_queue; + struct list_node node; }; struct serialqueue { // Input reading struct pollreactor *pr; int serial_fd, serial_fd_type, client_id; + int pipe_fds[2]; uint8_t input_buf[4096]; uint8_t need_sync; int input_pos; // Threading char name[16]; pthread_t tid; - // SerialHDL reader - struct receiver receiver; pthread_mutex_t lock; // protects variables below + pthread_cond_t cond; + int receive_waiting; // Baud / clock tracking int receive_window; double bittime_adjust, idle_time; @@ -78,16 +59,18 @@ struct serialqueue { struct list_head sent_queue; double srtt, rttvar, rto; // Pending transmission message queues - struct transmit_requests transmit_requests; - struct list_head ready_queues; - int ready_bytes, need_ack_bytes, last_ack_bytes; + struct list_head pending_queues; + int ready_bytes, upcoming_bytes, need_ack_bytes, last_ack_bytes; + uint64_t need_kick_clock; struct list_head notify_queue; double last_write_fail_time; + // Received messages + struct list_head receive_queue; // Fastreader support pthread_mutex_t fast_reader_dispatch_lock; struct list_head fast_readers; // Debugging - struct list_head old_sent; + struct list_head old_sent, old_receive; // Stats uint32_t bytes_write, bytes_read, bytes_retransmit, bytes_invalid; }; @@ -126,30 +109,23 @@ debug_queue_alloc(struct list_head *root, int count) } // Copy a message to a debug queue and free old debug messages -static struct queue_message * -_debug_queue_add(struct list_head *root, struct queue_message *qm) +static void +debug_queue_add(struct list_head *root, struct queue_message *qm) { list_add_tail(&qm->node, root); struct queue_message *old = list_first_entry( root, struct queue_message, node); list_del(&old->node); - return old; -} - -static void -debug_queue_add(struct list_head *root, struct queue_message *qm) -{ - struct queue_message *old = _debug_queue_add(root, qm); message_free(old); } // Wake up the receiver thread if it is waiting static void -check_wake_receive(struct receiver *receiver) +check_wake_receive(struct serialqueue *sq) { - if (receiver->waiting) { - receiver->waiting = 0; - pthread_cond_signal(&receiver->cond); + if (sq->receive_waiting) { + sq->receive_waiting = 0; + pthread_cond_signal(&sq->cond); } } @@ -157,7 +133,7 @@ check_wake_receive(struct receiver *receiver) static void kick_bg_thread(struct serialqueue *sq) { - int ret = write(sq->transmit_requests.pipe_fds[1], ".", 1); + int ret = write(sq->pipe_fds[1], ".", 1); if (ret < 0) report_errno("pipe write", ret); } @@ -263,8 +239,7 @@ handle_message(struct serialqueue *sq, double eventtime, int len) sq->bytes_read += len; // Check for pending messages on notify_queue - struct list_head received; - list_init(&received); + int must_wake = 0; while (!list_empty(&sq->notify_queue)) { struct queue_message *qm = list_first_entry( &sq->notify_queue, struct queue_message, node); @@ -276,7 +251,8 @@ handle_message(struct serialqueue *sq, double eventtime, int len) qm->len = 0; qm->sent_time = sq->last_receive_sent_time; qm->receive_time = eventtime; - list_add_tail(&qm->node, &received); + list_add_tail(&qm->node, &sq->receive_queue); + must_wake = 1; } // Process message @@ -294,14 +270,8 @@ handle_message(struct serialqueue *sq, double eventtime, int len) ? sq->last_receive_sent_time : 0.); qm->receive_time = get_monotonic(); // must be time post read() qm->receive_time -= calculate_bittime(sq, len); - list_add_tail(&qm->node, &received); - } - - if (!list_empty(&received)) { - pthread_mutex_lock(&sq->receiver.lock); - list_join_tail(&received, &sq->receiver.queue); - check_wake_receive(&sq->receiver); - pthread_mutex_unlock(&sq->receiver.lock); + list_add_tail(&qm->node, &sq->receive_queue); + must_wake = 1; } // Check fast readers @@ -313,11 +283,16 @@ handle_message(struct serialqueue *sq, double eventtime, int len) continue; // Release main lock and invoke callback pthread_mutex_lock(&sq->fast_reader_dispatch_lock); + if (must_wake) + check_wake_receive(sq); pthread_mutex_unlock(&sq->lock); fr->func(fr, sq->input_buf, len); pthread_mutex_unlock(&sq->fast_reader_dispatch_lock); return; } + + if (must_wake) + check_wake_receive(sq); pthread_mutex_unlock(&sq->lock); } @@ -376,7 +351,7 @@ static void kick_event(struct serialqueue *sq, double eventtime) { char dummy[4096]; - int ret = read(sq->transmit_requests.pipe_fds[0], dummy, sizeof(dummy)); + int ret = read(sq->pipe_fds[0], dummy, sizeof(dummy)); if (ret < 0) report_errno("pipe read", ret); pollreactor_update_timer(sq->pr, SQPT_COMMAND, PR_NOW); @@ -477,21 +452,23 @@ build_and_send_command(struct serialqueue *sq, uint8_t *buf, int pending uint64_t min_clock = MAX_CLOCK; struct command_queue *q, *cq = NULL; struct queue_message *qm = NULL; - list_for_each_entry(q, &sq->ready_queues, ready.node) { - struct queue_message *m = list_first_entry( - &q->ready.msg_queue, struct queue_message, node); - if (m->req_clock < min_clock) { - min_clock = m->req_clock; - cq = q; - qm = m; + list_for_each_entry(q, &sq->pending_queues, node) { + if (!list_empty(&q->ready_queue)) { + struct queue_message *m = list_first_entry( + &q->ready_queue, struct queue_message, node); + if (m->req_clock < min_clock) { + min_clock = m->req_clock; + cq = q; + qm = m; + } } } // Append message to outgoing command if (len + qm->len > MESSAGE_MAX - MESSAGE_TRAILER_SIZE) break; list_del(&qm->node); - if (list_empty(&cq->ready.msg_queue)) - list_del(&cq->ready.node); + if (list_empty(&cq->ready_queue) && list_empty(&cq->upcoming_queue)) + list_del(&cq->node); memcpy(&buf[len], qm->msg, qm->len); len += qm->len; sq->ready_bytes -= qm->len; @@ -553,68 +530,53 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) idletime += calculate_bittime(sq, pending + MESSAGE_MIN); uint64_t ack_clock = clock_from_time(&sq->ce, idletime); uint64_t min_stalled_clock = MAX_CLOCK, min_ready_clock = MAX_CLOCK; - struct command_queue *cq, *_ncq; - pthread_mutex_lock(&sq->transmit_requests.lock); - list_for_each_entry_safe(cq, _ncq, &sq->transmit_requests.upcoming_queues, - upcoming.node) { - int not_in_ready_queues = list_empty(&cq->ready.msg_queue); - // Move messages from the upcoming.msg_queue to the ready.msg_queue - while (!list_empty(&cq->upcoming.msg_queue)) { + struct command_queue *cq; + list_for_each_entry(cq, &sq->pending_queues, node) { + // Move messages from the upcoming_queue to the ready_queue + while (!list_empty(&cq->upcoming_queue)) { struct queue_message *qm = list_first_entry( - &cq->upcoming.msg_queue, struct queue_message, node); + &cq->upcoming_queue, struct queue_message, node); if (ack_clock < qm->min_clock) { if (qm->min_clock < min_stalled_clock) min_stalled_clock = qm->min_clock; break; } list_del(&qm->node); - list_add_tail(&qm->node, &cq->ready.msg_queue); - sq->transmit_requests.upcoming_bytes -= qm->len; + list_add_tail(&qm->node, &cq->ready_queue); + sq->upcoming_bytes -= qm->len; sq->ready_bytes += qm->len; } - // Remove cq from the list if it is now empty - if (list_empty(&cq->upcoming.msg_queue)) - list_del(&cq->upcoming.node); - // Add to ready queues - if (not_in_ready_queues && !list_empty(&cq->ready.msg_queue)) - list_add_tail(&cq->ready.node, &sq->ready_queues); - } - // Check if it is still needed to send messages from the ready_queues - list_for_each_entry(cq, &sq->ready_queues, ready.node) { // Update min_ready_clock - struct queue_message *qm = list_first_entry( - &cq->ready.msg_queue, struct queue_message, node); - uint64_t req_clock = qm->req_clock; - double bgtime = pending ? idletime : sq->idle_time; - double bgoffset = MIN_REQTIME_DELTA + MIN_BACKGROUND_DELTA; - if (req_clock == BACKGROUND_PRIORITY_CLOCK) - req_clock = clock_from_time(&sq->ce, bgtime + bgoffset); - if (req_clock < min_ready_clock) - min_ready_clock = req_clock; + if (!list_empty(&cq->ready_queue)) { + struct queue_message *qm = list_first_entry( + &cq->ready_queue, struct queue_message, node); + uint64_t req_clock = qm->req_clock; + double bgtime = pending ? idletime : sq->idle_time; + double bgoffset = MIN_REQTIME_DELTA + MIN_BACKGROUND_DELTA; + if (req_clock == BACKGROUND_PRIORITY_CLOCK) + req_clock = clock_from_time(&sq->ce, bgtime + bgoffset); + if (req_clock < min_ready_clock) + min_ready_clock = req_clock; + } } // Check for messages to send if (sq->ready_bytes >= MESSAGE_PAYLOAD_MAX) - goto now; + return PR_NOW; if (! sq->ce.est_freq) { if (sq->ready_bytes) - goto now; - sq->transmit_requests.need_kick_clock = MAX_CLOCK; - pthread_mutex_unlock(&sq->transmit_requests.lock); + return PR_NOW; + sq->need_kick_clock = MAX_CLOCK; return PR_NEVER; } uint64_t reqclock_delta = MIN_REQTIME_DELTA * sq->ce.est_freq; if (min_ready_clock <= ack_clock + reqclock_delta) - goto now; + return PR_NOW; uint64_t wantclock = min_ready_clock - reqclock_delta; if (min_stalled_clock < wantclock) wantclock = min_stalled_clock; - sq->transmit_requests.need_kick_clock = wantclock; - pthread_mutex_unlock(&sq->transmit_requests.lock); + sq->need_kick_clock = wantclock; return idletime + (wantclock - ack_clock) / sq->ce.est_freq; -now: - pthread_mutex_unlock(&sq->transmit_requests.lock); - return PR_NOW; } // Callback timer to send data to the serial port @@ -654,9 +616,9 @@ background_thread(void *data) set_thread_name(sq->name); pollreactor_run(sq->pr); - pthread_mutex_lock(&sq->receiver.lock); - check_wake_receive(&sq->receiver); - pthread_mutex_unlock(&sq->receiver.lock); + pthread_mutex_lock(&sq->lock); + check_wake_receive(sq); + pthread_mutex_unlock(&sq->lock); return NULL; } @@ -674,7 +636,7 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id strncpy(sq->name, name, sizeof(sq->name)); sq->name[sizeof(sq->name)-1] = '\0'; - int ret = pipe(sq->transmit_requests.pipe_fds); + int ret = pipe(sq->pipe_fds); if (ret) goto fail; @@ -682,13 +644,12 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id sq->pr = pollreactor_alloc(SQPF_NUM, SQPT_NUM, sq); pollreactor_add_fd(sq->pr, SQPF_SERIAL, serial_fd, input_event , serial_fd_type==SQT_DEBUGFILE); - pollreactor_add_fd(sq->pr, SQPF_PIPE, sq->transmit_requests.pipe_fds[0] - , kick_event, 0); + pollreactor_add_fd(sq->pr, SQPF_PIPE, sq->pipe_fds[0], kick_event, 0); pollreactor_add_timer(sq->pr, SQPT_RETRANSMIT, retransmit_event); pollreactor_add_timer(sq->pr, SQPT_COMMAND, command_event); fd_set_non_blocking(serial_fd); - fd_set_non_blocking(sq->transmit_requests.pipe_fds[0]); - fd_set_non_blocking(sq->transmit_requests.pipe_fds[1]); + fd_set_non_blocking(sq->pipe_fds[0]); + fd_set_non_blocking(sq->pipe_fds[1]); // Retransmit setup sq->send_seq = 1; @@ -702,29 +663,24 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id } // Queues - sq->transmit_requests.need_kick_clock = MAX_CLOCK; - list_init(&sq->transmit_requests.upcoming_queues); - pthread_mutex_init(&sq->transmit_requests.lock, NULL); - list_init(&sq->ready_queues); + sq->need_kick_clock = MAX_CLOCK; + list_init(&sq->pending_queues); list_init(&sq->sent_queue); - list_init(&sq->receiver.queue); + list_init(&sq->receive_queue); list_init(&sq->notify_queue); list_init(&sq->fast_readers); // Debugging list_init(&sq->old_sent); - list_init(&sq->receiver.old_receive); + list_init(&sq->old_receive); debug_queue_alloc(&sq->old_sent, DEBUG_QUEUE_SENT); - debug_queue_alloc(&sq->receiver.old_receive, DEBUG_QUEUE_RECEIVE); + debug_queue_alloc(&sq->old_receive, DEBUG_QUEUE_RECEIVE); // Thread setup ret = pthread_mutex_init(&sq->lock, NULL); if (ret) goto fail; - ret = pthread_mutex_init(&sq->receiver.lock, NULL); - if (ret) - goto fail; - ret = pthread_cond_init(&sq->receiver.cond, NULL); + ret = pthread_cond_init(&sq->cond, NULL); if (ret) goto fail; ret = pthread_mutex_init(&sq->fast_reader_dispatch_lock, NULL); @@ -762,27 +718,17 @@ serialqueue_free(struct serialqueue *sq) serialqueue_exit(sq); pthread_mutex_lock(&sq->lock); message_queue_free(&sq->sent_queue); - pthread_mutex_lock(&sq->receiver.lock); - message_queue_free(&sq->receiver.queue); - message_queue_free(&sq->receiver.old_receive); - pthread_mutex_unlock(&sq->receiver.lock); + message_queue_free(&sq->receive_queue); message_queue_free(&sq->notify_queue); message_queue_free(&sq->old_sent); - while (!list_empty(&sq->ready_queues)) { - struct command_queue* cq = list_first_entry( - &sq->ready_queues, struct command_queue, ready.node); - list_del(&cq->ready.node); - message_queue_free(&cq->ready.msg_queue); - } - pthread_mutex_lock(&sq->transmit_requests.lock); - while (!list_empty(&sq->transmit_requests.upcoming_queues)) { + message_queue_free(&sq->old_receive); + while (!list_empty(&sq->pending_queues)) { struct command_queue *cq = list_first_entry( - &sq->transmit_requests.upcoming_queues, - struct command_queue, upcoming.node); - list_del(&cq->upcoming.node); - message_queue_free(&cq->upcoming.msg_queue); + &sq->pending_queues, struct command_queue, node); + list_del(&cq->node); + message_queue_free(&cq->ready_queue); + message_queue_free(&cq->upcoming_queue); } - pthread_mutex_unlock(&sq->transmit_requests.lock); pthread_mutex_unlock(&sq->lock); pollreactor_free(sq->pr); free(sq); @@ -794,8 +740,8 @@ serialqueue_alloc_commandqueue(void) { struct command_queue *cq = malloc(sizeof(*cq)); memset(cq, 0, sizeof(*cq)); - list_init(&cq->ready.msg_queue); - list_init(&cq->upcoming.msg_queue); + list_init(&cq->ready_queue); + list_init(&cq->upcoming_queue); return cq; } @@ -805,8 +751,7 @@ serialqueue_free_commandqueue(struct command_queue *cq) { if (!cq) return; - if (!list_empty(&cq->ready.msg_queue) || - !list_empty(&cq->upcoming.msg_queue)) { + if (!list_empty(&cq->ready_queue) || !list_empty(&cq->upcoming_queue)) { errorf("Memory leak! Can't free non-empty commandqueue"); return; } @@ -853,19 +798,17 @@ serialqueue_send_batch(struct serialqueue *sq, struct command_queue *cq qm = list_first_entry(msgs, struct queue_message, node); // Add list to cq->upcoming_queue - pthread_mutex_lock(&sq->transmit_requests.lock); - if (list_empty(&cq->upcoming.msg_queue)) - list_add_tail(&cq->upcoming.node, - &sq->transmit_requests.upcoming_queues); - list_join_tail(msgs, &cq->upcoming.msg_queue); - sq->transmit_requests.upcoming_bytes += len; - + pthread_mutex_lock(&sq->lock); + if (list_empty(&cq->ready_queue) && list_empty(&cq->upcoming_queue)) + list_add_tail(&cq->node, &sq->pending_queues); + list_join_tail(msgs, &cq->upcoming_queue); + sq->upcoming_bytes += len; int mustwake = 0; - if (qm->min_clock < sq->transmit_requests.need_kick_clock) { - sq->transmit_requests.need_kick_clock = 0; + if (qm->min_clock < sq->need_kick_clock) { + sq->need_kick_clock = 0; mustwake = 1; } - pthread_mutex_unlock(&sq->transmit_requests.lock); + pthread_mutex_unlock(&sq->lock); // Wake the background thread if necessary if (mustwake) @@ -902,21 +845,20 @@ serialqueue_send(struct serialqueue *sq, struct command_queue *cq, uint8_t *msg void __visible serialqueue_pull(struct serialqueue *sq, struct pull_queue_message *pqm) { - struct receiver *receiver = &sq->receiver; - pthread_mutex_lock(&receiver->lock); + pthread_mutex_lock(&sq->lock); // Wait for message to be available - while (list_empty(&receiver->queue)) { + while (list_empty(&sq->receive_queue)) { if (pollreactor_is_exit(sq->pr)) goto exit; - receiver->waiting = 1; - int ret = pthread_cond_wait(&receiver->cond, &receiver->lock); + sq->receive_waiting = 1; + int ret = pthread_cond_wait(&sq->cond, &sq->lock); if (ret) report_errno("pthread_cond_wait", ret); } // Remove message from queue struct queue_message *qm = list_first_entry( - &receiver->queue, struct queue_message, node); + &sq->receive_queue, struct queue_message, node); list_del(&qm->node); // Copy message @@ -926,14 +868,16 @@ serialqueue_pull(struct serialqueue *sq, struct pull_queue_message *pqm) pqm->receive_time = qm->receive_time; pqm->notify_id = qm->notify_id; if (qm->len) - qm = _debug_queue_add(&receiver->old_receive, qm); - pthread_mutex_unlock(&receiver->lock); - message_free(qm); + debug_queue_add(&sq->old_receive, qm); + else + message_free(qm); + + pthread_mutex_unlock(&sq->lock); return; exit: pqm->len = -1; - pthread_mutex_unlock(&receiver->lock); + pthread_mutex_unlock(&sq->lock); } void __visible @@ -984,9 +928,7 @@ serialqueue_get_stats(struct serialqueue *sq, char *buf, int len) { struct serialqueue stats; pthread_mutex_lock(&sq->lock); - pthread_mutex_lock(&sq->transmit_requests.lock); memcpy(&stats, sq, sizeof(stats)); - pthread_mutex_unlock(&sq->transmit_requests.lock); pthread_mutex_unlock(&sq->lock); snprintf(buf, len, "bytes_write=%u bytes_read=%u" @@ -999,7 +941,7 @@ serialqueue_get_stats(struct serialqueue *sq, char *buf, int len) , (int)stats.send_seq, (int)stats.receive_seq , (int)stats.retransmit_seq , stats.srtt, stats.rttvar, stats.rto - , stats.ready_bytes, stats.transmit_requests.upcoming_bytes); + , stats.ready_bytes, stats.upcoming_bytes); } // Extract old messages stored in the debug queues @@ -1008,27 +950,18 @@ serialqueue_extract_old(struct serialqueue *sq, int sentq , struct pull_queue_message *q, int max) { int count = sentq ? DEBUG_QUEUE_SENT : DEBUG_QUEUE_RECEIVE; - struct list_head *rootp; - rootp = sentq ? &sq->old_sent : &sq->receiver.old_receive; + struct list_head *rootp = sentq ? &sq->old_sent : &sq->old_receive; struct list_head replacement, current; list_init(&replacement); debug_queue_alloc(&replacement, count); list_init(¤t); // Atomically replace existing debug list with new zero'd list - if (rootp == &sq->receiver.old_receive) { - pthread_mutex_lock(&sq->receiver.lock); - list_join_tail(rootp, ¤t); - list_init(rootp); - list_join_tail(&replacement, rootp); - pthread_mutex_unlock(&sq->receiver.lock); - } else { - pthread_mutex_lock(&sq->lock); - list_join_tail(rootp, ¤t); - list_init(rootp); - list_join_tail(&replacement, rootp); - pthread_mutex_unlock(&sq->lock); - } + pthread_mutex_lock(&sq->lock); + list_join_tail(rootp, ¤t); + list_init(rootp); + list_join_tail(&replacement, rootp); + pthread_mutex_unlock(&sq->lock); // Walk the debug list int pos = 0; From ff667075cf7e32c9e62d2f7bed5a0173abea777c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 18 Oct 2025 12:57:15 -0400 Subject: [PATCH 221/411] statistics: Verify nothing attempts to pause in a stats() callback Signed-off-by: Kevin O'Connor --- klippy/extras/statistics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/klippy/extras/statistics.py b/klippy/extras/statistics.py index 9e6188a81..ad694c83a 100644 --- a/klippy/extras/statistics.py +++ b/klippy/extras/statistics.py @@ -52,7 +52,7 @@ class PrinterSysStats: class PrinterStats: def __init__(self, config): self.printer = config.get_printer() - reactor = self.printer.get_reactor() + self.reactor = reactor = self.printer.get_reactor() self.stats_timer = reactor.register_timer(self.generate_stats) self.stats_cb = [] self.printer.register_event_handler("klippy:ready", self.handle_ready) @@ -60,10 +60,10 @@ class PrinterStats: self.stats_cb = [o.stats for n, o in self.printer.lookup_objects() if hasattr(o, 'stats')] if self.printer.get_start_args().get('debugoutput') is None: - reactor = self.printer.get_reactor() - reactor.update_timer(self.stats_timer, reactor.NOW) + self.reactor.update_timer(self.stats_timer, self.reactor.NOW) def generate_stats(self, eventtime): - stats = [cb(eventtime) for cb in self.stats_cb] + with self.reactor.assert_no_pause(): + stats = [cb(eventtime) for cb in self.stats_cb] if max([s[0] for s in stats]): stats_str = ' '.join([s[1] for s in stats if s[1]]) logging.info("Stats %.1f: %s", eventtime, stats_str) From 6465921a5adb3a3335494598463066a6c3540cb5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 18 Oct 2025 13:05:54 -0400 Subject: [PATCH 222/411] gcode_macro: Verify nothing attempts to pause in a get_status() callback Signed-off-by: Kevin O'Connor --- klippy/extras/gcode_macro.py | 7 ++-- klippy/webhooks.py | 67 ++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/klippy/extras/gcode_macro.py b/klippy/extras/gcode_macro.py index 3aa5087b1..58861fd89 100644 --- a/klippy/extras/gcode_macro.py +++ b/klippy/extras/gcode_macro.py @@ -24,9 +24,12 @@ class GetStatusWrapper: po = self.printer.lookup_object(sval, None) if po is None or not hasattr(po, 'get_status'): raise KeyError(val) + reactor = self.printer.get_reactor() if self.eventtime is None: - self.eventtime = self.printer.get_reactor().monotonic() - self.cache[sval] = res = copy.deepcopy(po.get_status(self.eventtime)) + self.eventtime = reactor.monotonic() + with reactor.assert_no_pause(): + sts = po.get_status(self.eventtime) + self.cache[sval] = res = copy.deepcopy(sts) return res def __contains__(self, val): try: diff --git a/klippy/webhooks.py b/klippy/webhooks.py index 78edc6e5b..da18bff5e 100644 --- a/klippy/webhooks.py +++ b/klippy/webhooks.py @@ -491,41 +491,42 @@ class QueryStatusHelper: self.pending_queries = [] msglist.extend(self.clients.values()) # Generate get_status() info for each client - for cconn, subscription, send_func, template in msglist: - is_query = cconn is None - if not is_query and cconn.is_closed(): - del self.clients[cconn] - continue - # Query each requested printer object - cquery = {} - for obj_name, req_items in subscription.items(): - res = query.get(obj_name, None) - if res is None: - po = self.printer.lookup_object(obj_name, None) - if po is None or not hasattr(po, 'get_status'): - res = query[obj_name] = {} - else: - res = query[obj_name] = po.get_status(eventtime) - if req_items is None: - req_items = list(res.keys()) - if req_items: - subscription[obj_name] = req_items - lres = last_query.get(obj_name, {}) - cres = {} - for ri in req_items: - rd = res.get(ri, None) - if is_query or rd != lres.get(ri): - cres[ri] = rd - if cres or is_query: - cquery[obj_name] = cres - # Send data - if cquery or is_query: - tmp = dict(template) - tmp['params'] = {'eventtime': eventtime, 'status': cquery} - send_func(tmp) + reactor = self.printer.get_reactor() + with reactor.assert_no_pause(): + for cconn, subscription, send_func, template in msglist: + is_query = cconn is None + if not is_query and cconn.is_closed(): + del self.clients[cconn] + continue + # Query each requested printer object + cquery = {} + for obj_name, req_items in subscription.items(): + res = query.get(obj_name, None) + if res is None: + po = self.printer.lookup_object(obj_name, None) + if po is None or not hasattr(po, 'get_status'): + res = query[obj_name] = {} + else: + res = query[obj_name] = po.get_status(eventtime) + if req_items is None: + req_items = list(res.keys()) + if req_items: + subscription[obj_name] = req_items + lres = last_query.get(obj_name, {}) + cres = {} + for ri in req_items: + rd = res.get(ri, None) + if is_query or rd != lres.get(ri): + cres[ri] = rd + if cres or is_query: + cquery[obj_name] = cres + # Send data + if cquery or is_query: + tmp = dict(template) + tmp['params'] = {'eventtime': eventtime, 'status': cquery} + send_func(tmp) if not query: # Unregister timer if there are no longer any subscriptions - reactor = self.printer.get_reactor() reactor.unregister_timer(self.query_timer) self.query_timer = None return reactor.NEVER From 99c0bfca4f4875e8a602c9b4ffc698d9e5cc59a0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 23 Oct 2025 11:39:17 -0400 Subject: [PATCH 223/411] stm32: Fix RTR and EFF canbus tx requests in can.c Commit 3f7d05dd attempted to add support for transmitting RTR and EFF frames to stm32/can.c , but the change was incomplete. Signed-off-by: Kevin O'Connor --- src/stm32/can.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stm32/can.c b/src/stm32/can.c index 30f8fc906..223260b36 100644 --- a/src/stm32/can.c +++ b/src/stm32/can.c @@ -127,7 +127,7 @@ canhw_send(struct canbus_msg *msg) else tir = (msg->id & 0x7ff) << CAN_TI0R_STID_Pos; tir |= msg->id & CANMSG_ID_RTR ? CAN_TI0R_RTR : 0; - mb->TIR = (msg->id << CAN_TI0R_STID_Pos) | CAN_TI0R_TXRQ; + mb->TIR = tir | CAN_TI0R_TXRQ; return CANMSG_DATA_LEN(msg); } From ba79d72fb47bc254f22382b355d8204eda9959c4 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Mon, 22 Sep 2025 18:19:55 +0200 Subject: [PATCH 224/411] serialqueue: decouple pending & ready queues Simply describe how the cmdqueue is moved between states. This is commit d7da45e1 resubmitted with a slighly different implementation. It is thought that the previous implementation was causing a gcc compiliation issue on gcc v12.0-v12.4 compilers. (It may be related to gcc bug report 107467). Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 108 ++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 45 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 914d4c395..5f205858a 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -29,11 +29,15 @@ #include "pyhelper.h" // get_monotonic #include "serialqueue.h" // struct queue_message -struct command_queue { - struct list_head upcoming_queue, ready_queue; +struct message_sub_queue { + struct list_head msg_queue; struct list_node node; }; +struct command_queue { + struct message_sub_queue ready, upcoming; +}; + struct serialqueue { // Input reading struct pollreactor *pr; @@ -59,8 +63,10 @@ struct serialqueue { struct list_head sent_queue; double srtt, rttvar, rto; // Pending transmission message queues - struct list_head pending_queues; - int ready_bytes, upcoming_bytes, need_ack_bytes, last_ack_bytes; + struct list_head upcoming_queues; + int upcoming_bytes; + struct list_head ready_queues; + int ready_bytes, need_ack_bytes, last_ack_bytes; uint64_t need_kick_clock; struct list_head notify_queue; double last_write_fail_time; @@ -452,23 +458,21 @@ build_and_send_command(struct serialqueue *sq, uint8_t *buf, int pending uint64_t min_clock = MAX_CLOCK; struct command_queue *q, *cq = NULL; struct queue_message *qm = NULL; - list_for_each_entry(q, &sq->pending_queues, node) { - if (!list_empty(&q->ready_queue)) { - struct queue_message *m = list_first_entry( - &q->ready_queue, struct queue_message, node); - if (m->req_clock < min_clock) { - min_clock = m->req_clock; - cq = q; - qm = m; - } + list_for_each_entry(q, &sq->ready_queues, ready.node) { + struct queue_message *m = list_first_entry( + &q->ready.msg_queue, struct queue_message, node); + if (m->req_clock < min_clock) { + min_clock = m->req_clock; + cq = q; + qm = m; } } // Append message to outgoing command if (len + qm->len > MESSAGE_MAX - MESSAGE_TRAILER_SIZE) break; list_del(&qm->node); - if (list_empty(&cq->ready_queue) && list_empty(&cq->upcoming_queue)) - list_del(&cq->node); + if (list_empty(&cq->ready.msg_queue)) + list_del(&cq->ready.node); memcpy(&buf[len], qm->msg, qm->len); len += qm->len; sq->ready_bytes -= qm->len; @@ -530,34 +534,41 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) idletime += calculate_bittime(sq, pending + MESSAGE_MIN); uint64_t ack_clock = clock_from_time(&sq->ce, idletime); uint64_t min_stalled_clock = MAX_CLOCK, min_ready_clock = MAX_CLOCK; - struct command_queue *cq; - list_for_each_entry(cq, &sq->pending_queues, node) { - // Move messages from the upcoming_queue to the ready_queue - while (!list_empty(&cq->upcoming_queue)) { - struct queue_message *qm = list_first_entry( - &cq->upcoming_queue, struct queue_message, node); + struct command_queue *cq, *_ncq; + list_for_each_entry_safe(cq, _ncq, &sq->upcoming_queues, upcoming.node) { + int not_in_ready_queues = list_empty(&cq->ready.msg_queue); + // Move messages from the upcoming.msg_queue to the ready.msg_queue + struct queue_message *qm, *_nqm; + list_for_each_entry_safe(qm, _nqm, &cq->upcoming.msg_queue, node) { if (ack_clock < qm->min_clock) { if (qm->min_clock < min_stalled_clock) min_stalled_clock = qm->min_clock; break; } list_del(&qm->node); - list_add_tail(&qm->node, &cq->ready_queue); + list_add_tail(&qm->node, &cq->ready.msg_queue); sq->upcoming_bytes -= qm->len; sq->ready_bytes += qm->len; } + // Remove cq from the list if it is now empty + if (list_empty(&cq->upcoming.msg_queue)) + list_del(&cq->upcoming.node); + // Add to ready queues + if (not_in_ready_queues && !list_empty(&cq->ready.msg_queue)) + list_add_tail(&cq->ready.node, &sq->ready_queues); + } + // Check if it is still needed to send messages from the ready_queues + list_for_each_entry(cq, &sq->ready_queues, ready.node) { // Update min_ready_clock - if (!list_empty(&cq->ready_queue)) { - struct queue_message *qm = list_first_entry( - &cq->ready_queue, struct queue_message, node); - uint64_t req_clock = qm->req_clock; - double bgtime = pending ? idletime : sq->idle_time; - double bgoffset = MIN_REQTIME_DELTA + MIN_BACKGROUND_DELTA; - if (req_clock == BACKGROUND_PRIORITY_CLOCK) - req_clock = clock_from_time(&sq->ce, bgtime + bgoffset); - if (req_clock < min_ready_clock) - min_ready_clock = req_clock; - } + struct queue_message *qm = list_first_entry( + &cq->ready.msg_queue, struct queue_message, node); + uint64_t req_clock = qm->req_clock; + double bgtime = pending ? idletime : sq->idle_time; + double bgoffset = MIN_REQTIME_DELTA + MIN_BACKGROUND_DELTA; + if (req_clock == BACKGROUND_PRIORITY_CLOCK) + req_clock = clock_from_time(&sq->ce, bgtime + bgoffset); + if (req_clock < min_ready_clock) + min_ready_clock = req_clock; } // Check for messages to send @@ -664,7 +675,8 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id // Queues sq->need_kick_clock = MAX_CLOCK; - list_init(&sq->pending_queues); + list_init(&sq->upcoming_queues); + list_init(&sq->ready_queues); list_init(&sq->sent_queue); list_init(&sq->receive_queue); list_init(&sq->notify_queue); @@ -722,12 +734,17 @@ serialqueue_free(struct serialqueue *sq) message_queue_free(&sq->notify_queue); message_queue_free(&sq->old_sent); message_queue_free(&sq->old_receive); - while (!list_empty(&sq->pending_queues)) { + while (!list_empty(&sq->ready_queues)) { + struct command_queue* cq = list_first_entry( + &sq->ready_queues, struct command_queue, ready.node); + list_del(&cq->ready.node); + message_queue_free(&cq->ready.msg_queue); + } + while (!list_empty(&sq->upcoming_queues)) { struct command_queue *cq = list_first_entry( - &sq->pending_queues, struct command_queue, node); - list_del(&cq->node); - message_queue_free(&cq->ready_queue); - message_queue_free(&cq->upcoming_queue); + &sq->upcoming_queues, struct command_queue, upcoming.node); + list_del(&cq->upcoming.node); + message_queue_free(&cq->upcoming.msg_queue); } pthread_mutex_unlock(&sq->lock); pollreactor_free(sq->pr); @@ -740,8 +757,8 @@ serialqueue_alloc_commandqueue(void) { struct command_queue *cq = malloc(sizeof(*cq)); memset(cq, 0, sizeof(*cq)); - list_init(&cq->ready_queue); - list_init(&cq->upcoming_queue); + list_init(&cq->ready.msg_queue); + list_init(&cq->upcoming.msg_queue); return cq; } @@ -751,7 +768,8 @@ serialqueue_free_commandqueue(struct command_queue *cq) { if (!cq) return; - if (!list_empty(&cq->ready_queue) || !list_empty(&cq->upcoming_queue)) { + if (!list_empty(&cq->ready.msg_queue) || + !list_empty(&cq->upcoming.msg_queue)) { errorf("Memory leak! Can't free non-empty commandqueue"); return; } @@ -799,9 +817,9 @@ serialqueue_send_batch(struct serialqueue *sq, struct command_queue *cq // Add list to cq->upcoming_queue pthread_mutex_lock(&sq->lock); - if (list_empty(&cq->ready_queue) && list_empty(&cq->upcoming_queue)) - list_add_tail(&cq->node, &sq->pending_queues); - list_join_tail(msgs, &cq->upcoming_queue); + if (list_empty(&cq->upcoming.msg_queue)) + list_add_tail(&cq->upcoming.node, &sq->upcoming_queues); + list_join_tail(msgs, &cq->upcoming.msg_queue); sq->upcoming_bytes += len; int mustwake = 0; if (qm->min_clock < sq->need_kick_clock) { From 05c4a38279727817421ce3210154094f34afd944 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 4 Nov 2025 10:57:27 -0500 Subject: [PATCH 225/411] docs: Fix minor typo in Eddy_Probe.md Signed-off-by: Kevin O'Connor --- docs/Eddy_Probe.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 3c36a613c..d819894f0 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -32,8 +32,8 @@ tool starts, follow the steps described at ["the paper test"](Bed_Level.md#the-paper-test) to determine the actual distance between the nozzle and bed at the given location. Once those steps are complete one can `ACCEPT` the position. The tool will -then move the the toolhead so that the sensor is above the point where -the nozzle used to be and run a series of movements to correlate the +then move the toolhead so that the sensor is above the point where the +nozzle used to be and run a series of movements to correlate the sensor to Z positions. This will take a couple of minutes. After the tool completes, issue a `SAVE_CONFIG` command to save the results to the printer.cfg and restart. From 877b63a28c2bafff8c6a45bf6fcf75e72ff7c707 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 4 Nov 2025 10:58:36 -0500 Subject: [PATCH 226/411] docs: Fix minor typo in Eddy_Probe.md Signed-off-by: Kevin O'Connor --- docs/Eddy_Probe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index d819894f0..8d81bd88b 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -26,7 +26,7 @@ command to save the results to the printer.cfg and restart. The second step in calibration is to correlate the sensor readings to the corresponding Z heights. Home the printer and navigate the -toolhead so that the nozzle is near the center of the bed. Then run an +toolhead so that the nozzle is near the center of the bed. Then run a `PROBE_EDDY_CURRENT_CALIBRATE CHIP=my_eddy_probe` command. Once the tool starts, follow the steps described at ["the paper test"](Bed_Level.md#the-paper-test) to determine the From 4a00e4b0ec45f284722a0248c571927a05137c58 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 19 Sep 2025 01:28:14 +0200 Subject: [PATCH 227/411] serialqueue: decouple serialhdl receive lock Signed-off-by: Timofey Titovets --- klippy/chelper/serialqueue.c | 121 +++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 47 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 5f205858a..59e799fb3 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -38,6 +38,14 @@ struct command_queue { struct message_sub_queue ready, upcoming; }; +struct receiver { + pthread_mutex_t lock; + pthread_cond_t cond; + int waiting; + struct list_head queue; + struct list_head old_receive; +}; + struct serialqueue { // Input reading struct pollreactor *pr; @@ -49,9 +57,9 @@ struct serialqueue { // Threading char name[16]; pthread_t tid; + // SerialHDL reader + struct receiver receiver; pthread_mutex_t lock; // protects variables below - pthread_cond_t cond; - int receive_waiting; // Baud / clock tracking int receive_window; double bittime_adjust, idle_time; @@ -70,13 +78,11 @@ struct serialqueue { uint64_t need_kick_clock; struct list_head notify_queue; double last_write_fail_time; - // Received messages - struct list_head receive_queue; // Fastreader support pthread_mutex_t fast_reader_dispatch_lock; struct list_head fast_readers; // Debugging - struct list_head old_sent, old_receive; + struct list_head old_sent; // Stats uint32_t bytes_write, bytes_read, bytes_retransmit, bytes_invalid; }; @@ -115,23 +121,30 @@ debug_queue_alloc(struct list_head *root, int count) } // Copy a message to a debug queue and free old debug messages -static void -debug_queue_add(struct list_head *root, struct queue_message *qm) +static struct queue_message * +_debug_queue_add(struct list_head *root, struct queue_message *qm) { list_add_tail(&qm->node, root); struct queue_message *old = list_first_entry( root, struct queue_message, node); list_del(&old->node); + return old; +} + +static void +debug_queue_add(struct list_head *root, struct queue_message *qm) +{ + struct queue_message *old = _debug_queue_add(root, qm); message_free(old); } // Wake up the receiver thread if it is waiting static void -check_wake_receive(struct serialqueue *sq) +check_wake_receive(struct receiver *receiver) { - if (sq->receive_waiting) { - sq->receive_waiting = 0; - pthread_cond_signal(&sq->cond); + if (receiver->waiting) { + receiver->waiting = 0; + pthread_cond_signal(&receiver->cond); } } @@ -245,7 +258,8 @@ handle_message(struct serialqueue *sq, double eventtime, int len) sq->bytes_read += len; // Check for pending messages on notify_queue - int must_wake = 0; + struct list_head received; + list_init(&received); while (!list_empty(&sq->notify_queue)) { struct queue_message *qm = list_first_entry( &sq->notify_queue, struct queue_message, node); @@ -257,8 +271,7 @@ handle_message(struct serialqueue *sq, double eventtime, int len) qm->len = 0; qm->sent_time = sq->last_receive_sent_time; qm->receive_time = eventtime; - list_add_tail(&qm->node, &sq->receive_queue); - must_wake = 1; + list_add_tail(&qm->node, &received); } // Process message @@ -276,8 +289,14 @@ handle_message(struct serialqueue *sq, double eventtime, int len) ? sq->last_receive_sent_time : 0.); qm->receive_time = get_monotonic(); // must be time post read() qm->receive_time -= calculate_bittime(sq, len); - list_add_tail(&qm->node, &sq->receive_queue); - must_wake = 1; + list_add_tail(&qm->node, &received); + } + + if (!list_empty(&received)) { + pthread_mutex_lock(&sq->receiver.lock); + list_join_tail(&received, &sq->receiver.queue); + check_wake_receive(&sq->receiver); + pthread_mutex_unlock(&sq->receiver.lock); } // Check fast readers @@ -289,16 +308,11 @@ handle_message(struct serialqueue *sq, double eventtime, int len) continue; // Release main lock and invoke callback pthread_mutex_lock(&sq->fast_reader_dispatch_lock); - if (must_wake) - check_wake_receive(sq); pthread_mutex_unlock(&sq->lock); fr->func(fr, sq->input_buf, len); pthread_mutex_unlock(&sq->fast_reader_dispatch_lock); return; } - - if (must_wake) - check_wake_receive(sq); pthread_mutex_unlock(&sq->lock); } @@ -627,9 +641,9 @@ background_thread(void *data) set_thread_name(sq->name); pollreactor_run(sq->pr); - pthread_mutex_lock(&sq->lock); - check_wake_receive(sq); - pthread_mutex_unlock(&sq->lock); + pthread_mutex_lock(&sq->receiver.lock); + check_wake_receive(&sq->receiver); + pthread_mutex_unlock(&sq->receiver.lock); return NULL; } @@ -678,21 +692,24 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id list_init(&sq->upcoming_queues); list_init(&sq->ready_queues); list_init(&sq->sent_queue); - list_init(&sq->receive_queue); + list_init(&sq->receiver.queue); list_init(&sq->notify_queue); list_init(&sq->fast_readers); // Debugging list_init(&sq->old_sent); - list_init(&sq->old_receive); + list_init(&sq->receiver.old_receive); debug_queue_alloc(&sq->old_sent, DEBUG_QUEUE_SENT); - debug_queue_alloc(&sq->old_receive, DEBUG_QUEUE_RECEIVE); + debug_queue_alloc(&sq->receiver.old_receive, DEBUG_QUEUE_RECEIVE); // Thread setup ret = pthread_mutex_init(&sq->lock, NULL); if (ret) goto fail; - ret = pthread_cond_init(&sq->cond, NULL); + ret = pthread_mutex_init(&sq->receiver.lock, NULL); + if (ret) + goto fail; + ret = pthread_cond_init(&sq->receiver.cond, NULL); if (ret) goto fail; ret = pthread_mutex_init(&sq->fast_reader_dispatch_lock, NULL); @@ -730,10 +747,12 @@ serialqueue_free(struct serialqueue *sq) serialqueue_exit(sq); pthread_mutex_lock(&sq->lock); message_queue_free(&sq->sent_queue); - message_queue_free(&sq->receive_queue); + pthread_mutex_lock(&sq->receiver.lock); + message_queue_free(&sq->receiver.queue); + message_queue_free(&sq->receiver.old_receive); + pthread_mutex_unlock(&sq->receiver.lock); message_queue_free(&sq->notify_queue); message_queue_free(&sq->old_sent); - message_queue_free(&sq->old_receive); while (!list_empty(&sq->ready_queues)) { struct command_queue* cq = list_first_entry( &sq->ready_queues, struct command_queue, ready.node); @@ -863,20 +882,21 @@ serialqueue_send(struct serialqueue *sq, struct command_queue *cq, uint8_t *msg void __visible serialqueue_pull(struct serialqueue *sq, struct pull_queue_message *pqm) { - pthread_mutex_lock(&sq->lock); + struct receiver *receiver = &sq->receiver; + pthread_mutex_lock(&receiver->lock); // Wait for message to be available - while (list_empty(&sq->receive_queue)) { + while (list_empty(&receiver->queue)) { if (pollreactor_is_exit(sq->pr)) goto exit; - sq->receive_waiting = 1; - int ret = pthread_cond_wait(&sq->cond, &sq->lock); + receiver->waiting = 1; + int ret = pthread_cond_wait(&receiver->cond, &receiver->lock); if (ret) report_errno("pthread_cond_wait", ret); } // Remove message from queue struct queue_message *qm = list_first_entry( - &sq->receive_queue, struct queue_message, node); + &receiver->queue, struct queue_message, node); list_del(&qm->node); // Copy message @@ -886,16 +906,14 @@ serialqueue_pull(struct serialqueue *sq, struct pull_queue_message *pqm) pqm->receive_time = qm->receive_time; pqm->notify_id = qm->notify_id; if (qm->len) - debug_queue_add(&sq->old_receive, qm); - else - message_free(qm); - - pthread_mutex_unlock(&sq->lock); + qm = _debug_queue_add(&receiver->old_receive, qm); + pthread_mutex_unlock(&receiver->lock); + message_free(qm); return; exit: pqm->len = -1; - pthread_mutex_unlock(&sq->lock); + pthread_mutex_unlock(&receiver->lock); } void __visible @@ -968,18 +986,27 @@ serialqueue_extract_old(struct serialqueue *sq, int sentq , struct pull_queue_message *q, int max) { int count = sentq ? DEBUG_QUEUE_SENT : DEBUG_QUEUE_RECEIVE; - struct list_head *rootp = sentq ? &sq->old_sent : &sq->old_receive; + struct list_head *rootp; + rootp = sentq ? &sq->old_sent : &sq->receiver.old_receive; struct list_head replacement, current; list_init(&replacement); debug_queue_alloc(&replacement, count); list_init(¤t); // Atomically replace existing debug list with new zero'd list - pthread_mutex_lock(&sq->lock); - list_join_tail(rootp, ¤t); - list_init(rootp); - list_join_tail(&replacement, rootp); - pthread_mutex_unlock(&sq->lock); + if (rootp == &sq->receiver.old_receive) { + pthread_mutex_lock(&sq->receiver.lock); + list_join_tail(rootp, ¤t); + list_init(rootp); + list_join_tail(&replacement, rootp); + pthread_mutex_unlock(&sq->receiver.lock); + } else { + pthread_mutex_lock(&sq->lock); + list_join_tail(rootp, ¤t); + list_init(rootp); + list_join_tail(&replacement, rootp); + pthread_mutex_unlock(&sq->lock); + } // Walk the debug list int pos = 0; From 46af133613e6bc67605f93d7969b3be1b1ee8d12 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 20 Sep 2025 02:42:40 +0200 Subject: [PATCH 228/411] serialqueue: decouple transmit requests Signed-off-by: Timofey Titovets --- klippy/chelper/serialqueue.c | 77 +++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 59e799fb3..0bdda0d01 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -46,11 +46,18 @@ struct receiver { struct list_head old_receive; }; +struct transmit_requests { + int pipe_fds[2]; + pthread_mutex_t lock; // protects variables below + struct list_head upcoming_queues; + int upcoming_bytes; + uint64_t need_kick_clock; +}; + struct serialqueue { // Input reading struct pollreactor *pr; int serial_fd, serial_fd_type, client_id; - int pipe_fds[2]; uint8_t input_buf[4096]; uint8_t need_sync; int input_pos; @@ -71,11 +78,9 @@ struct serialqueue { struct list_head sent_queue; double srtt, rttvar, rto; // Pending transmission message queues - struct list_head upcoming_queues; - int upcoming_bytes; + struct transmit_requests transmit_requests; struct list_head ready_queues; int ready_bytes, need_ack_bytes, last_ack_bytes; - uint64_t need_kick_clock; struct list_head notify_queue; double last_write_fail_time; // Fastreader support @@ -152,7 +157,7 @@ check_wake_receive(struct receiver *receiver) static void kick_bg_thread(struct serialqueue *sq) { - int ret = write(sq->pipe_fds[1], ".", 1); + int ret = write(sq->transmit_requests.pipe_fds[1], ".", 1); if (ret < 0) report_errno("pipe write", ret); } @@ -371,7 +376,7 @@ static void kick_event(struct serialqueue *sq, double eventtime) { char dummy[4096]; - int ret = read(sq->pipe_fds[0], dummy, sizeof(dummy)); + int ret = read(sq->transmit_requests.pipe_fds[0], dummy, sizeof(dummy)); if (ret < 0) report_errno("pipe read", ret); pollreactor_update_timer(sq->pr, SQPT_COMMAND, PR_NOW); @@ -549,7 +554,9 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) uint64_t ack_clock = clock_from_time(&sq->ce, idletime); uint64_t min_stalled_clock = MAX_CLOCK, min_ready_clock = MAX_CLOCK; struct command_queue *cq, *_ncq; - list_for_each_entry_safe(cq, _ncq, &sq->upcoming_queues, upcoming.node) { + pthread_mutex_lock(&sq->transmit_requests.lock); + list_for_each_entry_safe(cq, _ncq, &sq->transmit_requests.upcoming_queues, + upcoming.node) { int not_in_ready_queues = list_empty(&cq->ready.msg_queue); // Move messages from the upcoming.msg_queue to the ready.msg_queue struct queue_message *qm, *_nqm; @@ -561,7 +568,7 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) } list_del(&qm->node); list_add_tail(&qm->node, &cq->ready.msg_queue); - sq->upcoming_bytes -= qm->len; + sq->transmit_requests.upcoming_bytes -= qm->len; sq->ready_bytes += qm->len; } // Remove cq from the list if it is now empty @@ -587,21 +594,26 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) // Check for messages to send if (sq->ready_bytes >= MESSAGE_PAYLOAD_MAX) - return PR_NOW; + goto now; if (! sq->ce.est_freq) { if (sq->ready_bytes) - return PR_NOW; - sq->need_kick_clock = MAX_CLOCK; + goto now; + sq->transmit_requests.need_kick_clock = MAX_CLOCK; + pthread_mutex_unlock(&sq->transmit_requests.lock); return PR_NEVER; } uint64_t reqclock_delta = MIN_REQTIME_DELTA * sq->ce.est_freq; if (min_ready_clock <= ack_clock + reqclock_delta) - return PR_NOW; + goto now; uint64_t wantclock = min_ready_clock - reqclock_delta; if (min_stalled_clock < wantclock) wantclock = min_stalled_clock; - sq->need_kick_clock = wantclock; + sq->transmit_requests.need_kick_clock = wantclock; + pthread_mutex_unlock(&sq->transmit_requests.lock); return idletime + (wantclock - ack_clock) / sq->ce.est_freq; +now: + pthread_mutex_unlock(&sq->transmit_requests.lock); + return PR_NOW; } // Callback timer to send data to the serial port @@ -661,7 +673,7 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id strncpy(sq->name, name, sizeof(sq->name)); sq->name[sizeof(sq->name)-1] = '\0'; - int ret = pipe(sq->pipe_fds); + int ret = pipe(sq->transmit_requests.pipe_fds); if (ret) goto fail; @@ -669,12 +681,13 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id sq->pr = pollreactor_alloc(SQPF_NUM, SQPT_NUM, sq); pollreactor_add_fd(sq->pr, SQPF_SERIAL, serial_fd, input_event , serial_fd_type==SQT_DEBUGFILE); - pollreactor_add_fd(sq->pr, SQPF_PIPE, sq->pipe_fds[0], kick_event, 0); + pollreactor_add_fd(sq->pr, SQPF_PIPE, sq->transmit_requests.pipe_fds[0] + , kick_event, 0); pollreactor_add_timer(sq->pr, SQPT_RETRANSMIT, retransmit_event); pollreactor_add_timer(sq->pr, SQPT_COMMAND, command_event); fd_set_non_blocking(serial_fd); - fd_set_non_blocking(sq->pipe_fds[0]); - fd_set_non_blocking(sq->pipe_fds[1]); + fd_set_non_blocking(sq->transmit_requests.pipe_fds[0]); + fd_set_non_blocking(sq->transmit_requests.pipe_fds[1]); // Retransmit setup sq->send_seq = 1; @@ -688,8 +701,9 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id } // Queues - sq->need_kick_clock = MAX_CLOCK; - list_init(&sq->upcoming_queues); + sq->transmit_requests.need_kick_clock = MAX_CLOCK; + list_init(&sq->transmit_requests.upcoming_queues); + pthread_mutex_init(&sq->transmit_requests.lock, NULL); list_init(&sq->ready_queues); list_init(&sq->sent_queue); list_init(&sq->receiver.queue); @@ -759,12 +773,15 @@ serialqueue_free(struct serialqueue *sq) list_del(&cq->ready.node); message_queue_free(&cq->ready.msg_queue); } - while (!list_empty(&sq->upcoming_queues)) { + pthread_mutex_lock(&sq->transmit_requests.lock); + while (!list_empty(&sq->transmit_requests.upcoming_queues)) { struct command_queue *cq = list_first_entry( - &sq->upcoming_queues, struct command_queue, upcoming.node); + &sq->transmit_requests.upcoming_queues, + struct command_queue, upcoming.node); list_del(&cq->upcoming.node); message_queue_free(&cq->upcoming.msg_queue); } + pthread_mutex_unlock(&sq->transmit_requests.lock); pthread_mutex_unlock(&sq->lock); pollreactor_free(sq->pr); free(sq); @@ -835,17 +852,19 @@ serialqueue_send_batch(struct serialqueue *sq, struct command_queue *cq qm = list_first_entry(msgs, struct queue_message, node); // Add list to cq->upcoming_queue - pthread_mutex_lock(&sq->lock); + pthread_mutex_lock(&sq->transmit_requests.lock); if (list_empty(&cq->upcoming.msg_queue)) - list_add_tail(&cq->upcoming.node, &sq->upcoming_queues); + list_add_tail(&cq->upcoming.node, + &sq->transmit_requests.upcoming_queues); list_join_tail(msgs, &cq->upcoming.msg_queue); - sq->upcoming_bytes += len; + sq->transmit_requests.upcoming_bytes += len; + int mustwake = 0; - if (qm->min_clock < sq->need_kick_clock) { - sq->need_kick_clock = 0; + if (qm->min_clock < sq->transmit_requests.need_kick_clock) { + sq->transmit_requests.need_kick_clock = 0; mustwake = 1; } - pthread_mutex_unlock(&sq->lock); + pthread_mutex_unlock(&sq->transmit_requests.lock); // Wake the background thread if necessary if (mustwake) @@ -964,7 +983,9 @@ serialqueue_get_stats(struct serialqueue *sq, char *buf, int len) { struct serialqueue stats; pthread_mutex_lock(&sq->lock); + pthread_mutex_lock(&sq->transmit_requests.lock); memcpy(&stats, sq, sizeof(stats)); + pthread_mutex_unlock(&sq->transmit_requests.lock); pthread_mutex_unlock(&sq->lock); snprintf(buf, len, "bytes_write=%u bytes_read=%u" @@ -977,7 +998,7 @@ serialqueue_get_stats(struct serialqueue *sq, char *buf, int len) , (int)stats.send_seq, (int)stats.receive_seq , (int)stats.retransmit_seq , stats.srtt, stats.rttvar, stats.rto - , stats.ready_bytes, stats.upcoming_bytes); + , stats.ready_bytes, stats.transmit_requests.upcoming_bytes); } // Extract old messages stored in the debug queues From 7634773cf10727a34839b593fdedb0ed4b132abd Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 18 Oct 2025 14:32:39 -0400 Subject: [PATCH 229/411] serialqueue: Move definition of transmit_requests in serialqueue Move the definition of the struct so that it is more clear that it has its own locking. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 0bdda0d01..2c941d4c8 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -61,11 +61,12 @@ struct serialqueue { uint8_t input_buf[4096]; uint8_t need_sync; int input_pos; + // Multi-threaded support for pushing and pulling messages + struct receiver receiver; + struct transmit_requests transmit_requests; // Threading char name[16]; pthread_t tid; - // SerialHDL reader - struct receiver receiver; pthread_mutex_t lock; // protects variables below // Baud / clock tracking int receive_window; @@ -78,7 +79,6 @@ struct serialqueue { struct list_head sent_queue; double srtt, rttvar, rto; // Pending transmission message queues - struct transmit_requests transmit_requests; struct list_head ready_queues; int ready_bytes, need_ack_bytes, last_ack_bytes; struct list_head notify_queue; From d453ab3d1946631eaddd166249ecf1e504d7834e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 18 Oct 2025 14:10:42 -0400 Subject: [PATCH 230/411] serialqueue: Simplify command_event() There's no reason to attempt to handle multiple buffer transmissions in a single command_event() call. Handle the transmit case outside of the command building loop. If data is transmitted, then get a new timestamp from the pollreactor and retry before sleeping. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 2c941d4c8..ed5dafdc3 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -1,6 +1,6 @@ // Serial port command queuing // -// Copyright (C) 2016-2021 Kevin O'Connor +// Copyright (C) 2016-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -626,20 +626,19 @@ command_event(struct serialqueue *sq, double eventtime) double waketime; for (;;) { waketime = check_send_command(sq, buflen, eventtime); - if (waketime != PR_NOW || buflen + MESSAGE_MAX > sizeof(buf)) { - if (buflen) { - // Write message blocks - do_write(sq, buf, buflen); - sq->bytes_write += buflen; - double idletime = (eventtime > sq->idle_time - ? eventtime : sq->idle_time); - sq->idle_time = idletime + calculate_bittime(sq, buflen); - buflen = 0; - } - if (waketime != PR_NOW) - break; - } + if (waketime != PR_NOW) + break; buflen += build_and_send_command(sq, &buf[buflen], buflen, eventtime); + if (buflen + MESSAGE_MAX > sizeof(buf)) + break; + } + if (buflen) { + // Write message blocks + do_write(sq, buf, buflen); + sq->bytes_write += buflen; + double idletime = eventtime > sq->idle_time ? eventtime : sq->idle_time; + sq->idle_time = idletime + calculate_bittime(sq, buflen); + waketime = PR_NOW; } pthread_mutex_unlock(&sq->lock); return waketime; From cfb616664e5a59e540f07402bf7653a705fc9e85 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 18 Oct 2025 14:27:23 -0400 Subject: [PATCH 231/411] serialqueue: Introduce new check_upcoming_queues() internal function Move the upcoming queue movement logic to a new function. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 55 +++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index ed5dafdc3..55a6fb855 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -531,30 +531,12 @@ build_and_send_command(struct serialqueue *sq, uint8_t *buf, int pending return len; } -// Determine the time the next serial data should be sent -static double -check_send_command(struct serialqueue *sq, int pending, double eventtime) +// Move messages from upcoming queues to ready queues +static uint64_t +check_upcoming_queues(struct serialqueue *sq, uint64_t ack_clock) { - if (sq->send_seq - sq->receive_seq >= MAX_PENDING_BLOCKS - && sq->receive_seq != (uint64_t)-1) - // Need an ack before more messages can be sent - return PR_NEVER; - if (sq->send_seq > sq->receive_seq && sq->receive_window) { - int need_ack_bytes = sq->need_ack_bytes + MESSAGE_MAX; - if (sq->last_ack_seq < sq->receive_seq) - need_ack_bytes += sq->last_ack_bytes; - if (need_ack_bytes > sq->receive_window) - // Wait for ack from past messages before sending next message - return PR_NEVER; - } - - // Check for stalled messages now ready - double idletime = eventtime > sq->idle_time ? eventtime : sq->idle_time; - idletime += calculate_bittime(sq, pending + MESSAGE_MIN); - uint64_t ack_clock = clock_from_time(&sq->ce, idletime); - uint64_t min_stalled_clock = MAX_CLOCK, min_ready_clock = MAX_CLOCK; + uint64_t min_stalled_clock = MAX_CLOCK; struct command_queue *cq, *_ncq; - pthread_mutex_lock(&sq->transmit_requests.lock); list_for_each_entry_safe(cq, _ncq, &sq->transmit_requests.upcoming_queues, upcoming.node) { int not_in_ready_queues = list_empty(&cq->ready.msg_queue); @@ -578,7 +560,36 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) if (not_in_ready_queues && !list_empty(&cq->ready.msg_queue)) list_add_tail(&cq->ready.node, &sq->ready_queues); } + return min_stalled_clock; +} + +// Determine the time the next serial data should be sent +static double +check_send_command(struct serialqueue *sq, int pending, double eventtime) +{ + if (sq->send_seq - sq->receive_seq >= MAX_PENDING_BLOCKS + && sq->receive_seq != (uint64_t)-1) + // Need an ack before more messages can be sent + return PR_NEVER; + if (sq->send_seq > sq->receive_seq && sq->receive_window) { + int need_ack_bytes = sq->need_ack_bytes + MESSAGE_MAX; + if (sq->last_ack_seq < sq->receive_seq) + need_ack_bytes += sq->last_ack_bytes; + if (need_ack_bytes > sq->receive_window) + // Wait for ack from past messages before sending next message + return PR_NEVER; + } + + // Check for upcoming messages now ready + double idletime = eventtime > sq->idle_time ? eventtime : sq->idle_time; + idletime += calculate_bittime(sq, pending + MESSAGE_MIN); + uint64_t ack_clock = clock_from_time(&sq->ce, idletime); + pthread_mutex_lock(&sq->transmit_requests.lock); + uint64_t min_stalled_clock = check_upcoming_queues(sq, ack_clock); + // Check if it is still needed to send messages from the ready_queues + uint64_t min_ready_clock = MAX_CLOCK; + struct command_queue *cq; list_for_each_entry(cq, &sq->ready_queues, ready.node) { // Update min_ready_clock struct queue_message *qm = list_first_entry( From bfb627c3d0bd3545e7d34cacc0e23dfca9d2ae55 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 18 Oct 2025 14:44:10 -0400 Subject: [PATCH 232/411] serialqueue: Cache min_release_clock in pending queues Maintain the next needed wakeup time for entries in the pending queues. This avoids needing to walk the upcoming queues when it is known that nothing is ready to be released. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 55a6fb855..842399df1 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -51,7 +51,7 @@ struct transmit_requests { pthread_mutex_t lock; // protects variables below struct list_head upcoming_queues; int upcoming_bytes; - uint64_t need_kick_clock; + uint64_t need_kick_clock, min_release_clock; }; struct serialqueue { @@ -535,6 +535,9 @@ build_and_send_command(struct serialqueue *sq, uint8_t *buf, int pending static uint64_t check_upcoming_queues(struct serialqueue *sq, uint64_t ack_clock) { + if (ack_clock < sq->transmit_requests.min_release_clock) + return sq->transmit_requests.min_release_clock; + uint64_t min_stalled_clock = MAX_CLOCK; struct command_queue *cq, *_ncq; list_for_each_entry_safe(cq, _ncq, &sq->transmit_requests.upcoming_queues, @@ -560,6 +563,7 @@ check_upcoming_queues(struct serialqueue *sq, uint64_t ack_clock) if (not_in_ready_queues && !list_empty(&cq->ready.msg_queue)) list_add_tail(&cq->ready.node, &sq->ready_queues); } + sq->transmit_requests.min_release_clock = min_stalled_clock; return min_stalled_clock; } @@ -712,6 +716,7 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id // Queues sq->transmit_requests.need_kick_clock = MAX_CLOCK; + sq->transmit_requests.min_release_clock = MAX_CLOCK; list_init(&sq->transmit_requests.upcoming_queues); pthread_mutex_init(&sq->transmit_requests.lock, NULL); list_init(&sq->ready_queues); @@ -860,20 +865,23 @@ serialqueue_send_batch(struct serialqueue *sq, struct command_queue *cq if (! len) return; qm = list_first_entry(msgs, struct queue_message, node); + uint64_t min_clock = qm->min_clock; // Add list to cq->upcoming_queue + int mustwake = 0; pthread_mutex_lock(&sq->transmit_requests.lock); - if (list_empty(&cq->upcoming.msg_queue)) + if (list_empty(&cq->upcoming.msg_queue)) { list_add_tail(&cq->upcoming.node, &sq->transmit_requests.upcoming_queues); + if (min_clock < sq->transmit_requests.min_release_clock) + sq->transmit_requests.min_release_clock = min_clock; + if (min_clock < sq->transmit_requests.need_kick_clock) { + sq->transmit_requests.need_kick_clock = 0; + mustwake = 1; + } + } list_join_tail(msgs, &cq->upcoming.msg_queue); sq->transmit_requests.upcoming_bytes += len; - - int mustwake = 0; - if (qm->min_clock < sq->transmit_requests.need_kick_clock) { - sq->transmit_requests.need_kick_clock = 0; - mustwake = 1; - } pthread_mutex_unlock(&sq->transmit_requests.lock); // Wake the background thread if necessary From 804a9d11e8e9152ac0cba94d2719ea45f4a4d096 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 18 Oct 2025 15:07:56 -0400 Subject: [PATCH 233/411] serialqueue: Reduce time transmit_requests lock is held Only hold the lock in the serialqueue thread when moving messages to the ready queue and when setting the next need_kick_clock. Only set the need_kick_clock just prior to sleeping. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 65 +++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 842399df1..f1b8eb11e 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -535,8 +535,13 @@ build_and_send_command(struct serialqueue *sq, uint8_t *buf, int pending static uint64_t check_upcoming_queues(struct serialqueue *sq, uint64_t ack_clock) { - if (ack_clock < sq->transmit_requests.min_release_clock) - return sq->transmit_requests.min_release_clock; + pthread_mutex_lock(&sq->transmit_requests.lock); + sq->transmit_requests.need_kick_clock = 0; + uint64_t min_release_clock = sq->transmit_requests.min_release_clock; + if (ack_clock < min_release_clock) { + pthread_mutex_unlock(&sq->transmit_requests.lock); + return min_release_clock; + } uint64_t min_stalled_clock = MAX_CLOCK; struct command_queue *cq, *_ncq; @@ -564,9 +569,24 @@ check_upcoming_queues(struct serialqueue *sq, uint64_t ack_clock) list_add_tail(&cq->ready.node, &sq->ready_queues); } sq->transmit_requests.min_release_clock = min_stalled_clock; + pthread_mutex_unlock(&sq->transmit_requests.lock); return min_stalled_clock; } +// Set the next transmit queue need_kick_clock +static int +update_need_kick_clock(struct serialqueue *sq, uint64_t wantclock) +{ + pthread_mutex_lock(&sq->transmit_requests.lock); + if (wantclock > sq->transmit_requests.min_release_clock) { + pthread_mutex_unlock(&sq->transmit_requests.lock); + return -1; + } + sq->transmit_requests.need_kick_clock = wantclock; + pthread_mutex_unlock(&sq->transmit_requests.lock); + return 0; +} + // Determine the time the next serial data should be sent static double check_send_command(struct serialqueue *sq, int pending, double eventtime) @@ -588,9 +608,21 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) double idletime = eventtime > sq->idle_time ? eventtime : sq->idle_time; idletime += calculate_bittime(sq, pending + MESSAGE_MIN); uint64_t ack_clock = clock_from_time(&sq->ce, idletime); - pthread_mutex_lock(&sq->transmit_requests.lock); uint64_t min_stalled_clock = check_upcoming_queues(sq, ack_clock); + // Check if a block is fully ready to send + if (sq->ready_bytes >= MESSAGE_PAYLOAD_MAX) + return PR_NOW; + if (! sq->ce.est_freq) { + // Clock unknown during initial startup - recheck on each add + if (sq->ready_bytes) + return PR_NOW; + int mustwake = update_need_kick_clock(sq, 1); + if (mustwake) + return eventtime; + return PR_NEVER; + } + // Check if it is still needed to send messages from the ready_queues uint64_t min_ready_clock = MAX_CLOCK; struct command_queue *cq; @@ -606,29 +638,22 @@ check_send_command(struct serialqueue *sq, int pending, double eventtime) if (req_clock < min_ready_clock) min_ready_clock = req_clock; } - - // Check for messages to send - if (sq->ready_bytes >= MESSAGE_PAYLOAD_MAX) - goto now; - if (! sq->ce.est_freq) { - if (sq->ready_bytes) - goto now; - sq->transmit_requests.need_kick_clock = MAX_CLOCK; - pthread_mutex_unlock(&sq->transmit_requests.lock); - return PR_NEVER; - } uint64_t reqclock_delta = MIN_REQTIME_DELTA * sq->ce.est_freq; if (min_ready_clock <= ack_clock + reqclock_delta) - goto now; + return PR_NOW; + + // Determine next wakeup time + if (pending) + // Caller wont sleep anyway - just return + return eventtime; uint64_t wantclock = min_ready_clock - reqclock_delta; if (min_stalled_clock < wantclock) wantclock = min_stalled_clock; - sq->transmit_requests.need_kick_clock = wantclock; - pthread_mutex_unlock(&sq->transmit_requests.lock); + int mustwake = update_need_kick_clock(sq, wantclock); + if (mustwake) + // Raced with add of new command - avoid sleeping + return eventtime; return idletime + (wantclock - ack_clock) / sq->ce.est_freq; -now: - pthread_mutex_unlock(&sq->transmit_requests.lock); - return PR_NOW; } // Callback timer to send data to the serial port From 459e77c4f9ef8f0fb5f62ad671e337d4ba3815b4 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 18 Oct 2025 15:17:29 -0400 Subject: [PATCH 234/411] serialqueue: Keep moving messages from pending queues to ready queues Keep moving messages from the pending queues to the ready queues even if it's not currently valid to transmit messages to the mcu. This improves the statistics when debugging. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index f1b8eb11e..f90f3c8ad 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -587,29 +587,30 @@ update_need_kick_clock(struct serialqueue *sq, uint64_t wantclock) return 0; } -// Determine the time the next serial data should be sent +// Determine if ready to send commands (or the amount of time to sleep if not) static double check_send_command(struct serialqueue *sq, int pending, double eventtime) { + // Check for upcoming messages now ready + double idletime = eventtime > sq->idle_time ? eventtime : sq->idle_time; + idletime += calculate_bittime(sq, pending + MESSAGE_MIN); + uint64_t ack_clock = clock_from_time(&sq->ce, idletime); + uint64_t min_stalled_clock = check_upcoming_queues(sq, ack_clock); + + // Check if valid to send messages if (sq->send_seq - sq->receive_seq >= MAX_PENDING_BLOCKS && sq->receive_seq != (uint64_t)-1) // Need an ack before more messages can be sent - return PR_NEVER; + return eventtime + 0.250; if (sq->send_seq > sq->receive_seq && sq->receive_window) { int need_ack_bytes = sq->need_ack_bytes + MESSAGE_MAX; if (sq->last_ack_seq < sq->receive_seq) need_ack_bytes += sq->last_ack_bytes; if (need_ack_bytes > sq->receive_window) // Wait for ack from past messages before sending next message - return PR_NEVER; + return eventtime + 0.250; } - // Check for upcoming messages now ready - double idletime = eventtime > sq->idle_time ? eventtime : sq->idle_time; - idletime += calculate_bittime(sq, pending + MESSAGE_MIN); - uint64_t ack_clock = clock_from_time(&sq->ce, idletime); - uint64_t min_stalled_clock = check_upcoming_queues(sq, ack_clock); - // Check if a block is fully ready to send if (sq->ready_bytes >= MESSAGE_PAYLOAD_MAX) return PR_NOW; From 35dffc151684a928d2bc24fc8b89d5e617e54b05 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 4 Nov 2025 11:45:54 -0500 Subject: [PATCH 235/411] serialqueue: Rework check_wake_receive() to receive_append_wake() Rename the function and perform list appending and locking in that function. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index f90f3c8ad..2762d406f 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -143,14 +143,20 @@ debug_queue_add(struct list_head *root, struct queue_message *qm) message_free(old); } -// Wake up the receiver thread if it is waiting +// Add messages and wake up the receiver thread if it is waiting static void -check_wake_receive(struct receiver *receiver) +receive_append_wake(struct receiver *receiver, struct list_head *msgs) { + int dokick = 0; + pthread_mutex_lock(&receiver->lock); + list_join_tail(msgs, &receiver->queue); if (receiver->waiting) { receiver->waiting = 0; - pthread_cond_signal(&receiver->cond); + dokick = 1; } + pthread_mutex_unlock(&receiver->lock); + if (dokick) + pthread_cond_signal(&receiver->cond); } // Write to the internal pipe to wake the background thread if in poll @@ -297,12 +303,8 @@ handle_message(struct serialqueue *sq, double eventtime, int len) list_add_tail(&qm->node, &received); } - if (!list_empty(&received)) { - pthread_mutex_lock(&sq->receiver.lock); - list_join_tail(&received, &sq->receiver.queue); - check_wake_receive(&sq->receiver); - pthread_mutex_unlock(&sq->receiver.lock); - } + if (!list_empty(&received)) + receive_append_wake(&sq->receiver, &received); // Check fast readers struct fastreader *fr; @@ -693,9 +695,10 @@ background_thread(void *data) set_thread_name(sq->name); pollreactor_run(sq->pr); - pthread_mutex_lock(&sq->receiver.lock); - check_wake_receive(&sq->receiver); - pthread_mutex_unlock(&sq->receiver.lock); + // Wake any waiting receivers + struct list_head dummy; + list_init(&dummy); + receive_append_wake(&sq->receiver, &dummy); return NULL; } From b6c1b5a6dd5981ec7ce098622d906b0635dc576d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 4 Nov 2025 12:35:26 -0500 Subject: [PATCH 236/411] serialqueue: Minor simplification in serialqueue_extract_old() Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index 2762d406f..d30433554 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -1054,26 +1054,24 @@ serialqueue_extract_old(struct serialqueue *sq, int sentq , struct pull_queue_message *q, int max) { int count = sentq ? DEBUG_QUEUE_SENT : DEBUG_QUEUE_RECEIVE; - struct list_head *rootp; - rootp = sentq ? &sq->old_sent : &sq->receiver.old_receive; struct list_head replacement, current; list_init(&replacement); debug_queue_alloc(&replacement, count); list_init(¤t); // Atomically replace existing debug list with new zero'd list - if (rootp == &sq->receiver.old_receive) { - pthread_mutex_lock(&sq->receiver.lock); - list_join_tail(rootp, ¤t); - list_init(rootp); - list_join_tail(&replacement, rootp); - pthread_mutex_unlock(&sq->receiver.lock); - } else { + if (sentq) { pthread_mutex_lock(&sq->lock); - list_join_tail(rootp, ¤t); - list_init(rootp); - list_join_tail(&replacement, rootp); + list_join_tail(&sq->old_sent, ¤t); + list_init(&sq->old_sent); + list_join_tail(&replacement, &sq->old_sent); pthread_mutex_unlock(&sq->lock); + } else { + pthread_mutex_lock(&sq->receiver.lock); + list_join_tail(&sq->receiver.old_receive, ¤t); + list_init(&sq->receiver.old_receive); + list_join_tail(&replacement, &sq->receiver.old_receive); + pthread_mutex_unlock(&sq->receiver.lock); } // Walk the debug list From 01b3a6a8e4f015ef02bb0cb79c1ef425cea88613 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 18 Sep 2025 12:31:30 -0400 Subject: [PATCH 237/411] gcode: Replace Coord named tuple with custom tuple class Replace the existing Coord() class with one that supports more than 4 coordinates. Signed-off-by: Kevin O'Connor --- klippy/gcode.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/klippy/gcode.py b/klippy/gcode.py index 93ef38a1e..ca815b4d7 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -1,15 +1,26 @@ # Parse gcode commands # -# Copyright (C) 2016-2024 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, re, logging, collections, shlex +import os, re, logging, collections, shlex, operator class CommandError(Exception): pass -Coord = collections.namedtuple('Coord', ('x', 'y', 'z', 'e')) +# Custom "tuple" class for coordinates - add easy access to x, y, z components +class Coord(tuple): + __slots__ = () + def __new__(cls, x, y, z, e, *args): + return tuple.__new__(cls, (x, y, z, e) + args) + def __getnewargs__(self): + return tuple(self) + x = property(operator.itemgetter(0)) + y = property(operator.itemgetter(1)) + z = property(operator.itemgetter(2)) + e = property(operator.itemgetter(3)) +# Class for handling gcode command parameters (gcmd) class GCodeCommand: error = CommandError def __init__(self, gcode, command, commandline, params, need_ack): From d0b1b832ddc4c5515586e49b670e8bc6e19077e6 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 26 Sep 2025 22:31:55 -0400 Subject: [PATCH 238/411] gcode: Change Coord() class to initialize from a list or tuple Instead of passing arguments as parameters, pass them as a list (or tuple). This simplifies the callers and makes it easier to pass additional parameters. Signed-off-by: Kevin O'Connor --- klippy/extras/gcode_move.py | 6 +++--- klippy/extras/motion_report.py | 4 ++-- klippy/gcode.py | 8 ++++---- klippy/kinematics/cartesian.py | 4 ++-- klippy/kinematics/corexy.py | 4 ++-- klippy/kinematics/corexz.py | 4 ++-- klippy/kinematics/delta.py | 4 ++-- klippy/kinematics/deltesian.py | 4 ++-- klippy/kinematics/generic_cartesian.py | 4 ++-- klippy/kinematics/hybrid_corexy.py | 4 ++-- klippy/kinematics/hybrid_corexz.py | 4 ++-- klippy/kinematics/none.py | 2 +- klippy/kinematics/polar.py | 4 ++-- klippy/kinematics/rotary_delta.py | 4 ++-- klippy/kinematics/winch.py | 4 ++-- klippy/toolhead.py | 2 +- 16 files changed, 33 insertions(+), 33 deletions(-) diff --git a/klippy/extras/gcode_move.py b/klippy/extras/gcode_move.py index 929f7a807..21174a904 100644 --- a/klippy/extras/gcode_move.py +++ b/klippy/extras/gcode_move.py @@ -107,9 +107,9 @@ class GCodeMove: 'extrude_factor': self.extrude_factor, 'absolute_coordinates': self.absolute_coord, '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), + 'homing_origin': self.Coord(self.homing_position[:4]), + 'position': self.Coord(self.last_position[:4]), + 'gcode_position': self.Coord(move_position), } def reset_last_position(self): if self.is_printer_ready: diff --git a/klippy/extras/motion_report.py b/klippy/extras/motion_report.py index 9f3a0385b..1c2dc9a6b 100644 --- a/klippy/extras/motion_report.py +++ b/klippy/extras/motion_report.py @@ -151,7 +151,7 @@ class PrinterMotionReport: self.next_status_time = 0. gcode = self.printer.lookup_object('gcode') self.last_status = { - 'live_position': gcode.Coord(0., 0., 0., 0.), + 'live_position': gcode.Coord((0., 0., 0.)), 'live_velocity': 0., 'live_extruder_velocity': 0., 'steppers': [], 'trapq': [], } @@ -236,7 +236,7 @@ class PrinterMotionReport: evelocity = velocity # Report status self.last_status = dict(self.last_status) - self.last_status['live_position'] = toolhead.Coord(*(xyzpos + epos)) + self.last_status['live_position'] = toolhead.Coord(xyzpos + epos) self.last_status['live_velocity'] = xyzvelocity self.last_status['live_extruder_velocity'] = evelocity return self.last_status diff --git a/klippy/gcode.py b/klippy/gcode.py index ca815b4d7..11b57cfab 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -11,10 +11,10 @@ class CommandError(Exception): # Custom "tuple" class for coordinates - add easy access to x, y, z components class Coord(tuple): __slots__ = () - def __new__(cls, x, y, z, e, *args): - return tuple.__new__(cls, (x, y, z, e) + args) - def __getnewargs__(self): - return tuple(self) + def __new__(cls, t): + if len(t) < 4: + t = tuple(t) + (0,) * (4 - len(t)) + return tuple.__new__(cls, t) x = property(operator.itemgetter(0)) y = property(operator.itemgetter(1)) z = property(operator.itemgetter(2)) diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index edd9c1a04..5362e57d0 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -18,8 +18,8 @@ class CartKinematics: for rail, axis in zip(self.rails, 'xyz'): rail.setup_itersolve('cartesian_stepper_alloc', axis.encode()) ranges = [r.get_range() for r in self.rails] - self.axes_min = toolhead.Coord(*[r[0] for r in ranges], e=0.) - self.axes_max = toolhead.Coord(*[r[1] for r in ranges], e=0.) + self.axes_min = toolhead.Coord([r[0] for r in ranges]) + self.axes_max = toolhead.Coord([r[1] for r in ranges]) self.dc_module = None if config.has_section('dual_carriage'): dc_config = config.getsection('dual_carriage') diff --git a/klippy/kinematics/corexy.py b/klippy/kinematics/corexy.py index f940c57fa..7d074ae63 100644 --- a/klippy/kinematics/corexy.py +++ b/klippy/kinematics/corexy.py @@ -28,8 +28,8 @@ class CoreXYKinematics: 'max_z_accel', max_accel, above=0., maxval=max_accel) self.limits = [(1.0, -1.0)] * 3 ranges = [r.get_range() for r in self.rails] - self.axes_min = toolhead.Coord(*[r[0] for r in ranges], e=0.) - self.axes_max = toolhead.Coord(*[r[1] for r in ranges], e=0.) + self.axes_min = toolhead.Coord([r[0] for r in ranges]) + self.axes_max = toolhead.Coord([r[1] for r in ranges]) def get_steppers(self): return [s for rail in self.rails for s in rail.get_steppers()] def calc_position(self, stepper_positions): diff --git a/klippy/kinematics/corexz.py b/klippy/kinematics/corexz.py index d4a0eb350..6950dd7b0 100644 --- a/klippy/kinematics/corexz.py +++ b/klippy/kinematics/corexz.py @@ -28,8 +28,8 @@ class CoreXZKinematics: 'max_z_accel', max_accel, above=0., maxval=max_accel) self.limits = [(1.0, -1.0)] * 3 ranges = [r.get_range() for r in self.rails] - self.axes_min = toolhead.Coord(*[r[0] for r in ranges], e=0.) - self.axes_max = toolhead.Coord(*[r[1] for r in ranges], e=0.) + self.axes_min = toolhead.Coord([r[0] for r in ranges]) + self.axes_max = toolhead.Coord([r[1] for r in ranges]) def get_steppers(self): return [s for rail in self.rails for s in rail.get_steppers()] def calc_position(self, stepper_positions): diff --git a/klippy/kinematics/delta.py b/klippy/kinematics/delta.py index 5ec4d54cc..9f4932b09 100644 --- a/klippy/kinematics/delta.py +++ b/klippy/kinematics/delta.py @@ -85,8 +85,8 @@ class DeltaKinematics: " and %.2fmm)" % (max_xy, math.sqrt(self.slow_xy2), math.sqrt(self.very_slow_xy2))) - self.axes_min = toolhead.Coord(-max_xy, -max_xy, self.min_z, 0.) - self.axes_max = toolhead.Coord(max_xy, max_xy, self.max_z, 0.) + self.axes_min = toolhead.Coord((-max_xy, -max_xy, self.min_z)) + self.axes_max = toolhead.Coord((max_xy, max_xy, self.max_z)) self.set_position([0., 0., 0.], "") def get_steppers(self): return [s for rail in self.rails for s in rail.get_steppers()] diff --git a/klippy/kinematics/deltesian.py b/klippy/kinematics/deltesian.py index a62a58bd2..a4fb328b5 100644 --- a/klippy/kinematics/deltesian.py +++ b/klippy/kinematics/deltesian.py @@ -83,8 +83,8 @@ class DeltesianKinematics: above=0., maxval=max_velocity) self.max_z_accel = config.getfloat('max_z_accel', max_accel, above=0., maxval=max_accel) - self.axes_min = toolhead.Coord(*[l[0] for l in self.limits], e=0.) - self.axes_max = toolhead.Coord(*[l[1] for l in self.limits], e=0.) + self.axes_min = toolhead.Coord([l[0] for l in self.limits]) + self.axes_max = toolhead.Coord([l[1] for l in self.limits]) self.homed_axis = [False] * 3 self.set_position([0., 0., 0.], "") def get_steppers(self): diff --git a/klippy/kinematics/generic_cartesian.py b/klippy/kinematics/generic_cartesian.py index 49e16eec3..413cc11ca 100644 --- a/klippy/kinematics/generic_cartesian.py +++ b/klippy/kinematics/generic_cartesian.py @@ -306,8 +306,8 @@ class GenericCartesianKinematics: axes = [a for a, (l, h) in zip("xyz", self.limits) if l <= h] ranges = [c.get_rail().get_range() for c in self.get_primary_carriages()] - axes_min = gcode.Coord(*[r[0] for r in ranges], e=0.) - axes_max = gcode.Coord(*[r[1] for r in ranges], e=0.) + axes_min = gcode.Coord([r[0] for r in ranges]) + axes_max = gcode.Coord([r[1] for r in ranges]) return { 'homed_axes': "".join(axes), 'axis_minimum': axes_min, diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index 22884b93c..bbe7bfa55 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -21,8 +21,8 @@ class HybridCoreXYKinematics: self.rails[1].setup_itersolve('cartesian_stepper_alloc', b'y') self.rails[2].setup_itersolve('cartesian_stepper_alloc', b'z') ranges = [r.get_range() for r in self.rails] - self.axes_min = toolhead.Coord(*[r[0] for r in ranges], e=0.) - self.axes_max = toolhead.Coord(*[r[1] for r in ranges], e=0.) + self.axes_min = toolhead.Coord([r[0] for r in ranges]) + self.axes_max = toolhead.Coord([r[1] for r in ranges]) self.dc_module = None if config.has_section('dual_carriage'): dc_config = config.getsection('dual_carriage') diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index 1cd646e28..3e2dd4788 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -21,8 +21,8 @@ class HybridCoreXZKinematics: self.rails[1].setup_itersolve('cartesian_stepper_alloc', b'y') self.rails[2].setup_itersolve('cartesian_stepper_alloc', b'z') ranges = [r.get_range() for r in self.rails] - self.axes_min = toolhead.Coord(*[r[0] for r in ranges], e=0.) - self.axes_max = toolhead.Coord(*[r[1] for r in ranges], e=0.) + self.axes_min = toolhead.Coord([r[0] for r in ranges]) + self.axes_max = toolhead.Coord([r[1] for r in ranges]) self.dc_module = None if config.has_section('dual_carriage'): dc_config = config.getsection('dual_carriage') diff --git a/klippy/kinematics/none.py b/klippy/kinematics/none.py index d9f9d21d2..35ebd59ce 100644 --- a/klippy/kinematics/none.py +++ b/klippy/kinematics/none.py @@ -6,7 +6,7 @@ class NoneKinematics: def __init__(self, toolhead, config): - self.axes_minmax = toolhead.Coord(0., 0., 0., 0.) + self.axes_minmax = toolhead.Coord((0., 0., 0.)) def get_steppers(self): return [] def calc_position(self, stepper_positions): diff --git a/klippy/kinematics/polar.py b/klippy/kinematics/polar.py index 2e467d50e..422c9981f 100644 --- a/klippy/kinematics/polar.py +++ b/klippy/kinematics/polar.py @@ -31,8 +31,8 @@ class PolarKinematics: self.limit_xy2 = -1. max_xy = self.rails[0].get_range()[1] min_z, max_z = self.rails[1].get_range() - self.axes_min = toolhead.Coord(-max_xy, -max_xy, min_z, 0.) - self.axes_max = toolhead.Coord(max_xy, max_xy, max_z, 0.) + self.axes_min = toolhead.Coord((-max_xy, -max_xy, min_z)) + self.axes_max = toolhead.Coord((max_xy, max_xy, max_z)) def get_steppers(self): return list(self.steppers) def calc_position(self, stepper_positions): diff --git a/klippy/kinematics/rotary_delta.py b/klippy/kinematics/rotary_delta.py index 42b0a706d..2f6d5060b 100644 --- a/klippy/kinematics/rotary_delta.py +++ b/klippy/kinematics/rotary_delta.py @@ -71,8 +71,8 @@ class RotaryDeltaKinematics: "Delta max build height %.2fmm (radius tapered above %.2fmm)" % (self.max_z, self.limit_z)) max_xy = math.sqrt(self.max_xy2) - self.axes_min = toolhead.Coord(-max_xy, -max_xy, self.min_z, 0.) - self.axes_max = toolhead.Coord(max_xy, max_xy, self.max_z, 0.) + self.axes_min = toolhead.Coord((-max_xy, -max_xy, self.min_z)) + self.axes_max = toolhead.Coord((max_xy, max_xy, self.max_z)) self.set_position([0., 0., 0.], "") def get_steppers(self): return [s for rail in self.rails for s in rail.get_steppers()] diff --git a/klippy/kinematics/winch.py b/klippy/kinematics/winch.py index 2fa06ef22..77f3cc734 100644 --- a/klippy/kinematics/winch.py +++ b/klippy/kinematics/winch.py @@ -23,8 +23,8 @@ class WinchKinematics: s.set_trapq(toolhead.get_trapq()) # Setup boundary checks acoords = list(zip(*self.anchors)) - self.axes_min = toolhead.Coord(*[min(a) for a in acoords], e=0.) - self.axes_max = toolhead.Coord(*[max(a) for a in acoords], e=0.) + self.axes_min = toolhead.Coord([min(a) for a in acoords]) + self.axes_max = toolhead.Coord([max(a) for a in acoords]) self.set_position([0., 0., 0.], "") def get_steppers(self): return list(self.steppers) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 8f16e75b6..47ca7136c 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -500,7 +500,7 @@ class ToolHead: 'stalls': self.print_stall, 'estimated_print_time': estimated_print_time, 'extruder': extruder.get_name(), - 'position': self.Coord(*self.commanded_pos[:4]), + 'position': self.Coord(self.commanded_pos[:4]), 'max_velocity': self.max_velocity, 'max_accel': self.max_accel, 'minimum_cruise_ratio': self.min_cruise_ratio, From ac6cab9168c48a62770622850e0fab5057d32d20 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 9 Oct 2025 21:43:59 -0400 Subject: [PATCH 239/411] gcode_move: Export extra axes in status reference Change "homing_origin" and "position" to support more than 4 components. Export a new "axis_map" value to describe the contents at each component index. Signed-off-by: Kevin O'Connor --- klippy/extras/gcode_move.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/klippy/extras/gcode_move.py b/klippy/extras/gcode_move.py index 21174a904..c2b307663 100644 --- a/klippy/extras/gcode_move.py +++ b/klippy/extras/gcode_move.py @@ -107,9 +107,10 @@ class GCodeMove: 'extrude_factor': self.extrude_factor, 'absolute_coordinates': self.absolute_coord, 'absolute_extrude': self.absolute_extrude, - 'homing_origin': self.Coord(self.homing_position[:4]), - 'position': self.Coord(self.last_position[:4]), + 'homing_origin': self.Coord(self.homing_position), + 'position': self.Coord(self.last_position), 'gcode_position': self.Coord(move_position), + 'axis_map': self.axis_map, } def reset_last_position(self): if self.is_printer_ready: @@ -122,7 +123,8 @@ class GCodeMove: if ea is None: continue gcode_id = ea.get_axis_gcode_id() - if gcode_id is None or gcode_id in axis_map or gcode_id in "FN": + if (gcode_id is None or len(gcode_id) != 1 or not gcode_id.isupper() + or gcode_id in axis_map or gcode_id in "FN"): continue axis_map[gcode_id] = index self.axis_map = axis_map From 67db551be573f5c585f4bb9d0c59a415b957dab9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 9 Oct 2025 21:59:37 -0400 Subject: [PATCH 240/411] toolhead: Export "extra axes" coordinates in status Update the "position" status to include extra axes. Export a new "extra_axes" value that describes the class associated with each component of the coordinate. Signed-off-by: Kevin O'Connor --- klippy/extras/manual_stepper.py | 5 ++++- klippy/toolhead.py | 13 +++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 8a8911e18..175f20915 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -10,6 +10,7 @@ from . import force_move class ManualStepper: def __init__(self, config): self.printer = config.get_printer() + self.name = config.get_name() if config.get('endstop_pin', None) is not None: self.can_home = True self.rail = stepper.LookupRail( @@ -36,11 +37,13 @@ class ManualStepper: self.instant_corner_v = 0. self.gaxis_limit_velocity = self.gaxis_limit_accel = 0. # Register commands - stepper_name = config.get_name().split()[1] + stepper_name = self.name.split()[1] gcode = self.printer.lookup_object('gcode') gcode.register_mux_command('MANUAL_STEPPER', "STEPPER", stepper_name, self.cmd_MANUAL_STEPPER, desc=self.cmd_MANUAL_STEPPER_help) + def get_name(self): + return self.name def sync_print_time(self): toolhead = self.printer.lookup_object('toolhead') print_time = toolhead.get_last_move_time() diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 47ca7136c..e375d92e8 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -237,6 +237,8 @@ class ToolHead: self.Coord = gcode.Coord extruder = kinematics.extruder.DummyExtruder(self.printer) self.extra_axes = [extruder] + self.extra_axes_status = {} + self._build_extra_axes_status() kin_name = config.get('kinematics') try: mod = importlib.import_module('kinematics.' + kin_name) @@ -425,16 +427,21 @@ class ToolHead: if not self.can_pause: break eventtime = self.reactor.pause(eventtime + 0.100) + def _build_extra_axes_status(self): + self.extra_axes_status = {ea.get_name(): e_index + 3 + for e_index, ea in enumerate(self.extra_axes)} def set_extruder(self, extruder, extrude_pos): # XXX - should use add_extra_axis self.extra_axes[0] = extruder self.commanded_pos[3] = extrude_pos + self._build_extra_axes_status() def get_extruder(self): return self.extra_axes[0] def add_extra_axis(self, ea, axis_pos): self._flush_lookahead() self.extra_axes.append(ea) self.commanded_pos.append(axis_pos) + self._build_extra_axes_status() self.printer.send_event("toolhead:update_extra_axes") def remove_extra_axis(self, ea): self._flush_lookahead() @@ -443,6 +450,7 @@ class ToolHead: ea_index = self.extra_axes.index(ea) + 3 self.commanded_pos.pop(ea_index) self.extra_axes.pop(ea_index - 3) + self._build_extra_axes_status() self.printer.send_event("toolhead:update_extra_axes") def get_extra_axes(self): return [None, None, None] + self.extra_axes @@ -500,11 +508,12 @@ class ToolHead: 'stalls': self.print_stall, 'estimated_print_time': estimated_print_time, 'extruder': extruder.get_name(), - 'position': self.Coord(self.commanded_pos[:4]), + 'position': self.Coord(self.commanded_pos), 'max_velocity': self.max_velocity, 'max_accel': self.max_accel, 'minimum_cruise_ratio': self.min_cruise_ratio, - 'square_corner_velocity': self.square_corner_velocity}) + 'square_corner_velocity': self.square_corner_velocity, + 'extra_axes': self.extra_axes_status}) return res def _handle_shutdown(self): self.can_pause = False From 24a1116c7c6084491dc2dd3be35722939eb6f396 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 10 Oct 2025 20:07:05 -0400 Subject: [PATCH 241/411] motion_report: Support reporting of manual_stepper trapqs Signed-off-by: Kevin O'Connor --- klippy/extras/motion_report.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/klippy/extras/motion_report.py b/klippy/extras/motion_report.py index 1c2dc9a6b..ce8753182 100644 --- a/klippy/extras/motion_report.py +++ b/klippy/extras/motion_report.py @@ -177,6 +177,10 @@ class PrinterMotionReport: break etrapq = extruder.get_trapq() self.dtrapqs[ename] = DumpTrapQ(self.printer, ename, etrapq) + # Lookup manual_stepper trapqs + for msname, ms in self.printer.lookup_objects("manual_stepper"): + mstrapq = ms.get_trapq() + self.dtrapqs[msname] = DumpTrapQ(self.printer, msname, mstrapq) # Populate 'trapq' and 'steppers' in get_status result self.last_status['steppers'] = list(sorted(self.steppers.keys())) self.last_status['trapq'] = list(sorted(self.dtrapqs.keys())) From c3bf7b109e104cb9d727ceeb0af4afd7597a5d6e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 10 Oct 2025 20:20:18 -0400 Subject: [PATCH 242/411] motion_report: Support reporting live_position of extra axes Signed-off-by: Kevin O'Connor --- klippy/extras/motion_report.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/klippy/extras/motion_report.py b/klippy/extras/motion_report.py index ce8753182..83dffa874 100644 --- a/klippy/extras/motion_report.py +++ b/klippy/extras/motion_report.py @@ -220,27 +220,32 @@ class PrinterMotionReport: if eventtime < self.next_status_time or not self.dtrapqs: return self.last_status self.next_status_time = eventtime + STATUS_REFRESH_TIME - xyzpos = (0., 0., 0.) - epos = (0.,) xyzvelocity = evelocity = 0. # Calculate current requested toolhead position + toolhead = self.printer.lookup_object('toolhead') + extra_axes = toolhead.get_extra_axes() + live_pos = [0.] * len(extra_axes) mcu = self.printer.lookup_object('mcu') print_time = mcu.estimated_print_time(eventtime) pos, velocity = self.dtrapqs['toolhead'].get_trapq_position(print_time) if pos is not None: - xyzpos = pos[:3] + live_pos[:3] = pos[:3] xyzvelocity = velocity - # Calculate requested position of currently active extruder - toolhead = self.printer.lookup_object('toolhead') - ehandler = self.dtrapqs.get(toolhead.get_extruder().get_name()) - if ehandler is not None: - pos, velocity = ehandler.get_trapq_position(print_time) - if pos is not None: - epos = (pos[0],) - evelocity = velocity + # Calculate requested position of extra axes + for ea_index, ea in enumerate(extra_axes): + if ea is None: + continue + eaname = ea.get_name() + ehandler = self.dtrapqs.get(eaname) + if ehandler is not None: + pos, velocity = ehandler.get_trapq_position(print_time) + if pos is not None: + live_pos[ea_index] = pos[0] + if ea_index == 4: + evelocity = velocity # Report status self.last_status = dict(self.last_status) - self.last_status['live_position'] = toolhead.Coord(xyzpos + epos) + self.last_status['live_position'] = toolhead.Coord(live_pos) self.last_status['live_velocity'] = xyzvelocity self.last_status['live_extruder_velocity'] = evelocity return self.last_status From c7b1e401f0ef731b00bee7f7e707f71a32cf7dab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 9 Oct 2025 22:29:30 -0400 Subject: [PATCH 243/411] docs: Update Status_Reference.md with information on accessing coordinates Update the docs with information on how to access the "extra axes" of coordinate values. Signed-off-by: Kevin O'Connor --- docs/Status_Reference.md | 59 +++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index 77313682f..4e4ed5c76 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -214,17 +214,16 @@ The following information is available in the `gcode_move` object (this object is always available): - `gcode_position`: The current position of the toolhead relative to the current G-Code origin. That is, positions that one might - directly send to a `G1` command. It is possible to access the x, y, - z, and e components of this position (eg, `gcode_position.x`). + directly send to a `G1` command. This value is encoded as a + [coordinate](#accessing-coordinates). - `position`: The last commanded position of the toolhead using the - coordinate system specified in the config file. It is possible to - access the x, y, z, and e components of this position (eg, - `position.x`). + coordinate system specified in the config file. This value is + encoded as a [coordinate](#accessing-coordinates). - `homing_origin`: The origin of the gcode coordinate system (relative to the coordinate system specified in the config file) to use after a `G28` command. The `SET_GCODE_OFFSET` command can alter this - position. It is possible to access the x, y, and z components of - this position (eg, `homing_origin.x`). + position. This value is encoded as a + [coordinate](#accessing-coordinates). - `speed`: The last speed set in a `G1` command (in mm/s). - `speed_factor`: The "speed factor override" as set by an `M220` command. This is a floating point value such that 1.0 means no @@ -236,6 +235,10 @@ The following information is available in the `gcode_move` object coordinate mode or False if in `G91` relative mode. - `absolute_extrude`: This returns True if in `M82` absolute extrude mode or False if in `M83` relative mode. +- `axis_map`: Provides a mechanism for finding the coordinate + component for a given G-Code id that is used in `G1` commands. See + the [Accessing Coordinates](#accessing-coordinates) section for + details. ## hall_filament_width_sensor @@ -361,7 +364,8 @@ The following information is available in the `motion_report` object (this object is automatically available if any stepper config section is defined): - `live_position`: The requested toolhead position interpolated to the - current time. + current time. This value is encoded as a + [coordinate](#accessing-coordinates). - `live_velocity`: The requested toolhead velocity (in mm/s) at the current time. - `live_extruder_velocity`: The requested extruder velocity (in mm/s) @@ -550,9 +554,8 @@ objects (eg, `[tmc2208 stepper_x]`): The following information is available in the `toolhead` object (this object is always available): - `position`: The last commanded position of the toolhead relative to - the coordinate system specified in the config file. It is possible - to access the x, y, z, and e components of this position (eg, - `position.x`). + the coordinate system specified in the config file. This value is + encoded as a [coordinate](#accessing-coordinates). - `extruder`: The name of the currently active extruder. For example, in a macro one could use `printer[printer.toolhead.extruder].target` to get the target temperature of the current extruder. @@ -560,8 +563,8 @@ The following information is available in the `toolhead` object "homed" state. This is a string containing one or more of "x", "y", "z". - `axis_minimum`, `axis_maximum`: The axis travel limits (mm) after - homing. It is possible to access the x, y, z components of this - limit value (eg, `axis_minimum.x`, `axis_maximum.z`). + homing. This value is encoded as a + [coordinate](#accessing-coordinates). - For Delta printers the `cone_start_z` is the max z height at maximum radius (`printer.toolhead.cone_start_z`). - `max_velocity`, `max_accel`, `minimum_cruise_ratio`, @@ -571,6 +574,10 @@ The following information is available in the `toolhead` object - `stalls`: The total number of times (since the last restart) that the printer had to be paused because the toolhead moved faster than moves could be read from the G-Code input. +- `extra_axes`: Provides a mechanism for finding the coordinate + component for extra axes available in standard `G1` type move + commands. See the [Accessing Coordinates](#accessing-coordinates) + section for details. ## dual_carriage @@ -627,3 +634,29 @@ The following information is available in the `z_tilt` object (this object is available if z_tilt is defined): - `applied`: True if the z-tilt leveling process has been run and completed successfully. + +## Accessing Coordinates + +Some status fields provide a "coordinate". For macro users these +fields may be accessed by component name +(eg,`{printer.toolhead.position.x}`), where the component name may be +"x", "y", or "z". + +For developers using the Klipper API Server these fields are +transmitted as a list - for example: `{"toolhead": {"position": [1.0, +2.0, 3.0, 7.3, 19.2]}}` . The first three components of the list +correspond with the x, y, and z axes. + +A coordinate will typically have at least 3 components (x, y, and z), +however there may also be additional components. Care should be taken +when accessing any of these additional components as the ordering and +number of components may change at run-time. + +One may use `{printer.gcode_move.axis_map}` and/or +`{printer.toolhead.extra_axes}` to determine the number of components +and the ordering of components. For example, to access the "E" +component one could use +`{printer.toolhead.position[printer.gcode_move.axis_map.E]}`. Or, if +one wanted to find the component associated with the "extruder" +object, one could use +`{printer.toolhead.position[printer.toolhead.extra_axes.extruder]}`. From f04895f540d4a3cb0597535071e650e9f81c8c60 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 9 Oct 2025 22:45:41 -0400 Subject: [PATCH 244/411] docs: Update Config_Changes.md with recent coordinate changes Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index cb43d9564..cfa2fe9af 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,26 @@ All dates in this document are approximate. ## Changes +20251106: The status fields `{printer.toolhead.position}`, +`{printer.gcode_move.position}`, +`{printer.gcode_move.gcode_position}`, and +`{printer.motion_report.live_position}` are changing. These +coordinates used to always contain four components, but now may +contain additional components. The ordering and number of components +may change at run-time - see the +[status reference](Status_Reference.md#accessing-coordinates) for +important details. Accessing any of these coordinates in macros using +the ".e" accessor is deprecated - use something like +`{printer.toolhead.position[printer.gcode_move.axis_map.E]}` as an +alternative. + +20251106: The status fields `{printer.gcode_move.homing_origin}`, +`{printer.toolhead.axis_min}`, and `{printer.toolhead.axis_max}` +currently contain four components where the fourth component is always +zero. This behavior is deprecated. In the future these coordinates may +contain only three components. For additional information see the +[status reference](Status_Reference.md#accessing-coordinates). + 20251010: During normal printing the command processing will now attempt to stay one second ahead of printer movement (reduced from two seconds previously). From db7acaa34a367c74c5551ab9649409fdadc7c5ae Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 9 Nov 2025 18:57:16 -0500 Subject: [PATCH 245/411] motion_report: Fix off-by-one error in live_extruder_velocity calculation Signed-off-by: Kevin O'Connor --- klippy/extras/motion_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/motion_report.py b/klippy/extras/motion_report.py index 83dffa874..356d6f86a 100644 --- a/klippy/extras/motion_report.py +++ b/klippy/extras/motion_report.py @@ -241,7 +241,7 @@ class PrinterMotionReport: pos, velocity = ehandler.get_trapq_position(print_time) if pos is not None: live_pos[ea_index] = pos[0] - if ea_index == 4: + if ea_index == 3: evelocity = velocity # Report status self.last_status = dict(self.last_status) From ecccd21f2a6f5b0c07ea2c213bb9290a4de6bc2c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 6 Nov 2025 12:00:16 -0500 Subject: [PATCH 246/411] toolhead: Don't report dummy extruders in extra_axes status report A "dummy extruder" that is created when no "[extruder]" config section is defined does not have a "name". Avoid reporting that empty name in the `{printer.toolhead.extra_axes}" status report. Signed-off-by: Kevin O'Connor --- klippy/toolhead.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index e375d92e8..5f1224356 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -428,8 +428,9 @@ class ToolHead: break eventtime = self.reactor.pause(eventtime + 0.100) def _build_extra_axes_status(self): - self.extra_axes_status = {ea.get_name(): e_index + 3 - for e_index, ea in enumerate(self.extra_axes)} + enames = [ea.get_name() for ea in self.extra_axes] + self.extra_axes_status = {n: e_index + 3 + for e_index, n in enumerate(enames) if n} def set_extruder(self, extruder, extrude_pos): # XXX - should use add_extra_axis self.extra_axes[0] = extruder From 39ac48339aa77d3bce747f8f5980ed90a13daf79 Mon Sep 17 00:00:00 2001 From: Alistair Buxton Date: Fri, 7 Nov 2025 18:06:13 +0000 Subject: [PATCH 247/411] neopixel: Increase PULSE_LONG_TICKS to 800 for compatibility This timing is also known as T1H in various datasheets. Increasing it should improve compatibility with various revisions and clones of the WS2812 LED. The short version is that 800 is the timing used by Adafruit's popular NeoPixel Arduino library, and it has no problem driving my BTT RGBW kit LEDs, while Klipper cannot drive them properly without this patch. The real upper limit to this value is something like 5000ns so increasing it should not cause new compatibility problems for LEDs that currently work. Signed-off-by: Alistair Buxton --- src/neopixel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neopixel.c b/src/neopixel.c index fe3f724f9..d4121f057 100644 --- a/src/neopixel.c +++ b/src/neopixel.c @@ -75,7 +75,7 @@ neopixel_delay(neopixel_time_t start, neopixel_time_t ticks) #endif // Minimum amount of time for a '1 bit' to be reliably detected -#define PULSE_LONG_TICKS nsecs_to_ticks(650) +#define PULSE_LONG_TICKS nsecs_to_ticks(800) // Minimum amount of time for any level change to be reliably detected #define EDGE_MIN_TICKS nsecs_to_ticks(200) // Minimum average time needed to transmit each bit (two level changes) From edaa61471f796712cf98025e23538717633a36eb Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 24 Oct 2025 01:18:30 +0200 Subject: [PATCH 248/411] bme680: fix infinite data wait Signed-off-by: Timofey Titovets --- klippy/extras/bme280.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/klippy/extras/bme280.py b/klippy/extras/bme280.py index 284380d2e..50aa72e8a 100644 --- a/klippy/extras/bme280.py +++ b/klippy/extras/bme280.py @@ -87,8 +87,8 @@ MODE_PERIODIC = 3 RUN_GAS = 1 << 4 NB_CONV_0 = 0 EAS_NEW_DATA = 1 << 7 -GAS_DONE = 1 << 6 -MEASURE_DONE = 1 << 5 +GAS_IN_PROGRESS = 1 << 6 +MEASURE_IN_PROGRESS = 1 << 5 RESET_CHIP_VALUE = 0xB6 BME_CHIPS = { @@ -511,14 +511,6 @@ class BME280: return comp_press def _sample_bme680(self, eventtime): - def data_ready(stat, run_gas): - new_data = (stat & EAS_NEW_DATA) - gas_done = not (stat & GAS_DONE) - meas_done = not (stat & MEASURE_DONE) - if not run_gas: - gas_done = True - return new_data and gas_done and meas_done - run_gas = False # Check VOC once a while if self.reactor.monotonic() - self.last_gas_time > 3: @@ -536,11 +528,14 @@ class BME280: try: # wait until results are ready status = self.read_register('EAS_STATUS_0', 1)[0] - while not data_ready(status, run_gas): + while status & MEASURE_IN_PROGRESS: self.reactor.pause( self.reactor.monotonic() + self.max_sample_time) status = self.read_register('EAS_STATUS_0', 1)[0] + # Nothing in progress and no new data + if not status & EAS_NEW_DATA: + return self.reactor.monotonic() + REPORT_TIME data = self.read_register('PRESSURE_MSB', 8) gas_data = [0, 0] if run_gas: From 053342f64232416f8044c10d2ac29a162e3c36a4 Mon Sep 17 00:00:00 2001 From: Pascal Date: Tue, 18 Nov 2025 00:28:43 +0100 Subject: [PATCH 249/411] MCU: Fail early if clock speed is too high to support max nominal duration (#7122) This adds a dedicated check for that case to be able to find the root cause of a misconfiguration earlier. Also, replace occurrences of hardcoded max tick count. Signed-off-by: Pascal Pieper --- klippy/mcu.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 7798c7517..ea0fb050f 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -9,6 +9,14 @@ import serialhdl, msgproto, pins, chelper, clocksync class error(Exception): pass +# Minimum time host needs to get scheduled events queued into mcu +MIN_SCHEDULE_TIME = 0.100 +# The maximum number of clock cycles an MCU is expected +# to schedule into the future, due to the protocol and firmware. +MAX_SCHEDULE_TICKS = (1<<31) - 1 +# Maximum time all MCUs can internally schedule into the future. +# Directly caused by the limitation of MAX_SCHEDULE_TICKS. +MAX_NOMINAL_DURATION = 3.0 ###################################################################### # Command transmit helper classes @@ -382,7 +390,7 @@ class MCU_digital_out: raise pins.error("Pin with max duration must have start" " value equal to shutdown value") mdur_ticks = self._mcu.seconds_to_clock(self._max_duration) - if mdur_ticks >= 1<<31: + if mdur_ticks > MAX_SCHEDULE_TICKS: raise pins.error("Digital pin max duration too large") self._mcu.request_move_queue_slot() self._oid = self._mcu.create_oid() @@ -439,7 +447,7 @@ class MCU_pwm: self._last_clock = self._mcu.print_time_to_clock(printtime + 0.200) cycle_ticks = self._mcu.seconds_to_clock(self._cycle_time) mdur_ticks = self._mcu.seconds_to_clock(self._max_duration) - if mdur_ticks >= 1<<31: + if mdur_ticks > MAX_SCHEDULE_TICKS: raise pins.error("PWM pin max duration too large") if self._hardware_pwm: self._pwm_max = self._mcu.get_constant_float("PWM_MAX") @@ -461,7 +469,7 @@ class MCU_pwm: # Software PWM if self._shutdown_value not in [0., 1.]: raise pins.error("shutdown value must be 0.0 or 1.0 on soft pwm") - if cycle_ticks >= 1<<31: + if cycle_ticks > MAX_SCHEDULE_TICKS: raise pins.error("PWM pin cycle time too large") self._mcu.request_move_queue_slot() self._oid = self._mcu.create_oid() @@ -552,11 +560,6 @@ class MCU_adc: # Main MCU class (and its helper classes) ###################################################################### -# Minimum time host needs to get scheduled events queued into mcu -MIN_SCHEDULE_TIME = 0.100 -# Maximum time all MCUs can internally schedule into the future -MAX_NOMINAL_DURATION = 3.0 - # Support for restarting a micro-controller class MCURestartHelper: def __init__(self, config, conn_helper): @@ -996,6 +999,12 @@ class MCUConfigHelper: if cname.startswith("RESERVE_PINS_"): for pin in value.split(','): pin_resolver.reserve_pin(pin, cname[13:]) + if MAX_NOMINAL_DURATION * self._mcu_freq > MAX_SCHEDULE_TICKS: + max_possible = MAX_SCHEDULE_TICKS * 1 / self._mcu_freq + raise error("Too high clock speed for MCU '%s' " % (self._name,) + + "to be able to resolve a maximum nominal duration " + + "of %ds. " % (MAX_NOMINAL_DURATION) + + "Max possible duration: %ds" % (max_possible)) # Config creation helpers def setup_pin(self, pin_type, pin_params): pcs = {'endstop': MCU_endstop, From 90b7f8230d96e4a30bee51c456510574978f9e32 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 17 Nov 2025 18:30:49 -0500 Subject: [PATCH 250/411] mcu: Rework formatting of MAX_SCHEDULE_TICKS error string Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index ea0fb050f..14be8a5c0 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -1001,10 +1001,10 @@ class MCUConfigHelper: pin_resolver.reserve_pin(pin, cname[13:]) if MAX_NOMINAL_DURATION * self._mcu_freq > MAX_SCHEDULE_TICKS: max_possible = MAX_SCHEDULE_TICKS * 1 / self._mcu_freq - raise error("Too high clock speed for MCU '%s' " % (self._name,) + - "to be able to resolve a maximum nominal duration " + - "of %ds. " % (MAX_NOMINAL_DURATION) + - "Max possible duration: %ds" % (max_possible)) + raise error("Too high clock speed for MCU '%s'" + " to be able to resolve a maximum nominal duration" + " of %ds. Max possible duration: %ds" + % (self._name, MAX_NOMINAL_DURATION, max_possible)) # Config creation helpers def setup_pin(self, pin_type, pin_params): pcs = {'endstop': MCU_endstop, From 63ae0d7df4505c4bcc5b0d3a88b8bde6ca49a38f Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 30 Oct 2025 21:05:20 +0100 Subject: [PATCH 251/411] analyzers: label undefined derivatives Signed-off-by: Timofey Titovets --- scripts/motan/analyzers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/motan/analyzers.py b/scripts/motan/analyzers.py index 2796362fe..91678012f 100644 --- a/scripts/motan/analyzers.py +++ b/scripts/motan/analyzers.py @@ -33,7 +33,8 @@ class GenDerivative: elif '(mm/s)' in units: rep = [('Velocity', 'Acceleration'), ('(mm/s)', '(mm/s^2)')] else: - return {'label': 'Derivative', 'units': 'Unknown'} + return {'label': 'Derivative of %s' % (label['label']), + 'units': 'Unknown'} for old, new in rep: lname = lname.replace(old, new).replace(old.lower(), new.lower()) units = units.replace(old, new).replace(old.lower(), new.lower()) From b4c7cf4a3365a7bdc05b1ac146f8f48b5666ce8d Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 30 Oct 2025 23:30:14 +0100 Subject: [PATCH 252/411] analyzers: define sos filter Signed-off-by: Timofey Titovets --- scripts/motan/analyzers.py | 71 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/scripts/motan/analyzers.py b/scripts/motan/analyzers.py index 91678012f..c3476b931 100644 --- a/scripts/motan/analyzers.py +++ b/scripts/motan/analyzers.py @@ -197,6 +197,77 @@ class GenSmoothed: return data AHandlers["smooth"] = GenSmoothed +class GenSOSFilter: + ParametersMin = 5 + ParametersMax = 6 + DataSets = [ + ('sos(,{filt,filtfilt},highpass,,)', + 'SOS highpass filtered dataset'), + ('sos(,{filt,filtfilt},lowpass,,)', + 'SOS lowpass filtered dataset'), + ('sos(,{filt,filtfilt},bandpass,,,)', + 'SOS lowpass filtered dataset'), + ('sos(,{filt,filtfilt},notch,,)', + 'SOS notch filtered dataset'), + ] + def __init__(self, amanager, name_parts): + self.amanager = amanager + self.source = name_parts[1] + + from numpy import array + from scipy.signal import sosfiltfilt, sosfilt, sosfilt_zi + self.array = array + self.type = name_parts[2] + if self.type == "filt": + self.sosfilt = sosfilt + self.sosfilt_zi = sosfilt_zi + elif self.type == "filtfilt": + self.sosfilt = sosfiltfilt + else: + amanager.error("Unknown sos type '%s'" % (self.type,)) + + filter_type = name_parts[3] + amanager.setup_dataset(self.source) + inv_seg_time = 1.0 / amanager.get_segment_time() + + if filter_type in ('highpass', 'lowpass', 'bandpass'): + from scipy.signal import butter + order = int(name_parts[4]) + cutoff = float(name_parts[5]) + desc = "%.fHz" % (cutoff) + if filter_type == 'bandpass': + cutoff = (cutoff, float(name_parts[6])) + desc = "%.1f..%.1fHz" % cutoff + self.sos = butter(order, cutoff, filter_type, output='sos', + fs=inv_seg_time) + self.filter_desc = "%s %s order %d" % (filter_type, desc, order) + elif filter_type == 'notch': + from scipy.signal import iirnotch, tf2sos + freq = float(name_parts[4]) + quality = float(name_parts[5]) + b, a = iirnotch(freq, Q=quality, fs=inv_seg_time) + self.sos = tf2sos(b, a) + self.filter_desc = "notch %.1fHz Q: %.1f" % (freq, quality) + else: + raise amanager.error("Unknown filter type '%s'" % (filter_type,)) + + def get_label(self): + label = self.amanager.get_label(self.source) + lname = "SOS %s (%s)" % (self.filter_desc, label['label']) + return {'label': lname, 'units': label['units']} + + def generate_data(self): + data = self.amanager.get_datasets()[self.source] + data_array = self.array(data) + if self.type == "filt": + zi = self.sosfilt_zi(self.sos) * data_array[0] + filtered, _ = self.sosfilt(self.sos, data_array, zi=zi) + else: + filtered = self.sosfilt(self.sos, data_array) + return filtered.tolist() + +AHandlers["sos"] = GenSOSFilter + # Calculate a kinematic stepper position from the toolhead requested position class GenKinematicPosition: ParametersMin = ParametersMax = 1 From 2aff840f68e31acc0c2e470211291671e44a7b3a Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sun, 19 Oct 2025 02:04:02 +0200 Subject: [PATCH 253/411] shaper_calibrate: Reworked multi-file shaper autocalibration Signed-off-by: Dmitry Butyugin --- klippy/extras/resonance_tester.py | 15 ++- klippy/extras/shaper_calibrate.py | 142 ++++++++++++++++++---------- scripts/calibrate_shaper.py | 130 +++++++++++++++++--------- scripts/graph_accelerometer.py | 149 +++++++++++++++--------------- 4 files changed, 265 insertions(+), 171 deletions(-) diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index b92a163bc..c10b61d3a 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -307,7 +307,7 @@ class ResonanceTester: "'%s' is not an accelerometer" % chip_name) self.accel_chips.append((chip_axis, chip)) - def _run_test(self, gcmd, axes, helper, raw_name_suffix=None, + def _run_test(self, gcmd, axes, helper, name_suffix, raw_name_suffix=None, accel_chips=None, test_point=None): toolhead = self.printer.lookup_object('toolhead') calibration_data = {axis: None for axis in axes} @@ -367,7 +367,12 @@ class ResonanceTester: raise gcmd.error( "accelerometer '%s' measured no data" % ( chip_name,)) - new_data = helper.process_accelerometer_data(aclient) + name = self.get_filename( + 'resonances', name_suffix, axis, + point if len(test_points) > 1 else None, + chip_name if (accel_chips is not None + or len(raw_values) > 1) else None) + new_data = helper.process_accelerometer_data(name, aclient) if calibration_data[axis] is None: calibration_data[axis] = new_data else: @@ -427,7 +432,7 @@ class ResonanceTester: helper = None data = self._run_test( - gcmd, [axis], helper, + gcmd, [axis], helper, name_suffix, raw_name_suffix=name_suffix if raw_output else None, accel_chips=accel_chips, test_point=test_point)[axis] if csv_output: @@ -463,7 +468,7 @@ class ResonanceTester: helper = shaper_calibrate.ShaperCalibrate(self.printer) calibration_data = self._run_test(gcmd, calibrate_axes, helper, - accel_chips=accel_chips) + name_suffix, accel_chips=accel_chips) configfile = self.printer.lookup_object('configfile') for axis in calibrate_axes: @@ -512,7 +517,7 @@ class ResonanceTester: raise gcmd.error( "%s-axis accelerometer measured no data" % ( chip_axis,)) - data = helper.process_accelerometer_data(aclient) + data = helper.process_accelerometer_data(name=None, data=aclient) vx = data.psd_x.mean() vy = data.psd_y.mean() vz = data.psd_z.mean() diff --git a/klippy/extras/shaper_calibrate.py b/klippy/extras/shaper_calibrate.py index f497171f6..551ffca5a 100644 --- a/klippy/extras/shaper_calibrate.py +++ b/klippy/extras/shaper_calibrate.py @@ -20,7 +20,8 @@ AUTOTUNE_SHAPERS = ['zv', 'mzv', 'ei', '2hump_ei', '3hump_ei'] ###################################################################### class CalibrationData: - def __init__(self, freq_bins, psd_sum, psd_x, psd_y, psd_z): + def __init__(self, name, freq_bins, psd_sum, psd_x, psd_y, psd_z): + self.name = name self.freq_bins = freq_bins self.psd_sum = psd_sum self.psd_x = psd_x @@ -29,35 +30,37 @@ class CalibrationData: self._psd_list = [self.psd_sum, self.psd_x, self.psd_y, self.psd_z] self._psd_map = {'x': self.psd_x, 'y': self.psd_y, 'z': self.psd_z, 'all': self.psd_sum} - self.data_sets = 1 + self._normalized = False + self.data_sets = [] def add_data(self, other): - np = self.numpy - joined_data_sets = self.data_sets + other.data_sets - for psd, other_psd in zip(self._psd_list, other._psd_list): - # `other` data may be defined at different frequency bins, - # interpolating to fix that. - other_normalized = other.data_sets * np.interp( - self.freq_bins, other.freq_bins, other_psd) - psd *= self.data_sets - psd[:] = (psd + other_normalized) * (1. / joined_data_sets) - self.data_sets = joined_data_sets + self.data_sets.extend(other.get_datasets()) def set_numpy(self, numpy): self.numpy = numpy def normalize_to_frequencies(self): - for psd in self._psd_list: - # Avoid division by zero errors - psd /= self.freq_bins + .1 - # Remove low-frequency noise - low_freqs = self.freq_bins < 2. * MIN_FREQ - psd[low_freqs] *= self.numpy.exp( - -(2. * MIN_FREQ / (self.freq_bins[low_freqs] + .1))**2 + 1.) + if not self._normalized: + for psd in self._psd_list: + if psd is None: + continue + # Avoid division by zero errors + psd /= self.freq_bins + .1 + # Remove low-frequency noise + low_freqs = self.freq_bins < 2. * MIN_FREQ + psd[low_freqs] *= self.numpy.exp( + -(2. * MIN_FREQ / ( + self.freq_bins[low_freqs] + .1))**2 + 1.) + self._normalized = True + for other in self.data_sets: + other.normalize_to_frequencies() def get_psd(self, axis='all'): return self._psd_map[axis] + def get_datasets(self): + return [self] + self.data_sets CalibrationResult = collections.namedtuple( 'CalibrationResult', - ('name', 'freq', 'vals', 'vibrs', 'smoothing', 'score', 'max_accel')) + ('name', 'freq', 'freq_bins', 'vals', 'vibrs', + 'smoothing', 'score', 'max_accel')) class ShaperCalibrate: def __init__(self, printer): @@ -147,7 +150,7 @@ class ShaperCalibrate: freqs = np.fft.rfftfreq(nfft, 1. / fs) return freqs, psd - def calc_freq_response(self, raw_values): + def calc_freq_response(self, name, raw_values): np = self.numpy if raw_values is None: return None @@ -172,11 +175,11 @@ class ShaperCalibrate: fx, px = self._psd(data[:,1], SAMPLING_FREQ, M) fy, py = self._psd(data[:,2], SAMPLING_FREQ, M) fz, pz = self._psd(data[:,3], SAMPLING_FREQ, M) - return CalibrationData(fx, px+py+pz, px, py, pz) + return CalibrationData(name, fx, px+py+pz, px, py, pz) - def process_accelerometer_data(self, data): + def process_accelerometer_data(self, name, data): calibration_data = self.background_process_exec( - self.calc_freq_response, (data,)) + self.calc_freq_response, (name, data)) if calibration_data is None: raise self.error( "Internal error processing accelerometer data %s" % (data,)) @@ -250,36 +253,50 @@ class ShaperCalibrate: max_freq = max(max_freq or MAX_FREQ, test_freqs.max()) - freq_bins = calibration_data.freq_bins - psd = calibration_data.psd_sum[freq_bins <= max_freq] - freq_bins = freq_bins[freq_bins <= max_freq] - best_res = None results = [] + min_freq = max_freq + for data in calibration_data.get_datasets(): + min_freq = min(min_freq, data.freq_bins.min()) for test_freq in test_freqs[::-1]: shaper_vibrations = 0. - shaper_vals = np.zeros(shape=freq_bins.shape) shaper = shaper_cfg.init_func(test_freq, damping_ratio) shaper_smoothing = self._get_shaper_smoothing(shaper, scv=scv) if max_smoothing and shaper_smoothing > max_smoothing and best_res: return best_res - # Exact damping ratio of the printer is unknown, pessimizing - # remaining vibrations over possible damping values - for dr in test_damping_ratios: - vibrations, vals = self._estimate_remaining_vibrations( - shaper, dr, freq_bins, psd) - shaper_vals = np.maximum(shaper_vals, vals) - if vibrations > shaper_vibrations: - shaper_vibrations = vibrations max_accel = self.find_shaper_max_accel(shaper, scv) + all_shaper_vals = [] + + for data in calibration_data.get_datasets(): + freq_bins = data.freq_bins + psd = data.psd_sum[freq_bins <= max_freq] + freq_bins = freq_bins[freq_bins <= max_freq] + + shaper_vals = np.zeros(shape=freq_bins.shape) + # Exact damping ratio of the printer is unknown, pessimizing + # remaining vibrations over possible damping values + for dr in test_damping_ratios: + vibrations, vals = self._estimate_remaining_vibrations( + shaper, dr, freq_bins, psd) + shaper_vals = np.maximum(shaper_vals, vals) + if vibrations > shaper_vibrations: + shaper_vibrations = vibrations + all_shaper_vals.append((freq_bins, shaper_vals)) # The score trying to minimize vibrations, but also accounting # the growth of smoothing. The formula itself does not have any # special meaning, it simply shows good results on real user data shaper_score = shaper_smoothing * (shaper_vibrations**1.5 + shaper_vibrations * .2 + .01) + shaper_freq_bins = np.arange(min_freq, max_freq, 0.2) + shaper_vals = np.zeros(shape=shaper_freq_bins.shape) + for freq_bins, vals in all_shaper_vals: + shaper_vals = np.maximum( + shaper_vals, np.interp(shaper_freq_bins, + freq_bins, vals)) results.append( CalibrationResult( - name=shaper_cfg.name, freq=test_freq, vals=shaper_vals, + name=shaper_cfg.name, freq=test_freq, + freq_bins=shaper_freq_bins, vals=shaper_vals, vibrs=shaper_vibrations, smoothing=shaper_smoothing, score=shaper_score, max_accel=max_accel)) if best_res is None or best_res.vibrs > results[-1].vibrs: @@ -370,27 +387,56 @@ class ShaperCalibrate: "SHAPER_TYPE_" + axis: shaper_name, "SHAPER_FREQ_" + axis: shaper_freq})) + def _escape_for_csv(self, name): + name = name.replace('"', '""') + if ',' in name: + return '"' + name + '"' + return name + def save_calibration_data(self, output, calibration_data, shapers=None, max_freq=None): try: + np = calibration_data.numpy + datasets = calibration_data.get_datasets() max_freq = max_freq or MAX_FREQ - with open(output, "w") as csvfile: - csvfile.write("freq,psd_x,psd_y,psd_z,psd_xyz") + if len(datasets) > 1: if shapers: + freq_bins = shapers[0].freq_bins + else: + min_freq = max_freq + for data in datasets: + min_freq = min(min_freq, data.freq_bins.min()) + freq_bins = np.arange(min_freq, max_freq, 0.2) + psd_data_to_write = [] + for data in datasets: + psd_data_to_write.append(np.interp( + freq_bins, data.freq_bins, data.psd_sum)) + else: + freq_bins = calibration_data.freq_bins + psd_data_to_write = [ + calibration_data.psd_x, calibration_data.psd_y, + calibration_data.psd_z, calibration_data.psd_sum] + with open(output, "w") as csvfile: + csvfile.write("freq,") + if len(datasets) > 1: + csvfile.write(','.join([self._escape_for_csv(d.name) + for d in datasets])) + else: + csvfile.write("psd_x,psd_y,psd_z,psd_xyz") + if shapers: + csvfile.write(',shapers:') for shaper in shapers: csvfile.write(",%s(%.1f)" % (shaper.name, shaper.freq)) csvfile.write("\n") - num_freqs = calibration_data.freq_bins.shape[0] + num_freqs = freq_bins.shape[0] for i in range(num_freqs): - if calibration_data.freq_bins[i] >= max_freq: + if freq_bins[i] >= max_freq: break - csvfile.write("%.1f,%.3e,%.3e,%.3e,%.3e" % ( - calibration_data.freq_bins[i], - calibration_data.psd_x[i], - calibration_data.psd_y[i], - calibration_data.psd_z[i], - calibration_data.psd_sum[i])) + csvfile.write("%.1f" % freq_bins[i]) + for psd in psd_data_to_write: + csvfile.write(",%.3e" % psd[i]) if shapers: + csvfile.write(',') for shaper in shapers: csvfile.write(",%.3f" % (shaper.vals[i],)) csvfile.write("\n") diff --git a/scripts/calibrate_shaper.py b/scripts/calibrate_shaper.py index ebf5c8aa1..a8bf11197 100755 --- a/scripts/calibrate_shaper.py +++ b/scripts/calibrate_shaper.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # Shaper auto-calibration script # -# Copyright (C) 2020-2024 Dmitry Butyugin +# Copyright (C) 2020-2025 Dmitry Butyugin # Copyright (C) 2020 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. from __future__ import print_function -import importlib, optparse, os, sys +import csv, importlib, optparse, os, sys from textwrap import wrap import numpy as np, matplotlib sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), @@ -20,18 +20,39 @@ def parse_log(logname): for header in f: if not header.startswith('#'): break - if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'): - # Raw accelerometer data - return np.loadtxt(logname, comments='#', delimiter=',') + if not header.startswith('freq,'): + # Process raw accelerometer data + data = np.loadtxt(logname, comments='#', delimiter=',') + helper = shaper_calibrate.ShaperCalibrate(printer=None) + calibration_data = helper.process_accelerometer_data(logname, data) + calibration_data.normalize_to_frequencies() + return calibration_data # Parse power spectral density data - data = np.loadtxt(logname, skiprows=1, comments='#', delimiter=',') - calibration_data = shaper_calibrate.CalibrationData( - freq_bins=data[:,0], psd_sum=data[:,4], - psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3]) - calibration_data.set_numpy(np) + data = np.genfromtxt(logname, dtype=np.float64, skip_header=1, + comments='#', delimiter=',', filling_values=0.) + if header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'): + calibration_data = shaper_calibrate.CalibrationData( + name=logname, freq_bins=data[:,0], psd_sum=data[:,4], + psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3]) + calibration_data.set_numpy(np) + else: + parsed_header = next(csv.reader([header], delimiter=',')) + calibration_data = None + for i, dataset_name in enumerate(parsed_header[1:]): + if dataset_name == 'shapers:': + break + cdata = shaper_calibrate.CalibrationData( + name=dataset_name, freq_bins=data[:,0], psd_sum=data[:,i+1], + # Individual per-axis data is not stored + psd_x=None, psd_y=None, psd_z=None) + cdata.set_numpy(np) + if calibration_data is None: + calibration_data = cdata + else: + calibration_data.add_data(cdata) # If input shapers are present in the CSV file, the frequency # response is already normalized to input frequencies - if 'mzv' not in header: + if ',shapers:' not in header: calibration_data.normalize_to_frequencies() return calibration_data @@ -43,19 +64,14 @@ def parse_log(logname): def calibrate_shaper(datas, csv_output, *, shapers, damping_ratio, scv, shaper_freqs, max_smoothing, test_damping_ratios, max_freq): + # Combine accelerometer data + calibration_data = datas[0] + for data in datas[1:]: + calibration_data.add_data(data) + + print("Processing resonances from %s" + % ",".join(d.name for d in calibration_data.get_datasets())) helper = shaper_calibrate.ShaperCalibrate(printer=None) - if isinstance(datas[0], shaper_calibrate.CalibrationData): - calibration_data = datas[0] - for data in datas[1:]: - calibration_data.add_data(data) - else: - # Process accelerometer data - calibration_data = helper.process_accelerometer_data(datas[0]) - for data in datas[1:]: - calibration_data.add_data(helper.process_accelerometer_data(data)) - calibration_data.normalize_to_frequencies() - - shaper, all_shapers = helper.find_best_shaper( calibration_data, shapers=shapers, damping_ratio=damping_ratio, scv=scv, shaper_freqs=shaper_freqs, max_smoothing=max_smoothing, @@ -75,32 +91,48 @@ def calibrate_shaper(datas, csv_output, *, shapers, damping_ratio, scv, # Plot frequency response and suggested input shapers ###################################################################### -def plot_freq_response(lognames, calibration_data, shapers, +def plot_freq_response(calibration_data, shapers, selected_shaper, max_freq): - max_freq_bin = calibration_data.freq_bins.max() + selected_shaper_data = [s for s in shapers if s.name == selected_shaper][0] + max_freq_bin = selected_shaper_data.freq_bins.max() if max_freq > max_freq_bin: max_freq = max_freq_bin - freqs = calibration_data.freq_bins - psd = calibration_data.psd_sum[freqs <= max_freq] - px = calibration_data.psd_x[freqs <= max_freq] - py = calibration_data.psd_y[freqs <= max_freq] - pz = calibration_data.psd_z[freqs <= max_freq] - freqs = freqs[freqs <= max_freq] fontP = matplotlib.font_manager.FontProperties() fontP.set_size('x-small') - fig, ax = matplotlib.pyplot.subplots() + fig, ax = matplotlib.pyplot.subplots(figsize=(8, 5)) ax.set_xlabel('Frequency, Hz') ax.set_xlim([0, max_freq]) ax.set_ylabel('Power spectral density') - ax.plot(freqs, psd, label='X+Y+Z', color='purple') - ax.plot(freqs, px, label='X', color='red') - ax.plot(freqs, py, label='Y', color='green') - ax.plot(freqs, pz, label='Z', color='blue') + datasets = calibration_data.get_datasets() + if len(datasets) == 1: + freqs = calibration_data.freq_bins + psd = calibration_data.psd_sum[freqs <= max_freq] + px = calibration_data.psd_x[freqs <= max_freq] + py = calibration_data.psd_y[freqs <= max_freq] + pz = calibration_data.psd_z[freqs <= max_freq] + freqs = freqs[freqs <= max_freq] + after_shaper = np.interp(selected_shaper_data.freq_bins, freqs, psd) + ax.plot(freqs, psd, label='X+Y+Z', color='purple') + ax.plot(freqs, px, label='X', color='red') + ax.plot(freqs, py, label='Y', color='green') + ax.plot(freqs, pz, label='Z', color='blue') + title = "Frequency response and shapers (%s)" % calibration_data.name + else: + after_shaper = np.zeros(shape=selected_shaper_data.freq_bins.shape) + for data in datasets: + freqs = data.freq_bins + psd = data.psd_sum[freqs <= max_freq] + freqs = freqs[freqs <= max_freq] + after_shaper = np.maximum( + after_shaper, np.interp(selected_shaper_data.freq_bins, + freqs, psd)) + ax.plot(freqs, psd, label=data.name) + title = "Frequency responses and shapers" + after_shaper *= selected_shaper_data.vals - title = "Frequency response and shapers (%s)" % (', '.join(lognames)) ax.set_title("\n".join(wrap(title, MAX_TITLE_LENGTH))) ax.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(5)) ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) @@ -119,10 +151,11 @@ def plot_freq_response(lognames, calibration_data, shapers, linestyle = 'dotted' if shaper.name == selected_shaper: linestyle = 'dashdot' - best_shaper_vals = shaper.vals - ax2.plot(freqs, shaper.vals, label=label, linestyle=linestyle) - ax.plot(freqs, psd * best_shaper_vals, - label='After\nshaper', color='cyan') + ax2.plot(shaper.freq_bins, shaper.vals, + label=label, linestyle=linestyle) + ax.plot(selected_shaper_data.freq_bins, after_shaper, + label='After%sshaper' % ('\n' if len(datasets) == 1 else ' '), + color='cyan') # A hack to add a human-readable shaper recommendation to legend ax2.plot([], [], ' ', label="Recommended shaper: %s" % (selected_shaper.upper())) @@ -153,7 +186,7 @@ def main(): default=None, help="filename of output graph") opts.add_option("-c", "--csv", type="string", dest="csv", default=None, help="filename of output csv file") - opts.add_option("-f", "--max_freq", type="float", default=200., + opts.add_option("-f", "--max_freq", type="float", default=None, help="maximum frequency to plot") opts.add_option("-s", "--max_smoothing", type="float", dest="max_smoothing", default=None, help="maximum shaper smoothing to allow") @@ -201,13 +234,15 @@ def main(): opts.error("--shaper_freq param does not specify correct range " + "in the format [start]:end[:step]") shaper_freqs = (freq_start, freq_end, freq_step) - max_freq = max(max_freq, freq_end * 4./3.) + if max_freq is not None: + max_freq = max(max_freq, freq_end * 4./3.) else: try: shaper_freqs = [float(s) for s in options.shaper_freq.split(',')] except ValueError: opts.error("invalid floating point value in --shaper_freq param") - max_freq = max(max_freq, max(shaper_freqs) * 4./3.) + if max_freq is not None: + max_freq = max(max_freq, max(shaper_freqs) * 4./3.) if options.test_damping_ratios: try: test_damping_ratios = [float(s) for s in @@ -235,12 +270,15 @@ def main(): max_freq=max_freq) if selected_shaper is None: return - + if max_freq is None: + max_freq = 0. + for data in calibration_data.get_datasets(): + max_freq = max(max_freq, data.freq_bins.max()) if not options.csv or options.output: # Draw graph setup_matplotlib(options.output is not None) - fig = plot_freq_response(args, calibration_data, shapers, + fig = plot_freq_response(calibration_data, shapers, selected_shaper, max_freq) # Show graph diff --git a/scripts/graph_accelerometer.py b/scripts/graph_accelerometer.py index 84b313115..99a797fc9 100755 --- a/scripts/graph_accelerometer.py +++ b/scripts/graph_accelerometer.py @@ -2,10 +2,10 @@ # Generate adxl345 accelerometer graphs # # Copyright (C) 2020 Kevin O'Connor -# Copyright (C) 2020 Dmitry Butyugin +# Copyright (C) 2020-2025 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. -import importlib, optparse, os, sys +import csv, importlib, optparse, os, sys from textwrap import wrap import numpy as np, matplotlib sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), @@ -19,32 +19,49 @@ def parse_log(logname, opts): for header in f: if header.startswith('#'): continue - if header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'): + if header.startswith('freq,'): # Processed power spectral density file break # Raw accelerometer data return np.loadtxt(logname, comments='#', delimiter=',') # Parse power spectral density data - data = np.loadtxt(logname, skiprows=1, comments='#', delimiter=',') - calibration_data = shaper_calibrate.CalibrationData( - freq_bins=data[:,0], psd_sum=data[:,4], - psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3]) - calibration_data.set_numpy(np) + data = np.genfromtxt(logname, dtype=np.float64, skip_header=1, + comments='#', delimiter=',', filling_values=0.) + if header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'): + calibration_data = shaper_calibrate.CalibrationData( + name=logname, freq_bins=data[:,0], psd_sum=data[:,4], + psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3]) + calibration_data.set_numpy(np) + else: + parsed_header = next(csv.reader([header], delimiter=',')) + calibration_data = None + for i, dataset_name in enumerate(parsed_header[1:]): + if dataset_name == 'shapers:': + break + cdata = shaper_calibrate.CalibrationData( + name=dataset_name, freq_bins=data[:,0], psd_sum=data[:,i+1], + # Individual axes data is not stored + psd_x=None, psd_y=None, psd_z=None) + cdata.set_numpy(np) + if calibration_data is None: + calibration_data = cdata + else: + calibration_data.add_data(cdata) return calibration_data ###################################################################### # Raw accelerometer graphing ###################################################################### -def plot_accel(datas, lognames): +def plot_accel(opts, datas, lognames): fig, axes = matplotlib.pyplot.subplots(nrows=3, sharex=True) axes[0].set_title("\n".join(wrap( "Accelerometer data (%s)" % (', '.join(lognames)), MAX_TITLE_LENGTH))) axis_names = ['x', 'y', 'z'] for data, logname in zip(datas, lognames): if isinstance(data, shaper_calibrate.CalibrationData): - raise error("Cannot plot raw accelerometer data using the processed" - " resonances, raw_data input is required") + opts.error("Cannot plot raw accelerometer data using the processed" + " resonances, raw_data input is required") first_time = data[0, 0] times = data[:,0] - first_time for i in range(len(axis_names)): @@ -70,16 +87,13 @@ def plot_accel(datas, lognames): ###################################################################### # Calculate estimated "power spectral density" -def calc_freq_response(data, max_freq): +def calc_freq_response(name, data, max_freq): if isinstance(data, shaper_calibrate.CalibrationData): return data helper = shaper_calibrate.ShaperCalibrate(printer=None) - return helper.process_accelerometer_data(data) + return helper.process_accelerometer_data(name, data) def calc_specgram(data, axis): - if isinstance(data, shaper_calibrate.CalibrationData): - raise error("Cannot calculate the spectrogram using the processed" - " resonances, raw_data input is required") N = data.shape[0] Fs = N / (data[-1,0] - data[0,0]) # Round up to a power of 2 for faster FFT @@ -99,28 +113,45 @@ def calc_specgram(data, axis): pdata += _specgram(d[ax])[0] return pdata, bins, t -def plot_frequency(datas, lognames, max_freq): - calibration_data = calc_freq_response(datas[0], max_freq) - for data in datas[1:]: - calibration_data.add_data(calc_freq_response(data, max_freq)) - freqs = calibration_data.freq_bins - psd = calibration_data.psd_sum[freqs <= max_freq] - px = calibration_data.psd_x[freqs <= max_freq] - py = calibration_data.psd_y[freqs <= max_freq] - pz = calibration_data.psd_z[freqs <= max_freq] - freqs = freqs[freqs <= max_freq] +def plot_frequency(opts, datas, lognames, max_freq, axis): + calibration_data = calc_freq_response(lognames[0], datas[0], max_freq) + for logname, data in zip(lognames[1:], datas[1:]): + calibration_data.add_data(calc_freq_response(logname, data, max_freq)) fig, ax = matplotlib.pyplot.subplots() - ax.set_title("\n".join(wrap( - "Frequency response (%s)" % (', '.join(lognames)), MAX_TITLE_LENGTH))) ax.set_xlabel('Frequency (Hz)') ax.set_ylabel('Power spectral density') - ax.plot(freqs, psd, label='X+Y+Z', alpha=0.6) - ax.plot(freqs, px, label='X', alpha=0.6) - ax.plot(freqs, py, label='Y', alpha=0.6) - ax.plot(freqs, pz, label='Z', alpha=0.6) + datasets = calibration_data.get_datasets() + if len(datasets) == 1: + freqs = calibration_data.freq_bins + if axis == 'all': + psd = calibration_data.psd_sum[freqs <= max_freq] + px = calibration_data.psd_x[freqs <= max_freq] + py = calibration_data.psd_y[freqs <= max_freq] + pz = calibration_data.psd_z[freqs <= max_freq] + freqs = freqs[freqs <= max_freq] + ax.plot(freqs, psd, label='X+Y+Z', alpha=0.6) + ax.plot(freqs, px, label='X', alpha=0.6) + ax.plot(freqs, py, label='Y', alpha=0.6) + ax.plot(freqs, pz, label='Z', alpha=0.6) + else: + psd = calibration_data.get_psd(axis)[freqs <= max_freq] + freqs = freqs[freqs <= max_freq] + ax.plot(freqs, psd, label=axis.upper(), alpha=0.6) + title = "Frequency response (%s)" % calibration_data.name + else: + for data in datasets: + freqs = data.freq_bins + psd = data.get_psd(axis) + if psd is None: + opts.error("Per-axis data is not present in the input file(s)") + psd = psd[freqs <= max_freq] + freqs = freqs[freqs <= max_freq] + ax.plot(freqs, psd, label=data.name) + title = "Frequency responses comparison" + ax.set_title("\n".join(wrap(title, MAX_TITLE_LENGTH))) ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) ax.grid(which='major', color='grey') @@ -133,31 +164,11 @@ def plot_frequency(datas, lognames, max_freq): fig.tight_layout() return fig -def plot_compare_frequency(datas, lognames, max_freq, axis): - fig, ax = matplotlib.pyplot.subplots() - ax.set_title('Frequency responses comparison') - ax.set_xlabel('Frequency (Hz)') - ax.set_ylabel('Power spectral density') - - for data, logname in zip(datas, lognames): - calibration_data = calc_freq_response(data, max_freq) - freqs = calibration_data.freq_bins - psd = calibration_data.get_psd(axis)[freqs <= max_freq] - freqs = freqs[freqs <= max_freq] - ax.plot(freqs, psd, label="\n".join(wrap(logname, 60)), alpha=0.6) - - ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) - ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) - ax.grid(which='major', color='grey') - ax.grid(which='minor', color='lightgrey') - fontP = matplotlib.font_manager.FontProperties() - fontP.set_size('x-small') - ax.legend(loc='best', prop=fontP) - fig.tight_layout() - return fig - # Plot data in a "spectrogram colormap" -def plot_specgram(data, logname, max_freq, axis): +def plot_specgram(opts, data, logname, max_freq, axis): + if isinstance(data, shaper_calibrate.CalibrationData): + opts.error("Cannot calculate the spectrogram using the processed" + " resonances, raw_data input is required") pdata, bins, t = calc_specgram(data, axis) fig, ax = matplotlib.pyplot.subplots() @@ -174,11 +185,12 @@ def plot_specgram(data, logname, max_freq, axis): # CSV output ###################################################################### -def write_frequency_response(datas, output): +def write_frequency_response(lognames, datas, output): helper = shaper_calibrate.ShaperCalibrate(printer=None) - calibration_data = helper.process_accelerometer_data(datas[0]) - for data in datas[1:]: - calibration_data.add_data(helper.process_accelerometer_data(data)) + calibration_data = helper.process_accelerometer_data(lognames[0], datas[0]) + for logname, data in zip(lognames[1:], datas[1:]): + calibration_data.add_data( + helper.process_accelerometer_data(logname, data)) helper.save_calibration_data(output, calibration_data) def write_specgram(psd, freq_bins, time, output): @@ -223,9 +235,6 @@ def main(): help="maximum frequency to graph") opts.add_option("-r", "--raw", action="store_true", help="graph raw accelerometer data") - opts.add_option("-c", "--compare", action="store_true", - help="graph comparison of power spectral density " - "between different accelerometer data files") opts.add_option("-s", "--specgram", action="store_true", help="graph spectrogram of accelerometer data") opts.add_option("-a", type="string", dest="axis", default="all", @@ -242,29 +251,25 @@ def main(): if is_csv_output(options.output): if options.raw: opts.error("raw mode is not supported with csv output") - if options.compare: - opts.error("comparison mode is not supported with csv output") if options.specgram: if len(args) > 1: opts.error("Only 1 input is supported in specgram mode") pdata, bins, t = calc_specgram(datas[0], options.axis) write_specgram(pdata, bins, t, options.output) else: - write_frequency_response(datas, options.output) + write_frequency_response(args, datas, options.output) return # Draw graph if options.raw: - fig = plot_accel(datas, args) + fig = plot_accel(opts, datas, args) elif options.specgram: if len(args) > 1: opts.error("Only 1 input is supported in specgram mode") - fig = plot_specgram(datas[0], args[0], options.max_freq, options.axis) - elif options.compare: - fig = plot_compare_frequency(datas, args, options.max_freq, - options.axis) + fig = plot_specgram(opts, datas[0], args[0], + options.max_freq, options.axis) else: - fig = plot_frequency(datas, args, options.max_freq) + fig = plot_frequency(opts, datas, args, options.max_freq, options.axis) # Show graph if options.output is None: From c339bb0cdf1ecfca4cd7e5ff4c5787da21fe7f88 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sun, 19 Oct 2025 18:42:18 +0200 Subject: [PATCH 254/411] shaper_calibrate: Reworked best shaper recommendations * Do not recommend ZV shaper by default if possible * Try to find more optimal shaper out of more aggressive ones Signed-off-by: Dmitry Butyugin --- klippy/extras/shaper_calibrate.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/klippy/extras/shaper_calibrate.py b/klippy/extras/shaper_calibrate.py index 551ffca5a..ae88ad58a 100644 --- a/klippy/extras/shaper_calibrate.py +++ b/klippy/extras/shaper_calibrate.py @@ -263,7 +263,7 @@ class ShaperCalibrate: shaper = shaper_cfg.init_func(test_freq, damping_ratio) shaper_smoothing = self._get_shaper_smoothing(shaper, scv=scv) if max_smoothing and shaper_smoothing > max_smoothing and best_res: - return best_res + return best_res, results max_accel = self.find_shaper_max_accel(shaper, scv) all_shaper_vals = [] @@ -306,9 +306,10 @@ class ShaperCalibrate: # much worse than the 'best' one, but gives much less smoothing selected = best_res for res in results[::-1]: - if res.vibrs < best_res.vibrs * 1.1 and res.score < selected.score: + if res.vibrs < best_res.vibrs * 1.1 + .0005 \ + and res.score < selected.score: selected = res - return selected + return selected, results def _bisect(self, func): left = right = 1. @@ -346,9 +347,19 @@ class ShaperCalibrate: for shaper_cfg in shaper_defs.INPUT_SHAPERS: if shaper_cfg.name not in shapers: continue - shaper = self.background_process_exec(self.fit_shaper, ( + shaper, results = self.background_process_exec(self.fit_shaper, ( shaper_cfg, calibration_data, shaper_freqs, damping_ratio, scv, max_smoothing, test_damping_ratios, max_freq)) + if (best_shaper is None or shaper.score * 1.2 < best_shaper.score or + (shaper.score * 1.05 < best_shaper.score and + shaper.smoothing * 1.1 < best_shaper.smoothing)): + # Either the shaper significantly improves the score (by 20%), + # or it improves the score and smoothing (by 5% and 10% resp.) + best_shaper = shaper + for s in results[::-1]: + if s.vibrs < best_shaper.vibrs and \ + s.smoothing < best_shaper.smoothing: + best_shaper = shaper = s if logger is not None: logger("Fitted shaper '%s' frequency = %.1f Hz " "(vibrations = %.1f%%, smoothing ~= %.3f)" % ( @@ -358,12 +369,12 @@ class ShaperCalibrate: "max_accel <= %.0f mm/sec^2" % ( shaper.name, round(shaper.max_accel / 100.) * 100.)) all_shapers.append(shaper) - if (best_shaper is None or shaper.score * 1.2 < best_shaper.score or - (shaper.score * 1.05 < best_shaper.score and - shaper.smoothing * 1.1 < best_shaper.smoothing)): - # Either the shaper significantly improves the score (by 20%), - # or it improves the score and smoothing (by 5% and 10% resp.) - best_shaper = shaper + if best_shaper.name == 'zv': + for tuned_shaper in all_shapers: + if tuned_shaper.name != 'zv' and \ + tuned_shaper.vibrs * 1.1 < best_shaper.vibrs: + best_shaper = tuned_shaper + break return best_shaper, all_shapers def save_params(self, configfile, axis, shaper_name, shaper_freq): From baf188bd62bb9c82775d679cf0db30a72f9e9173 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Tue, 21 Oct 2025 00:12:39 +0200 Subject: [PATCH 255/411] shaper_calibrate: Fixed sending large objects via Pipe from bg process Signed-off-by: Dmitry Butyugin --- klippy/extras/shaper_calibrate.py | 33 +++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/klippy/extras/shaper_calibrate.py b/klippy/extras/shaper_calibrate.py index ae88ad58a..db5c8e1ca 100644 --- a/klippy/extras/shaper_calibrate.py +++ b/klippy/extras/shaper_calibrate.py @@ -87,7 +87,13 @@ class ShaperCalibrate: child_conn.send((True, traceback.format_exc())) child_conn.close() return - child_conn.send((False, res)) + if isinstance(res, list): + child_conn.send((True, None)) + for el in res: + child_conn.send((False, el)) + child_conn.send((True, None)) + else: + child_conn.send((False, res)) child_conn.close() # Start a process to perform the calculation calc_proc = multiprocessing.Process(target=wrapper) @@ -97,15 +103,24 @@ class ShaperCalibrate: reactor = self.printer.get_reactor() gcode = self.printer.lookup_object("gcode") eventtime = last_report_time = reactor.monotonic() - while calc_proc.is_alive(): + while calc_proc.is_alive() and not parent_conn.poll(): if eventtime > last_report_time + 5.: last_report_time = eventtime gcode.respond_info("Wait for calculations..", log=False) eventtime = reactor.pause(eventtime + .1) # Return results - is_err, res = parent_conn.recv() - if is_err: - raise self.error("Error in remote calculation: %s" % (res,)) + status, recv = parent_conn.recv() + if recv is None: + res = [] + while True: + status, recv = parent_conn.recv() + if status: + break + res.append(recv) + else: + res = recv + if status and recv is not None: + raise self.error("Error in remote calculation: %s" % (recv,)) calc_proc.join() parent_conn.close() return res @@ -263,7 +278,7 @@ class ShaperCalibrate: shaper = shaper_cfg.init_func(test_freq, damping_ratio) shaper_smoothing = self._get_shaper_smoothing(shaper, scv=scv) if max_smoothing and shaper_smoothing > max_smoothing and best_res: - return best_res, results + return [best_res] + results max_accel = self.find_shaper_max_accel(shaper, scv) all_shaper_vals = [] @@ -309,7 +324,7 @@ class ShaperCalibrate: if res.vibrs < best_res.vibrs * 1.1 + .0005 \ and res.score < selected.score: selected = res - return selected, results + return [selected] + results def _bisect(self, func): left = right = 1. @@ -347,9 +362,11 @@ class ShaperCalibrate: for shaper_cfg in shaper_defs.INPUT_SHAPERS: if shaper_cfg.name not in shapers: continue - shaper, results = self.background_process_exec(self.fit_shaper, ( + fit_results = self.background_process_exec(self.fit_shaper, ( shaper_cfg, calibration_data, shaper_freqs, damping_ratio, scv, max_smoothing, test_damping_ratios, max_freq)) + shaper = fit_results[0] + results = fit_results[1:] if (best_shaper is None or shaper.score * 1.2 < best_shaper.score or (shaper.score * 1.05 < best_shaper.score and shaper.smoothing * 1.1 < best_shaper.smoothing)): From 0ba01beaa858eae78cf8435c8a3fe6520ca2192b Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Mon, 1 Sep 2025 01:29:36 +0200 Subject: [PATCH 256/411] kinematic_stepper: Fixed reporting of parse errors Signed-off-by: Dmitry Butyugin --- klippy/kinematics/kinematic_stepper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/klippy/kinematics/kinematic_stepper.py b/klippy/kinematics/kinematic_stepper.py index c82f08559..1c4788da6 100644 --- a/klippy/kinematics/kinematic_stepper.py +++ b/klippy/kinematics/kinematic_stepper.py @@ -24,7 +24,7 @@ def parse_carriages_string(carriages_str, printer_carriages, parse_error): try: coeff = float(term_lst[0]) except ValueError: - raise error("Invalid float '%s'" % term_lst[0]) + raise parse_error("Invalid float '%s'" % term_lst[0]) else: coeff = -1. if term_lst[0].startswith('-') else 1. if term_lst[0].startswith('-') or term_lst[0].startswith('+'): @@ -36,8 +36,8 @@ def parse_carriages_string(carriages_str, printer_carriages, parse_error): carriage = printer_carriages[c] j = carriage.get_axis() if coeffs[j]: - raise error("Carriage '%s' was referenced multiple times in '%s'" % - (c, carriages_str)) + raise parse_error("Axis '%s' was referenced multiple times by " + "carriages in '%s'" % ("xyz"[j], carriages_str)) coeffs[j] = coeff ref_carriages.append(carriage) nxt = end From 79189203e253b84a0830304ecea5df1e22f03cce Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sat, 4 Oct 2025 23:14:42 +0200 Subject: [PATCH 257/411] generic_cartesian: Added option 'axis' for primary carriages This also enables arbitrary using names for primary carriages with generic_cartesian kinematics. Signed-off-by: Dmitry Butyugin --- config/example-generic-caretesian.cfg | 29 +++++----- config/sample-corexyuv.cfg | 74 +++++++++++++------------- docs/Config_Changes.md | 4 ++ docs/Config_Reference.md | 64 +++++++++++----------- docs/G-Codes.md | 10 ++-- docs/Resonance_Compensation.md | 2 + klippy/extras/manual_probe.py | 7 ++- klippy/kinematics/generic_cartesian.py | 56 ++++++++++++------- test/klippy/corexyuv.cfg | 49 +++++++++-------- test/klippy/corexyuv.test | 8 +-- test/klippy/generic_cartesian.cfg | 41 +++++++------- test/klippy/generic_cartesian.test | 14 ++--- 12 files changed, 201 insertions(+), 157 deletions(-) diff --git a/config/example-generic-caretesian.cfg b/config/example-generic-caretesian.cfg index 2d6b3994e..7c8aa4a4b 100644 --- a/config/example-generic-caretesian.cfg +++ b/config/example-generic-caretesian.cfg @@ -7,36 +7,39 @@ # See docs/Config_Reference.md for a description of parameters. -[carriage x] +[carriage carriage_x] +axis: x position_endstop: 0 position_max: 300 homing_speed: 50 endstop_pin: ^PE5 -[carriage y] +[carriage carriage_y] +axis: y position_endstop: 0 position_max: 200 homing_speed: 50 endstop_pin: ^PJ1 -[extra_carriage y1] -primary_carriage: y +[extra_carriage carriage_y1] +primary_carriage: carriage_y endstop_pin: ^PB6 -[carriage z] +[carriage carriage_z] +axis: z position_endstop: 0.5 position_max: 100 endstop_pin: ^PD3 -[dual_carriage u] -primary_carriage: x +[dual_carriage carriage_u] +primary_carriage: carriage_x position_endstop: 300 position_max: 300 homing_speed: 50 endstop_pin: ^PE4 [stepper my_stepper_x] -carriages: x+y +carriages: carriage_x+carriage_y step_pin: PF0 dir_pin: PF1 enable_pin: !PD7 @@ -44,7 +47,7 @@ microsteps: 16 rotation_distance: 40 [stepper my_stepper_u] -carriages: u-y1 +carriages: carriage_u-carriage_y1 step_pin: PH1 dir_pin: PH0 enable_pin: !PA1 @@ -52,7 +55,7 @@ microsteps: 16 rotation_distance: 40 [stepper my_stepper_y0] -carriages: y +carriages: carriage_y step_pin: PF6 dir_pin: !PF7 enable_pin: !PF2 @@ -60,7 +63,7 @@ microsteps: 16 rotation_distance: 40 [stepper my_stepper_y1] -carriages: y1 +carriages: carriage_y1 step_pin: PE3 dir_pin: !PH6 enable_pin: !PG5 @@ -68,7 +71,7 @@ microsteps: 16 rotation_distance: 40 [stepper my_stepper_z0] -carriages: z +carriages: carriage_z step_pin: PL3 dir_pin: PL1 enable_pin: !PK0 @@ -76,7 +79,7 @@ microsteps: 16 rotation_distance: 8 [stepper my_stepper_z1] -carriages: z +carriages: carriage_z step_pin: PG1 dir_pin: PG0 enable_pin: !PH3 diff --git a/config/sample-corexyuv.cfg b/config/sample-corexyuv.cfg index 368b64647..e33d8681a 100644 --- a/config/sample-corexyuv.cfg +++ b/config/sample-corexyuv.cfg @@ -3,28 +3,30 @@ # See docs/Config_Reference.md for a description of parameters. -[carriage x] +[carriage carriage_x] +axis: x position_endstop: 0 position_max: 300 homing_speed: 50 endstop_pin: ^PE5 -[carriage y] +[carriage carriage_y] +axis: y position_endstop: 0 position_max: 200 homing_speed: 50 endstop_pin: ^PJ1 -[dual_carriage u] -primary_carriage: x +[dual_carriage carriage_u] +primary_carriage: carriage_x safe_distance: 70 position_endstop: 300 position_max: 300 homing_speed: 50 endstop_pin: ^PE4 -[dual_carriage v] -primary_carriage: y +[dual_carriage carriage_v] +primary_carriage: carriage_y safe_distance: 50 position_endstop: 200 position_max: 200 @@ -32,7 +34,7 @@ homing_speed: 50 endstop_pin: ^PD4 [stepper a] -carriages: x+y +carriages: carriage_x+carriage_y step_pin: PF0 dir_pin: PF1 enable_pin: !PD7 @@ -40,7 +42,7 @@ microsteps: 16 rotation_distance: 40 [stepper b] -carriages: u-v +carriages: carriage_u-carriage_v step_pin: PH1 dir_pin: PH0 enable_pin: !PA1 @@ -48,7 +50,7 @@ microsteps: 16 rotation_distance: 40 [stepper c] -carriages: x-y +carriages: carriage_x-carriage_y step_pin: PF6 dir_pin: !PF7 enable_pin: !PF2 @@ -56,7 +58,7 @@ microsteps: 16 rotation_distance: 40 [stepper d] -carriages: u+v +carriages: carriage_u+carriage_v step_pin: PE3 dir_pin: !PH6 enable_pin: !PG5 @@ -83,8 +85,8 @@ max_temp: 250 [gcode_macro PARK_extruder] gcode: - SET_DUAL_CARRIAGE CARRIAGE=x - SET_DUAL_CARRIAGE CARRIAGE=y + SET_DUAL_CARRIAGE CARRIAGE=carriage_x + SET_DUAL_CARRIAGE CARRIAGE=carriage_y G90 G1 X0 Y0 @@ -92,8 +94,8 @@ gcode: gcode: PARK_{printer.toolhead.extruder} ACTIVATE_EXTRUDER EXTRUDER=extruder - SET_DUAL_CARRIAGE CARRIAGE=x - SET_DUAL_CARRIAGE CARRIAGE=y + SET_DUAL_CARRIAGE CARRIAGE=carriage_x + SET_DUAL_CARRIAGE CARRIAGE=carriage_y [extruder1] step_pin: PC1 @@ -115,8 +117,8 @@ max_temp: 250 [gcode_macro PARK_extruder1] gcode: - SET_DUAL_CARRIAGE CARRIAGE=u - SET_DUAL_CARRIAGE CARRIAGE=v + SET_DUAL_CARRIAGE CARRIAGE=carriage_u + SET_DUAL_CARRIAGE CARRIAGE=carriage_v G90 G1 X300 Y200 @@ -124,35 +126,35 @@ gcode: gcode: PARK_{printer.toolhead.extruder} ACTIVATE_EXTRUDER EXTRUDER=extruder1 - SET_DUAL_CARRIAGE CARRIAGE=u - SET_DUAL_CARRIAGE CARRIAGE=v + SET_DUAL_CARRIAGE CARRIAGE=carriage_u + SET_DUAL_CARRIAGE CARRIAGE=carriage_v # A helper script to activate copy mode [gcode_macro ACTIVATE_COPY_MODE] gcode: - SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY - SET_DUAL_CARRIAGE CARRIAGE=y MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_x MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_y MODE=PRIMARY G1 X0 Y0 ACTIVATE_EXTRUDER EXTRUDER=extruder - SET_DUAL_CARRIAGE CARRIAGE=u MODE=PRIMARY - SET_DUAL_CARRIAGE CARRIAGE=v MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=PRIMARY G1 X150 Y100 - SET_DUAL_CARRIAGE CARRIAGE=u MODE=COPY - SET_DUAL_CARRIAGE CARRIAGE=v MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=COPY SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder # A helper script to activate mirror mode [gcode_macro ACTIVATE_MIRROR_MODE] gcode: - SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY - SET_DUAL_CARRIAGE CARRIAGE=y MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_x MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_y MODE=PRIMARY G1 X0 Y0 ACTIVATE_EXTRUDER EXTRUDER=extruder - SET_DUAL_CARRIAGE CARRIAGE=u MODE=PRIMARY - SET_DUAL_CARRIAGE CARRIAGE=v MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=PRIMARY G1 X300 Y100 - SET_DUAL_CARRIAGE CARRIAGE=u MODE=MIRROR - SET_DUAL_CARRIAGE CARRIAGE=v MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=COPY SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder [printer] @@ -169,9 +171,9 @@ max_z_accel: 100 #[delayed_gcode init_shaper] #initial_duration: 0.1 #gcode: -# SET_DUAL_CARRIAGE CARRIAGE=u -# SET_DUAL_CARRIAGE CARRIAGE=v -# SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= -# SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY -# SET_DUAL_CARRIAGE CARRIAGE=y MODE=PRIMARY -# SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= +# SET_DUAL_CARRIAGE CARRIAGE=carriage_u +# SET_DUAL_CARRIAGE CARRIAGE=carriage_v +# SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= +# SET_DUAL_CARRIAGE CARRIAGE=carriage_x MODE=PRIMARY +# SET_DUAL_CARRIAGE CARRIAGE=carriage_y MODE=PRIMARY +# SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index cfa2fe9af..6846e034d 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,10 @@ All dates in this document are approximate. ## Changes +20251122: An option `axis` has been added to `[carriage ]` sections +for `generic_cartesian` kinematics, allowing arbitrary names for primary +carriages. Users are encouraged to explicitly specify `axis` option now. + 20251106: The status fields `{printer.toolhead.position}`, `{printer.gcode_move.position}`, `{printer.gcode_move.gcode_position}`, and diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 94d3a9730..dbf2f4eba 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -743,10 +743,12 @@ max_accel: ``` -Then a user must define the following three carriages: `[carriage x]`, -`[carriage y]`, and `[carriage z]`, e.g. +Then a user must define three carriages for X, Y, and Z axes, e.g.: ``` -[carriage x] +[carriage carriage_x] +axis: +# Axis of a carriage, either x, y, or z. This parameter must be provided, +# unless a carriage name is x, y, or z itself. endstop_pin: # Endstop switch detection pin. If this endstop pin is on a # different mcu than the stepper motor(s) moving this carriage, @@ -788,7 +790,8 @@ for instance carriages: # A string describing the carriages the stepper moves. All defined # carriages can be specified here, as well as their linear combinations, -# e.g. x, x+y, y-0.5*z, x-z, etc. This parameter must be provided. +# e.g. carriage_x, carriage_x+carriage_y, carriage_y-0.5*carriage_z, +# carriage_x-carriage_z, etc. This parameter must be provided. step_pin: dir_pin: enable_pin: @@ -800,28 +803,29 @@ microsteps: ``` See [stepper](#stepper) section for more information on the regular stepper parameters. The `carriages` parameter defines how the stepper -affects the motion of the carriages. For example, `x+y` indicates that -the motion of the stepper in the positive direction by the distance `d` -moves the carriages `x` and `y` by the same distance `d` in the positive -direction, while `x-0.5*y` means the motion of the stepper in the positive -direction by the distance `d` moves the carriage `x` by the distance `d` -in the positive direction, but the carriage `y` will travel distance `d/2` -in the negative direction. +affects the motion of the carriages. For example, `carriage_x+carriage_y` +indicates that the motion of the stepper in the positive direction by the +distance `d` moves the carriages `carriage_x` and `carriage_y` by the same +distance `d` in the positive direction, while `carriage_x-0.5*carriage_y` +means the motion of the stepper in the positive direction by the distance +`d` moves the carriage `carriage_x` by the distance `d` in the positive +direction, but the carriage `carriage_y` will travel distance `d/2` in +the negative direction. More than a single stepper motor can be defined to drive the same axis or belt. For example, on a CoreXY AWD setups two motors driving the same belt can be defined as ``` -[carriage x] +[carriage carriage_x] endstop_pin: ... ... -[carriage y] +[carriage carriage_y] endstop_pin: ... ... [stepper a0] -carriages: x-y +carriages: carriage_x-carriage_y step_pin: ... dir_pin: ... enable_pin: ... @@ -829,7 +833,7 @@ rotation_distance: ... ... [stepper a1] -carriages: x-y +carriages: carriage_x-carriage_y step_pin: ... dir_pin: ... enable_pin: ... @@ -842,7 +846,7 @@ sharing the same `carriages` and corresponding endstops. There are situations when a user wants to have more than one endstop per axis. Examples of such configurations include Y axis driven by two independent stepper motors with belts attached to both ends of the -X beam, with effectively two carriages on Y axis each having an +X gantry, with effectively two carriages on Y axis each having an independent endstop, and multi-stepper Z axis with each stepper having its own endstop (not to be confused with the configurations with multiple Z motors but only a single endstop). These configurations @@ -860,12 +864,12 @@ endstop_pin: and the corresponding stepper motors, for example: ``` -[extra_carriage y1] -primary_carriage: y +[extra_carriage carriage_y1] +primary_carriage: carriage_y endstop_pin: ... [stepper sy1] -carriages: y1 +carriages: carriage_y1 ... ``` Notably, an `[extra_carriage]` does not define parameters such as @@ -2450,7 +2454,7 @@ from the configuration described above: [dual_carriage my_dc_carriage] primary_carriage: # Defines the matching primary carriage of this dual carriage and -# the corresponding IDEX axis. Valid choices are x, y, z. +# the corresponding IDEX axis. Must match a name of a defined `[carriage]`. # This parameter must be provided. #safe_distance: # The minimum distance (in mm) to enforce between the dual and the primary @@ -2478,18 +2482,18 @@ on the regular `carriage` parameters. Then a user must define one or more stepper motors moving the dual carriage (and other carriages as appropriate), for instance ``` -[carriage x] +[carriage carriage_x] ... -[carriage y] +[carriage carriage_y] ... -[dual_carriage u] -primary_carriage: x +[dual_carriage carriage_u] +primary_carriage: carriage_x ... [stepper dc_stepper] -carriages: u-y +carriages: carriage_u-carriage_y ... ``` @@ -2505,14 +2509,14 @@ example above: [delayed_gcode init_shaper] initial_duration: 0.1 gcode: - SET_DUAL_CARRIAGE CARRIAGE=u - SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= - SET_DUAL_CARRIAGE CARRIAGE=x - SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= + SET_DUAL_CARRIAGE CARRIAGE=carriage_u + SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= + SET_DUAL_CARRIAGE CARRIAGE=carriage_x + SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= ``` Note that `SHAPER_TYPE_Y` and `SHAPER_FREQ_Y` must be the same in both commands in this case, since the same motors drive Y axis when either -of the `x` and `u` carriages are active. +of the `carriage_x` and `carriage_u` carriages are active. It is worth noting that `generic_cartesian` kinematic can support two dual carriages for X and Y axes. For reference, see for instance a diff --git a/docs/G-Codes.md b/docs/G-Codes.md index eb38e0134..893993e85 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -753,11 +753,11 @@ stepper at a time, some sequences of changes can lead to invalid intermediate kinematic configurations, even if the final configuration is valid. In such cases a user can pass `DISABLE_CHECKS=1` parameters to all but the last command to disable intermediate checks. For example, -if `stepper a` and `stepper b` initially have `x-y` and `x+y` carriages -correspondingly, then the following sequence of commands will let a user -effectively swap the carriage controls: -`SET_STEPPER_CARRIAGES STEPPER=a CARRIAGES=x+y DISABLE_CHECKS=1` -and `SET_STEPPER_CARRIAGES STEPPER=b CARRIAGES=x-y`, while +if `stepper a` and `stepper b` initially have `carriage_x-carriage_y` and +`carriage_x+carriage_y` carriages correspondingly, then the following +sequence of commands will let a user effectively swap the carriage controls: +`SET_STEPPER_CARRIAGES STEPPER=a CARRIAGES=carriage_x+carriage_y DISABLE_CHECKS=1` +and `SET_STEPPER_CARRIAGES STEPPER=b CARRIAGES=carriage_x-carriage_y`, while still validating the final kinematics state. ### [hall_filament_width_sensor] diff --git a/docs/Resonance_Compensation.md b/docs/Resonance_Compensation.md index dcfb4ed78..b5a5e5284 100644 --- a/docs/Resonance_Compensation.md +++ b/docs/Resonance_Compensation.md @@ -438,6 +438,8 @@ gcode: SET_DUAL_CARRIAGE CARRIAGE=0 SET_INPUT_SHAPER SHAPER_TYPE_X= SHAPER_FREQ_X= SHAPER_TYPE_Y= SHAPER_FREQ_Y= ``` +However, users of `generic_cartesian` kinematics should specify carriage names +in `CARRIAGE=` parameters of `SET_DUAL_CARRIAGE` instead of their numbers. Note that `SHAPER_TYPE_Y` and `SHAPER_FREQ_Y` should be the same in both commands. If you need to configure an input shaper for Z axis, include its parameters in both `SET_INPUT_SHAPER` commands. diff --git a/klippy/extras/manual_probe.py b/klippy/extras/manual_probe.py index 92f2d0f6f..44b2c719f 100644 --- a/klippy/extras/manual_probe.py +++ b/klippy/extras/manual_probe.py @@ -9,8 +9,11 @@ import logging, bisect def lookup_z_endstop_config(config): if config.has_section('stepper_z'): return config.getsection('stepper_z') - elif config.has_section('carriage z'): - return config.getsection('carriage z') + for cconfig in config.get_prefix_sections('carriage '): + carriage_name = cconfig.get_name().split()[-1].strip() + axis_name = cconfig.get('axis', carriage_name, note_valid=False) + if axis_name == 'z': + return cconfig return None class ManualProbe: diff --git a/klippy/kinematics/generic_cartesian.py b/klippy/kinematics/generic_cartesian.py index 413cc11ca..230997be3 100644 --- a/klippy/kinematics/generic_cartesian.py +++ b/klippy/kinematics/generic_cartesian.py @@ -32,10 +32,16 @@ def mat_pseudo_inverse(m): return pinv class MainCarriage: - def __init__(self, config, axis): + def __init__(self, config): self.rail = stepper.GenericPrinterRail(config) - self.axis = ord(axis) - ord('x') - self.axis_name = axis + carriage_name = self.rail.get_name(short=True) + valid_axes = ['x', 'y', 'z'] + if carriage_name in valid_axes: + axis_name = config.getchoice('axis', valid_axes, carriage_name) + else: + axis_name = config.getchoice('axis', valid_axes) + self.axis = ord(axis_name) - ord('x') + self.axis_name = axis_name self.dual_carriage = None def get_name(self): return self.rail.get_name(short=True) @@ -83,7 +89,7 @@ class DualCarriage: self.axis = self.primary_carriage.get_axis() if self.axis > 1: raise config.error("Invalid axis '%s' for dual_carriage" % - self.primary_carriage.get_axis_name()) + "xyz"[self.axis]) self.safe_dist = config.getfloat('safe_distance', None, minval=0.) def get_name(self): return self.rail.get_name(short=True) @@ -146,11 +152,19 @@ class GenericCartesianKinematics: self.cmd_SET_STEPPER_CARRIAGES, desc=self.cmd_SET_STEPPER_CARRIAGES_help) def _load_kinematics(self, config): - carriages = {a : MainCarriage(config.getsection('carriage ' + a), a) - for a in 'xyz'} + carriages = {} + for mcconfig in config.get_prefix_sections('carriage '): + c = MainCarriage(mcconfig) + axis = c.get_axis() + dups = [mc for mc in carriages.values() if mc.get_axis() == axis] + if dups: + raise config.error( + "Axis '%s' referenced by multiple carriages (%s, %s)" + % ("xyz"[axis], c.get_name(), dups[0].get_name())) + carriages[c.get_name()] = c dc_carriages = [] - for c in config.get_prefix_sections('dual_carriage '): - dc_carriages.append(DualCarriage(c, carriages)) + for dcconfig in config.get_prefix_sections('dual_carriage '): + dc_carriages.append(DualCarriage(dcconfig, carriages)) for dc in dc_carriages: name = dc.get_name() if name in carriages: @@ -159,8 +173,8 @@ class GenericCartesianKinematics: self.carriages = dict(carriages) self.dc_carriages = dc_carriages ec_carriages = [] - for c in config.get_prefix_sections('extra_carriage '): - ec_carriages.append(ExtraCarriage(c, carriages)) + for ecconfig in config.get_prefix_sections('extra_carriage '): + ec_carriages.append(ExtraCarriage(ecconfig, carriages)) for ec in ec_carriages: name = ec.get_name() if name in carriages: @@ -193,13 +207,16 @@ class GenericCartesianKinematics: def get_steppers(self): return [s.get_stepper() for s in self.kin_steppers] def get_primary_carriages(self): - carriages = [self.carriages[a] for a in "xyz"] - if self.dc_module: - for a in self.dc_module.get_axes(): + carriages = [None] * 3 + for carriage in self.carriages.values(): + a = carriage.get_axis() + if carriage.get_dual_carriage() is not None: primary_rail = self.dc_module.get_primary_rail(a) for c in self.carriages.values(): if c.get_rail() == primary_rail: carriages[a] = c + else: + carriages[a] = carriage return carriages def _get_kinematics_coeffs(self): matr = {s.get_name() : list(s.get_kin_coeffs()) @@ -274,11 +291,14 @@ class GenericCartesianKinematics: self._check_kinematics(self.printer.command_error) # Each axis is homed independently and in order for axis in homing_state.get_axes(): - carriage = self.carriages["xyz"[axis]] - if carriage.get_dual_carriage() != None: - self.dc_module.home(homing_state, axis) - else: - self.home_axis(homing_state, axis, carriage.get_rail()) + for carriage in self.carriages.values(): + if carriage.get_axis() != axis: + continue + if carriage.get_dual_carriage() != None: + self.dc_module.home(homing_state, axis) + else: + self.home_axis(homing_state, axis, carriage.get_rail()) + break def _check_endstops(self, move): end_pos = move.end_pos for i in (0, 1, 2): diff --git a/test/klippy/corexyuv.cfg b/test/klippy/corexyuv.cfg index 4c0ab7488..e9b0cd02a 100644 --- a/test/klippy/corexyuv.cfg +++ b/test/klippy/corexyuv.cfg @@ -1,35 +1,38 @@ # Test config for generic cartesian kinematics with dual carriage -[carriage x] +[carriage carriage_x] +axis: x position_endstop: 0 position_max: 300 homing_speed: 50 endstop_pin: ^PE5 -[carriage y] +[carriage carriage_y] +axis: y position_endstop: 0 position_max: 200 homing_speed: 50 endstop_pin: ^PJ1 -[carriage z] +[carriage carriage_z] +axis: z position_endstop: 0.5 position_max: 100 endstop_pin: ^PD3 -[extra_carriage z1] -primary_carriage: z +[extra_carriage carriage_z1] +primary_carriage: carriage_z endstop_pin: ^PD2 -[dual_carriage u] -primary_carriage: x +[dual_carriage carriage_u] +primary_carriage: carriage_x safe_distance: 70 position_endstop: 300 position_max: 300 homing_speed: 50 endstop_pin: ^PE4 -[dual_carriage v] -primary_carriage: y +[dual_carriage carriage_v] +primary_carriage: carriage_y safe_distance: 50 position_endstop: 200 position_max: 200 @@ -37,7 +40,7 @@ homing_speed: 50 endstop_pin: ^PD4 [stepper a] -carriages: x+y +carriages: carriage_x+carriage_y step_pin: PF0 dir_pin: PF1 enable_pin: !PD7 @@ -45,7 +48,7 @@ microsteps: 16 rotation_distance: 40 [stepper b] -carriages: u-v +carriages: carriage_u-carriage_v step_pin: PH1 dir_pin: PH0 enable_pin: !PA1 @@ -53,7 +56,7 @@ microsteps: 16 rotation_distance: 40 [stepper c] -carriages: x-y +carriages: carriage_x-carriage_y step_pin: PF6 dir_pin: !PF7 enable_pin: !PF2 @@ -61,7 +64,7 @@ microsteps: 16 rotation_distance: 40 [stepper d] -carriages: u+v +carriages: carriage_u+carriage_v step_pin: PE3 dir_pin: !PH6 enable_pin: !PG5 @@ -69,7 +72,7 @@ microsteps: 16 rotation_distance: 40 [stepper z] -carriages: z +carriages: carriage_z step_pin: PL3 dir_pin: PL1 enable_pin: !PK0 @@ -77,7 +80,7 @@ microsteps: 16 rotation_distance: 8 [stepper z1] -carriages: z1 +carriages: carriage_z1 step_pin: PG1 dir_pin: PG0 enable_pin: !PH3 @@ -104,8 +107,8 @@ max_temp: 250 [gcode_macro PARK_extruder] gcode: - SET_DUAL_CARRIAGE CARRIAGE=x - SET_DUAL_CARRIAGE CARRIAGE=y + SET_DUAL_CARRIAGE CARRIAGE=carriage_x + SET_DUAL_CARRIAGE CARRIAGE=carriage_y G90 G1 X0 Y0 @@ -114,8 +117,8 @@ gcode: PARK_{printer.toolhead.extruder} SET_SERVO SERVO=my_servo angle=100 ACTIVATE_EXTRUDER EXTRUDER=extruder - SET_DUAL_CARRIAGE CARRIAGE=x - SET_DUAL_CARRIAGE CARRIAGE=y + SET_DUAL_CARRIAGE CARRIAGE=carriage_x + SET_DUAL_CARRIAGE CARRIAGE=carriage_y [extruder1] step_pin: PC1 @@ -137,8 +140,8 @@ max_temp: 250 [gcode_macro PARK_extruder1] gcode: - SET_DUAL_CARRIAGE CARRIAGE=u - SET_DUAL_CARRIAGE CARRIAGE=v + SET_DUAL_CARRIAGE CARRIAGE=carriage_u + SET_DUAL_CARRIAGE CARRIAGE=carriage_v G90 G1 X300 Y200 @@ -147,8 +150,8 @@ gcode: PARK_{printer.toolhead.extruder} SET_SERVO SERVO=my_servo angle=50 ACTIVATE_EXTRUDER EXTRUDER=extruder1 - SET_DUAL_CARRIAGE CARRIAGE=u - SET_DUAL_CARRIAGE CARRIAGE=v + SET_DUAL_CARRIAGE CARRIAGE=carriage_u + SET_DUAL_CARRIAGE CARRIAGE=carriage_v [servo my_servo] pin: PH4 diff --git a/test/klippy/corexyuv.test b/test/klippy/corexyuv.test index 837f9b808..2555e9249 100644 --- a/test/klippy/corexyuv.test +++ b/test/klippy/corexyuv.test @@ -10,12 +10,12 @@ G28 G1 X10 Y20 F6000 # Activate alternate carriage -SET_DUAL_CARRIAGE CARRIAGE=u -SET_DUAL_CARRIAGE CARRIAGE=v +SET_DUAL_CARRIAGE CARRIAGE=carriage_u +SET_DUAL_CARRIAGE CARRIAGE=carriage_v G1 X170 Y190 F6000 # Go back to main carriage on X axis -SET_DUAL_CARRIAGE CARRIAGE=x +SET_DUAL_CARRIAGE CARRIAGE=carriage_x G1 X20 F6000 # Save dual carriage state @@ -24,7 +24,7 @@ SAVE_DUAL_CARRIAGE_STATE G1 Y150 F6000 # Go back to main carriage on Y axis -SET_DUAL_CARRIAGE CARRIAGE=y +SET_DUAL_CARRIAGE CARRIAGE=carriage_y G1 X10 Y50 F6000 # Restore dual carriage state diff --git a/test/klippy/generic_cartesian.cfg b/test/klippy/generic_cartesian.cfg index 361a3f75b..ecf5f8037 100644 --- a/test/klippy/generic_cartesian.cfg +++ b/test/klippy/generic_cartesian.cfg @@ -1,38 +1,41 @@ # Test config for generic cartesian kinematics with dual carriage -[carriage x] +[carriage carriage_x] +axis: x position_endstop: 0 position_max: 300 homing_speed: 50 endstop_pin: ^PE5 -[carriage y] +[carriage carriage_y] +axis: y position_endstop: 0 position_max: 200 homing_speed: 50 endstop_pin: ^PJ1 -[extra_carriage y1] -primary_carriage: y +[extra_carriage carriage_y1] +primary_carriage: carriage_y endstop_pin: ^PB6 -[carriage z] +[carriage carriage_z] +axis: z position_endstop: 0.5 position_max: 100 endstop_pin: ^PD3 -[extra_carriage z1] -primary_carriage: z +[extra_carriage carriage_z1] +primary_carriage: carriage_z endstop_pin: ^PD2 -[dual_carriage u] -primary_carriage: x +[dual_carriage carriage_u] +primary_carriage: carriage_x position_endstop: 300 position_max: 300 homing_speed: 50 endstop_pin: ^PE4 [stepper stepper_x] -carriages: x+y +carriages: carriage_x+carriage_y step_pin: PF0 dir_pin: PF1 enable_pin: !PD7 @@ -40,7 +43,7 @@ microsteps: 16 rotation_distance: 40 [stepper dual_carriage] -carriages: u-y1 +carriages: carriage_u-carriage_y1 step_pin: PH1 dir_pin: PH0 enable_pin: !PA1 @@ -48,7 +51,7 @@ microsteps: 16 rotation_distance: 40 [stepper stepper_y] -carriages: 1*y+0.5*z +carriages: 1*carriage_y+0.5*carriage_z step_pin: PF6 dir_pin: !PF7 enable_pin: !PF2 @@ -56,7 +59,7 @@ microsteps: 16 rotation_distance: 40 [stepper stepper_y1] -carriages: 1*y1-0.5*z1 +carriages: 1*carriage_y1-0.5*carriage_z1 step_pin: PE3 dir_pin: !PH6 enable_pin: !PG5 @@ -64,7 +67,7 @@ microsteps: 16 rotation_distance: 40 [stepper stepper_z] -carriages: z +carriages: carriage_z step_pin: PL3 dir_pin: PL1 enable_pin: !PK0 @@ -72,7 +75,7 @@ microsteps: 16 rotation_distance: 8 [stepper stepper_z1] -carriages: z1 +carriages: carriage_z1 step_pin: PG1 dir_pin: PG0 enable_pin: !PH3 @@ -99,7 +102,7 @@ max_temp: 250 [gcode_macro PARK_extruder] gcode: - SET_DUAL_CARRIAGE CARRIAGE=x + SET_DUAL_CARRIAGE CARRIAGE=carriage_x G90 G1 X0 @@ -108,7 +111,7 @@ gcode: PARK_{printer.toolhead.extruder} SET_SERVO SERVO=my_servo angle=100 ACTIVATE_EXTRUDER EXTRUDER=extruder - SET_DUAL_CARRIAGE CARRIAGE=x + SET_DUAL_CARRIAGE CARRIAGE=carriage_x [extruder1] step_pin: PC1 @@ -130,7 +133,7 @@ max_temp: 250 [gcode_macro PARK_extruder1] gcode: - SET_DUAL_CARRIAGE CARRIAGE=u + SET_DUAL_CARRIAGE CARRIAGE=carriage_u G90 G1 X300 @@ -139,7 +142,7 @@ gcode: PARK_{printer.toolhead.extruder} SET_SERVO SERVO=my_servo angle=50 ACTIVATE_EXTRUDER EXTRUDER=extruder1 - SET_DUAL_CARRIAGE CARRIAGE=u + SET_DUAL_CARRIAGE CARRIAGE=carriage_u [servo my_servo] pin: PH4 diff --git a/test/klippy/generic_cartesian.test b/test/klippy/generic_cartesian.test index 869636a87..373475139 100644 --- a/test/klippy/generic_cartesian.test +++ b/test/klippy/generic_cartesian.test @@ -3,9 +3,9 @@ CONFIG generic_cartesian.cfg DICTIONARY atmega2560.dict # Configure the input shaper -SET_DUAL_CARRIAGE CARRIAGE=u +SET_DUAL_CARRIAGE CARRIAGE=carriage_u SET_INPUT_SHAPER SHAPER_TYPE_X=ei SHAPER_FREQ_X=50 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=80 -SET_DUAL_CARRIAGE CARRIAGE=x +SET_DUAL_CARRIAGE CARRIAGE=carriage_x SET_INPUT_SHAPER SHAPER_TYPE_X=ei SHAPER_FREQ_X=50 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=80 # Then home the printer @@ -16,11 +16,11 @@ G28 G1 X10 F6000 # Activate alternate carriage -SET_DUAL_CARRIAGE CARRIAGE=u +SET_DUAL_CARRIAGE CARRIAGE=carriage_u G1 X190 F6000 # Go back to main carriage -SET_DUAL_CARRIAGE CARRIAGE=x +SET_DUAL_CARRIAGE CARRIAGE=carriage_x G1 X100 F6000 # Save dual carriage state @@ -29,7 +29,7 @@ SAVE_DUAL_CARRIAGE_STATE G1 X50 F6000 # Go back to alternate carriage -SET_DUAL_CARRIAGE CARRIAGE=u +SET_DUAL_CARRIAGE CARRIAGE=carriage_u G1 X130 F6000 # Restore dual carriage state @@ -46,8 +46,8 @@ G1 X20 E.2 G90 # Test changing the stepper kinematics -SET_STEPPER_CARRIAGES STEPPER=dual_carriage CARRIAGES=u+y1 -SET_STEPPER_CARRIAGES STEPPER=stepper_x CARRIAGES=x-y +SET_STEPPER_CARRIAGES STEPPER=dual_carriage CARRIAGES=carriage_u+carriage_y1 +SET_STEPPER_CARRIAGES STEPPER=stepper_x CARRIAGES=carriage_x-carriage_y G1 X30 E.2 G1 Z3 From 900c908db80ea0c5280e604b2aee20c09418e99b Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 16 Nov 2025 05:16:58 +0100 Subject: [PATCH 258/411] sosfilter: fix case if no notch is defined Signed-off-by: Timofey Titovets --- klippy/extras/sos_filter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index 293b2258a..76dd4bc26 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -40,6 +40,8 @@ class DigitalFilter: if lowpass: self.filter_sections.append( self._butter(lowpass, "lowpass", lowpass_order)) + if notches is None: + notches = [] for notch_freq in notches: self.filter_sections.append(self._notch(notch_freq, notch_quality)) if len(self.filter_sections) > 0: From a9cf02c41268b8f4e7a2c0adbddc8b4b46b0b9aa Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 16 Nov 2025 05:10:17 +0100 Subject: [PATCH 259/411] sosfilter: handle high order butter filters Signed-off-by: Timofey Titovets --- klippy/extras/sos_filter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index 76dd4bc26..cbd6c51c9 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -35,10 +35,10 @@ class DigitalFilter: except: raise cfg_error("DigitalFilter require the SciPy module") if highpass: - self.filter_sections.append( + self.filter_sections.extend( self._butter(highpass, "highpass", highpass_order)) if lowpass: - self.filter_sections.append( + self.filter_sections.extend( self._butter(lowpass, "lowpass", lowpass_order)) if notches is None: notches = [] @@ -50,7 +50,7 @@ class DigitalFilter: def _butter(self, frequency, btype, order): import scipy.signal as signal return signal.butter(order, Wn=frequency, btype=btype, - fs=self.sample_frequency, output='sos')[0] + fs=self.sample_frequency, output='sos') def _notch(self, freq, quality): import scipy.signal as signal From 938300f3c3cc25448c499a3a8ca5b47b7a6d4fa8 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 4 Nov 2025 04:16:58 +0100 Subject: [PATCH 260/411] stm32: f0 i2c clean nackcf interrupt on handle Signed-off-by: Timofey Titovets --- src/stm32/stm32f0_i2c.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stm32/stm32f0_i2c.c b/src/stm32/stm32f0_i2c.c index 61c848e45..fcb61a273 100644 --- a/src/stm32/stm32f0_i2c.c +++ b/src/stm32/stm32f0_i2c.c @@ -188,8 +188,10 @@ i2c_wait(I2C_TypeDef *i2c, uint32_t set, uint32_t timeout) uint32_t isr = i2c->ISR; if (isr & set) return I2C_BUS_SUCCESS; - if (isr & I2C_ISR_NACKF) + if (isr & I2C_ISR_NACKF) { + i2c->ICR = I2C_ICR_NACKCF; return I2C_BUS_NACK; + } if (!timer_is_before(timer_read_time(), timeout)) return I2C_BUS_TIMEOUT; } From 355b6cee8cbd0f7ddc886525b6b468470bf30c2b Mon Sep 17 00:00:00 2001 From: minicx Date: Sat, 15 Nov 2025 15:31:37 +0300 Subject: [PATCH 261/411] aht10: Fix status bit masks Signed-off-by: Lev Voronov --- klippy/extras/aht10.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klippy/extras/aht10.py b/klippy/extras/aht10.py index 001f7e54d..c4773be3a 100644 --- a/klippy/extras/aht10.py +++ b/klippy/extras/aht10.py @@ -91,8 +91,8 @@ class AHT10: " expected 6 [%d]"%len(data)) continue - self.is_calibrated = True if (data[0] & 0b00000100) else False - is_busy = True if (data[0] & 0b01000000) else False + self.is_calibrated = True if (data[0] & 0b00001000) else False + is_busy = True if (data[0] & 0b10000000) else False if is_busy: return False From 1f43be0b8b55d90753578d06ac06356d1ab9a768 Mon Sep 17 00:00:00 2001 From: minicx Date: Sat, 15 Nov 2025 15:34:43 +0300 Subject: [PATCH 262/411] aht10: Add support for AHT2x/AHT3x families Split into three classes with proper init commands: - AHT1x: 0xE1 (AHT10, AHT15) - AHT2x: 0xBE (AHT20, AHT21, AHT25) - AHT3x: auto-cal (AHT30) Signed-off-by: Lev Voronov --- klippy/extras/aht10.py | 173 +++++++++++++++++++++++++---------------- 1 file changed, 105 insertions(+), 68 deletions(-) diff --git a/klippy/extras/aht10.py b/klippy/extras/aht10.py index c4773be3a..6e4b5562e 100644 --- a/klippy/extras/aht10.py +++ b/klippy/extras/aht10.py @@ -1,6 +1,7 @@ -# AHT10/AHT20/AHT21 I2c-based humiditure sensor support +# Support for AHTxx family I2C temperature and humidity sensors # # Copyright (C) 2023 Scott Mudge +# Copyright (C) 2025 Lev Voronov # # This file may be distributed under the terms of the GNU GPLv3 license. import logging @@ -9,49 +10,67 @@ from . import bus ###################################################################### # Compatible Sensors: # AHT10 - Tested w/ BTT GTR 1.0 MCU on i2c3 -# AHT20 - Untested but should work +# AHT20 - Tested w/ N32G455 on i2c2 # AHT21 - Tested w/ BTT GTR 1.0 MCU on i2c3 +# AHT30 - Untested, but should work ###################################################################### -AHT10_I2C_ADDR= 0x38 +I2C_ADDR = 0x38 -AHT10_COMMANDS = { - 'INIT' :[0xE1, 0x08, 0x00], - 'MEASURE' :[0xAC, 0x33, 0x00], - 'RESET' :[0xBA, 0x08, 0x00] -} +CMD_MEASURE = [0xAC, 0x33, 0x00] +CMD_RESET = [0xBA] +CMD_INIT_AHT1X = [0xE1, 0x08, 0x00] +CMD_INIT_AHT2X = [0xBE, 0x08, 0x00] -AHT10_MAX_BUSY_CYCLES= 5 +# Status bits +STATUS_BUSY = 0x80 +STATUS_CALIBRATED = 0x08 + +MAX_BUSY_CYCLES = 5 + +class AHTBase: + model = None -class AHT10: def __init__(self, config): self.printer = config.get_printer() self.name = config.get_name().split()[-1] self.reactor = self.printer.get_reactor() self.i2c = bus.MCU_I2C_from_config( - config, default_addr=AHT10_I2C_ADDR, default_speed=100000) - self.report_time = config.getint('aht10_report_time',30,minval=5) + config, default_addr=I2C_ADDR, default_speed=100000) + self.report_time = config.getint('aht10_report_time', 30, minval=5) self.temp = self.min_temp = self.max_temp = self.humidity = 0. - self.sample_timer = self.reactor.register_timer(self._sample_aht10) + self.sample_timer = self.reactor.register_timer(self._sample_aht) + self.printer.add_object("aht10 " + self.name, self) self.printer.register_event_handler("klippy:connect", - self.handle_connect) - self.is_calibrated = False + self.handle_connect) + self.is_calibrated = False self.init_sent = False + self._callback = None def handle_connect(self): - self._init_aht10() + self._init_sensor() self.reactor.update_timer(self.sample_timer, self.reactor.NOW) - def setup_minmax(self, min_temp, max_temp): - self.min_temp = min_temp - self.max_temp = max_temp + def _send_init(self): + raise NotImplementedError("Subclass must implement _send_init") - def setup_callback(self, cb): - self._callback = cb + def _init_sensor(self): + self._send_init() - def get_report_time_delta(self): - return self.report_time + self.init_sent = True + + if self._make_measurement(): + if not self.is_calibrated: + logging.warning("%s %s: not calibrated, possible OTP fault" + % (self.model, self.name)) + logging.info("%s %s: successfully initialized, " + "initial temp: %.3f, humidity: %.3f" + % (self.model, self.name, self.temp, self.humidity)) + def _soft_reset(self): + logging.info("%s %s: performing soft reset" % (self.model, self.name)) + self.i2c.i2c_write(CMD_RESET) + self.reactor.pause(self.reactor.monotonic() + 0.020) def _make_measurement(self): if not self.init_sent: @@ -66,45 +85,51 @@ class AHT10: while is_busy: # Check if we're constantly busy. If so, send soft-reset # and issue warning. - if is_busy and cycles > AHT10_MAX_BUSY_CYCLES: - logging.warning("aht10: device reported busy after " + - "%d cycles, resetting device"% AHT10_MAX_BUSY_CYCLES) - self._reset_device() + if is_busy and cycles > MAX_BUSY_CYCLES: + logging.warning("%s %s: device reported busy after " + "%d cycles, resetting device" + % (self.model, self.name, MAX_BUSY_CYCLES)) + self._soft_reset() data = None break cycles += 1 # Write command for updating temperature+status bit - self.i2c.i2c_write(AHT10_COMMANDS['MEASURE']) + self.i2c.i2c_write(CMD_MEASURE) # Wait 110ms after first read, 75ms minimum self.reactor.pause(self.reactor.monotonic() + .110) - # Read data + # Read 6 bytes of data read = self.i2c.i2c_read([], 6) if read is None: - logging.warning("aht10: received data from" + - " i2c_read is None") - continue - data = bytearray(read['response']) - if len(data) < 6: - logging.warning("aht10: received bytes less than" + - " expected 6 [%d]"%len(data)) + logging.warning("%s %s: received data from i2c_read is None" + % (self.model, self.name)) continue - self.is_calibrated = True if (data[0] & 0b00001000) else False - is_busy = True if (data[0] & 0b10000000) else False + data = bytearray(read['response']) + if len(data) < 6: + logging.warning("%s %s: received bytes less than expected:" + " got %d, need 6" + % (self.model, self.name, len(data))) + continue + + self.is_calibrated = bool(data[0] & STATUS_CALIBRATED) + is_busy = bool(data[0] & STATUS_BUSY) if is_busy: return False except Exception as e: - logging.exception("aht10: exception encountered" + - " reading data: %s"%str(e)) + logging.exception("%s %s: exception encountered reading data: %s" + % (self.model, self.name, str(e))) return False - temp = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5] - self.temp = ((temp*200) / 1048576) - 50 - hum = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4 - self.humidity = int(hum * 100 / 1048576) + # Parse temperature: 20 bits starting at data[3] (low nibble) + temp_raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5] + self.temp = ((temp_raw * 200.0) / 1048576.0) - 50.0 + + # Parse humidity: 20 bits starting at data[1] + hum_raw = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4 + self.humidity = int(hum_raw * 100 / 1048576) # Clamp humidity if (self.humidity > 100): @@ -114,49 +139,61 @@ class AHT10: return True - def _reset_device(self): - if not self.init_sent: - return - - # Reset device - self.i2c.i2c_write(AHT10_COMMANDS['RESET']) - # Wait 100ms after reset - self.reactor.pause(self.reactor.monotonic() + .10) - - def _init_aht10(self): - # Init device - self.i2c.i2c_write(AHT10_COMMANDS['INIT']) - # Wait 100ms after init - self.reactor.pause(self.reactor.monotonic() + .10) - self.init_sent = True - - if self._make_measurement(): - logging.info("aht10: successfully initialized, initial temp: " + - "%.3f, humidity: %.3f"%(self.temp, self.humidity)) - - def _sample_aht10(self, eventtime): + def _sample_aht(self, eventtime): if not self._make_measurement(): self.temp = self.humidity = .0 return self.reactor.NEVER if self.temp < self.min_temp or self.temp > self.max_temp: self.printer.invoke_shutdown( - "AHT10 temperature %0.1f outside range of %0.1f:%.01f" - % (self.temp, self.min_temp, self.max_temp)) + "%s temperature %.1f outside range of %.1f:%.1f" % + (self.model.upper(), self.temp, self.min_temp, self.max_temp)) measured_time = self.reactor.monotonic() print_time = self.i2c.get_mcu().estimated_print_time(measured_time) self._callback(print_time, self.temp) return measured_time + self.report_time + def setup_minmax(self, min_temp, max_temp): + self.min_temp = min_temp + self.max_temp = max_temp + + def setup_callback(self, cb): + self._callback = cb + + def get_report_time_delta(self): + return self.report_time + def get_status(self, eventtime): return { 'temperature': round(self.temp, 2), 'humidity': self.humidity, } +class AHT1x(AHTBase): + model = "aht1x" + + def _send_init(self): + self.i2c.i2c_write(CMD_INIT_AHT1X) + self.reactor.pause(self.reactor.monotonic() + 0.040) + +class AHT2x(AHTBase): + model = "aht2x" + + def _send_init(self): + self.i2c.i2c_write(CMD_INIT_AHT2X) + self.reactor.pause(self.reactor.monotonic() + 0.100) + +class AHT3x(AHTBase): + model = "aht3x" + + def _send_init(self): + # Wait for auto-calibration at power-on + self.reactor.pause(self.reactor.monotonic() + 0.100) def load_config(config): # Register sensor pheater = config.get_printer().lookup_object("heaters") - pheater.add_sensor_factory("AHT10", AHT10) + pheater.add_sensor_factory("AHT1X", AHT1x) + pheater.add_sensor_factory("AHT2X", AHT2x) + pheater.add_sensor_factory("AHT3X", AHT3x) From 4e3a16d17b98a0e2faf3cdab1a3fff3bdb65bdc4 Mon Sep 17 00:00:00 2001 From: minicx Date: Sat, 15 Nov 2025 15:35:59 +0300 Subject: [PATCH 263/411] docs: Update AHT family sensors documentation - Document AHT15 and AHT30 sensor support - Update sensor_type options (AHT1X, AHT2X, AHT3X) - Add note about some AHT20 working with AHT1X sensor type Signed-off-by: Lev Voronov --- docs/Config_Reference.md | 7 ++++--- docs/Features.md | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index dbf2f4eba..528465e64 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2945,7 +2945,7 @@ sensor_type: BME280 ### AHT10/AHT20/AHT21 temperature sensor -AHT10/AHT20/AHT21 two wire interface (I2C) environmental sensors. +AHT10/AHT15/AHT20/AHT21/AHT30 two wire interface (I2C) environmental sensors. Note that these sensors are not intended for use with extruders and heater beds, but rather for monitoring ambient temperature (C) and relative humidity. See @@ -2953,8 +2953,9 @@ relative humidity. See that may be used to report humidity in addition to temperature. ``` -sensor_type: AHT10 -# Also use AHT10 for AHT20 and AHT21 sensors. +sensor_type: AHT1X +# Must be "AHT1X" , "AHT2X", "AHT3X" +# Some AHT20 sensors can use "AHT1X" #i2c_address: # Default is 56 (0x38). Some AHT10 sensors give the option to use # 57 (0x39) by moving a resistor. diff --git a/docs/Features.md b/docs/Features.md index 6c5520d1b..ad0d81a81 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -120,7 +120,7 @@ Klipper supports many standard 3d printer features: * Support for common temperature sensors (eg, common thermistors, AD595, AD597, AD849x, PT100, PT1000, MAX6675, MAX31855, MAX31856, - MAX31865, BME280, HTU21D, DS18B20, AHT10, SHT3x, and LM75). Custom + MAX31865, BME280, HTU21D, DS18B20, AHT1X, AHT2X, AHT3X, SHT3x, and LM75). Custom thermistors and custom analog temperature sensors can also be configured. One can monitor the internal micro-controller temperature sensor and the internal temperature sensor of a From 9ac90f8752ce4b9d0e875ede07f7ba5efff085b1 Mon Sep 17 00:00:00 2001 From: minicx Date: Thu, 27 Nov 2025 10:44:56 +0300 Subject: [PATCH 264/411] aht10: Add AHT10 as alias for AHT1X for backwards compatibility Signed-off-by: Lev Voronov --- klippy/extras/aht10.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/klippy/extras/aht10.py b/klippy/extras/aht10.py index 6e4b5562e..ead58fbce 100644 --- a/klippy/extras/aht10.py +++ b/klippy/extras/aht10.py @@ -194,6 +194,10 @@ class AHT3x(AHTBase): def load_config(config): # Register sensor pheater = config.get_printer().lookup_object("heaters") + + # Backwards compatibility alias + pheater.add_sensor_factory("AHT10", AHT1x) + pheater.add_sensor_factory("AHT1X", AHT1x) pheater.add_sensor_factory("AHT2X", AHT2x) pheater.add_sensor_factory("AHT3X", AHT3x) From 9c84895a09fa408b2838ce85a2540ee7d4eeb117 Mon Sep 17 00:00:00 2001 From: hilbo86 Date: Sun, 30 Nov 2025 22:52:22 +0100 Subject: [PATCH 265/411] ads1x1x: Interface for "QUERY_ADC" (#7132) "QUERY_ADC" command will fail without "get_last_value" function. Signed-off-by: Timo Hilbig --- klippy/extras/ads1x1x.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/klippy/extras/ads1x1x.py b/klippy/extras/ads1x1x.py index 9290b3f14..bcdbaf698 100644 --- a/klippy/extras/ads1x1x.py +++ b/klippy/extras/ads1x1x.py @@ -322,7 +322,7 @@ class ADS1X1X_pin: self.mcu = chip.mcu self.chip = chip self.pcfg = pcfg - + self._last_state = (0., 0.) self.invalid_count = 0 self.chip._printer.register_event_handler("klippy:connect", \ @@ -362,6 +362,7 @@ class ADS1X1X_pin: # Publish result measured_time = self._reactor.monotonic() + self._last_state = (target_value, measured_time) self.callback(self.chip.mcu.estimated_print_time(measured_time), target_value) else: @@ -389,5 +390,8 @@ class ADS1X1X_pin: self.maxval = maxval self.range_check_count = range_check_count + def get_last_value(self): + return self._last_state + def load_config_prefix(config): return ADS1X1X_chip(config) From 3a700a5f62f7b8b0be698de2a16490d0a8cf6c2b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 24 Nov 2025 17:09:41 -0500 Subject: [PATCH 266/411] timer_irq: Remove TIMER_IDLE_REPEAT_TICKS special case The TIMER_IDLE_REPEAT_TICKS was intended to reduce the number of cases where timers would defer to tasks when tasks are mostly idle. However, with commit ea546c78 this became less important because timers now only defer to tasks if tasks are consistently busy for two consecutive calls to sched_check_set_tasks_busy(). The TIMER_IDLE_REPEAT_TICKS mechanism could result in extended task delays if timers do become busy. Timers can become busy in normal operation if timers are scheduled to run more than 500,000 times a second (every 2us or faster). This can occur on stepper movement when using high microstep settings. If timers become busy, it could take up to 1ms for tasks to next be given an opportunity to run (two calls to sched_check_set_tasks_busy() at 500us per call). This wouldn't typically be an issue if tasks are also busy, but in some loads tasks may need to run periodically in such a way that the task status alternates between idle and busy. In this case, the TIMER_IDLE_REPEAT_TICKS mechanism could effectively limit the number of task wakeups to only ~1000 per second. The "USB to canbus bridge" code uses tasks to transfer data to/from USB and canbus. If timers become busy, the limiting of task wakeups could lead to a situation where the effective bandwidth becomes severely constrained. In particular, this can be an issue on USB implementations that do not support "double buffering" (such as the stm32 usbotg code). There are reports of "Timer too close" errors on "USB to canbus bridge" mode as a result of this issue. Fix by removing the TIMER_IDLE_REPEAT_TICKS check. Check for busy tasks every TIMER_REPEAT_TICKS instead (100us). Signed-off-by: Kevin O'Connor --- src/avr/timer.c | 3 +-- src/generic/armcm_timer.c | 3 +-- src/generic/timer_irq.c | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/avr/timer.c b/src/avr/timer.c index d306b7214..ef6269f06 100644 --- a/src/avr/timer.c +++ b/src/avr/timer.c @@ -154,7 +154,6 @@ static struct timer wrap_timer = { .waketime = 0x8000, }; -#define TIMER_IDLE_REPEAT_TICKS 8000 #define TIMER_REPEAT_TICKS 3000 #define TIMER_MIN_ENTRY_TICKS 44 @@ -202,7 +201,7 @@ ISR(TIMER1_COMPA_vect) next = now + TIMER_DEFER_REPEAT_TICKS; goto done; } - timer_repeat_set(now + TIMER_IDLE_REPEAT_TICKS); + timer_repeat_set(now + TIMER_REPEAT_TICKS); timer_set(now); } } diff --git a/src/generic/armcm_timer.c b/src/generic/armcm_timer.c index e77081804..b7f572685 100644 --- a/src/generic/armcm_timer.c +++ b/src/generic/armcm_timer.c @@ -112,7 +112,6 @@ timer_init(void) DECL_INIT(timer_init); static uint32_t timer_repeat_until; -#define TIMER_IDLE_REPEAT_TICKS timer_from_us(500) #define TIMER_REPEAT_TICKS timer_from_us(100) #define TIMER_MIN_TRY_TICKS timer_from_us(2) @@ -141,7 +140,7 @@ timer_dispatch_many(void) timer_repeat_until = now + TIMER_REPEAT_TICKS; return TIMER_DEFER_REPEAT_TICKS; } - timer_repeat_until = tru = now + TIMER_IDLE_REPEAT_TICKS; + timer_repeat_until = tru = now + TIMER_REPEAT_TICKS; } // Next timer in the past or near future - wait for it to be ready diff --git a/src/generic/timer_irq.c b/src/generic/timer_irq.c index 7c2e871bd..9f6b59aa3 100644 --- a/src/generic/timer_irq.c +++ b/src/generic/timer_irq.c @@ -30,7 +30,6 @@ timer_is_before(uint32_t time1, uint32_t time2) } static uint32_t timer_repeat_until; -#define TIMER_IDLE_REPEAT_TICKS timer_from_us(500) #define TIMER_REPEAT_TICKS timer_from_us(100) #define TIMER_MIN_TRY_TICKS timer_from_us(2) @@ -59,7 +58,7 @@ timer_dispatch_many(void) timer_repeat_until = now + TIMER_REPEAT_TICKS; return now + TIMER_DEFER_REPEAT_TICKS; } - timer_repeat_until = tru = now + TIMER_IDLE_REPEAT_TICKS; + timer_repeat_until = tru = now + TIMER_REPEAT_TICKS; } // Next timer in the past or near future - wait for it to be ready From a6a6b21e4d2f289737d16d938632df7b73ed492e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 24 Nov 2025 19:15:02 -0500 Subject: [PATCH 267/411] armcm_timer: Use a static instruction count for TIMER_MIN_TRY_TICKS Change TIMER_MIN_TRY_TICKS from 2us to 90 instructions. On newer chips 2us is a large amount of time - for example on the 520Mhz stm32h723 it would be 1040 instructions. Using a large time can result in "busy waiting" in the irq handler when the cpu may be better spent running tasks. The armcm_timer.c code is used on most ARM cortex-M chips and on all of these chips the SysTick timer should be tied directly to the instruction counter. This change should be safe because it should not take more than 90 instructions to reschedule the timer on any of these chips. Also, all of these chips should be able to exit the irq handler and reenter it in less than 90 instructions allowing more time for tasks to run if the next timer is more than 90 timer ticks in the future. Signed-off-by: Kevin O'Connor --- src/generic/armcm_timer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generic/armcm_timer.c b/src/generic/armcm_timer.c index b7f572685..e2d77924d 100644 --- a/src/generic/armcm_timer.c +++ b/src/generic/armcm_timer.c @@ -114,7 +114,7 @@ DECL_INIT(timer_init); static uint32_t timer_repeat_until; #define TIMER_REPEAT_TICKS timer_from_us(100) -#define TIMER_MIN_TRY_TICKS timer_from_us(2) +#define TIMER_MIN_TRY_TICKS 90 #define TIMER_DEFER_REPEAT_TICKS timer_from_us(5) // Invoke timers From 2e5802370c1ab944373814f96f9d35a3db9d3143 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 24 Nov 2025 19:30:35 -0500 Subject: [PATCH 268/411] serialqueue: Tune MIN_REQTIME_DELTA timing The MIN_REQTIME_DELTA parameter controls when the host will flush incomplete message blocks to the mcu. If the message had a target time less than 250ms it would result in a flush even if a message block was not completely full. In the situation where the host generates lots of queue_step commands to the point that it fills the mcu move_queue, then it would be possible for individual queue_step commands to become eligible for transmit only microseconds apart. It could also lead to a situation where the target time was less than 250ms in the future. The result could lead to many small message blocks as each became flushed individually. Tune the MIN_REQTIME_DELTA to 100ms to reduce the chance of this. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index d30433554..dab6b8a1a 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -107,7 +107,7 @@ struct serialqueue { #define MIN_RTO 0.025 #define MAX_RTO 5.000 #define MAX_PENDING_BLOCKS 12 -#define MIN_REQTIME_DELTA 0.250 +#define MIN_REQTIME_DELTA 0.100 #define MIN_BACKGROUND_DELTA 0.005 #define IDLE_QUERY_TIME 1.0 From 2e88d8b5dfdcf0e8ea826166cb0ad1677390aafc Mon Sep 17 00:00:00 2001 From: Benjie Date: Tue, 9 Dec 2025 18:15:15 +0000 Subject: [PATCH 269/411] config: Clarify configuration settings for Ender 5 Pro (#7126) Signed-off-by: Benjie Gillam --- config/printer-creality-ender5pro-2020.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/printer-creality-ender5pro-2020.cfg b/config/printer-creality-ender5pro-2020.cfg index ded26b408..2321cfb6d 100644 --- a/config/printer-creality-ender5pro-2020.cfg +++ b/config/printer-creality-ender5pro-2020.cfg @@ -1,7 +1,7 @@ # This file contains pin mappings for the stock 2020 Creality Ender 5 # Pro with the 32-bit Creality 4.2.2 board. To use this config, during # "make menuconfig" select the STM32F103 with a "28KiB bootloader" and -# with "Use USB for communication" disabled. +# communication interface set to "Serial (on USART1 PA10/PA9)". # If you prefer a direct serial connection, in "make menuconfig" # select "Enable extra low-level configuration options" and select the From f9108496a18f158221d20a53197acc04d9c9b5f9 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 27 Nov 2025 01:26:52 +0100 Subject: [PATCH 270/411] ldc1612: fix data rate calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no need to remove 4 from data rate. Formula for conversion time is: (RCOUNT0×16)/Æ’REF0 Signed-off-by: Timofey Titovets --- klippy/extras/ldc1612.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index dd41b43ae..973556af1 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -176,7 +176,7 @@ class LDC1612: "(e.g. faulty wiring) or a faulty ldc1612 chip." % (manuf_id, dev_id, LDC1612_MANUF_ID, LDC1612_DEV_ID)) # Setup chip in requested query rate - rcount0 = self.frequency / (16. * (self.data_rate - 4)) + rcount0 = self.frequency / (16. * self.data_rate) self.set_reg(REG_RCOUNT0, int(rcount0 + 0.5)) self.set_reg(REG_OFFSET0, 0) self.set_reg(REG_SETTLECOUNT0, int(SETTLETIME*self.frequency/16. + .5)) From 2b4c55ffd118a4982cfb04a01052746bb8cb45d9 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 3 Dec 2025 01:12:53 +0100 Subject: [PATCH 271/411] servo: sync pwm clock times Arriving of SW PWM out of sync can cause pulse width distortion - make them longer Synchronize the update clock to avoid that Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/multi_pin.py | 2 ++ klippy/extras/output_pin.py | 9 +++++++++ klippy/extras/replicape.py | 2 ++ klippy/extras/servo.py | 7 ++++++- klippy/extras/sx1509.py | 2 ++ klippy/mcu.py | 17 +++++++++++++++++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/klippy/extras/multi_pin.py b/klippy/extras/multi_pin.py index c834ee077..f126f928c 100644 --- a/klippy/extras/multi_pin.py +++ b/klippy/extras/multi_pin.py @@ -46,6 +46,8 @@ class PrinterMultiPin: def set_digital(self, print_time, value): for mcu_pin in self.mcu_pins: mcu_pin.set_digital(print_time, value) + def next_aligned_print_time(self, print_time, allow_early=0.): + return print_time def set_pwm(self, print_time, value): for mcu_pin in self.mcu_pins: mcu_pin.set_pwm(print_time, value) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index a51292990..a15d55166 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -46,6 +46,11 @@ class GCodeRequestQueue: if action == "discard": del rqueue[:pos+1] continue + if action == "reschedule": + del rqueue[:pos] + self.next_min_flush_time = max(self.next_min_flush_time, + min_wait) + continue if action == "delay": pos -= 1 del rqueue[:pos+1] @@ -75,6 +80,10 @@ class GCodeRequestQueue: action, min_wait = ret if action == "discard": break + if action == "reschedule": + self.next_min_flush_time = max(self.next_min_flush_time, + min_wait) + continue self.next_min_flush_time = next_time + max(min_wait, min_sched_time) if action != "delay": break diff --git a/klippy/extras/replicape.py b/klippy/extras/replicape.py index f7f7bb64b..eaca8b83d 100644 --- a/klippy/extras/replicape.py +++ b/klippy/extras/replicape.py @@ -67,6 +67,8 @@ class pca9685_pwm: cmd_queue = self._mcu.alloc_command_queue() self._set_cmd = self._mcu.lookup_command( "queue_pca9685_out oid=%c clock=%u value=%hu", cq=cmd_queue) + def next_aligned_print_time(self, print_time, allow_early=0.): + return print_time def set_pwm(self, print_time, value): clock = self._mcu.print_time_to_clock(print_time) if self._invert: diff --git a/klippy/extras/servo.py b/klippy/extras/servo.py index f1ce99763..303e39377 100644 --- a/klippy/extras/servo.py +++ b/klippy/extras/servo.py @@ -6,6 +6,7 @@ from . import output_pin SERVO_SIGNAL_PERIOD = 0.020 +RESCHEDULE_SLACK = 0.000500 class PrinterServo: def __init__(self, config): @@ -47,8 +48,12 @@ class PrinterServo: def _set_pwm(self, print_time, value): if value == self.last_value: return "discard", 0. + aligned_ptime = self.mcu_servo.next_aligned_print_time(print_time, + RESCHEDULE_SLACK) + if aligned_ptime > print_time + RESCHEDULE_SLACK: + return "reschedule", aligned_ptime self.last_value = value - self.mcu_servo.set_pwm(print_time, value) + self.mcu_servo.set_pwm(aligned_ptime, value) def _get_pwm_from_angle(self, angle): angle = max(0., min(self.max_angle, angle)) width = self.min_width + angle * self.angle_to_width diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index 99df55df3..ce25bd027 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -178,6 +178,8 @@ class SX1509_pwm(object): self._shutdown_value = max(0., min(1., shutdown_value)) self._sx1509.set_register(self._i_on_reg, ~int(255 * self._start_value) & 0xFF) + def next_aligned_print_time(self, print_time, allow_early=0.): + return print_time def set_pwm(self, print_time, value): self._sx1509.set_register(self._i_on_reg, ~int(255 * value) if not self._invert diff --git a/klippy/mcu.py b/klippy/mcu.py index 14be8a5c0..d9bd34fb8 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -422,6 +422,7 @@ class MCU_pwm: self._invert = pin_params['invert'] self._start_value = self._shutdown_value = float(self._invert) self._last_clock = 0 + self._last_value = .0 self._pwm_max = 0. self._set_cmd = None def get_mcu(self): @@ -437,6 +438,7 @@ class MCU_pwm: shutdown_value = 1. - shutdown_value self._start_value = max(0., min(1., start_value)) self._shutdown_value = max(0., min(1., shutdown_value)) + self._last_value = self._start_value def _build_config(self): if self._max_duration and self._start_value != self._shutdown_value: raise pins.error("Pin with max duration must have start" @@ -488,6 +490,20 @@ class MCU_pwm: % (self._oid, self._last_clock, svalue), is_init=True) self._set_cmd = self._mcu.lookup_command( "queue_digital_out oid=%c clock=%u on_ticks=%u", cq=cmd_queue) + def next_aligned_print_time(self, print_time, allow_early=0.): + # Filter cases where there is no need to sync anything + if self._hardware_pwm: + return print_time + if self._last_value == 1. or self._last_value == .0: + return print_time + # Simplify the calling and allow scheduling slightly earlier + req_ptime = print_time - min(allow_early, 0.5 * self._cycle_time) + cycle_ticks = self._mcu.seconds_to_clock(self._cycle_time) + req_clock = self._mcu.print_time_to_clock(req_ptime) + last_clock = self._last_clock + pulses = (req_clock - last_clock + cycle_ticks - 1) // cycle_ticks + next_clock = last_clock + pulses * cycle_ticks + return self._mcu.clock_to_print_time(next_clock) def set_pwm(self, print_time, value): if self._invert: value = 1. - value @@ -496,6 +512,7 @@ class MCU_pwm: self._set_cmd.send([self._oid, clock, v], minclock=self._last_clock, reqclock=clock) self._last_clock = clock + self._last_value = value class MCU_adc: def __init__(self, mcu, pin_params): From f52a6f9491e6bbb6c04f3634012ae23f41e8b857 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 3 Dec 2025 17:35:12 -0500 Subject: [PATCH 272/411] output_pin: Rename "delay" flag to "repeat" in GCodeRequestQueue() Rename the flag to make it more clear what it does. Signed-off-by: Kevin O'Connor --- klippy/extras/fan.py | 2 +- klippy/extras/output_pin.py | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/klippy/extras/fan.py b/klippy/extras/fan.py index c5677ba06..2bcc512d7 100644 --- a/klippy/extras/fan.py +++ b/klippy/extras/fan.py @@ -63,7 +63,7 @@ class Fan: self.last_req_value = value self.last_fan_value = self.max_power self.mcu_fan.set_pwm(print_time, self.max_power) - return "delay", self.kick_start_time + return "repeat", print_time + self.kick_start_time self.last_fan_value = self.last_req_value = value self.mcu_fan.set_pwm(print_time, value) def set_speed(self, value, print_time=None): diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index a15d55166..58a7302ca 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -38,23 +38,23 @@ class GCodeRequestQueue: pos += 1 req_pt, req_val = rqueue[pos] # Invoke callback for the request - min_wait = 0. ret = self.callback(next_time, req_val) if ret is not None: # Handle special cases - action, min_wait = ret + action, next_min_time = ret + self.next_min_flush_time = max(self.next_min_flush_time, + next_min_time) if action == "discard": del rqueue[:pos+1] continue if action == "reschedule": del rqueue[:pos] - self.next_min_flush_time = max(self.next_min_flush_time, - min_wait) continue - if action == "delay": + if action == "repeat": pos -= 1 del rqueue[:pos+1] - self.next_min_flush_time = next_time + max(min_wait, min_sched_time) + self.next_min_flush_time = max(self.next_min_flush_time, + next_time + min_sched_time) # Ensure following queue items are flushed self.motion_queuing.note_mcu_movequeue_activity( self.next_min_flush_time, is_step_gen=False) @@ -73,19 +73,20 @@ class GCodeRequestQueue: while 1: next_time = max(print_time, self.next_min_flush_time) # Invoke callback for the request - action, min_wait = "normal", 0. + action, next_min_time = "normal", 0. ret = self.callback(next_time, value) if ret is not None: # Handle special cases - action, min_wait = ret + action, next_min_time = ret + self.next_min_flush_time = max(self.next_min_flush_time, + next_min_time) if action == "discard": break if action == "reschedule": - self.next_min_flush_time = max(self.next_min_flush_time, - min_wait) continue - self.next_min_flush_time = next_time + max(min_wait, min_sched_time) - if action != "delay": + self.next_min_flush_time = max(self.next_min_flush_time, + next_time + min_sched_time) + if action != "repeat": break From 8e6e467ebc16f93ab01ed63c55d24af52b020b54 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 14 Dec 2025 15:31:09 -0500 Subject: [PATCH 273/411] mcu: Fix incorrect reqclock during endstop homing For correct operation the trsync system must be programmed prior to the start of endstop checking. This means the desired "reqclock" for the trsync configuration messages need to use the same "clock" that the endstop start message uses - even though the actual deadline for these messages is later. Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index d9bd34fb8..507f77347 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -219,12 +219,11 @@ class MCU_trsync: self._mcu.register_response(self._handle_trsync_state, "trsync_state", self._oid) self._trsync_start_cmd.send([self._oid, report_clock, report_ticks, - self.REASON_COMMS_TIMEOUT], - reqclock=report_clock) + self.REASON_COMMS_TIMEOUT], reqclock=clock) for s in self._steppers: self._stepper_stop_cmd.send([s.get_oid(), self._oid]) self._trsync_set_timeout_cmd.send([self._oid, expire_clock], - reqclock=expire_clock) + reqclock=clock) def set_home_end_time(self, home_end_time): self._home_end_clock = self._mcu.print_time_to_clock(home_end_time) def stop(self): From 867d73f0b8988a8df79abb3223018132c11448ab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 14 Dec 2025 15:55:57 -0500 Subject: [PATCH 274/411] serialqueue: Make 31-bit clock overflow check a little more robust Allow reqclock to be slightly less than the transmitted messages's deadline. That is, delay messages with a reqclock far in the future to slightly past (1<<31) ticks from its deadline. Use (3<<29) instead, which gives an additional (1<<29) grace period to avoid clock overflows. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index dab6b8a1a..3ab7216b9 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -886,9 +886,10 @@ serialqueue_send_batch(struct serialqueue *sq, struct command_queue *cq int len = 0; struct queue_message *qm; list_for_each_entry(qm, msgs, node) { - if (qm->min_clock + (1LL<<31) < qm->req_clock + if (qm->min_clock + (3LL<<29) < qm->req_clock && qm->req_clock != BACKGROUND_PRIORITY_CLOCK) - qm->min_clock = qm->req_clock - (1LL<<31); + // Avoid mcu clock comparison 31-bit overflow issues + qm->min_clock = qm->req_clock - (3LL<<29); len += qm->len; } if (! len) From d92dda439eb0f86a5a7235fa78878103be3e1de8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 12 Dec 2025 18:30:23 -0500 Subject: [PATCH 275/411] lib: Update pico-sdk to v2.2.0 The new rp2350 chips with A4 stepping require pico-sdk v2.2.0 . Signed-off-by: Kevin O'Connor --- lib/README | 2 +- lib/pico-sdk/boot/bootrom_constants.h | 344 ++++++++++++++++++ lib/pico-sdk/boot/picoboot_constants.h | 8 +- lib/pico-sdk/boot/uf2.h | 21 +- lib/pico-sdk/hardware/platform_defs.h | 3 + lib/pico-sdk/pico-sdk.patch | 26 ++ lib/pico-sdk/pico/bootrom_constants.h | 338 +---------------- lib/pico-sdk/pico/platform.h | 14 +- lib/pico-sdk/rp2040/boot_stage2/BUILD.bazel | 5 + .../rp2040/boot_stage2/CMakeLists.txt | 22 +- .../rp2040/boot_stage2/boot2_w25x10cl.S | 4 +- .../rp2040/boot_stage2/boot_stage2.ld | 1 + .../boot_stage2/include/boot_stage2/config.h | 14 +- lib/pico-sdk/rp2040/hardware/platform_defs.h | 20 + lib/pico-sdk/rp2040/hardware/regs/intctrl.h | 18 + .../rp2040/hardware/structs/busctrl.h | 1 - lib/pico-sdk/rp2350/cmsis_include/RP2350.h | 10 +- .../rp2350/cmsis_include/system_RP2350.h | 14 +- lib/pico-sdk/rp2350/hardware/regs/clocks.h | 6 +- lib/pico-sdk/rp2350/hardware/regs/dreq.h | 8 +- .../rp2350/hardware/regs/glitch_detector.h | 6 +- lib/pico-sdk/rp2350/hardware/regs/intctrl.h | 36 +- lib/pico-sdk/rp2350/hardware/regs/pio.h | 6 +- lib/pico-sdk/rp2350/hardware/regs/powman.h | 59 +-- lib/pico-sdk/rp2350/hardware/regs/rosc.h | 14 +- lib/pico-sdk/rp2350/hardware/regs/rvcsr.h | 86 ++++- lib/pico-sdk/rp2350/hardware/regs/syscfg.h | 5 +- lib/pico-sdk/rp2350/hardware/regs/ticks.h | 36 +- lib/pico-sdk/rp2350/hardware/regs/usb.h | 7 +- .../rp2350/hardware/structs/busctrl.h | 1 - lib/pico-sdk/rp2350/hardware/structs/powman.h | 16 +- lib/pico-sdk/rp2350/hardware/structs/rosc.h | 4 +- lib/pico-sdk/rp2350/hardware/structs/syscfg.h | 2 +- src/rp2040/rp2350_bootrom.c | 4 +- 34 files changed, 662 insertions(+), 499 deletions(-) create mode 100644 lib/pico-sdk/boot/bootrom_constants.h diff --git a/lib/README b/lib/README index a106abfd0..387714105 100644 --- a/lib/README +++ b/lib/README @@ -107,7 +107,7 @@ taken from the Drivers/CMSIS/Device/ST/STM32H7xx/ directory. The pico-sdk directory contains code from the pico sdk: https://github.com/raspberrypi/pico-sdk.git -version 2.0.0 (efe2103f9b28458a1615ff096054479743ade236). It has been +version 2.2.0 (a1438dff1d38bd9c65dbd693f0e5db4b9ae91779). It has been modified so that it can build outside of the pico sdk. See pico-sdk.patch for the modifications. diff --git a/lib/pico-sdk/boot/bootrom_constants.h b/lib/pico-sdk/boot/bootrom_constants.h new file mode 100644 index 000000000..e7fe6f63b --- /dev/null +++ b/lib/pico-sdk/boot/bootrom_constants.h @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef _BOOT_BOOTROM_CONSTANTS_H +#define _BOOT_BOOTROM_CONSTANTS_H + +#ifndef NO_PICO_PLATFORM +#include "pico/platform.h" +#endif + +// ROOT ADDRESSES +#define BOOTROM_MAGIC_OFFSET 0x10 +#define BOOTROM_FUNC_TABLE_OFFSET 0x14 +#if PICO_RP2040 +#define BOOTROM_DATA_TABLE_OFFSET 0x16 +#endif + +#if PICO_RP2040 +#define BOOTROM_VTABLE_OFFSET 0x00 +#define BOOTROM_TABLE_LOOKUP_OFFSET 0x18 +#else +#define BOOTROM_WELL_KNOWN_PTR_SIZE 2 +#if defined(__riscv) +#define BOOTROM_ENTRY_OFFSET 0x7dfc +#define BOOTROM_TABLE_LOOKUP_ENTRY_OFFSET (BOOTROM_ENTRY_OFFSET - BOOTROM_WELL_KNOWN_PTR_SIZE) +#define BOOTROM_TABLE_LOOKUP_OFFSET (BOOTROM_ENTRY_OFFSET - BOOTROM_WELL_KNOWN_PTR_SIZE*2) +#else +#define BOOTROM_VTABLE_OFFSET 0x00 +#define BOOTROM_TABLE_LOOKUP_OFFSET (BOOTROM_FUNC_TABLE_OFFSET + BOOTROM_WELL_KNOWN_PTR_SIZE) +#endif +#endif + +#if !PICO_RP2040 || PICO_COMBINED_DOCS + +#define BOOTROM_OK 0 +//#define BOOTROM_ERROR_TIMEOUT (-1) +//#define BOOTROM_ERROR_GENERIC (-2) +//#define BOOTROM_ERROR_NO_DATA (-3) // E.g. read from an empty buffer/FIFO +#define BOOTROM_ERROR_NOT_PERMITTED (-4) // Permission violation e.g. write to read-only flash partition +#define BOOTROM_ERROR_INVALID_ARG (-5) // Argument is outside of range of supported values` +//#define BOOTROM_ERROR_IO (-6) +//#define BOOTROM_ERROR_BADAUTH (-7) +//#define BOOTROM_ERROR_CONNECT_FAILED (-8) +//#define BOOTROM_ERROR_INSUFFICIENT_RESOURCES (-9) // Dynamic allocation of resources failed +#define BOOTROM_ERROR_INVALID_ADDRESS (-10) // Address argument was out-of-bounds or was determined to be an address that the caller may not access +#define BOOTROM_ERROR_BAD_ALIGNMENT (-11) // Address modulo transfer chunk size was nonzero (e.g. word-aligned transfer with address % 4 != 0) +#define BOOTROM_ERROR_INVALID_STATE (-12) // Something happened or failed to happen in the past, and consequently we (currently) can't service the request +#define BOOTROM_ERROR_BUFFER_TOO_SMALL (-13) // A user-allocated buffer was too small to hold the result or working state of this function +#define BOOTROM_ERROR_PRECONDITION_NOT_MET (-14) // This call failed because another ROM function must be called first +#define BOOTROM_ERROR_MODIFIED_DATA (-15) // Cached data was determined to be inconsistent with the full version of the data it was calculated from +#define BOOTROM_ERROR_INVALID_DATA (-16) // A data structure failed to validate +#define BOOTROM_ERROR_NOT_FOUND (-17) // Attempted to access something that does not exist; or, a search failed +#define BOOTROM_ERROR_UNSUPPORTED_MODIFICATION (-18) // Write is impossible based on previous writes; e.g. attempted to clear an OTP bit +#define BOOTROM_ERROR_LOCK_REQUIRED (-19) // A required lock is not owned +#define BOOTROM_ERROR_LAST (-19) + +#define RT_FLAG_FUNC_RISCV 0x0001 +#define RT_FLAG_FUNC_RISCV_FAR 0x0003 +#define RT_FLAG_FUNC_ARM_SEC 0x0004 +// reserved for 32-bit pointer: 0x0008 +#define RT_FLAG_FUNC_ARM_NONSEC 0x0010 +// reserved for 32-bit pointer: 0x0020 +#define RT_FLAG_DATA 0x0040 +// reserved for 32-bit pointer: 0x0080 + +#define PARTITION_TABLE_MAX_PARTITIONS 16 +// note this is deliberately > MAX_PARTITIONs is likely to be, and also -1 as a signed byte +#define PARTITION_TABLE_NO_PARTITION_INDEX 0xff + +// todo these are duplicated in picoboot_constants.h +// values 0-7 are secure/non-secure +#define BOOT_TYPE_NORMAL 0 +#define BOOT_TYPE_BOOTSEL 2 +#define BOOT_TYPE_RAM_IMAGE 3 +#define BOOT_TYPE_FLASH_UPDATE 4 + +// values 8-15 are secure only +#define BOOT_TYPE_PC_SP 0xd + +// ORed in if a bootloader chained into the image +#define BOOT_TYPE_CHAINED_FLAG 0x80 + +// call from NS to S +#ifndef __riscv +#define BOOTROM_API_CALLBACK_secure_call 0 +#endif +#define BOOTROM_API_CALLBACK_COUNT 1 + +#define BOOTROM_LOCK_SHA_256 0 +#define BOOTROM_LOCK_FLASH_OP 1 +#define BOOTROM_LOCK_OTP 2 +#define BOOTROM_LOCK_MAX 2 + +#define BOOTROM_LOCK_ENABLE 7 + +#define BOOT_PARTITION_NONE (-1) +#define BOOT_PARTITION_SLOT0 (-2) +#define BOOT_PARTITION_SLOT1 (-3) +#define BOOT_PARTITION_WINDOW (-4) + +#define BOOT_DIAGNOSTIC_WINDOW_SEARCHED 0x01 +// note if both BOOT_DIAGNOSTIC_INVALID_BLOCK_LOOP and BOOT_DIAGNOSTIC_VALID_BLOCK_LOOP then the block loop was valid +// but it has a PARTITION_TABLE which while it passed the initial verification (and hash/sig) had invalid contents +// (discovered when it was later loaded) +#define BOOT_DIAGNOSTIC_INVALID_BLOCK_LOOP 0x02 +#define BOOT_DIAGNOSTIC_VALID_BLOCK_LOOP 0x04 +#define BOOT_DIAGNOSTIC_VALID_IMAGE_DEF 0x08 +#define BOOT_DIAGNOSTIC_HAS_PARTITION_TABLE 0x10 +#define BOOT_DIAGNOSTIC_CONSIDERED 0x20 +#define BOOT_DIAGNOSTIC_CHOSEN 0x40 +#define BOOT_DIAGNOSTIC_PARTITION_TABLE_LSB 7 +#define BOOT_DIAGNOSTIC_PARTITION_TABLE_MATCHING_KEY_FOR_VERIFY 0x80 +#define BOOT_DIAGNOSTIC_PARTITION_TABLE_HASH_FOR_VERIFY 0x100 +#define BOOT_DIAGNOSTIC_PARTITION_TABLE_VERIFIED_OK 0x200 +#define BOOT_DIAGNOSTIC_IMAGE_DEF_LSB 10 +#define BOOT_DIAGNOSTIC_IMAGE_DEF_MATCHING_KEY_FOR_VERIFY 0x400 +#define BOOT_DIAGNOSTIC_IMAGE_DEF_HASH_FOR_VERIFY 0x800 +#define BOOT_DIAGNOSTIC_IMAGE_DEF_VERIFIED_OK 0x1000 + +#define BOOT_DIAGNOSTIC_LOAD_MAP_ENTRIES_LOADED 0x2000 +#define BOOT_DIAGNOSTIC_IMAGE_LAUNCHED 0x4000 +#define BOOT_DIAGNOSTIC_IMAGE_CONDITION_FAILURE 0x8000 + +#define BOOT_PARSED_BLOCK_DIAGNOSTIC_MATCHING_KEY_FOR_VERIFY 0x1 // if this is present and VERIFIED_OK isn't the sig check failed +#define BOOT_PARSED_BLOCK_DIAGNOSTIC_HASH_FOR_VERIFY 0x2 // if this is present and VERIFIED_OL isn't then hash check failed +#define BOOT_PARSED_BLOCK_DIAGNOSTIC_VERIFIED_OK 0x4 + +#define BOOT_TBYB_AND_UPDATE_FLAG_BUY_PENDING 0x1 +#define BOOT_TBYB_AND_UPDATE_FLAG_OTP_VERSION_APPLIED 0x2 +#define BOOT_TBYB_AND_UPDATE_FLAG_OTHER_ERASED 0x4 + +#ifndef __ASSEMBLER__ +// Limited to 3 arguments in case of varm multiplex hint (trashes Arm r3) +typedef int (*bootrom_api_callback_generic_t)(uint32_t r0, uint32_t r1, uint32_t r2); +// Return negative for error, else number of bytes transferred: +//typedef int (*bootrom_api_callback_stdout_put_blocking_t)(const uint8_t *buffer, uint32_t size); +//typedef int (*bootrom_api_callback_stdin_get_t)(uint8_t *buffer, uint32_t size); +//typedef void (*bootrom_api_callback_core1_security_setup_t)(void); +#endif + +#endif + +/*! \brief Return a bootrom lookup code based on two ASCII characters + * \ingroup pico_bootrom + * + * These codes are uses to lookup data or function addresses in the bootrom + * + * \param c1 the first character + * \param c2 the second character + * \return the 'code' to use in rom_func_lookup() or rom_data_lookup() + */ +#define ROM_TABLE_CODE(c1, c2) ((c1) | ((c2) << 8)) + +// ROM FUNCTIONS + +// RP2040 & RP2350 +#define ROM_DATA_SOFTWARE_GIT_REVISION ROM_TABLE_CODE('G', 'R') +#define ROM_FUNC_FLASH_ENTER_CMD_XIP ROM_TABLE_CODE('C', 'X') +#define ROM_FUNC_FLASH_EXIT_XIP ROM_TABLE_CODE('E', 'X') +#define ROM_FUNC_FLASH_FLUSH_CACHE ROM_TABLE_CODE('F', 'C') +#define ROM_FUNC_CONNECT_INTERNAL_FLASH ROM_TABLE_CODE('I', 'F') +#define ROM_FUNC_FLASH_RANGE_ERASE ROM_TABLE_CODE('R', 'E') +#define ROM_FUNC_FLASH_RANGE_PROGRAM ROM_TABLE_CODE('R', 'P') + + +#if PICO_RP2040 +// RP2040 only +#define ROM_FUNC_MEMCPY44 ROM_TABLE_CODE('C', '4') +#define ROM_DATA_COPYRIGHT ROM_TABLE_CODE('C', 'R') +#define ROM_FUNC_CLZ32 ROM_TABLE_CODE('L', '3') +#define ROM_FUNC_MEMCPY ROM_TABLE_CODE('M', 'C') +#define ROM_FUNC_MEMSET ROM_TABLE_CODE('M', 'S') +#define ROM_FUNC_POPCOUNT32 ROM_TABLE_CODE('P', '3') +#define ROM_FUNC_REVERSE32 ROM_TABLE_CODE('R', '3') +#define ROM_FUNC_MEMSET4 ROM_TABLE_CODE('S', '4') +#define ROM_FUNC_CTZ32 ROM_TABLE_CODE('T', '3') +#define ROM_FUNC_RESET_USB_BOOT ROM_TABLE_CODE('U', 'B') +#endif + +#if !PICO_RP2040 || PICO_COMBINED_DOCS +// RP2350 only +#define ROM_FUNC_PICK_AB_PARTITION ROM_TABLE_CODE('A', 'B') +#define ROM_FUNC_CHAIN_IMAGE ROM_TABLE_CODE('C', 'I') +#define ROM_FUNC_EXPLICIT_BUY ROM_TABLE_CODE('E', 'B') +#define ROM_FUNC_FLASH_RUNTIME_TO_STORAGE_ADDR ROM_TABLE_CODE('F', 'A') +#define ROM_DATA_FLASH_DEVINFO16_PTR ROM_TABLE_CODE('F', 'D') +#define ROM_FUNC_FLASH_OP ROM_TABLE_CODE('F', 'O') +#define ROM_FUNC_GET_B_PARTITION ROM_TABLE_CODE('G', 'B') +#define ROM_FUNC_GET_PARTITION_TABLE_INFO ROM_TABLE_CODE('G', 'P') +#define ROM_FUNC_GET_SYS_INFO ROM_TABLE_CODE('G', 'S') +#define ROM_FUNC_GET_UF2_TARGET_PARTITION ROM_TABLE_CODE('G', 'U') +#define ROM_FUNC_LOAD_PARTITION_TABLE ROM_TABLE_CODE('L', 'P') +#define ROM_FUNC_OTP_ACCESS ROM_TABLE_CODE('O', 'A') +#define ROM_DATA_PARTITION_TABLE_PTR ROM_TABLE_CODE('P', 'T') +#define ROM_FUNC_FLASH_RESET_ADDRESS_TRANS ROM_TABLE_CODE('R', 'A') +#define ROM_FUNC_REBOOT ROM_TABLE_CODE('R', 'B') +#define ROM_FUNC_SET_ROM_CALLBACK ROM_TABLE_CODE('R', 'C') +#define ROM_FUNC_SECURE_CALL ROM_TABLE_CODE('S', 'C') +#define ROM_FUNC_SET_NS_API_PERMISSION ROM_TABLE_CODE('S', 'P') +#define ROM_FUNC_BOOTROM_STATE_RESET ROM_TABLE_CODE('S', 'R') +#define ROM_FUNC_SET_BOOTROM_STACK ROM_TABLE_CODE('S', 'S') +#define ROM_DATA_SAVED_XIP_SETUP_FUNC_PTR ROM_TABLE_CODE('X', 'F') +#define ROM_FUNC_FLASH_SELECT_XIP_READ_MODE ROM_TABLE_CODE('X', 'M') +#define ROM_FUNC_VALIDATE_NS_BUFFER ROM_TABLE_CODE('V', 'B') +#endif + +// these form a bit set +#define BOOTROM_STATE_RESET_CURRENT_CORE 0x01 +#define BOOTROM_STATE_RESET_OTHER_CORE 0x02 +#define BOOTROM_STATE_RESET_GLOBAL_STATE 0x04 // reset any global state (e.g. permissions) + +// partition level stuff is returned first (note PT_INFO flags is only 16 bits) + +// 3 words: pt_count, unpartitioned_perm_loc, unpartioned_perm_flags +#define PT_INFO_PT_INFO 0x0001 +#define PT_INFO_SINGLE_PARTITION 0x8000 // marker to just include a single partition in the results) + +// then in order per partition selected + +// 2 words: unpartitioned_perm_loc, unpartioned_perm_flags +#define PT_INFO_PARTITION_LOCATION_AND_FLAGS 0x0010 +// 2 words: id lsb first +#define PT_INFO_PARTITION_ID 0x0020 +// n+1 words: n, family_id... +#define PT_INFO_PARTITION_FAMILY_IDS 0x0040 +// (n+3)/4 words... bytes are: n (len), c0, c1, ... cn-1 padded to word boundary with zeroes +#define PT_INFO_PARTITION_NAME 0x0080 + +// items are returned in order +// 3 words package_id, device_id_lo, device_id_hi +#define SYS_INFO_CHIP_INFO 0x0001 +// 1 word: chip specific critical bits +#define SYS_INFO_CRITICAL 0x0002 +// 1 word: bytes: cpu_type, supported_cpu_type_bitfield +#define SYS_INFO_CPU_INFO 0x0004 +// 1 word: same as FLASH_DEVINFO row in OTP +#define SYS_INFO_FLASH_DEV_INFO 0x0008 +// 4 words +#define SYS_INFO_BOOT_RANDOM 0x0010 +// 2 words lsb first +#define SYS_INFO_NONCE 0x0020 +// 4 words boot_info, boot_diagnostic, boot_param0, boot_param1 +#define SYS_INFO_BOOT_INFO 0x0040 + +#define BOOTROM_NS_API_get_sys_info 0 +#define BOOTROM_NS_API_checked_flash_op 1 +#define BOOTROM_NS_API_flash_runtime_to_storage_addr 2 +#define BOOTROM_NS_API_get_partition_table_info 3 +#define BOOTROM_NS_API_secure_call 4 +#define BOOTROM_NS_API_otp_access 5 +#define BOOTROM_NS_API_reboot 6 +#define BOOTROM_NS_API_get_b_partition 7 +#define BOOTROM_NS_API_COUNT 8 + +#define OTP_CMD_ROW_BITS _u(0x0000ffff) +#define OTP_CMD_ROW_LSB _u(0) +#define OTP_CMD_WRITE_BITS _u(0x00010000) +#define OTP_CMD_WRITE_LSB _u(16) +#define OTP_CMD_ECC_BITS _u(0x00020000) +#define OTP_CMD_ECC_LSB _u(17) + +#ifndef __ASSEMBLER__ +static_assert(OTP_CMD_WRITE_BITS == (1 << OTP_CMD_WRITE_LSB), ""); +static_assert(OTP_CMD_ECC_BITS == (1 << OTP_CMD_ECC_LSB), ""); + +typedef struct { + uint32_t permissions_and_location; + uint32_t permissions_and_flags; +} resident_partition_t; +static_assert(sizeof(resident_partition_t) == 8, ""); + +typedef struct otp_cmd { + uint32_t flags; +} otp_cmd_t; + +typedef enum { + BOOTROM_XIP_MODE_03H_SERIAL = 0, + BOOTROM_XIP_MODE_0BH_SERIAL, + BOOTROM_XIP_MODE_BBH_DUAL, + BOOTROM_XIP_MODE_EBH_QUAD, + BOOTROM_XIP_MODE_N_MODES +} bootrom_xip_mode_t; + +// The checked flash API wraps the low-level flash routines from generic_flash, adding bounds +// checking, permission checking against the resident partition table, and simple address +// translation. The low-level API deals with flash offsets (i.e. distance from the start of the +// first flash device, measured in bytes) but the checked flash API accepts one of two types of +// address: +// +// - Flash runtime addresses: the address of some flash-resident data or code in the currently +// running image. The flash addresses your binary is "linked at" by the linker. +// - Flash storage addresses: a flash offset, plus the address base where QSPI hardware is first +// mapped on the system bus (XIP_BASE constant from addressmap.h) +// +// These addresses are one and the same *if* the currently running program is stored at the +// beginning of flash. They are different if the start of your image has been "rolled" by the flash +// boot path to make it appear at the address it was linked at even though it is stored at a +// different location in flash, which is necessary when you have A/B images for example. +// +// The address translation between flash runtime and flash storage addresses is configured in +// hardware by the QMI_ATRANSx registers, and this API assumes those registers contain a valid +// address mapping which it can use to translate runtime to storage addresses. + +typedef struct cflash_flags { + uint32_t flags; +} cflash_flags_t; + +#endif // #ifdef __ASSEMBLER__ + +// Bits which are permitted to be set in a flags variable -- any other bits being set is an error +#define CFLASH_FLAGS_BITS 0x00070301u + +// Used to tell checked flash API which space a given address belongs to +#define CFLASH_ASPACE_BITS 0x00000001u +#define CFLASH_ASPACE_LSB _u(0) +#define CFLASH_ASPACE_VALUE_STORAGE _u(0) +#define CFLASH_ASPACE_VALUE_RUNTIME _u(1) + +// Used to tell checked flash APIs the effective security level of a flash access (may be forced to +// one of these values for the NonSecure-exported version of this API) +#define CFLASH_SECLEVEL_BITS 0x00000300u +#define CFLASH_SECLEVEL_LSB _u(8) +// Zero is not a valid security level: +#define CFLASH_SECLEVEL_VALUE_SECURE _u(1) +#define CFLASH_SECLEVEL_VALUE_NONSECURE _u(2) +#define CFLASH_SECLEVEL_VALUE_BOOTLOADER _u(3) + +#define CFLASH_OP_BITS 0x00070000u +#define CFLASH_OP_LSB _u(16) +// Erase size_bytes bytes of flash, starting at address addr. Both addr and size_bytes must be a +// multiple of 4096 bytes (one flash sector). +#define CFLASH_OP_VALUE_ERASE _u(0) +// Program size_bytes bytes of flash, starting at address addr. Both addr and size_bytes must be a +// multiple of 256 bytes (one flash page). +#define CFLASH_OP_VALUE_PROGRAM _u(1) +// Read size_bytes bytes of flash, starting at address addr. There are no alignment restrictions on +// addr or size_bytes. +#define CFLASH_OP_VALUE_READ _u(2) +#define CFLASH_OP_MAX _u(2) + +#endif diff --git a/lib/pico-sdk/boot/picoboot_constants.h b/lib/pico-sdk/boot/picoboot_constants.h index ac78ea213..ffb3b8cc4 100644 --- a/lib/pico-sdk/boot/picoboot_constants.h +++ b/lib/pico-sdk/boot/picoboot_constants.h @@ -9,11 +9,11 @@ #define REBOOT2_TYPE_MASK 0x0f -// note these match REBOOT_TYPE in pico/bootrom_constants.h (also 0 is used for PC_SP for backwards compatibility with RP2040) +// note these match REBOOT_TYPE in pico/bootrom_constants.h // values 0-7 are secure/non-secure #define REBOOT2_FLAG_REBOOT_TYPE_NORMAL 0x0 // param0 = diagnostic partition -#define REBOOT2_FLAG_REBOOT_TYPE_BOOTSEL 0x2 // param0 = bootsel_flags, param1 = gpio_config -#define REBOOT2_FLAG_REBOOT_TYPE_RAM_IMAGE 0x3 // param0 = image_base, param1 = image_end +#define REBOOT2_FLAG_REBOOT_TYPE_BOOTSEL 0x2 // param0 = gpio_pin_number, param1 = flags +#define REBOOT2_FLAG_REBOOT_TYPE_RAM_IMAGE 0x3 // param0 = image_region_base, param1 = image_region_size #define REBOOT2_FLAG_REBOOT_TYPE_FLASH_UPDATE 0x4 // param0 = update_base // values 8-15 are secure only @@ -39,4 +39,4 @@ #define UF2_STATUS_ABORT_BAD_ADDRESS 0x20 #define UF2_STATUS_ABORT_WRITE_ERROR 0x40 #define UF2_STATUS_ABORT_REBOOT_FAILED 0x80 -#endif \ No newline at end of file +#endif diff --git a/lib/pico-sdk/boot/uf2.h b/lib/pico-sdk/boot/uf2.h index 271540a20..279d4a131 100644 --- a/lib/pico-sdk/boot/uf2.h +++ b/lib/pico-sdk/boot/uf2.h @@ -20,19 +20,30 @@ #define UF2_MAGIC_START1 0x9E5D5157u #define UF2_MAGIC_END 0x0AB16F30u -#define UF2_FLAG_NOT_MAIN_FLASH 0x00000001u -#define UF2_FLAG_FILE_CONTAINER 0x00001000u -#define UF2_FLAG_FAMILY_ID_PRESENT 0x00002000u -#define UF2_FLAG_MD5_PRESENT 0x00004000u +#define UF2_FLAG_NOT_MAIN_FLASH 0x00000001u +#define UF2_FLAG_FILE_CONTAINER 0x00001000u +#define UF2_FLAG_FAMILY_ID_PRESENT 0x00002000u +#define UF2_FLAG_MD5_PRESENT 0x00004000u +#define UF2_FLAG_EXTENSION_FLAGS_PRESENT 0x00008000u +// Extra family IDs +#define CYW43_FIRMWARE_FAMILY_ID 0xe48bff55u + +// Bootrom supported family IDs #define RP2040_FAMILY_ID 0xe48bff56u #define ABSOLUTE_FAMILY_ID 0xe48bff57u #define DATA_FAMILY_ID 0xe48bff58u #define RP2350_ARM_S_FAMILY_ID 0xe48bff59u #define RP2350_RISCV_FAMILY_ID 0xe48bff5au #define RP2350_ARM_NS_FAMILY_ID 0xe48bff5bu -#define FAMILY_ID_MAX 0xe48bff5bu +#define BOOTROM_FAMILY_ID_MIN RP2040_FAMILY_ID +#define BOOTROM_FAMILY_ID_MAX RP2350_ARM_NS_FAMILY_ID +// Defined for backwards compatibility +#define FAMILY_ID_MAX BOOTROM_FAMILY_ID_MAX + +// 04 e3 57 99 +#define UF2_EXTENSION_RP2_IGNORE_BLOCK 0x9957e304 struct uf2_block { // 32 byte header diff --git a/lib/pico-sdk/hardware/platform_defs.h b/lib/pico-sdk/hardware/platform_defs.h index 924336a92..c1a6f5f44 100644 --- a/lib/pico-sdk/hardware/platform_defs.h +++ b/lib/pico-sdk/hardware/platform_defs.h @@ -15,6 +15,9 @@ #define NUM_ALARMS 4u #define NUM_IRQS 32u +#define NUM_USER_IRQS 6u +#define FIRST_USER_IRQ (NUM_IRQS - NUM_USER_IRQS) +#define VTABLE_FIRST_IRQ 16 #define NUM_SPIN_LOCKS 32u diff --git a/lib/pico-sdk/pico-sdk.patch b/lib/pico-sdk/pico-sdk.patch index 0a27b4605..91b5dfd9c 100644 --- a/lib/pico-sdk/pico-sdk.patch +++ b/lib/pico-sdk/pico-sdk.patch @@ -1,3 +1,29 @@ +This file summarizes the local changes from the upstream pico-sdk +repository (version 2.2.0). In brief, the following steps can be used +to recreate the code here from the main pico-sdk code: + +cp /pico-sdk/src/common/boot_uf2_headers/include/boot/*.h boot/ +cp /pico-sdk/src/common/boot_picoboot_headers/include/boot/*.h boot/ +cp /pico-sdk/src/rp2_common/boot_bootrom_headers/include/boot/*.h boot/ + +cp /pico-sdk/src/rp2_common/hardware_base/include/hardware/*.h hardware/ +cp /pico-sdk/src/host/pico_platform/include/hardware/*.h hardware/ + +cp /pico-sdk/src/rp2_common/pico_bootrom/include/pico/bootrom_constants.h pico/ +cp /pico-sdk/src/host/pico_platform/include/pico/*.h pico/ + +cp -a /pico-sdk/src/rp2040/boot_stage2/ rp2040/ +cp /pico-sdk/src/rp2_common/cmsis/stub/CMSIS/Device/RP2040/Include/*.h rp2040/cmsis_include/ +cp -a /pico-sdk/src/rp2040/hardware_regs/include/hardware rp2040/ +cp -a /pico-sdk/src/rp2040/hardware_structs/include/hardware rp2040/ +cp /pico-sdk/src/rp2040/pico_platform/include/pico/*.S rp2040/pico/ + +cp /pico-sdk/src/rp2_common/cmsis/stub/CMSIS/Device/RP2350/Include/*.h rp2350/cmsis_include/ +cp /pico-sdk/src/rp2350/hardware_regs/include/hardware/regs/*.h rp2350/hardware/regs/ +cp /pico-sdk/src/rp2350/hardware_structs/include/hardware/structs/*.h rp2350/hardware/structs/ + +patch -p3 < pico-sdk.patch + diff --git a/lib/pico-sdk/hardware/address_mapped.h b/lib/pico-sdk/hardware/address_mapped.h index b384f5572..635a275b5 100644 --- a/lib/pico-sdk/hardware/address_mapped.h diff --git a/lib/pico-sdk/pico/bootrom_constants.h b/lib/pico-sdk/pico/bootrom_constants.h index 924487f8c..b3bfd47ea 100644 --- a/lib/pico-sdk/pico/bootrom_constants.h +++ b/lib/pico-sdk/pico/bootrom_constants.h @@ -4,339 +4,5 @@ * SPDX-License-Identifier: BSD-3-Clause */ -#ifndef _PICO_BOOTROM_CONSTANTS_H -#define _PICO_BOOTROM_CONSTANTS_H - -#ifndef NO_PICO_PLATFORM -#include "pico/platform.h" -#endif - -// ROOT ADDRESSES -#define BOOTROM_MAGIC_OFFSET 0x10 -#define BOOTROM_FUNC_TABLE_OFFSET 0x14 -#if PICO_RP2040 -#define BOOTROM_DATA_TABLE_OFFSET 0x16 -#endif - -#if PICO_RP2040 -#define BOOTROM_VTABLE_OFFSET 0x00 -#define BOOTROM_TABLE_LOOKUP_OFFSET 0x18 -#else -// todo remove this (or #ifdef it for A1/A2) -#define BOOTROM_IS_A2() ((*(volatile uint8_t *)0x13) == 2) -#define BOOTROM_WELL_KNOWN_PTR_SIZE (BOOTROM_IS_A2() ? 2 : 4) -#if defined(__riscv) -#define BOOTROM_ENTRY_OFFSET 0x7dfc -#define BOOTROM_TABLE_LOOKUP_ENTRY_OFFSET (BOOTROM_ENTRY_OFFSET - BOOTROM_WELL_KNOWN_PTR_SIZE) -#define BOOTROM_TABLE_LOOKUP_OFFSET (BOOTROM_ENTRY_OFFSET - BOOTROM_WELL_KNOWN_PTR_SIZE*2) -#else -#define BOOTROM_VTABLE_OFFSET 0x00 -#define BOOTROM_TABLE_LOOKUP_OFFSET (BOOTROM_FUNC_TABLE_OFFSET + BOOTROM_WELL_KNOWN_PTR_SIZE) -#endif -#endif - -#if !PICO_RP2040 || PICO_COMBINED_DOCS - -#define BOOTROM_OK 0 -//#define BOOTROM_ERROR_TIMEOUT (-1) -//#define BOOTROM_ERROR_GENERIC (-2) -//#define BOOTROM_ERROR_NO_DATA (-3) // E.g. read from an empty buffer/FIFO -#define BOOTROM_ERROR_NOT_PERMITTED (-4) // Permission violation e.g. write to read-only flash partition -#define BOOTROM_ERROR_INVALID_ARG (-5) // Argument is outside of range of supported values` -//#define BOOTROM_ERROR_IO (-6) -//#define BOOTROM_ERROR_BADAUTH (-7) -//#define BOOTROM_ERROR_CONNECT_FAILED (-8) -//#define BOOTROM_ERROR_INSUFFICIENT_RESOURCES (-9) // Dynamic allocation of resources failed -#define BOOTROM_ERROR_INVALID_ADDRESS (-10) // Address argument was out-of-bounds or was determined to be an address that the caller may not access -#define BOOTROM_ERROR_BAD_ALIGNMENT (-11) // Address modulo transfer chunk size was nonzero (e.g. word-aligned transfer with address % 4 != 0) -#define BOOTROM_ERROR_INVALID_STATE (-12) // Something happened or failed to happen in the past, and consequently we (currently) can't service the request -#define BOOTROM_ERROR_BUFFER_TOO_SMALL (-13) // A user-allocated buffer was too small to hold the result or working state of this function -#define BOOTROM_ERROR_PRECONDITION_NOT_MET (-14) // This call failed because another ROM function must be called first -#define BOOTROM_ERROR_MODIFIED_DATA (-15) // Cached data was determined to be inconsistent with the full version of the data it was calculated from -#define BOOTROM_ERROR_INVALID_DATA (-16) // A data structure failed to validate -#define BOOTROM_ERROR_NOT_FOUND (-17) // Attempted to access something that does not exist; or, a search failed -#define BOOTROM_ERROR_UNSUPPORTED_MODIFICATION (-18) // Write is impossible based on previous writes; e.g. attempted to clear an OTP bit -#define BOOTROM_ERROR_LOCK_REQUIRED (-19) // A required lock is not owned -#define BOOTROM_ERROR_LAST (-19) - -#define RT_FLAG_FUNC_RISCV 0x0001 -#define RT_FLAG_FUNC_RISCV_FAR 0x0003 -#define RT_FLAG_FUNC_ARM_SEC 0x0004 -// reserved for 32-bit pointer: 0x0008 -#define RT_FLAG_FUNC_ARM_NONSEC 0x0010 -// reserved for 32-bit pointer: 0x0020 -#define RT_FLAG_DATA 0x0040 -// reserved for 32-bit pointer: 0x0080 - -#define PARTITION_TABLE_MAX_PARTITIONS 16 -// note this is deliberately > MAX_PARTITIONs is likely to be, and also -1 as a signed byte -#define PARTITION_TABLE_NO_PARTITION_INDEX 0xff - -// todo these are duplicated in picoboot_constants.h -// values 0-7 are secure/non-secure -#define BOOT_TYPE_NORMAL 0 -#define BOOT_TYPE_BOOTSEL 2 -#define BOOT_TYPE_RAM_IMAGE 3 -#define BOOT_TYPE_FLASH_UPDATE 4 - -// values 8-15 are secure only -#define BOOT_TYPE_PC_SP 0xd - -// ORed in if a bootloader chained into the image -#define BOOT_TYPE_CHAINED_FLAG 0x80 - -// call from NS to S -#ifndef __riscv -#define BOOTROM_API_CALLBACK_secure_call 0 -#endif -#define BOOTROM_API_CALLBACK_COUNT 1 - -#define BOOTROM_LOCK_SHA_256 0 -#define BOOTROM_LOCK_FLASH_OP 1 -#define BOOTROM_LOCK_OTP 2 -#define BOOTROM_LOCK_MAX 2 - -#define BOOTROM_LOCK_ENABLE 7 - -#define BOOT_PARTITION_NONE (-1) -#define BOOT_PARTITION_SLOT0 (-2) -#define BOOT_PARTITION_SLOT1 (-3) -#define BOOT_PARTITION_WINDOW (-4) - -#define BOOT_DIAGNOSTIC_WINDOW_SEARCHED 0x01 -// note if both BOOT_DIAGNOSTIC_INVALID_BLOCK_LOOP and BOOT_DIAGNOSTIC_VALID_BLOCK_LOOP then the block loop was valid -// but it has a PARTITION_TABLE which while it passed the initial verification (and hash/sig) had invalid contents -// (discovered when it was later loaded) -#define BOOT_DIAGNOSTIC_INVALID_BLOCK_LOOP 0x02 -#define BOOT_DIAGNOSTIC_VALID_BLOCK_LOOP 0x04 -#define BOOT_DIAGNOSTIC_VALID_IMAGE_DEF 0x08 -#define BOOT_DIAGNOSTIC_HAS_PARTITION_TABLE 0x10 -#define BOOT_DIAGNOSTIC_CONSIDERED 0x20 -#define BOOT_DIAGNOSTIC_CHOSEN 0x40 -#define BOOT_DIAGNOSTIC_PARTITION_TABLE_LSB 7 -#define BOOT_DIAGNOSTIC_PARTITION_TABLE_MATCHING_KEY_FOR_VERIFY 0x80 -#define BOOT_DIAGNOSTIC_PARTITION_TABLE_HASH_FOR_VERIFY 0x100 -#define BOOT_DIAGNOSTIC_PARTITION_TABLE_VERIFIED_OK 0x200 -#define BOOT_DIAGNOSTIC_IMAGE_DEF_LSB 10 -#define BOOT_DIAGNOSTIC_IMAGE_DEF_MATCHING_KEY_FOR_VERIFY 0x400 -#define BOOT_DIAGNOSTIC_IMAGE_DEF_HASH_FOR_VERIFY 0x800 -#define BOOT_DIAGNOSTIC_IMAGE_DEF_VERIFIED_OK 0x1000 - -#define BOOT_DIAGNOSTIC_LOAD_MAP_ENTRIES_LOADED 0x2000 -#define BOOT_DIAGNOSTIC_IMAGE_LAUNCHED 0x4000 -#define BOOT_DIAGNOSTIC_IMAGE_CONDITION_FAILURE 0x8000 - -#define BOOT_PARSED_BLOCK_DIAGNOSTIC_MATCHING_KEY_FOR_VERIFY 0x1 // if this is present and VERIFIED_OK isn't the sig check failed -#define BOOT_PARSED_BLOCK_DIAGNOSTIC_HASH_FOR_VERIFY 0x2 // if this is present and VERIFIED_OL isn't then hash check failed -#define BOOT_PARSED_BLOCK_DIAGNOSTIC_VERIFIED_OK 0x4 - -#define BOOT_TBYB_AND_UPDATE_FLAG_BUY_PENDING 0x1 -#define BOOT_TBYB_AND_UPDATE_FLAG_OTP_VERSION_APPLIED 0x2 -#define BOOT_TBYB_AND_UPDATE_FLAG_OTHER_ERASED 0x4 - -#ifndef __ASSEMBLER__ -// Limited to 3 arguments in case of varm multiplex hint (trashes Arm r3) -typedef int (*bootrom_api_callback_generic_t)(uint32_t r0, uint32_t r1, uint32_t r2); -// Return negative for error, else number of bytes transferred: -//typedef int (*bootrom_api_callback_stdout_put_blocking_t)(const uint8_t *buffer, uint32_t size); -//typedef int (*bootrom_api_callback_stdin_get_t)(uint8_t *buffer, uint32_t size); -//typedef void (*bootrom_api_callback_core1_security_setup_t)(void); -#endif - -#endif - -/*! \brief Return a bootrom lookup code based on two ASCII characters - * \ingroup pico_bootrom - * - * These codes are uses to lookup data or function addresses in the bootrom - * - * \param c1 the first character - * \param c2 the second character - * \return the 'code' to use in rom_func_lookup() or rom_data_lookup() - */ -#define ROM_TABLE_CODE(c1, c2) ((c1) | ((c2) << 8)) - -// ROM FUNCTIONS - -// RP2040 & RP2350 -#define ROM_DATA_SOFTWARE_GIT_REVISION ROM_TABLE_CODE('G', 'R') -#define ROM_FUNC_FLASH_ENTER_CMD_XIP ROM_TABLE_CODE('C', 'X') -#define ROM_FUNC_FLASH_EXIT_XIP ROM_TABLE_CODE('E', 'X') -#define ROM_FUNC_FLASH_FLUSH_CACHE ROM_TABLE_CODE('F', 'C') -#define ROM_FUNC_CONNECT_INTERNAL_FLASH ROM_TABLE_CODE('I', 'F') -#define ROM_FUNC_FLASH_RANGE_ERASE ROM_TABLE_CODE('R', 'E') -#define ROM_FUNC_FLASH_RANGE_PROGRAM ROM_TABLE_CODE('R', 'P') - - -#if PICO_RP2040 -// RP2040 only -#define ROM_FUNC_MEMCPY44 ROM_TABLE_CODE('C', '4') -#define ROM_DATA_COPYRIGHT ROM_TABLE_CODE('C', 'R') -#define ROM_FUNC_CLZ32 ROM_TABLE_CODE('L', '3') -#define ROM_FUNC_MEMCPY ROM_TABLE_CODE('M', 'C') -#define ROM_FUNC_MEMSET ROM_TABLE_CODE('M', 'S') -#define ROM_FUNC_POPCOUNT32 ROM_TABLE_CODE('P', '3') -#define ROM_FUNC_REVERSE32 ROM_TABLE_CODE('R', '3') -#define ROM_FUNC_MEMSET4 ROM_TABLE_CODE('S', '4') -#define ROM_FUNC_CTZ32 ROM_TABLE_CODE('T', '3') -#define ROM_FUNC_RESET_USB_BOOT ROM_TABLE_CODE('U', 'B') -#endif - -#if !PICO_RP2040 || PICO_COMBINED_DOCS -// RP2350 only -#define ROM_FUNC_PICK_AB_PARTITION ROM_TABLE_CODE('A', 'B') -#define ROM_FUNC_CHAIN_IMAGE ROM_TABLE_CODE('C', 'I') -#define ROM_FUNC_EXPLICIT_BUY ROM_TABLE_CODE('E', 'B') -#define ROM_FUNC_FLASH_RUNTIME_TO_STORAGE_ADDR ROM_TABLE_CODE('F', 'A') -#define ROM_DATA_FLASH_DEVINFO16_PTR ROM_TABLE_CODE('F', 'D') -#define ROM_FUNC_FLASH_OP ROM_TABLE_CODE('F', 'O') -#define ROM_FUNC_GET_B_PARTITION ROM_TABLE_CODE('G', 'B') -#define ROM_FUNC_GET_PARTITION_TABLE_INFO ROM_TABLE_CODE('G', 'P') -#define ROM_FUNC_GET_SYS_INFO ROM_TABLE_CODE('G', 'S') -#define ROM_FUNC_GET_UF2_TARGET_PARTITION ROM_TABLE_CODE('G', 'U') -#define ROM_FUNC_LOAD_PARTITION_TABLE ROM_TABLE_CODE('L', 'P') -#define ROM_FUNC_OTP_ACCESS ROM_TABLE_CODE('O', 'A') -#define ROM_DATA_PARTITION_TABLE_PTR ROM_TABLE_CODE('P', 'T') -#define ROM_FUNC_FLASH_RESET_ADDRESS_TRANS ROM_TABLE_CODE('R', 'A') -#define ROM_FUNC_REBOOT ROM_TABLE_CODE('R', 'B') -#define ROM_FUNC_SET_ROM_CALLBACK ROM_TABLE_CODE('R', 'C') -#define ROM_FUNC_SECURE_CALL ROM_TABLE_CODE('S', 'C') -#define ROM_FUNC_SET_NS_API_PERMISSION ROM_TABLE_CODE('S', 'P') -#define ROM_FUNC_BOOTROM_STATE_RESET ROM_TABLE_CODE('S', 'R') -#define ROM_FUNC_SET_BOOTROM_STACK ROM_TABLE_CODE('S', 'S') -#define ROM_DATA_SAVED_XIP_SETUP_FUNC_PTR ROM_TABLE_CODE('X', 'F') -#define ROM_FUNC_FLASH_SELECT_XIP_READ_MODE ROM_TABLE_CODE('X', 'M') -#define ROM_FUNC_VALIDATE_NS_BUFFER ROM_TABLE_CODE('V', 'B') -#endif - -// these form a bit set -#define BOOTROM_STATE_RESET_CURRENT_CORE 0x01 -#define BOOTROM_STATE_RESET_OTHER_CORE 0x02 -#define BOOTROM_STATE_RESET_GLOBAL_STATE 0x04 // reset any global state (e.g. permissions) - -// partition level stuff is returned first (note PT_INFO flags is only 16 bits) - -// 3 words: pt_count, unpartitioned_perm_loc, unpartioned_perm_flags -#define PT_INFO_PT_INFO 0x0001 -#define PT_INFO_SINGLE_PARTITION 0x8000 // marker to just include a single partition in the results) - -// then in order per partition selected - -// 2 words: unpartitioned_perm_loc, unpartioned_perm_flags -#define PT_INFO_PARTITION_LOCATION_AND_FLAGS 0x0010 -// 2 words: id lsb first -#define PT_INFO_PARTITION_ID 0x0020 -// n+1 words: n, family_id... -#define PT_INFO_PARTITION_FAMILY_IDS 0x0040 -// (n+3)/4 words... bytes are: n (len), c0, c1, ... cn-1 padded to word boundary with zeroes -#define PT_INFO_PARTITION_NAME 0x0080 - -// items are returned in order -// 3 words package_id, device_id, wafer_id -#define SYS_INFO_CHIP_INFO 0x0001 -// 1 word: chip specific critical bits -#define SYS_INFO_CRITICAL 0x0002 -// 1 word: bytes: cpu_type, supported_cpu_type_bitfield -#define SYS_INFO_CPU_INFO 0x0004 -// 1 word: same as FLASH_DEVINFO row in OTP -#define SYS_INFO_FLASH_DEV_INFO 0x0008 -// 4 words -#define SYS_INFO_BOOT_RANDOM 0x0010 -// 2 words lsb first -#define SYS_INFO_NONCE 0x0020 -// 4 words boot_info, boot_diagnostic, boot_param0, boot_param1 -#define SYS_INFO_BOOT_INFO 0x0040 - -#define BOOTROM_NS_API_get_sys_info 0 -#define BOOTROM_NS_API_checked_flash_op 1 -#define BOOTROM_NS_API_flash_runtime_to_storage_addr 2 -#define BOOTROM_NS_API_get_partition_table_info 3 -#define BOOTROM_NS_API_secure_call 4 -#define BOOTROM_NS_API_otp_access 5 -#define BOOTROM_NS_API_reboot 6 -#define BOOTROM_NS_API_get_b_partition 7 -#define BOOTROM_NS_API_COUNT 8 - -#ifndef __ASSEMBLER__ - -typedef struct { - uint32_t permissions_and_location; - uint32_t permissions_and_flags; -} resident_partition_t; -static_assert(sizeof(resident_partition_t) == 8, ""); - -#define OTP_CMD_ROW_BITS 0x0000ffffu -#define OTP_CMD_ROW_LSB 0u -#define OTP_CMD_WRITE_BITS 0x00010000u -#define OTP_CMD_ECC_BITS 0x00020000u - -typedef struct otp_cmd { - uint32_t flags; -} otp_cmd_t; - -typedef enum { - BOOTROM_XIP_MODE_03H_SERIAL = 0, - BOOTROM_XIP_MODE_0BH_SERIAL, - BOOTROM_XIP_MODE_BBH_DUAL, - BOOTROM_XIP_MODE_EBH_QUAD, - BOOTROM_XIP_MODE_N_MODES -} bootrom_xip_mode_t; - -// The checked flash API wraps the low-level flash routines from generic_flash, adding bounds -// checking, permission checking against the resident partition table, and simple address -// translation. The low-level API deals with flash offsets (i.e. distance from the start of the -// first flash device, measured in bytes) but the checked flash API accepts one of two types of -// address: -// -// - Flash runtime addresses: the address of some flash-resident data or code in the currently -// running image. The flash addresses your binary is "linked at" by the linker. -// - Flash storage addresses: a flash offset, plus the address base where QSPI hardware is first -// mapped on the system bus (XIP_BASE constant from addressmap.h) -// -// These addresses are one and the same *if* the currently running program is stored at the -// beginning of flash. They are different if the start of your image has been "rolled" by the flash -// boot path to make it appear at the address it was linked at even though it is stored at a -// different location in flash, which is necessary when you have A/B images for example. -// -// The address translation between flash runtime and flash storage addresses is configured in -// hardware by the QMI_ATRANSx registers, and this API assumes those registers contain a valid -// address mapping which it can use to translate runtime to storage addresses. - -typedef struct cflash_flags { - uint32_t flags; -} cflash_flags_t; - -// Bits which are permitted to be set in a flags variable -- any other bits being set is an error -#define CFLASH_FLAGS_BITS 0x00070301u - -// Used to tell checked flash API which space a given address belongs to -#define CFLASH_ASPACE_BITS 0x00000001u -#define CFLASH_ASPACE_LSB 0u -#define CFLASH_ASPACE_VALUE_STORAGE 0u -#define CFLASH_ASPACE_VALUE_RUNTIME 1u - -// Used to tell checked flash APIs the effective security level of a flash access (may be forced to -// one of these values for the NonSecure-exported version of this API) -#define CFLASH_SECLEVEL_BITS 0x00000300u -#define CFLASH_SECLEVEL_LSB 8u -// Zero is not a valid security level: -#define CFLASH_SECLEVEL_VALUE_SECURE 1u -#define CFLASH_SECLEVEL_VALUE_NONSECURE 2u -#define CFLASH_SECLEVEL_VALUE_BOOTLOADER 3u - -#define CFLASH_OP_BITS 0x00070000u -#define CFLASH_OP_LSB 16u -// Erase size_bytes bytes of flash, starting at address addr. Both addr and size_bytes must be a -// multiple of 4096 bytes (one flash sector). -#define CFLASH_OP_VALUE_ERASE 0u -// Program size_bytes bytes of flash, starting at address addr. Both addr and size_bytes must be a -// multiple of 256 bytes (one flash page). -#define CFLASH_OP_VALUE_PROGRAM 1u -// Read size_bytes bytes of flash, starting at address addr. There are no alignment restrictions on -// addr or size_bytes. -#define CFLASH_OP_VALUE_READ 2u -#define CFLASH_OP_MAX 2u - -#endif - -#endif +// new location; this file kept for backwards compatibility +#include "boot/bootrom_constants.h" diff --git a/lib/pico-sdk/pico/platform.h b/lib/pico-sdk/pico/platform.h index dca69f265..0338354e5 100644 --- a/lib/pico-sdk/pico/platform.h +++ b/lib/pico-sdk/pico/platform.h @@ -11,7 +11,7 @@ #include #include -#ifdef __unix__ +#if defined __unix__ && defined __GLIBC__ #include @@ -47,7 +47,7 @@ extern void tight_loop_contents(); #define __STRING(x) #x #endif -#ifndef _MSC_VER +#if !defined(_MSC_VER) || defined(__clang__) #ifndef __noreturn #define __noreturn __attribute((noreturn)) #endif @@ -60,6 +60,12 @@ extern void tight_loop_contents(); #define __noinline __attribute__((noinline)) #endif +#ifndef __force_inline +// don't think it is critical to inline in host mode, and this is simpler than picking the +// correct attribute incantation for always_inline on different compiler versions +#define __force_inline inline +#endif + #ifndef __aligned #define __aligned(x) __attribute__((aligned(x))) #endif @@ -148,7 +154,11 @@ uint get_core_num(); static inline uint __get_current_exception(void) { return 0; + } + +void busy_wait_at_least_cycles(uint32_t minimum_cycles); + #ifdef __cplusplus } #endif diff --git a/lib/pico-sdk/rp2040/boot_stage2/BUILD.bazel b/lib/pico-sdk/rp2040/boot_stage2/BUILD.bazel index 65c9e76b2..56ed5f3c3 100644 --- a/lib/pico-sdk/rp2040/boot_stage2/BUILD.bazel +++ b/lib/pico-sdk/rp2040/boot_stage2/BUILD.bazel @@ -76,6 +76,11 @@ cc_binary( copts = ["-fPIC"], # Incompatible with section garbage collection. features = ["-gc_sections"], + # Platforms will commonly depend on bootloader components in every + # binary via `link_extra_libs`, so we must drop these deps when + # building the bootloader binaries themselves in order to avoid a + # circular dependency. + link_extra_lib = "//bazel:empty_cc_lib", linkopts = [ "-Wl,--no-gc-sections", "-nostartfiles", diff --git a/lib/pico-sdk/rp2040/boot_stage2/CMakeLists.txt b/lib/pico-sdk/rp2040/boot_stage2/CMakeLists.txt index c5768785b..2798b3640 100644 --- a/lib/pico-sdk/rp2040/boot_stage2/CMakeLists.txt +++ b/lib/pico-sdk/rp2040/boot_stage2/CMakeLists.txt @@ -30,10 +30,16 @@ pico_register_common_scope_var(PICO_DEFAULT_BOOT_STAGE2_FILE) # needed by function below set(PICO_BOOT_STAGE2_DIR "${CMAKE_CURRENT_LIST_DIR}" CACHE INTERNAL "") -add_library(boot_stage2_headers INTERFACE) +pico_add_library(boot_stage2_headers) target_include_directories(boot_stage2_headers SYSTEM INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include) -# by convention the first source file name without extension is used for the binary info name +# pico_define_boot_stage2(NAME SOURCES) +# \brief\ Define a boot stage 2 target. +# +# By convention the first source file name without extension is used for the binary info name +# +# \param\ NAME The name of the boot stage 2 target +# \param\ SOURCES The source files to link into the boot stage 2 function(pico_define_boot_stage2 NAME SOURCES) add_executable(${NAME} ${SOURCES} @@ -66,15 +72,12 @@ function(pico_define_boot_stage2 NAME SOURCES) add_custom_command(OUTPUT ${ORIGINAL_BIN} DEPENDS ${NAME} COMMAND ${CMAKE_OBJCOPY} -Obinary $ ${ORIGINAL_BIN} VERBATIM) - add_custom_target(${NAME}_padded_checksummed_asm DEPENDS ${PADDED_CHECKSUMMED_ASM}) add_custom_command(OUTPUT ${PADDED_CHECKSUMMED_ASM} DEPENDS ${ORIGINAL_BIN} COMMAND ${Python3_EXECUTABLE} ${PICO_BOOT_STAGE2_DIR}/pad_checksum -s 0xffffffff ${ORIGINAL_BIN} ${PADDED_CHECKSUMMED_ASM} VERBATIM) - add_library(${NAME}_library INTERFACE) - add_dependencies(${NAME}_library ${NAME}_padded_checksummed_asm) - # not strictly (or indeed actually) a link library, but this avoids dependency cycle - target_link_libraries(${NAME}_library INTERFACE ${PADDED_CHECKSUMMED_ASM}) + add_library(${NAME}_library OBJECT ${PADDED_CHECKSUMMED_ASM}) + target_link_libraries(${NAME}_library INTERFACE "$") target_link_libraries(${NAME}_library INTERFACE boot_stage2_headers) list(GET SOURCES 0 FIRST_SOURCE) @@ -100,7 +103,12 @@ endmacro() pico_define_boot_stage2(bs2_default ${PICO_DEFAULT_BOOT_STAGE2_FILE}) +# pico_clone_default_boot_stage2(NAME) +# \brief_nodesc\ Clone the default boot stage 2 target. +# # Create a new boot stage 2 target using the default implementation for the current build (PICO_BOARD derived) +# +# \param\ NAME The name of the new boot stage 2 target function(pico_clone_default_boot_stage2 NAME) pico_define_boot_stage2(${NAME} ${PICO_DEFAULT_BOOT_STAGE2_FILE}) endfunction() diff --git a/lib/pico-sdk/rp2040/boot_stage2/boot2_w25x10cl.S b/lib/pico-sdk/rp2040/boot_stage2/boot2_w25x10cl.S index 9aa51ac57..b0e6a10fc 100644 --- a/lib/pico-sdk/rp2040/boot_stage2/boot2_w25x10cl.S +++ b/lib/pico-sdk/rp2040/boot_stage2/boot2_w25x10cl.S @@ -144,10 +144,10 @@ regular_func _stage2_boot // status register and checking for the "RX FIFO Not Empty" flag to assert. movs r1, #SSI_SR_RFNE_BITS -00: +1: ldr r0, [r3, #SSI_SR_OFFSET] // Read status register tst r0, r1 // RFNE status flag set? - beq 00b // If not then wait + beq 1b // If not then wait // At this point CN# will be deasserted and the SPI clock will not be running. // The Winbond WX25X10CL device will be in continuous read, dual I/O mode and diff --git a/lib/pico-sdk/rp2040/boot_stage2/boot_stage2.ld b/lib/pico-sdk/rp2040/boot_stage2/boot_stage2.ld index f8669ab64..32978a16e 100644 --- a/lib/pico-sdk/rp2040/boot_stage2/boot_stage2.ld +++ b/lib/pico-sdk/rp2040/boot_stage2/boot_stage2.ld @@ -7,6 +7,7 @@ MEMORY { SECTIONS { . = ORIGIN(SRAM); .text : { + _start = .; /* make LLVM happy */ *(.entry) *(.text) } >SRAM diff --git a/lib/pico-sdk/rp2040/boot_stage2/include/boot_stage2/config.h b/lib/pico-sdk/rp2040/boot_stage2/include/boot_stage2/config.h index e4d32628c..568aca1ef 100644 --- a/lib/pico-sdk/rp2040/boot_stage2/include/boot_stage2/config.h +++ b/lib/pico-sdk/rp2040/boot_stage2/include/boot_stage2/config.h @@ -11,13 +11,15 @@ #include "pico.h" -// PICO_CONFIG: PICO_BUILD_BOOT_STAGE2_NAME, The name of the boot stage 2 if selected by the build, group=boot_stage2 +// PICO_CONFIG: PICO_FLASH_SPI_CLKDIV, Clock divider from clk_sys to use for serial flash communications in boot stage 2. On RP2040 this must be a multiple of 2. This define applies to compilation of the boot stage 2 not the main application, type=int, default=varies; often specified in board header, advanced=true, group=boot_stage2 + +// PICO_CONFIG: PICO_BUILD_BOOT_STAGE2_NAME, Name of the boot stage 2 if selected in the build system. This define applies to compilation of the boot stage 2 not the main application, group=boot_stage2 #ifdef PICO_BUILD_BOOT_STAGE2_NAME #define _BOOT_STAGE2_SELECTED #else // check that multiple boot stage 2 options haven't been set... -// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_IS25LP080, Select boot2_is25lp080 as the boot stage 2 when no boot stage 2 selection is made by the CMake build, type=bool, default=0, group=boot_stage2 +// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_IS25LP080, Select boot2_is25lp080 as the boot stage 2 when no boot stage 2 selection is made by the CMake build. This define applies to compilation of the boot stage 2 not the main application, type=bool, default=0, group=boot_stage2 #ifndef PICO_BOOT_STAGE2_CHOOSE_IS25LP080 #define PICO_BOOT_STAGE2_CHOOSE_IS25LP080 0 #elif PICO_BOOT_STAGE2_CHOOSE_IS25LP080 @@ -26,7 +28,7 @@ #endif #define _BOOT_STAGE2_SELECTED #endif -// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_W25Q080, Select boot2_w25q080 as the boot stage 2 when no boot stage 2 selection is made by the CMake build, type=bool, default=0, group=boot_stage2 +// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_W25Q080, Select boot2_w25q080 as the boot stage 2 when no boot stage 2 selection is made by the CMake build. This define applies to compilation of the boot stage 2 not the main application, type=bool, default=0, group=boot_stage2 #ifndef PICO_BOOT_STAGE2_CHOOSE_W25Q080 #define PICO_BOOT_STAGE2_CHOOSE_W25Q080 0 #elif PICO_BOOT_STAGE2_CHOOSE_W25Q080 @@ -35,7 +37,7 @@ #endif #define _BOOT_STAGE2_SELECTED #endif -// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_W25X10CL, Select boot2_w25x10cl as the boot stage 2 when no boot stage 2 selection is made by the CMake build, type=bool, default=0, group=boot_stage2 +// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_W25X10CL, Select boot2_w25x10cl as the boot stage 2 when no boot stage 2 selection is made by the CMake build. This define applies to compilation of the boot stage 2 not the main application, type=bool, default=0, group=boot_stage2 #ifndef PICO_BOOT_STAGE2_CHOOSE_W25X10CL #define PICO_BOOT_STAGE2_CHOOSE_W25X10CL 0 #elif PICO_BOOT_STAGE2_CHOOSE_W25X10CL @@ -44,7 +46,7 @@ #endif #define _BOOT_STAGE2_SELECTED #endif -// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_AT25SF128A, Select boot2_at25sf128a as the boot stage 2 when no boot stage 2 selection is made by the CMake build, type=bool, default=0, group=boot_stage2 +// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_AT25SF128A, Select boot2_at25sf128a as the boot stage 2 when no boot stage 2 selection is made by the CMake build. This define applies to compilation of the boot stage 2 not the main application, type=bool, default=0, group=boot_stage2 #ifndef PICO_BOOT_STAGE2_CHOOSE_AT25SF128A #define PICO_BOOT_STAGE2_CHOOSE_AT25SF128A 0 #elif PICO_BOOT_STAGE2_CHOOSE_AT25SF128A @@ -54,7 +56,7 @@ #define _BOOT_STAGE2_SELECTED #endif -// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_GENERIC_03H, Select boot2_generic_03h as the boot stage 2 when no boot stage 2 selection is made by the CMake build, type=bool, default=1, group=boot_stage2 +// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_GENERIC_03H, Select boot2_generic_03h as the boot stage 2 when no boot stage 2 selection is made by the CMake build. This define applies to compilation of the boot stage 2 not the main application, type=bool, default=1, group=boot_stage2 #if defined(PICO_BOOT_STAGE2_CHOOSE_GENERIC_03H) && PICO_BOOT_STAGE2_CHOOSE_GENERIC_03H #ifdef _BOOT_STAGE2_SELECTED #error multiple boot stage 2 options chosen diff --git a/lib/pico-sdk/rp2040/hardware/platform_defs.h b/lib/pico-sdk/rp2040/hardware/platform_defs.h index 54d9344c8..a877710d0 100644 --- a/lib/pico-sdk/rp2040/hardware/platform_defs.h +++ b/lib/pico-sdk/rp2040/hardware/platform_defs.h @@ -46,6 +46,15 @@ #define HAS_SIO_DIVIDER 1 #define HAS_RP2040_RTC 1 + +#ifndef FPGA_CLK_SYS_HZ +#define FPGA_CLK_SYS_HZ (48 * MHZ) +#endif + +#ifndef FPGA_CLK_REF_HZ +#define FPGA_CLK_REF_HZ (12 * MHZ) +#endif + // PICO_CONFIG: XOSC_HZ, Crystal oscillator frequency in Hz, type=int, default=12000000, advanced=true, group=hardware_base // NOTE: The system and USB clocks are generated from the frequency using two PLLs. // If you override this define, or SYS_CLK_HZ/USB_CLK_HZ below, you will *also* need to add your own adjusted PLL set-up defines to @@ -61,6 +70,11 @@ #endif #endif +// PICO_CONFIG: PICO_USE_FASTEST_SUPPORTED_CLOCK, Use the fastest officially supported clock by default, type=bool, default=0, group=hardware_base +#ifndef PICO_USE_FASTEST_SUPPORTED_CLOCK +#define PICO_USE_FASTEST_SUPPORTED_CLOCK 0 +#endif + // PICO_CONFIG: SYS_CLK_HZ, System operating frequency in Hz, type=int, default=125000000, advanced=true, group=hardware_base #ifndef SYS_CLK_HZ #ifdef SYS_CLK_KHZ @@ -68,9 +82,13 @@ #elif defined(SYS_CLK_MHZ) #define SYS_CLK_HZ ((SYS_CLK_MHZ) * _u(1000000)) #else +#if PICO_USE_FASTEST_SUPPORTED_CLOCK +#define SYS_CLK_HZ _u(200000000) +#else #define SYS_CLK_HZ _u(125000000) #endif #endif +#endif // PICO_CONFIG: USB_CLK_HZ, USB clock frequency. Must be 48MHz for the USB interface to operate correctly, type=int, default=48000000, advanced=true, group=hardware_base #ifndef USB_CLK_HZ @@ -116,4 +134,6 @@ #define FIRST_USER_IRQ (NUM_IRQS - NUM_USER_IRQS) #define VTABLE_FIRST_IRQ 16 +#define REG_FIELD_WIDTH(f) (f ## _MSB + 1 - f ## _LSB) + #endif diff --git a/lib/pico-sdk/rp2040/hardware/regs/intctrl.h b/lib/pico-sdk/rp2040/hardware/regs/intctrl.h index 3190b413d..71c6eb90b 100644 --- a/lib/pico-sdk/rp2040/hardware/regs/intctrl.h +++ b/lib/pico-sdk/rp2040/hardware/regs/intctrl.h @@ -39,6 +39,12 @@ #define I2C0_IRQ 23 #define I2C1_IRQ 24 #define RTC_IRQ 25 +#define SPARE_IRQ_0 26 +#define SPARE_IRQ_1 27 +#define SPARE_IRQ_2 28 +#define SPARE_IRQ_3 29 +#define SPARE_IRQ_4 30 +#define SPARE_IRQ_5 31 #else /** * \brief Interrupt numbers on RP2040 (used as typedef \ref irq_num_t) @@ -71,6 +77,12 @@ typedef enum irq_num_rp2040 { I2C0_IRQ = 23, ///< Select I2C0's IRQ output I2C1_IRQ = 24, ///< Select I2C1's IRQ output RTC_IRQ = 25, ///< Select RTC's IRQ output + SPARE_IRQ_0 = 26, ///< Select SPARE IRQ 0 + SPARE_IRQ_1 = 27, ///< Select SPARE IRQ 1 + SPARE_IRQ_2 = 28, ///< Select SPARE IRQ 2 + SPARE_IRQ_3 = 29, ///< Select SPARE IRQ 3 + SPARE_IRQ_4 = 30, ///< Select SPARE IRQ 4 + SPARE_IRQ_5 = 31, ///< Select SPARE IRQ 5 IRQ_COUNT } irq_num_t; #endif @@ -101,6 +113,12 @@ typedef enum irq_num_rp2040 { #define isr_i2c0 isr_irq23 #define isr_i2c1 isr_irq24 #define isr_rtc isr_irq25 +#define isr_spare_0 isr_irq26 +#define isr_spare_1 isr_irq27 +#define isr_spare_2 isr_irq28 +#define isr_spare_3 isr_irq29 +#define isr_spare_4 isr_irq30 +#define isr_spare_5 isr_irq31 #endif // _INTCTRL_H diff --git a/lib/pico-sdk/rp2040/hardware/structs/busctrl.h b/lib/pico-sdk/rp2040/hardware/structs/busctrl.h index 65893227d..2302025e7 100644 --- a/lib/pico-sdk/rp2040/hardware/structs/busctrl.h +++ b/lib/pico-sdk/rp2040/hardware/structs/busctrl.h @@ -24,7 +24,6 @@ // BITMASK [BITRANGE] FIELDNAME (RESETVALUE) DESCRIPTION /** \brief Bus fabric performance counters on RP2040 (used as typedef \ref bus_ctrl_perf_counter_t) - * \ingroup hardware_busctrl */ typedef enum bus_ctrl_perf_counter_rp2040 { arbiter_rom_perf_event_access = 19, diff --git a/lib/pico-sdk/rp2350/cmsis_include/RP2350.h b/lib/pico-sdk/rp2350/cmsis_include/RP2350.h index 94d0f178c..77869ea80 100644 --- a/lib/pico-sdk/rp2350/cmsis_include/RP2350.h +++ b/lib/pico-sdk/rp2350/cmsis_include/RP2350.h @@ -4,10 +4,10 @@ * @file src/rp2_common/cmsis/stub/CMSIS/Device/RP2350/Include/RP2350.h * @brief CMSIS HeaderFile * @version 0.1 - * @date Thu Aug 8 04:04:02 2024 - * @note Generated by SVDConv V3.3.47 - * from File 'src/rp2_common/cmsis/../../rp2350/hardware_regs/RP2350.svd', - * last modified on Thu Aug 8 03:59:33 2024 + * @date Mon Jul 28 11:37:41 2025 + * @note Generated by SVDConv V3.3.45 + * from File 'src/rp2350/hardware_regs/RP2350.svd', + * last modified on Mon Jul 28 11:35:05 2025 */ @@ -2028,7 +2028,7 @@ typedef struct { /*!< POWMAN Structure ignore the power down requests. To do nothing would risk entering an unrecoverable lock-up state. Invalid requests are: any combination of power up and power down requests - any request that results in swcore boing powered and xip + any request that results in swcore being powered and xip unpowered If the request is to power down the switched-core domain then POWMAN_STATE_WAITING stays active until the processors halt. During this time the POWMAN_STATE_REQ diff --git a/lib/pico-sdk/rp2350/cmsis_include/system_RP2350.h b/lib/pico-sdk/rp2350/cmsis_include/system_RP2350.h index 30881ccc6..d85fbeb6c 100644 --- a/lib/pico-sdk/rp2350/cmsis_include/system_RP2350.h +++ b/lib/pico-sdk/rp2350/cmsis_include/system_RP2350.h @@ -1,9 +1,9 @@ /*************************************************************************//** - * @file system_RP2040.h + * @file system_RP2350.h * @brief CMSIS-Core(M) Device Peripheral Access Layer Header File for - * Device RP2040 - * @version V1.0.0 - * @date 5. May 2021 + * Device RP2350 + * @version V1.0.1 + * @date 6. Sep 2024 *****************************************************************************/ /* * Copyright (c) 2009-2021 Arm Limited. All rights reserved. @@ -26,8 +26,8 @@ * SPDX-License-Identifier: BSD-3-Clause */ -#ifndef _CMSIS_SYSTEM_RP2040_H -#define _CMSIS_SYSTEM_RP2040_H +#ifndef _CMSIS_SYSTEM_RP2350_H +#define _CMSIS_SYSTEM_RP2350_H #ifdef __cplusplus extern "C" { @@ -62,4 +62,4 @@ extern void SystemCoreClockUpdate (void); } #endif -#endif /* _CMSIS_SYSTEM_RP2040_H */ +#endif /* _CMSIS_SYSTEM_RP2350_H */ diff --git a/lib/pico-sdk/rp2350/hardware/regs/clocks.h b/lib/pico-sdk/rp2350/hardware/regs/clocks.h index fd560c910..ce46345f8 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/clocks.h +++ b/lib/pico-sdk/rp2350/hardware/regs/clocks.h @@ -615,7 +615,7 @@ // Description : Clock control, can be changed on-the-fly (except for auxsrc) #define CLOCKS_CLK_SYS_CTRL_OFFSET _u(0x0000003c) #define CLOCKS_CLK_SYS_CTRL_BITS _u(0x000000e1) -#define CLOCKS_CLK_SYS_CTRL_RESET _u(0x00000000) +#define CLOCKS_CLK_SYS_CTRL_RESET _u(0x00000041) // ----------------------------------------------------------------------------- // Field : CLOCKS_CLK_SYS_CTRL_AUXSRC // Description : Selects the auxiliary clock source, will glitch when switching @@ -625,7 +625,7 @@ // 0x3 -> xosc_clksrc // 0x4 -> clksrc_gpin0 // 0x5 -> clksrc_gpin1 -#define CLOCKS_CLK_SYS_CTRL_AUXSRC_RESET _u(0x0) +#define CLOCKS_CLK_SYS_CTRL_AUXSRC_RESET _u(0x2) #define CLOCKS_CLK_SYS_CTRL_AUXSRC_BITS _u(0x000000e0) #define CLOCKS_CLK_SYS_CTRL_AUXSRC_MSB _u(7) #define CLOCKS_CLK_SYS_CTRL_AUXSRC_LSB _u(5) @@ -642,7 +642,7 @@ // fly // 0x0 -> clk_ref // 0x1 -> clksrc_clk_sys_aux -#define CLOCKS_CLK_SYS_CTRL_SRC_RESET _u(0x0) +#define CLOCKS_CLK_SYS_CTRL_SRC_RESET _u(0x1) #define CLOCKS_CLK_SYS_CTRL_SRC_BITS _u(0x00000001) #define CLOCKS_CLK_SYS_CTRL_SRC_MSB _u(0) #define CLOCKS_CLK_SYS_CTRL_SRC_LSB _u(0) diff --git a/lib/pico-sdk/rp2350/hardware/regs/dreq.h b/lib/pico-sdk/rp2350/hardware/regs/dreq.h index 6d126c0df..edcdae60b 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/dreq.h +++ b/lib/pico-sdk/rp2350/hardware/regs/dreq.h @@ -121,8 +121,8 @@ typedef enum dreq_num_rp2350 { DREQ_PWM_WRAP7 = 39, ///< Select PWM Counter 7's Wrap Value as DREQ DREQ_PWM_WRAP8 = 40, ///< Select PWM Counter 8's Wrap Value as DREQ DREQ_PWM_WRAP9 = 41, ///< Select PWM Counter 9's Wrap Value as DREQ - DREQ_PWM_WRAP10 = 42, ///< Select PWM Counter 0's Wrap Value as DREQ - DREQ_PWM_WRAP11 = 43, ///< Select PWM Counter 1's Wrap Value as DREQ + DREQ_PWM_WRAP10 = 42, ///< Select PWM Counter 10's Wrap Value as DREQ + DREQ_PWM_WRAP11 = 43, ///< Select PWM Counter 11's Wrap Value as DREQ DREQ_I2C0_TX = 44, ///< Select I2C0's TX FIFO as DREQ DREQ_I2C0_RX = 45, ///< Select I2C0's RX FIFO as DREQ DREQ_I2C1_TX = 46, ///< Select I2C1's TX FIFO as DREQ @@ -135,8 +135,8 @@ typedef enum dreq_num_rp2350 { DREQ_CORESIGHT = 53, ///< Select CORESIGHT as DREQ DREQ_SHA256 = 54, ///< Select SHA256 as DREQ DREQ_DMA_TIMER0 = 59, ///< Select DMA_TIMER0 as DREQ - DREQ_DMA_TIMER1 = 60, ///< Select DMA_TIMER0 as DREQ - DREQ_DMA_TIMER2 = 61, ///< Select DMA_TIMER1 as DREQ + DREQ_DMA_TIMER1 = 60, ///< Select DMA_TIMER1 as DREQ + DREQ_DMA_TIMER2 = 61, ///< Select DMA_TIMER2 as DREQ DREQ_DMA_TIMER3 = 62, ///< Select DMA_TIMER3 as DREQ DREQ_FORCE = 63, ///< Select FORCE as DREQ DREQ_COUNT diff --git a/lib/pico-sdk/rp2350/hardware/regs/glitch_detector.h b/lib/pico-sdk/rp2350/hardware/regs/glitch_detector.h index efdf434b3..6e108e2b7 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/glitch_detector.h +++ b/lib/pico-sdk/rp2350/hardware/regs/glitch_detector.h @@ -37,8 +37,7 @@ #define GLITCH_DETECTOR_ARM_VALUE_YES _u(0x0000) // ============================================================================= // Register : GLITCH_DETECTOR_DISARM -// Description : None -// Forcibly disarm the glitch detectors, if they are armed by OTP. +// Description : Forcibly disarm the glitch detectors, if they are armed by OTP. // Ignored if ARM is YES. // // This register is Secure read/write only. @@ -142,8 +141,7 @@ #define GLITCH_DETECTOR_SENSITIVITY_DET0_ACCESS "RW" // ============================================================================= // Register : GLITCH_DETECTOR_LOCK -// Description : None -// Write any nonzero value to disable writes to ARM, DISARM, +// Description : Write any nonzero value to disable writes to ARM, DISARM, // SENSITIVITY and LOCK. This register is Secure read/write only. #define GLITCH_DETECTOR_LOCK_OFFSET _u(0x0000000c) #define GLITCH_DETECTOR_LOCK_BITS _u(0x000000ff) diff --git a/lib/pico-sdk/rp2350/hardware/regs/intctrl.h b/lib/pico-sdk/rp2350/hardware/regs/intctrl.h index 96ce815e4..1e19e33d1 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/intctrl.h +++ b/lib/pico-sdk/rp2350/hardware/regs/intctrl.h @@ -59,12 +59,12 @@ #define PLL_USB_IRQ 43 #define POWMAN_IRQ_POW 44 #define POWMAN_IRQ_TIMER 45 -#define SPAREIRQ_IRQ_0 46 -#define SPAREIRQ_IRQ_1 47 -#define SPAREIRQ_IRQ_2 48 -#define SPAREIRQ_IRQ_3 49 -#define SPAREIRQ_IRQ_4 50 -#define SPAREIRQ_IRQ_5 51 +#define SPARE_IRQ_0 46 +#define SPARE_IRQ_1 47 +#define SPARE_IRQ_2 48 +#define SPARE_IRQ_3 49 +#define SPARE_IRQ_4 50 +#define SPARE_IRQ_5 51 #else /** * \brief Interrupt numbers on RP2350 (used as typedef \ref irq_num_t) @@ -79,8 +79,8 @@ typedef enum irq_num_rp2350 { TIMER1_IRQ_1 = 5, ///< Select TIMER1's IRQ 1 output TIMER1_IRQ_2 = 6, ///< Select TIMER1's IRQ 2 output TIMER1_IRQ_3 = 7, ///< Select TIMER1's IRQ 3 output - PWM_IRQ_WRAP_0 = 8, ///< Select PWM's IRQ_WRAP 0 output - PWM_IRQ_WRAP_1 = 9, ///< Select PWM's IRQ_WRAP 1 output + PWM_IRQ_WRAP_0 = 8, ///< Select PWM's WRAP_0 IRQ output + PWM_IRQ_WRAP_1 = 9, ///< Select PWM's WRAP_1 IRQ output DMA_IRQ_0 = 10, ///< Select DMA's IRQ 0 output DMA_IRQ_1 = 11, ///< Select DMA's IRQ 1 output DMA_IRQ_2 = 12, ///< Select DMA's IRQ 2 output @@ -96,27 +96,27 @@ typedef enum irq_num_rp2350 { IO_IRQ_BANK0_NS = 22, ///< Select IO_BANK0_NS's IRQ output IO_IRQ_QSPI = 23, ///< Select IO_QSPI's IRQ output IO_IRQ_QSPI_NS = 24, ///< Select IO_QSPI_NS's IRQ output - SIO_IRQ_FIFO = 25, ///< Select SIO's IRQ_FIFO output - SIO_IRQ_BELL = 26, ///< Select SIO's IRQ_BELL output - SIO_IRQ_FIFO_NS = 27, ///< Select SIO_NS's IRQ_FIFO output - SIO_IRQ_BELL_NS = 28, ///< Select SIO_NS's IRQ_BELL output - SIO_IRQ_MTIMECMP = 29, ///< Select SIO_IRQ_MTIMECMP's IRQ output + SIO_IRQ_FIFO = 25, ///< Select SIO's FIFO IRQ output + SIO_IRQ_BELL = 26, ///< Select SIO's BELL IRQ output + SIO_IRQ_FIFO_NS = 27, ///< Select SIO_NS's FIFO IRQ output + SIO_IRQ_BELL_NS = 28, ///< Select SIO_NS's BELL IRQ output + SIO_IRQ_MTIMECMP = 29, ///< Select SIO's MTIMECMP IRQ output CLOCKS_IRQ = 30, ///< Select CLOCKS's IRQ output SPI0_IRQ = 31, ///< Select SPI0's IRQ output SPI1_IRQ = 32, ///< Select SPI1's IRQ output UART0_IRQ = 33, ///< Select UART0's IRQ output UART1_IRQ = 34, ///< Select UART1's IRQ output - ADC_IRQ_FIFO = 35, ///< Select ADC's IRQ_FIFO output + ADC_IRQ_FIFO = 35, ///< Select ADC's FIFO IRQ output I2C0_IRQ = 36, ///< Select I2C0's IRQ output I2C1_IRQ = 37, ///< Select I2C1's IRQ output OTP_IRQ = 38, ///< Select OTP's IRQ output TRNG_IRQ = 39, ///< Select TRNG's IRQ output - PROC0_IRQ_CTI = 40, ///< Select PROC0's IRQ_CTI output - PROC1_IRQ_CTI = 41, ///< Select PROC1's IRQ_CTI output + PROC0_IRQ_CTI = 40, ///< Select PROC0's CTI IRQ output + PROC1_IRQ_CTI = 41, ///< Select PROC1's CTI IRQ output PLL_SYS_IRQ = 42, ///< Select PLL_SYS's IRQ output PLL_USB_IRQ = 43, ///< Select PLL_USB's IRQ output - POWMAN_IRQ_POW = 44, ///< Select POWMAN's IRQ_POW output - POWMAN_IRQ_TIMER = 45, ///< Select POWMAN's IRQ_TIMER output + POWMAN_IRQ_POW = 44, ///< Select POWMAN's POW IRQ output + POWMAN_IRQ_TIMER = 45, ///< Select POWMAN's TIMER IRQ output SPARE_IRQ_0 = 46, ///< Select SPARE IRQ 0 SPARE_IRQ_1 = 47, ///< Select SPARE IRQ 1 SPARE_IRQ_2 = 48, ///< Select SPARE IRQ 2 diff --git a/lib/pico-sdk/rp2350/hardware/regs/pio.h b/lib/pico-sdk/rp2350/hardware/regs/pio.h index 4a18b5c6f..d20569708 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/pio.h +++ b/lib/pico-sdk/rp2350/hardware/regs/pio.h @@ -461,8 +461,7 @@ // ============================================================================= // Register : PIO_DBG_PADOUT // Description : Read to sample the pad output values PIO is currently driving -// to the GPIOs. On RP2040 there are 30 GPIOs, so the two most -// significant bits are hardwired to 0. +// to the GPIOs. #define PIO_DBG_PADOUT_OFFSET _u(0x0000003c) #define PIO_DBG_PADOUT_BITS _u(0xffffffff) #define PIO_DBG_PADOUT_RESET _u(0x00000000) @@ -472,8 +471,7 @@ // ============================================================================= // Register : PIO_DBG_PADOE // Description : Read to sample the pad output enables (direction) PIO is -// currently driving to the GPIOs. On RP2040 there are 30 GPIOs, -// so the two most significant bits are hardwired to 0. +// currently driving to the GPIOs. #define PIO_DBG_PADOE_OFFSET _u(0x00000040) #define PIO_DBG_PADOE_BITS _u(0xffffffff) #define PIO_DBG_PADOE_RESET _u(0x00000000) diff --git a/lib/pico-sdk/rp2350/hardware/regs/powman.h b/lib/pico-sdk/rp2350/hardware/regs/powman.h index edfbabbcc..8beb5650b 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/powman.h +++ b/lib/pico-sdk/rp2350/hardware/regs/powman.h @@ -944,7 +944,7 @@ // requests and ignore the power down requests. To do nothing // would risk entering an unrecoverable lock-up state. Invalid // requests are: any combination of power up and power down -// requests any request that results in swcore boing powered and +// requests any request that results in swcore being powered and // xip unpowered If the request is to power down the switched-core // domain then POWMAN_STATE_WAITING stays active until the // processors halt. During this time the POWMAN_STATE_REQ field @@ -957,6 +957,7 @@ #define POWMAN_STATE_RESET _u(0x0000000f) // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_CHANGING +// Description : Indicates a power state change is in progress #define POWMAN_STATE_CHANGING_RESET _u(0x0) #define POWMAN_STATE_CHANGING_BITS _u(0x00002000) #define POWMAN_STATE_CHANGING_MSB _u(13) @@ -964,6 +965,9 @@ #define POWMAN_STATE_CHANGING_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_WAITING +// Description : Indicates the power manager has received a state change request +// and is waiting for other actions to complete before executing +// it #define POWMAN_STATE_WAITING_RESET _u(0x0) #define POWMAN_STATE_WAITING_BITS _u(0x00001000) #define POWMAN_STATE_WAITING_MSB _u(12) @@ -971,8 +975,8 @@ #define POWMAN_STATE_WAITING_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_BAD_HW_REQ -// Description : Bad hardware initiated state request. Went back to state 0 -// (i.e. everything powered up) +// Description : Invalid hardware initiated state request, power up requests +// actioned, power down requests ignored #define POWMAN_STATE_BAD_HW_REQ_RESET _u(0x0) #define POWMAN_STATE_BAD_HW_REQ_BITS _u(0x00000800) #define POWMAN_STATE_BAD_HW_REQ_MSB _u(11) @@ -980,7 +984,7 @@ #define POWMAN_STATE_BAD_HW_REQ_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_BAD_SW_REQ -// Description : Bad software initiated state request. No action taken. +// Description : Invalid software initiated state request ignored #define POWMAN_STATE_BAD_SW_REQ_RESET _u(0x0) #define POWMAN_STATE_BAD_SW_REQ_BITS _u(0x00000400) #define POWMAN_STATE_BAD_SW_REQ_MSB _u(10) @@ -988,9 +992,8 @@ #define POWMAN_STATE_BAD_SW_REQ_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_PWRUP_WHILE_WAITING -// Description : Request ignored because of a pending pwrup request. See -// current_pwrup_req. Note this blocks powering up AND powering -// down. +// Description : Indicates that a power state change request was ignored because +// of a pending power state change request #define POWMAN_STATE_PWRUP_WHILE_WAITING_RESET _u(0x0) #define POWMAN_STATE_PWRUP_WHILE_WAITING_BITS _u(0x00000200) #define POWMAN_STATE_PWRUP_WHILE_WAITING_MSB _u(9) @@ -998,6 +1001,8 @@ #define POWMAN_STATE_PWRUP_WHILE_WAITING_ACCESS "WC" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_REQ_IGNORED +// Description : Indicates that a software state change request was ignored +// because it clashed with an ongoing hardware or debugger request #define POWMAN_STATE_REQ_IGNORED_RESET _u(0x0) #define POWMAN_STATE_REQ_IGNORED_BITS _u(0x00000100) #define POWMAN_STATE_REQ_IGNORED_MSB _u(8) @@ -1005,6 +1010,8 @@ #define POWMAN_STATE_REQ_IGNORED_ACCESS "WC" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_REQ +// Description : This is written by software or hardware to request a new power +// state #define POWMAN_STATE_REQ_RESET _u(0x0) #define POWMAN_STATE_REQ_BITS _u(0x000000f0) #define POWMAN_STATE_REQ_MSB _u(7) @@ -1012,6 +1019,7 @@ #define POWMAN_STATE_REQ_ACCESS "RW" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_CURRENT +// Description : Indicates the current power state #define POWMAN_STATE_CURRENT_RESET _u(0xf) #define POWMAN_STATE_CURRENT_BITS _u(0x0000000f) #define POWMAN_STATE_CURRENT_MSB _u(3) @@ -1019,8 +1027,7 @@ #define POWMAN_STATE_CURRENT_ACCESS "RO" // ============================================================================= // Register : POWMAN_POW_FASTDIV -// Description : None -// divides the POWMAN clock to provide a tick for the delay module +// Description : divides the POWMAN clock to provide a tick for the delay module // and state machines // when clk_pow is running from the slow clock it is not divided // when clk_pow is running from the fast clock it is divided by @@ -1187,6 +1194,10 @@ #define POWMAN_EXT_TIME_REF_SOURCE_SEL_MSB _u(1) #define POWMAN_EXT_TIME_REF_SOURCE_SEL_LSB _u(0) #define POWMAN_EXT_TIME_REF_SOURCE_SEL_ACCESS "RW" +#define POWMAN_EXT_TIME_REF_SOURCE_SEL_VALUE_GPIO12 _u(0x0) +#define POWMAN_EXT_TIME_REF_SOURCE_SEL_VALUE_GPIO20 _u(0x1) +#define POWMAN_EXT_TIME_REF_SOURCE_SEL_VALUE_GPIO14 _u(0x2) +#define POWMAN_EXT_TIME_REF_SOURCE_SEL_VALUE_GPIO22 _u(0x3) // ============================================================================= // Register : POWMAN_LPOSC_FREQ_KHZ_INT // Description : Informs the AON Timer of the integer component of the clock @@ -1241,8 +1252,7 @@ #define POWMAN_XOSC_FREQ_KHZ_FRAC_ACCESS "RW" // ============================================================================= // Register : POWMAN_SET_TIME_63TO48 -// Description : None -// For setting the time, do not use for reading the time, use +// Description : For setting the time, do not use for reading the time, use // POWMAN_READ_TIME_UPPER and POWMAN_READ_TIME_LOWER. This field // must only be written when POWMAN_TIMER_RUN=0 #define POWMAN_SET_TIME_63TO48_OFFSET _u(0x00000060) @@ -1253,8 +1263,7 @@ #define POWMAN_SET_TIME_63TO48_ACCESS "RW" // ============================================================================= // Register : POWMAN_SET_TIME_47TO32 -// Description : None -// For setting the time, do not use for reading the time, use +// Description : For setting the time, do not use for reading the time, use // POWMAN_READ_TIME_UPPER and POWMAN_READ_TIME_LOWER. This field // must only be written when POWMAN_TIMER_RUN=0 #define POWMAN_SET_TIME_47TO32_OFFSET _u(0x00000064) @@ -1265,8 +1274,7 @@ #define POWMAN_SET_TIME_47TO32_ACCESS "RW" // ============================================================================= // Register : POWMAN_SET_TIME_31TO16 -// Description : None -// For setting the time, do not use for reading the time, use +// Description : For setting the time, do not use for reading the time, use // POWMAN_READ_TIME_UPPER and POWMAN_READ_TIME_LOWER. This field // must only be written when POWMAN_TIMER_RUN=0 #define POWMAN_SET_TIME_31TO16_OFFSET _u(0x00000068) @@ -1277,8 +1285,7 @@ #define POWMAN_SET_TIME_31TO16_ACCESS "RW" // ============================================================================= // Register : POWMAN_SET_TIME_15TO0 -// Description : None -// For setting the time, do not use for reading the time, use +// Description : For setting the time, do not use for reading the time, use // POWMAN_READ_TIME_UPPER and POWMAN_READ_TIME_LOWER. This field // must only be written when POWMAN_TIMER_RUN=0 #define POWMAN_SET_TIME_15TO0_OFFSET _u(0x0000006c) @@ -1289,8 +1296,7 @@ #define POWMAN_SET_TIME_15TO0_ACCESS "RW" // ============================================================================= // Register : POWMAN_READ_TIME_UPPER -// Description : None -// For reading bits 63:32 of the timer. When reading all 64 bits +// Description : For reading bits 63:32 of the timer. When reading all 64 bits // it is possible for the LOWER count to rollover during the read. // It is recommended to read UPPER, then LOWER, then re-read UPPER // and, if it has changed, re-read LOWER. @@ -1302,8 +1308,7 @@ #define POWMAN_READ_TIME_UPPER_ACCESS "RO" // ============================================================================= // Register : POWMAN_READ_TIME_LOWER -// Description : None -// For reading bits 31:0 of the timer. +// Description : For reading bits 31:0 of the timer. #define POWMAN_READ_TIME_LOWER_OFFSET _u(0x00000074) #define POWMAN_READ_TIME_LOWER_BITS _u(0xffffffff) #define POWMAN_READ_TIME_LOWER_RESET _u(0x00000000) @@ -1312,8 +1317,7 @@ #define POWMAN_READ_TIME_LOWER_ACCESS "RO" // ============================================================================= // Register : POWMAN_ALARM_TIME_63TO48 -// Description : None -// This field must only be written when POWMAN_ALARM_ENAB=0 +// Description : This field must only be written when POWMAN_ALARM_ENAB=0 #define POWMAN_ALARM_TIME_63TO48_OFFSET _u(0x00000078) #define POWMAN_ALARM_TIME_63TO48_BITS _u(0x0000ffff) #define POWMAN_ALARM_TIME_63TO48_RESET _u(0x00000000) @@ -1322,8 +1326,7 @@ #define POWMAN_ALARM_TIME_63TO48_ACCESS "RW" // ============================================================================= // Register : POWMAN_ALARM_TIME_47TO32 -// Description : None -// This field must only be written when POWMAN_ALARM_ENAB=0 +// Description : This field must only be written when POWMAN_ALARM_ENAB=0 #define POWMAN_ALARM_TIME_47TO32_OFFSET _u(0x0000007c) #define POWMAN_ALARM_TIME_47TO32_BITS _u(0x0000ffff) #define POWMAN_ALARM_TIME_47TO32_RESET _u(0x00000000) @@ -1332,8 +1335,7 @@ #define POWMAN_ALARM_TIME_47TO32_ACCESS "RW" // ============================================================================= // Register : POWMAN_ALARM_TIME_31TO16 -// Description : None -// This field must only be written when POWMAN_ALARM_ENAB=0 +// Description : This field must only be written when POWMAN_ALARM_ENAB=0 #define POWMAN_ALARM_TIME_31TO16_OFFSET _u(0x00000080) #define POWMAN_ALARM_TIME_31TO16_BITS _u(0x0000ffff) #define POWMAN_ALARM_TIME_31TO16_RESET _u(0x00000000) @@ -1342,8 +1344,7 @@ #define POWMAN_ALARM_TIME_31TO16_ACCESS "RW" // ============================================================================= // Register : POWMAN_ALARM_TIME_15TO0 -// Description : None -// This field must only be written when POWMAN_ALARM_ENAB=0 +// Description : This field must only be written when POWMAN_ALARM_ENAB=0 #define POWMAN_ALARM_TIME_15TO0_OFFSET _u(0x00000084) #define POWMAN_ALARM_TIME_15TO0_BITS _u(0x0000ffff) #define POWMAN_ALARM_TIME_15TO0_RESET _u(0x00000000) diff --git a/lib/pico-sdk/rp2350/hardware/regs/rosc.h b/lib/pico-sdk/rp2350/hardware/regs/rosc.h index 4865c2ee3..4caa07a7b 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/rosc.h +++ b/lib/pico-sdk/rp2350/hardware/regs/rosc.h @@ -39,9 +39,9 @@ // Field : ROSC_CTRL_FREQ_RANGE // Description : Controls the number of delay stages in the ROSC ring // LOW uses stages 0 to 7 -// MEDIUM uses stages 2 to 7 -// HIGH uses stages 4 to 7 -// TOOHIGH uses stages 6 to 7 and should not be used because its +// MEDIUM uses stages 0 to 5 +// HIGH uses stages 0 to 3 +// TOOHIGH uses stages 0 to 1 and should not be used because its // frequency exceeds design specifications // The clock output will not glitch when changing the range up one // step at a time @@ -77,7 +77,7 @@ // DS1_RANDOM=1 #define ROSC_FREQA_OFFSET _u(0x00000004) #define ROSC_FREQA_BITS _u(0xffff77ff) -#define ROSC_FREQA_RESET _u(0x00000000) +#define ROSC_FREQA_RESET _u(0x00000088) // ----------------------------------------------------------------------------- // Field : ROSC_FREQA_PASSWD // Description : Set to 0x9696 to apply the settings @@ -108,7 +108,7 @@ // ----------------------------------------------------------------------------- // Field : ROSC_FREQA_DS1_RANDOM // Description : Randomises the stage 1 drive strength -#define ROSC_FREQA_DS1_RANDOM_RESET _u(0x0) +#define ROSC_FREQA_DS1_RANDOM_RESET _u(0x1) #define ROSC_FREQA_DS1_RANDOM_BITS _u(0x00000080) #define ROSC_FREQA_DS1_RANDOM_MSB _u(7) #define ROSC_FREQA_DS1_RANDOM_LSB _u(7) @@ -124,7 +124,7 @@ // ----------------------------------------------------------------------------- // Field : ROSC_FREQA_DS0_RANDOM // Description : Randomises the stage 0 drive strength -#define ROSC_FREQA_DS0_RANDOM_RESET _u(0x0) +#define ROSC_FREQA_DS0_RANDOM_RESET _u(0x1) #define ROSC_FREQA_DS0_RANDOM_BITS _u(0x00000008) #define ROSC_FREQA_DS0_RANDOM_MSB _u(3) #define ROSC_FREQA_DS0_RANDOM_LSB _u(3) @@ -206,7 +206,7 @@ // On power-up this field is initialised to WAKE // An invalid write will also select WAKE // Warning: setup the irq before selecting dormant mode -// 0x636f6d61 -> dormant +// 0x636f6d61 -> DORMANT // 0x77616b65 -> WAKE #define ROSC_DORMANT_OFFSET _u(0x00000010) #define ROSC_DORMANT_BITS _u(0xffffffff) diff --git a/lib/pico-sdk/rp2350/hardware/regs/rvcsr.h b/lib/pico-sdk/rp2350/hardware/regs/rvcsr.h index f5ff378ab..a375a19d1 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/rvcsr.h +++ b/lib/pico-sdk/rp2350/hardware/regs/rvcsr.h @@ -86,7 +86,7 @@ // In addition the following custom extensions are configured: // Xh3bm, Xh3power, Xh3irq, Xh3pmpm #define RVCSR_MISA_OFFSET _u(0x00000301) -#define RVCSR_MISA_BITS _u(0xc0901107) +#define RVCSR_MISA_BITS _u(0xc0b511bf) #define RVCSR_MISA_RESET _u(0x40901105) // ----------------------------------------------------------------------------- // Field : RVCSR_MISA_MXL @@ -106,6 +106,14 @@ #define RVCSR_MISA_X_LSB _u(23) #define RVCSR_MISA_X_ACCESS "RO" // ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_V +// Description : Vector extension (not implemented). +#define RVCSR_MISA_V_RESET _u(0x0) +#define RVCSR_MISA_V_BITS _u(0x00200000) +#define RVCSR_MISA_V_MSB _u(21) +#define RVCSR_MISA_V_LSB _u(21) +#define RVCSR_MISA_V_ACCESS "RO" +// ----------------------------------------------------------------------------- // Field : RVCSR_MISA_U // Description : Value of 1 indicates U-mode is implemented. #define RVCSR_MISA_U_RESET _u(0x1) @@ -114,6 +122,22 @@ #define RVCSR_MISA_U_LSB _u(20) #define RVCSR_MISA_U_ACCESS "RO" // ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_S +// Description : Supervisor extension (not implemented). +#define RVCSR_MISA_S_RESET _u(0x0) +#define RVCSR_MISA_S_BITS _u(0x00040000) +#define RVCSR_MISA_S_MSB _u(18) +#define RVCSR_MISA_S_LSB _u(18) +#define RVCSR_MISA_S_ACCESS "RO" +// ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_Q +// Description : Quad-precision floating point extension (not implemented). +#define RVCSR_MISA_Q_RESET _u(0x0) +#define RVCSR_MISA_Q_BITS _u(0x00010000) +#define RVCSR_MISA_Q_MSB _u(16) +#define RVCSR_MISA_Q_LSB _u(16) +#define RVCSR_MISA_Q_ACCESS "RO" +// ----------------------------------------------------------------------------- // Field : RVCSR_MISA_M // Description : Value of 1 indicates the M extension (integer multiply/divide) // is implemented. @@ -132,6 +156,39 @@ #define RVCSR_MISA_I_LSB _u(8) #define RVCSR_MISA_I_ACCESS "RO" // ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_H +// Description : Hypervisor extension (not implemented, I agree it would be +// pretty cool on a microcontroller through). +#define RVCSR_MISA_H_RESET _u(0x0) +#define RVCSR_MISA_H_BITS _u(0x00000080) +#define RVCSR_MISA_H_MSB _u(7) +#define RVCSR_MISA_H_LSB _u(7) +#define RVCSR_MISA_H_ACCESS "RO" +// ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_F +// Description : Single-precision floating point extension (not implemented). +#define RVCSR_MISA_F_RESET _u(0x0) +#define RVCSR_MISA_F_BITS _u(0x00000020) +#define RVCSR_MISA_F_MSB _u(5) +#define RVCSR_MISA_F_LSB _u(5) +#define RVCSR_MISA_F_ACCESS "RO" +// ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_E +// Description : RV32E/64E base ISA (not implemented). +#define RVCSR_MISA_E_RESET _u(0x0) +#define RVCSR_MISA_E_BITS _u(0x00000010) +#define RVCSR_MISA_E_MSB _u(4) +#define RVCSR_MISA_E_LSB _u(4) +#define RVCSR_MISA_E_ACCESS "RO" +// ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_D +// Description : Double-precision floating point extension (not implemented). +#define RVCSR_MISA_D_RESET _u(0x0) +#define RVCSR_MISA_D_BITS _u(0x00000008) +#define RVCSR_MISA_D_MSB _u(3) +#define RVCSR_MISA_D_LSB _u(3) +#define RVCSR_MISA_D_ACCESS "RO" +// ----------------------------------------------------------------------------- // Field : RVCSR_MISA_C // Description : Value of 1 indicates the C extension (compressed instructions) // is implemented. @@ -207,7 +264,7 @@ // Description : Timer interrupt enable. The processor transfers to the timer // interrupt vector when `mie.mtie`, `mip.mtip` and `mstatus.mie` // are all 1, unless a software or external interrupt request is -// also valid at this time. +// also both pending and enabled at this time. #define RVCSR_MIE_MTIE_RESET _u(0x0) #define RVCSR_MIE_MTIE_BITS _u(0x00000080) #define RVCSR_MIE_MTIE_MSB _u(7) @@ -216,9 +273,9 @@ // ----------------------------------------------------------------------------- // Field : RVCSR_MIE_MSIE // Description : Software interrupt enable. The processor transfers to the -// software interrupt vector `mie.msie`, `mip.msip` and +// software interrupt vector when `mie.msie`, `mip.msip` and // `mstatus.mie` are all 1, unless an external interrupt request -// is also valid at this time. +// is also both pending and enabled at this time. #define RVCSR_MIE_MSIE_RESET _u(0x0) #define RVCSR_MIE_MSIE_BITS _u(0x00000008) #define RVCSR_MIE_MSIE_MSB _u(3) @@ -336,7 +393,7 @@ #define RVCSR_MENVCFGH_RESET _u(0x00000000) #define RVCSR_MENVCFGH_MSB _u(31) #define RVCSR_MENVCFGH_LSB _u(0) -#define RVCSR_MENVCFGH_ACCESS "RW" +#define RVCSR_MENVCFGH_ACCESS "-" // ============================================================================= // Register : RVCSR_MCOUNTINHIBIT // Description : Count inhibit register for `mcycle`/`minstret` @@ -732,7 +789,7 @@ // Description : Timer interrupt pending. The processor transfers to the timer // interrupt vector when `mie.mtie`, `mip.mtip` and `mstatus.mie` // are all 1, unless a software or external interrupt request is -// also valid at this time. +// also both pending and enabled at this time. #define RVCSR_MIP_MTIP_RESET _u(0x0) #define RVCSR_MIP_MTIP_BITS _u(0x00000080) #define RVCSR_MIP_MTIP_MSB _u(7) @@ -741,9 +798,9 @@ // ----------------------------------------------------------------------------- // Field : RVCSR_MIP_MSIP // Description : Software interrupt pending. The processor transfers to the -// software interrupt vector `mie.msie`, `mip.msip` and +// software interrupt vector when `mie.msie`, `mip.msip` and // `mstatus.mie` are all 1, unless an external interrupt request -// is also valid at this time. +// is also both pending and enabled at this time. #define RVCSR_MIP_MSIP_RESET _u(0x0) #define RVCSR_MIP_MSIP_BITS _u(0x00000008) #define RVCSR_MIP_MSIP_MSB _u(3) @@ -3099,14 +3156,18 @@ #define RVCSR_MVENDORID_RESET _u(0x00000000) // ----------------------------------------------------------------------------- // Field : RVCSR_MVENDORID_BANK -#define RVCSR_MVENDORID_BANK_RESET "-" +// Description : Value of 9 indicates 9 continuation codes, which is JEP106 bank +// 10. +#define RVCSR_MVENDORID_BANK_RESET _u(0x0000009) #define RVCSR_MVENDORID_BANK_BITS _u(0xffffff80) #define RVCSR_MVENDORID_BANK_MSB _u(31) #define RVCSR_MVENDORID_BANK_LSB _u(7) #define RVCSR_MVENDORID_BANK_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : RVCSR_MVENDORID_OFFSET -#define RVCSR_MVENDORID_OFFSET_RESET "-" +// Description : ID 0x13 in bank 10 is the JEP106 ID for Raspberry Pi Ltd, the +// vendor of RP2350. +#define RVCSR_MVENDORID_OFFSET_RESET _u(0x13) #define RVCSR_MVENDORID_OFFSET_BITS _u(0x0000007f) #define RVCSR_MVENDORID_OFFSET_MSB _u(6) #define RVCSR_MVENDORID_OFFSET_LSB _u(0) @@ -3122,10 +3183,11 @@ #define RVCSR_MARCHID_ACCESS "RO" // ============================================================================= // Register : RVCSR_MIMPID -// Description : Implementation ID +// Description : Implementation ID. On RP2350 this reads as 0x86fc4e3f, which is +// release v1.0-rc1 of Hazard3. #define RVCSR_MIMPID_OFFSET _u(0x00000f13) #define RVCSR_MIMPID_BITS _u(0xffffffff) -#define RVCSR_MIMPID_RESET "-" +#define RVCSR_MIMPID_RESET _u(0x86fc4e3f) #define RVCSR_MIMPID_MSB _u(31) #define RVCSR_MIMPID_LSB _u(0) #define RVCSR_MIMPID_ACCESS "RO" diff --git a/lib/pico-sdk/rp2350/hardware/regs/syscfg.h b/lib/pico-sdk/rp2350/hardware/regs/syscfg.h index 455ebf175..74fb596f4 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/syscfg.h +++ b/lib/pico-sdk/rp2350/hardware/regs/syscfg.h @@ -255,7 +255,10 @@ // ============================================================================= // Register : SYSCFG_AUXCTRL // Description : Auxiliary system control register -// * Bits 7:2: Reserved +// * Bits 7:3: Reserved +// +// * Bit 2: Set to mask OTP power analogue power supply detection +// from resetting OTP controller and PSM // // * Bit 1: When clear, the LPOSC output is XORed into the TRNG // ROSC output as an additional, uncorrelated entropy source. When diff --git a/lib/pico-sdk/rp2350/hardware/regs/ticks.h b/lib/pico-sdk/rp2350/hardware/regs/ticks.h index 79e13523d..3dac57216 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/ticks.h +++ b/lib/pico-sdk/rp2350/hardware/regs/ticks.h @@ -36,8 +36,7 @@ #define TICKS_PROC0_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_PROC0_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_PROC0_CYCLES_OFFSET _u(0x00000004) #define TICKS_PROC0_CYCLES_BITS _u(0x000001ff) #define TICKS_PROC0_CYCLES_RESET _u(0x00000000) @@ -46,8 +45,7 @@ #define TICKS_PROC0_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_PROC0_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_PROC0_COUNT_OFFSET _u(0x00000008) #define TICKS_PROC0_COUNT_BITS _u(0x000001ff) @@ -79,8 +77,7 @@ #define TICKS_PROC1_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_PROC1_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_PROC1_CYCLES_OFFSET _u(0x00000010) #define TICKS_PROC1_CYCLES_BITS _u(0x000001ff) #define TICKS_PROC1_CYCLES_RESET _u(0x00000000) @@ -89,8 +86,7 @@ #define TICKS_PROC1_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_PROC1_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_PROC1_COUNT_OFFSET _u(0x00000014) #define TICKS_PROC1_COUNT_BITS _u(0x000001ff) @@ -122,8 +118,7 @@ #define TICKS_TIMER0_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_TIMER0_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_TIMER0_CYCLES_OFFSET _u(0x0000001c) #define TICKS_TIMER0_CYCLES_BITS _u(0x000001ff) #define TICKS_TIMER0_CYCLES_RESET _u(0x00000000) @@ -132,8 +127,7 @@ #define TICKS_TIMER0_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_TIMER0_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_TIMER0_COUNT_OFFSET _u(0x00000020) #define TICKS_TIMER0_COUNT_BITS _u(0x000001ff) @@ -165,8 +159,7 @@ #define TICKS_TIMER1_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_TIMER1_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_TIMER1_CYCLES_OFFSET _u(0x00000028) #define TICKS_TIMER1_CYCLES_BITS _u(0x000001ff) #define TICKS_TIMER1_CYCLES_RESET _u(0x00000000) @@ -175,8 +168,7 @@ #define TICKS_TIMER1_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_TIMER1_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_TIMER1_COUNT_OFFSET _u(0x0000002c) #define TICKS_TIMER1_COUNT_BITS _u(0x000001ff) @@ -208,8 +200,7 @@ #define TICKS_WATCHDOG_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_WATCHDOG_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_WATCHDOG_CYCLES_OFFSET _u(0x00000034) #define TICKS_WATCHDOG_CYCLES_BITS _u(0x000001ff) #define TICKS_WATCHDOG_CYCLES_RESET _u(0x00000000) @@ -218,8 +209,7 @@ #define TICKS_WATCHDOG_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_WATCHDOG_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_WATCHDOG_COUNT_OFFSET _u(0x00000038) #define TICKS_WATCHDOG_COUNT_BITS _u(0x000001ff) @@ -251,8 +241,7 @@ #define TICKS_RISCV_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_RISCV_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_RISCV_CYCLES_OFFSET _u(0x00000040) #define TICKS_RISCV_CYCLES_BITS _u(0x000001ff) #define TICKS_RISCV_CYCLES_RESET _u(0x00000000) @@ -261,8 +250,7 @@ #define TICKS_RISCV_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_RISCV_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_RISCV_COUNT_OFFSET _u(0x00000044) #define TICKS_RISCV_COUNT_BITS _u(0x000001ff) diff --git a/lib/pico-sdk/rp2350/hardware/regs/usb.h b/lib/pico-sdk/rp2350/hardware/regs/usb.h index fbf1b7b36..4bb142bf5 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/usb.h +++ b/lib/pico-sdk/rp2350/hardware/regs/usb.h @@ -1082,13 +1082,14 @@ #define USB_SIE_STATUS_SPEED_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : USB_SIE_STATUS_SUSPENDED -// Description : Bus in suspended state. Valid for device. Device will go into -// suspend if neither Keep Alive / SOF frames are enabled. +// Description : Bus in suspended state. Valid for device and host. Host and +// device will go into suspend if neither Keep Alive / SOF frames +// are enabled. #define USB_SIE_STATUS_SUSPENDED_RESET _u(0x0) #define USB_SIE_STATUS_SUSPENDED_BITS _u(0x00000010) #define USB_SIE_STATUS_SUSPENDED_MSB _u(4) #define USB_SIE_STATUS_SUSPENDED_LSB _u(4) -#define USB_SIE_STATUS_SUSPENDED_ACCESS "WC" +#define USB_SIE_STATUS_SUSPENDED_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : USB_SIE_STATUS_LINE_STATE // Description : USB bus line state diff --git a/lib/pico-sdk/rp2350/hardware/structs/busctrl.h b/lib/pico-sdk/rp2350/hardware/structs/busctrl.h index 2eb83a992..b38797b25 100644 --- a/lib/pico-sdk/rp2350/hardware/structs/busctrl.h +++ b/lib/pico-sdk/rp2350/hardware/structs/busctrl.h @@ -24,7 +24,6 @@ // BITMASK [BITRANGE] FIELDNAME (RESETVALUE) DESCRIPTION /** \brief Bus fabric performance counters on RP2350 (used as typedef \ref bus_ctrl_perf_counter_t) - * \ingroup hardware_busctrl */ typedef enum bus_ctrl_perf_counter_rp2350 { arbiter_rom_perf_event_access = 19, diff --git a/lib/pico-sdk/rp2350/hardware/structs/powman.h b/lib/pico-sdk/rp2350/hardware/structs/powman.h index a81890e3c..3aa8015c0 100644 --- a/lib/pico-sdk/rp2350/hardware/structs/powman.h +++ b/lib/pico-sdk/rp2350/hardware/structs/powman.h @@ -137,14 +137,14 @@ typedef struct { _REG_(POWMAN_STATE_OFFSET) // POWMAN_STATE // This register controls the power state of the 4 power domains - // 0x00002000 [13] CHANGING (0) - // 0x00001000 [12] WAITING (0) - // 0x00000800 [11] BAD_HW_REQ (0) Bad hardware initiated state request - // 0x00000400 [10] BAD_SW_REQ (0) Bad software initiated state request - // 0x00000200 [9] PWRUP_WHILE_WAITING (0) Request ignored because of a pending pwrup request - // 0x00000100 [8] REQ_IGNORED (0) - // 0x000000f0 [7:4] REQ (0x0) - // 0x0000000f [3:0] CURRENT (0xf) + // 0x00002000 [13] CHANGING (0) Indicates a power state change is in progress + // 0x00001000 [12] WAITING (0) Indicates the power manager has received a state change... + // 0x00000800 [11] BAD_HW_REQ (0) Invalid hardware initiated state request, power up... + // 0x00000400 [10] BAD_SW_REQ (0) Invalid software initiated state request ignored + // 0x00000200 [9] PWRUP_WHILE_WAITING (0) Indicates that a power state change request was ignored... + // 0x00000100 [8] REQ_IGNORED (0) Indicates that a software state change request was... + // 0x000000f0 [7:4] REQ (0x0) This is written by software or hardware to request a new... + // 0x0000000f [3:0] CURRENT (0xf) Indicates the current power state io_rw_32 state; _REG_(POWMAN_POW_FASTDIV_OFFSET) // POWMAN_POW_FASTDIV diff --git a/lib/pico-sdk/rp2350/hardware/structs/rosc.h b/lib/pico-sdk/rp2350/hardware/structs/rosc.h index 73503cc15..4147cfb41 100644 --- a/lib/pico-sdk/rp2350/hardware/structs/rosc.h +++ b/lib/pico-sdk/rp2350/hardware/structs/rosc.h @@ -35,9 +35,9 @@ typedef struct { // 0xffff0000 [31:16] PASSWD (0x0000) Set to 0x9696 to apply the settings + // 0x00007000 [14:12] DS3 (0x0) Stage 3 drive strength // 0x00000700 [10:8] DS2 (0x0) Stage 2 drive strength - // 0x00000080 [7] DS1_RANDOM (0) Randomises the stage 1 drive strength + // 0x00000080 [7] DS1_RANDOM (1) Randomises the stage 1 drive strength // 0x00000070 [6:4] DS1 (0x0) Stage 1 drive strength - // 0x00000008 [3] DS0_RANDOM (0) Randomises the stage 0 drive strength + // 0x00000008 [3] DS0_RANDOM (1) Randomises the stage 0 drive strength // 0x00000007 [2:0] DS0 (0x0) Stage 0 drive strength io_rw_32 freqa; diff --git a/lib/pico-sdk/rp2350/hardware/structs/syscfg.h b/lib/pico-sdk/rp2350/hardware/structs/syscfg.h index 8909c0dbf..71660e5fb 100644 --- a/lib/pico-sdk/rp2350/hardware/structs/syscfg.h +++ b/lib/pico-sdk/rp2350/hardware/structs/syscfg.h @@ -72,7 +72,7 @@ typedef struct { _REG_(SYSCFG_AUXCTRL_OFFSET) // SYSCFG_AUXCTRL // Auxiliary system control register - // 0x000000ff [7:0] AUXCTRL (0x00) * Bits 7:2: Reserved + // 0x000000ff [7:0] AUXCTRL (0x00) * Bits 7:3: Reserved io_rw_32 auxctrl; } syscfg_hw_t; diff --git a/src/rp2040/rp2350_bootrom.c b/src/rp2040/rp2350_bootrom.c index 257fb3d44..e691ac7e8 100644 --- a/src/rp2040/rp2350_bootrom.c +++ b/src/rp2040/rp2350_bootrom.c @@ -5,10 +5,10 @@ // This file may be distributed under the terms of the GNU GPLv3 license. #include // uint32_t -#include "boot/picoboot_constants.h" // REBOOT2_FLAG_REBOOT_TYPE_BOOTSEL #include "hardware/address_mapped.h" // static_assert +#include "boot/bootrom_constants.h" // RT_FLAG_FUNC_ARM_NONSEC +#include "boot/picoboot_constants.h" // REBOOT2_FLAG_REBOOT_TYPE_BOOTSEL #include "internal.h" // bootrom_read_unique_id -#include "pico/bootrom_constants.h" // RT_FLAG_FUNC_ARM_NONSEC static void * rom_func_lookup(uint32_t code) From 3f72e519ed08ff8b647d4e18f4cd78b38a33d91e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 12 Dec 2025 23:03:01 -0500 Subject: [PATCH 276/411] atsamd: Fix possible buffer overflow in usbserial.c The USB buffer size register PCKSIZE.SIZE was not being assigned correctly. As a result, it was possible for an incoming USB transmission to write past the allocated buffer space, which could corrupt memory of other storage. In particular, in some cases gcc may layout ram in such a way that the trailing bytes of an incoming message might overlap the buffer for an outgoing message. This could cause sporadic transmit errors and unstable connections. Fix by correctly configuring the PCKSIZE.SIZE register. Signed-off-by: Kevin O'Connor --- src/atsamd/usbserial.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/atsamd/usbserial.c b/src/atsamd/usbserial.c index 460de4645..d083beb86 100644 --- a/src/atsamd/usbserial.c +++ b/src/atsamd/usbserial.c @@ -25,27 +25,30 @@ static uint8_t __aligned(4) acmin[USB_CDC_EP_ACM_SIZE]; static uint8_t __aligned(4) bulkout[USB_CDC_EP_BULK_OUT_SIZE]; static uint8_t __aligned(4) bulkin[USB_CDC_EP_BULK_IN_SIZE]; +// Convert 64, 32, 16, 8 sized buffer to 3, 2, 1, 0 for PCKSIZE.SIZE register +#define BSIZE(bufname) (__builtin_ctz(sizeof(bufname)) - 3) + static UsbDeviceDescriptor usb_desc[] = { [0] = { { { .ADDR.reg = (uint32_t)ep0out, - .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(sizeof(ep0out) >> 4), + .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(BSIZE(ep0out)), }, { .ADDR.reg = (uint32_t)ep0in, - .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(sizeof(ep0in) >> 4), + .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(BSIZE(ep0in)), }, } }, [USB_CDC_EP_ACM] = { { { }, { .ADDR.reg = (uint32_t)acmin, - .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(sizeof(acmin) >> 4), + .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(BSIZE(acmin)), }, } }, [USB_CDC_EP_BULK_OUT] = { { { .ADDR.reg = (uint32_t)bulkout, - .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(sizeof(bulkout) >> 4), + .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(BSIZE(bulkout)), }, { }, } }, @@ -53,7 +56,7 @@ static UsbDeviceDescriptor usb_desc[] = { { }, { .ADDR.reg = (uint32_t)bulkin, - .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(sizeof(bulkin) >> 4), + .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(BSIZE(bulkin)), }, } }, }; From a8cbc935522554b6676a787ca39883bc16d32441 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 17 Dec 2025 10:48:04 -0500 Subject: [PATCH 277/411] bus: Verify that software i2c pins are all on the same mcu Signed-off-by: Kevin O'Connor --- klippy/extras/bus.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index b04fbe764..19e278418 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -240,6 +240,10 @@ def MCU_I2C_from_config(config, default_addr=None, default_speed=100000): for name in ['scl', 'sda']] sw_pin_params = [ppins.lookup_pin(config.get(name), share_type=name) for name in sw_pin_names] + for pin_params in sw_pin_params: + if pin_params['chip'] != i2c_mcu: + raise ppins.error("%s: i2c pins must be on same mcu" % ( + config.get_name(),)) sw_pins = tuple([pin_params['pin'] for pin_params in sw_pin_params]) bus = None else: From 3a3e9fa2f14e6d0aa7403283d1600d1122c74a87 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Dec 2025 21:10:51 -0500 Subject: [PATCH 278/411] rp2040: Support rp2350b extra gpios Add support for gpio31 through gpio47 on rp2350, as these pins are available on the rp2350b chips. Signed-off-by: Kevin O'Connor --- src/rp2040/gpio.c | 62 ++++++++++++++++++++++++++++++----------------- src/rp2040/gpio.h | 2 ++ 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/rp2040/gpio.c b/src/rp2040/gpio.c index 98e077897..a564d1282 100644 --- a/src/rp2040/gpio.c +++ b/src/rp2040/gpio.c @@ -1,6 +1,6 @@ // GPIO functions on rp2040 // -// Copyright (C) 2021 Kevin O'Connor +// Copyright (C) 2021-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -19,12 +19,16 @@ * Pin mappings ****************************************************************/ -DECL_ENUMERATION_RANGE("pin", "gpio0", 0, 30); +#define NUM_GPIO (CONFIG_MACH_RP2040 ? 30 : 48) + +DECL_ENUMERATION_RANGE("pin", "gpio0", 0, NUM_GPIO); // Set the mode and extended function of a pin void gpio_peripheral(uint32_t gpio, int func, int pull_up) { + if (gpio >= NUM_GPIO) + shutdown("Not a valid pin"); padsbank0_hw->io[gpio] = ( PADS_BANK0_GPIO0_IE_BITS | (PADS_BANK0_GPIO0_DRIVE_VALUE_4MA << PADS_BANK0_GPIO0_DRIVE_MSB) @@ -35,9 +39,12 @@ gpio_peripheral(uint32_t gpio, int func, int pull_up) // Convert a register and bit location back to an integer pin identifier static int -mask_to_pin(uint32_t mask) +mask_to_pin(void *sio, uint32_t mask) { - return ffs(mask)-1; + int pin = ffs(mask)-1; + if (CONFIG_MACH_RP2350 && sio != (void*)sio_hw) + pin += 32; + return pin; } @@ -48,22 +55,26 @@ mask_to_pin(uint32_t mask) struct gpio_out gpio_out_setup(uint8_t pin, uint8_t val) { - if (pin >= 30) - goto fail; - struct gpio_out g = { .bit=1<= NUM_GPIO) + shutdown("Not a valid pin"); + void *sio = (void*)sio_hw; + if (CONFIG_MACH_RP2350 && pin >= 32) { + pin -= 32; + sio += 4; + } + struct gpio_out g = { .sio=sio, .bit=1<gpio_oe_set = g.bit; + sio_hw_t *sio = g.sio; + sio->gpio_oe_set = g.bit; gpio_peripheral(pin, 5, 0); irq_restore(flag); } @@ -71,7 +82,8 @@ gpio_out_reset(struct gpio_out g, uint8_t val) void gpio_out_toggle_noirq(struct gpio_out g) { - sio_hw->gpio_togl = g.bit; + sio_hw_t *sio = g.sio; + sio->gpio_togl = g.bit; } void @@ -83,37 +95,43 @@ gpio_out_toggle(struct gpio_out g) void gpio_out_write(struct gpio_out g, uint8_t val) { + sio_hw_t *sio = g.sio; if (val) - sio_hw->gpio_set = g.bit; + sio->gpio_set = g.bit; else - sio_hw->gpio_clr = g.bit; + sio->gpio_clr = g.bit; } struct gpio_in gpio_in_setup(uint8_t pin, int8_t pull_up) { - if (pin >= 30) - goto fail; - struct gpio_in g = { .bit=1<= NUM_GPIO) + shutdown("Not a valid pin"); + void *sio = (void*)sio_hw; + if (CONFIG_MACH_RP2350 && pin >= 32) { + pin -= 32; + sio += 4; + } + struct gpio_in g = { .sio=sio, .bit=1<gpio_oe_clr = g.bit; + sio_hw_t *sio = g.sio; + sio->gpio_oe_clr = g.bit; irq_restore(flag); } uint8_t gpio_in_read(struct gpio_in g) { - return !!(sio_hw->gpio_in & g.bit); + sio_hw_t *sio = g.sio; + return !!(sio->gpio_in & g.bit); } diff --git a/src/rp2040/gpio.h b/src/rp2040/gpio.h index 0dd393bfe..23d432f63 100644 --- a/src/rp2040/gpio.h +++ b/src/rp2040/gpio.h @@ -4,6 +4,7 @@ #include // uint32_t struct gpio_out { + void *sio; uint32_t bit; }; struct gpio_out gpio_out_setup(uint8_t pin, uint8_t val); @@ -13,6 +14,7 @@ void gpio_out_toggle(struct gpio_out g); void gpio_out_write(struct gpio_out g, uint8_t val); struct gpio_in { + void *sio; uint32_t bit; }; struct gpio_in gpio_in_setup(uint8_t pin, int8_t pull_up); From a3e24d2172525aaada595d86114735b28e7a822f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Dec 2025 21:52:47 -0500 Subject: [PATCH 279/411] rp2040: Add support for ADC on rp2350b chips Signed-off-by: Kevin O'Connor --- src/rp2040/adc.c | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/rp2040/adc.c b/src/rp2040/adc.c index 2daf380a6..477858c54 100644 --- a/src/rp2040/adc.c +++ b/src/rp2040/adc.c @@ -1,6 +1,6 @@ // ADC functions on rp2040 // -// Copyright (C) 2021 Kevin O'Connor +// Copyright (C) 2021-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -10,6 +10,7 @@ #include "hardware/structs/adc.h" // adc_hw #include "hardware/structs/padsbank0.h" // padsbank0_hw #include "hardware/structs/resets.h" // RESETS_RESET_ADC_BITS +#include "hardware/structs/sysinfo.h" // sysinfo_hw #include "internal.h" // enable_pclock #include "sched.h" // sched_shutdown @@ -21,7 +22,19 @@ DECL_ENUMERATION("pin", "ADC_TEMPERATURE", ADC_TEMPERATURE_PIN); struct gpio_adc gpio_adc_setup(uint32_t pin) { - if ((pin < 26 || pin > 29) && pin != ADC_TEMPERATURE_PIN) + uint32_t min_gpio = 26, max_gpio = 29, adc_temp_chan = 4; +#if CONFIG_MACH_RP2350 + // Check for rp2350b package + if (!is_enabled_pclock(RESETS_RESET_SYSINFO_BITS)) + enable_pclock(RESETS_RESET_SYSINFO_BITS); + if (!(sysinfo_hw->package_sel & SYSINFO_PACKAGE_SEL_BITS)) { + min_gpio = 40; + max_gpio = 47; + adc_temp_chan = 8; + } +#endif + + if ((pin < min_gpio || pin > max_gpio) && pin != ADC_TEMPERATURE_PIN) shutdown("Not a valid ADC pin"); // Enable the ADC @@ -32,10 +45,10 @@ gpio_adc_setup(uint32_t pin) uint8_t chan; if (pin == ADC_TEMPERATURE_PIN) { - chan = 4; + chan = adc_temp_chan; adc_hw->cs |= ADC_CS_TS_EN_BITS; } else { - chan = pin - 26; + chan = pin - min_gpio; padsbank0_hw->io[pin] = PADS_BANK0_GPIO0_OD_BITS; } From 59a754c48c478fc35c638c0cb3ad018ca4b58164 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 6 Feb 2025 13:43:15 -0500 Subject: [PATCH 280/411] stm32: Implement manual double buffering tx for usbotg It is possible for USB host controllers to send back-to-back IN tokens which only gives the MCU ~3us to queue the next USB packet in the hardware. That can be difficult to do if the MCU has to wake up the task code. The stm32 "usbotg" hardware does not support a builtin generic double buffering transmit capability, but it is possible to load the next packet directly from the irq handler code. This change adds support for queuing the next packet destined for the host so that the USB irq handler can directly load it into the hardware. Signed-off-by: Kevin O'Connor --- src/stm32/Kconfig | 3 +++ src/stm32/usbotg.c | 52 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index 1e0df93d2..72be8a4d1 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -156,6 +156,9 @@ config HAVE_STM32_USBFS config HAVE_STM32_USBOTG bool default y if MACH_STM32F2 || MACH_STM32F4 || MACH_STM32F7 || MACH_STM32H7 +config STM32_USB_DOUBLE_BUFFER_TX + bool + default y config HAVE_STM32_CANBUS bool default y if MACH_STM32F1 || MACH_STM32F2 || MACH_STM32F4x5 || MACH_STM32F446 || MACH_STM32F0x2 diff --git a/src/stm32/usbotg.c b/src/stm32/usbotg.c index b2d52456d..b41daf48c 100644 --- a/src/stm32/usbotg.c +++ b/src/stm32/usbotg.c @@ -1,6 +1,6 @@ // Hardware interface to "USB OTG (on the go) controller" on stm32 // -// Copyright (C) 2019 Kevin O'Connor +// Copyright (C) 2019-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -119,6 +119,24 @@ fifo_write_packet(uint32_t ep, const uint8_t *src, uint32_t len) return len; } +// Write a packet to a tx fifo (optimized for already aligned data) +static int +fifo_write_packet_fast(uint32_t ep, const uint32_t *src, uint32_t len) +{ + void *fifo = EPFIFO(ep); + USB_OTG_INEndpointTypeDef *epi = EPIN(ep); + uint32_t ctl = epi->DIEPCTL; + if (ctl & USB_OTG_DIEPCTL_EPENA) + return -1; + epi->DIEPINT = USB_OTG_DIEPINT_XFRC; + epi->DIEPTSIZ = len | (1 << USB_OTG_DIEPTSIZ_PKTCNT_Pos); + epi->DIEPCTL = ctl | USB_OTG_DIEPCTL_EPENA | USB_OTG_DIEPCTL_CNAK; + uint32_t i; + for (i=0; i < DIV_ROUND_UP(len, sizeof(uint32_t)); i++) + writel(fifo, src[i]); + return 0; +} + // Read a packet from the rx queue static int_fast8_t fifo_read_packet(uint8_t *dest, uint_fast8_t max_len) @@ -208,6 +226,12 @@ usb_read_bulk_out(void *data, uint_fast8_t max_len) return ret; } +// Storage for "bulk in" transmissions for a kind of manual "double buffering" +static struct { + uint32_t len; + uint32_t buf[USB_CDC_EP_BULK_IN_SIZE / sizeof(uint32_t)]; +} TX_BUF; + int_fast8_t usb_send_bulk_in(void *data, uint_fast8_t len) { @@ -219,10 +243,21 @@ usb_send_bulk_in(void *data, uint_fast8_t len) return len; } if (ctl & USB_OTG_DIEPCTL_EPENA) { - // Wait for space to transmit + if (!CONFIG_STM32_USB_DOUBLE_BUFFER_TX || TX_BUF.len || !len) { + // Wait for space to transmit + OTGD->DAINTMSK |= 1 << USB_CDC_EP_BULK_IN; + usb_irq_enable(); + return -1; + } + // Buffer next packet for transmission from irq handler + len = len > USB_CDC_EP_BULK_IN_SIZE ? USB_CDC_EP_BULK_IN_SIZE : len; + uint32_t blocks = DIV_ROUND_UP(len, sizeof(uint32_t)); + TX_BUF.buf[blocks-1] = 0; + memcpy(TX_BUF.buf, data, len); + TX_BUF.len = len; OTGD->DAINTMSK |= 1 << USB_CDC_EP_BULK_IN; usb_irq_enable(); - return -1; + return len; } int_fast8_t ret = fifo_write_packet(USB_CDC_EP_BULK_IN, data, len); usb_irq_enable(); @@ -373,6 +408,8 @@ usb_set_configure(void) | USB_OTG_GRSTCTL_TXFFLSH); while (OTG->GRSTCTL & USB_OTG_GRSTCTL_TXFFLSH) ; + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX) + TX_BUF.len = 0; usb_irq_enable(); } @@ -401,8 +438,15 @@ OTG_FS_IRQHandler(void) OTGD->DAINTMSK = msk & ~daint; if (pend & (1 << 0)) usb_notify_ep0(); - if (pend & (1 << USB_CDC_EP_BULK_IN)) + if (pend & (1 << USB_CDC_EP_BULK_IN)) { usb_notify_bulk_in(); + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX && TX_BUF.len) { + int ret = fifo_write_packet_fast(USB_CDC_EP_BULK_IN + , TX_BUF.buf, TX_BUF.len); + if (!ret) + TX_BUF.len = 0; + } + } } } From c3b660dfbe61f8851fba9541f6658484e389d5eb Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 14 Dec 2025 12:30:32 -0500 Subject: [PATCH 281/411] stm32: Simplify USBx_IRQn per-chip definitions in usbfs.c Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index 5385c956c..2e1bc420d 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -1,6 +1,6 @@ // Hardware interface to "fullspeed USB controller" // -// Copyright (C) 2018-2023 Kevin O'Connor +// Copyright (C) 2018-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -19,29 +19,27 @@ // Transfer memory is accessed with 32bits, but contains only 16bits of data typedef volatile uint32_t epmword_t; #define WSIZE 2 - #define USBx_IRQn USB_LP_IRQn -#elif CONFIG_MACH_STM32F0 || CONFIG_MACH_STM32L4 +#elif CONFIG_MACH_STM32F0 || CONFIG_MACH_STM32L4 || CONFIG_MACH_STM32G4 // Transfer memory is accessed with 16bits and contains 16bits of data typedef volatile uint16_t epmword_t; #define WSIZE 2 - #define USBx_IRQn USB_IRQn -#elif CONFIG_MACH_STM32G4 - // Transfer memory is accessed with 16bits and contains 16bits of data - typedef volatile uint16_t epmword_t; - #define WSIZE 2 - #define USBx_IRQn USB_LP_IRQn #elif CONFIG_MACH_STM32G0 // Transfer memory is accessed with 32bits and contains 32bits of data typedef volatile uint32_t epmword_t; #define WSIZE 4 +#endif + +// Different chips have different names for the USB irq +#if CONFIG_MACH_STM32F1 || CONFIG_MACH_STM32G4 + #define USBx_IRQn USB_LP_IRQn +#elif CONFIG_MACH_STM32G0B1 + #define USBx_IRQn USB_UCPD1_2_IRQn +#else #define USBx_IRQn USB_IRQn #endif // The stm32g0 has slightly different register names #if CONFIG_MACH_STM32G0 - #if CONFIG_MACH_STM32G0B1 - #define USB_IRQn USB_UCPD1_2_IRQn - #endif #define USB USB_DRD_FS #define USB_PMAADDR USB_DRD_PMAADDR #define USB_EPADDR_FIELD USB_CHEP_ADDR @@ -55,8 +53,8 @@ // Some chip variants do not define these fields #ifndef USB_EP_DTOG_TX_Pos -#define USB_EP_DTOG_TX_Pos 6 -#define USB_EP_DTOG_RX_Pos 14 + #define USB_EP_DTOG_TX_Pos 6 + #define USB_EP_DTOG_RX_Pos 14 #endif From dc622f4ac329de2d987cbdfd24477641bdb742fb Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 14 Dec 2025 13:32:38 -0500 Subject: [PATCH 282/411] stm32: Allow disabling double buffering transmit in usbfs.c Only enable double buffering transmit if CONFIG_STM32_USB_DOUBLE_BUFFER_TX is set. Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index 2e1bc420d..3751a4f5a 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -242,8 +242,9 @@ usb_read_bulk_out(void *data, uint_fast8_t max_len) static uint32_t bulk_in_push_pos, bulk_in_pop_flag; #define BI_START 2 -int_fast8_t -usb_send_bulk_in(void *data, uint_fast8_t len) +// Send bulk packet to host with double buffering optimization +static int_fast8_t +usb_send_bulk_in_double_buffer(void *data, uint_fast8_t len) { if (readl(&bulk_in_pop_flag)) // No buffer space available @@ -275,6 +276,21 @@ usb_send_bulk_in(void *data, uint_fast8_t len) return len; } +// Send bulk usb packet to host +int_fast8_t +usb_send_bulk_in(void *data, uint_fast8_t len) +{ + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX) + return usb_send_bulk_in_double_buffer(data, len); + uint32_t ep = USB_CDC_EP_BULK_IN, epr = USB_EPR[ep]; + if ((epr & USB_EPTX_STAT) != USB_EP_TX_NAK) + // No buffer space available + return -1; + btable_write_packet(ep, BUFTX, data, len); + USB_EPR[ep] = calc_epr_bits(epr, USB_EPTX_STAT, USB_EP_TX_VALID); + return len; +} + int_fast8_t usb_read_ep0(void *data, uint_fast8_t max_len) { @@ -333,9 +349,10 @@ usb_set_configure(void) bulk_out_pop_count = 0; USB_EPR[ep] = calc_epr_bits(USB_EPR[ep], USB_EPRX_STAT, USB_EP_RX_VALID); - ep = USB_CDC_EP_BULK_IN; - bulk_in_push_pos = BI_START; - writel(&bulk_in_pop_flag, 0); + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX) { + bulk_in_push_pos = BI_START; + writel(&bulk_in_pop_flag, 0); + } } @@ -360,9 +377,12 @@ usb_reset(void) bulk_out_push_flag = USB_EP_DTOG_TX; ep = USB_CDC_EP_BULK_IN; - USB_EPR[ep] = (USB_CDC_EP_BULK_IN | USB_EP_BULK | USB_EP_KIND - | USB_EP_TX_NAK); - bulk_in_pop_flag = USB_EP_DTOG_RX; + uint32_t bi_epr_flags = USB_CDC_EP_BULK_IN | USB_EP_BULK | USB_EP_TX_NAK; + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX) { + bi_epr_flags |= USB_EP_KIND; + bulk_in_pop_flag = USB_EP_DTOG_RX; + } + USB_EPR[ep] = bi_epr_flags; USB->CNTR = USB_CNTR_CTRM | USB_CNTR_RESETM; USB->DADDR = USB_DADDR_EF; @@ -382,9 +402,12 @@ USB_IRQHandler(void) bulk_out_push_flag = 0; usb_notify_bulk_out(); } else if (ep == USB_CDC_EP_BULK_IN) { - USB_EPR[ep] = (calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0) - | bulk_in_pop_flag); - bulk_in_pop_flag = 0; + uint32_t ne = calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0); + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX) { + ne |= bulk_in_pop_flag; + bulk_in_pop_flag = 0; + } + USB_EPR[ep] = ne; usb_notify_bulk_in(); } else if (ep == 0) { USB_EPR[ep] = calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0); From a40beb7b1b340257cce1c21a6e45442603a77cbc Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 15 Nov 2025 22:14:36 +0100 Subject: [PATCH 283/411] probe_eddy_current: filter noisy calibration points A misplaced sensor or a misconfigured one can return unreliable results. Assist with this by refusing to use the too noisy points. Filter noisy points by the frequency difference to noise ratio. Signed-off-by: Timofey Titovets --- klippy/extras/probe_eddy_current.py | 87 +++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 96c766708..4dc46e413 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -134,16 +134,68 @@ class EddyCalibration: raise self.printer.command_error( "Failed calibration - incomplete sensor data") return cal + + def _median(self, values): + values = sorted(values) + n = len(values) + if n % 2 == 0: + return (values[n//2 - 1] + values[n//2]) / 2.0 + return values[n // 2] def calc_freqs(self, meas): - total_count = total_variance = 0 positions = {} for pos, freqs in meas.items(): count = len(freqs) freq_avg = float(sum(freqs)) / count - positions[pos] = freq_avg - total_count += count - total_variance += sum([(f - freq_avg)**2 for f in freqs]) - return positions, math.sqrt(total_variance / total_count), total_count + mads = [abs(f - freq_avg) for f in freqs] + mad = self._median(mads) + positions[pos] = (freq_avg, mad, count) + return positions + def validate_calibration_data(self, positions): + last_freq = 40000000. + last_pos = last_mad = .0 + gcode = self.printer.lookup_object("gcode") + filtered = [] + mad_hz_total = .0 + mad_mm_total = .0 + samples_count = 0 + for pos, (freq_avg, mad_hz, count) in sorted(positions.items()): + if freq_avg > last_freq: + gcode.respond_info( + "Frequency stops decreasing at step %.3f" % (pos)) + break + diff_mad = math.sqrt(last_mad**2 + mad_hz**2) + # Calculate if samples have a significant difference + freq_diff = last_freq - freq_avg + last_freq = freq_avg + if freq_diff < 2.5 * diff_mad: + gcode.respond_info( + "Frequency too noisy at step %.3f -> %.3f" % ( + last_pos, pos)) + gcode.respond_info( + "Frequency diff: %.3f, MAD_Hz: %.3f -> MAD_Hz: %.3f" % ( + freq_diff, last_mad, mad_hz + )) + break + last_mad = mad_hz + delta_dist = pos - last_pos + last_pos = pos + # MAD is Median Absolute Deviation to Frequency avg ~ delta_hz_1 + # Signal is delta_hz_2 / delta_dist + # SNR ~= delta_hz_1 / (delta_hz_2 / delta_mm) = d_1 * d_mm / d_2 + mad_mm = mad_hz * delta_dist / freq_diff + filtered.append((pos, freq_avg, mad_hz, mad_mm)) + mad_hz_total += mad_hz + mad_mm_total += mad_mm + samples_count += count + avg_mad = mad_hz_total / len(filtered) + avg_mad_mm = mad_mm_total / len(filtered) + gcode.respond_info( + "probe_eddy_current: noise %.6fmm, MAD_Hz=%.3f in %d queries\n" % ( + avg_mad_mm, avg_mad, samples_count)) + freq_list = [freq for _, freq, _, _ in filtered] + freq_diff = max(freq_list) - min(freq_list) + gcode.respond_info("Total frequency range: %.3f Hz\n" % (freq_diff)) + return filtered def post_manual_probe(self, kin_pos): if kin_pos is None: # Manual Probe was aborted @@ -166,24 +218,27 @@ class EddyCalibration: # Perform calibration movement and capture cal = self.do_calibration_moves(self.probe_speed) # Calculate each sample position average and variance - positions, std, total = self.calc_freqs(cal) - last_freq = 0. - for pos, freq in reversed(sorted(positions.items())): - if freq <= last_freq: - raise self.printer.command_error( - "Failed calibration - frequency not increasing each step") - last_freq = freq + _positions = self.calc_freqs(cal) + # Fix Z position offset + positions = {} + for k in _positions: + v = _positions[k] + k = k - probe_calibrate_z + positions[k] = v + filtered = self.validate_calibration_data(positions) + if len(filtered) <= 8: + raise self.printer.command_error( + "Failed calibration - No usable data") gcode = self.printer.lookup_object("gcode") gcode.respond_info( - "probe_eddy_current: stddev=%.3f in %d queries\n" "The SAVE_CONFIG command will update the printer config file\n" - "and restart the printer." % (std, total)) + "and restart the printer.") # Save results cal_contents = [] - for i, (pos, freq) in enumerate(sorted(positions.items())): + for i, (pos, freq, _, _) in enumerate(filtered): if not i % 3: cal_contents.append('\n') - cal_contents.append("%.6f:%.3f" % (pos - probe_calibrate_z, freq)) + cal_contents.append("%.6f:%.3f" % (pos, freq)) cal_contents.append(',') cal_contents.pop() configfile = self.printer.lookup_object('configfile') From 8b58aa130294994571b365b83c6364f930a03244 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Mon, 24 Nov 2025 21:46:58 +0100 Subject: [PATCH 284/411] probe_eddy_current: show noise at distinct calibration points Signed-off-by: Timofey Titovets --- klippy/extras/probe_eddy_current.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 4dc46e413..779a904fb 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -195,6 +195,13 @@ class EddyCalibration: freq_list = [freq for _, freq, _, _ in filtered] freq_diff = max(freq_list) - min(freq_list) gcode.respond_info("Total frequency range: %.3f Hz\n" % (freq_diff)) + points = [0.25, 0.5, 1.0, 2.0, 3.0] + for pos, _, mad_hz, mad_mm in filtered: + if len(points) and points[0] <= pos: + points.pop(0) + msg = "z_offset: %.3f # noise %.6fmm, MAD_Hz=%.3f\n" % ( + pos, mad_mm, mad_hz) + gcode.respond_info(msg) return filtered def post_manual_probe(self, kin_pos): if kin_pos is None: From 94cbde75174d2f4beb92dce45963f118feab060d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 15:24:39 -0500 Subject: [PATCH 285/411] _klipper3d: Rename build-translations.sh to build-website.sh Rename the build script to make it more clear that it builds the entire github hosted website. Signed-off-by: Kevin O'Connor --- .github/workflows/klipper3d-deploy.yaml | 2 +- .../_klipper3d/{build-translations.sh => build-website.sh} | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) rename docs/_klipper3d/{build-translations.sh => build-website.sh} (93%) diff --git a/.github/workflows/klipper3d-deploy.yaml b/.github/workflows/klipper3d-deploy.yaml index 609644bbc..076be8dac 100644 --- a/.github/workflows/klipper3d-deploy.yaml +++ b/.github/workflows/klipper3d-deploy.yaml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: pip install -r docs/_klipper3d/mkdocs-requirements.txt - name: Build MkDocs Pages - run: docs/_klipper3d/build-translations.sh + run: docs/_klipper3d/build-website.sh - name: Deploy uses: JamesIves/github-pages-deploy-action@v4.4.3 with: diff --git a/docs/_klipper3d/build-translations.sh b/docs/_klipper3d/build-website.sh similarity index 93% rename from docs/_klipper3d/build-translations.sh rename to docs/_klipper3d/build-website.sh index 4a0117c24..e8fd62504 100755 --- a/docs/_klipper3d/build-translations.sh +++ b/docs/_klipper3d/build-website.sh @@ -1,7 +1,8 @@ #!/bin/bash -# This script extracts the Klipper translations and builds multiple -# mdocs sites - one for each supported language. See the README file -# for additional details. +# This script creates the main klipper3d.org website hosted on github. +# It extracts the Klipper translations and builds multiple mdocs sites +# - one for each supported language. See the README file for +# additional details. MKDOCS_DIR="docs/_klipper3d/" WORK_DIR="work/" From d9f5da7196fde06b5c60550050df829a34af7a54 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 15:27:16 -0500 Subject: [PATCH 286/411] _klipper3d: Remove unnecessary files copied into the main website Signed-off-by: Kevin O'Connor --- docs/_klipper3d/build-website.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/_klipper3d/build-website.sh b/docs/_klipper3d/build-website.sh index e8fd62504..83240949c 100755 --- a/docs/_klipper3d/build-website.sh +++ b/docs/_klipper3d/build-website.sh @@ -23,6 +23,10 @@ done < <(egrep -v '^ *(#|$)' ${TRANS_FILE}) echo "building site for en" mkdocs build -f ${MKDOCS_MAIN} +# Cleanup files (mkdocs copies _klipper3d dir and its sitemap doesn't work) +rm -r ${PWD}/site/_klipper3d +rm ${PWD}/site/sitemap.xml ${PWD}/site/sitemap.xml.gz + # Build each additional language website while IFS="," read dirname langsite langdesc langsearch; do new_docs_dir="${WORK_DIR}lang/${langsite}/docs/" @@ -82,4 +86,9 @@ while IFS="," read dirname langsite langdesc langsearch; do mkdir -p "${PWD}/site/${langsite}/" ln -sf "${PWD}/site/${langsite}/" "${WORK_DIR}lang/${langsite}/site" mkdocs build -f "${new_mkdocs_file}" + + # Cleanup files (mkdocs copies _klipper3d dir and its sitemap doesn't work) + rm -r "${PWD}/site/${langsite}/_klipper3d" + rm "${PWD}/site/${langsite}/sitemap.xml" "${PWD}/site/${langsite}/sitemap.xml.gz" + done < <(egrep -v '^ *(#|$)' ${TRANS_FILE}) From 5d24122c0439a2be020622dea161cab54ebe24be Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 18:25:59 -0500 Subject: [PATCH 287/411] _klipper3d: Make sure custom css file is preserved Signed-off-by: Kevin O'Connor --- docs/_klipper3d/build-website.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/_klipper3d/build-website.sh b/docs/_klipper3d/build-website.sh index 83240949c..9c1ae0205 100755 --- a/docs/_klipper3d/build-website.sh +++ b/docs/_klipper3d/build-website.sh @@ -24,7 +24,8 @@ echo "building site for en" mkdocs build -f ${MKDOCS_MAIN} # Cleanup files (mkdocs copies _klipper3d dir and its sitemap doesn't work) -rm -r ${PWD}/site/_klipper3d +rm -rf ${PWD}/site/_klipper3d/__pycache__ +find ${PWD}/site/_klipper3d ! -name '*.css' -type f -delete rm ${PWD}/site/sitemap.xml ${PWD}/site/sitemap.xml.gz # Build each additional language website @@ -88,7 +89,8 @@ while IFS="," read dirname langsite langdesc langsearch; do mkdocs build -f "${new_mkdocs_file}" # Cleanup files (mkdocs copies _klipper3d dir and its sitemap doesn't work) - rm -r "${PWD}/site/${langsite}/_klipper3d" + rm -rf "${PWD}/site/${langsite}/_klipper3d/__pycache__" + find "${PWD}/site/${langsite}/_klipper3d" ! -name '*.css' -type f -delete rm "${PWD}/site/${langsite}/sitemap.xml" "${PWD}/site/${langsite}/sitemap.xml.gz" done < <(egrep -v '^ *(#|$)' ${TRANS_FILE}) From 2cc360894536ef3e902acaabc9a495a27d268a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20K=C3=BCffner?= <11882946+mkuf@users.noreply.github.com> Date: Tue, 30 Dec 2025 16:15:35 +0100 Subject: [PATCH 288/411] buildcommands: retrieve version from klippy version file (#7134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Markus Küffner --- scripts/buildcommands.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/buildcommands.py b/scripts/buildcommands.py index b35873840..1e2ac1041 100644 --- a/scripts/buildcommands.py +++ b/scripts/buildcommands.py @@ -483,11 +483,22 @@ def git_version(): logging.debug("Got git version: %s" % (repr(ver),)) return ver +# Obtain version info from "klippy/.version" file +def file_version(): + if not os.path.exists('klippy/.version'): + logging.debug("No 'klippy/.version' file/directory found") + return "" + ver = check_output("cat klippy/.version").strip() + logging.debug("Got klippy version: %s" % (repr(ver),)) + return ver + def build_version(extra, cleanbuild): version = git_version() if not version: cleanbuild = False - version = "?" + version = file_version() + if not version: + version = "?" elif 'dirty' in version: cleanbuild = False if not cleanbuild: From 12cc944fa0c56adf5f31b788efd5e2cc5f4a5094 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 15 Oct 2025 03:06:31 +0200 Subject: [PATCH 289/411] stm32: F042 define PB4 HW PWM Signed-off-by: Timofey Titovets --- src/stm32/Kconfig | 2 +- src/stm32/hard_pwm.c | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index 72be8a4d1..c8466df38 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -10,7 +10,7 @@ config STM32_SELECT select HAVE_GPIO_I2C if !MACH_STM32F031 select HAVE_GPIO_SPI if !MACH_STM32F031 select HAVE_GPIO_SDIO if MACH_STM32F4 - select HAVE_GPIO_HARD_PWM if MACH_STM32F070 || MACH_STM32F072 || MACH_STM32F1 || MACH_STM32F4 || MACH_STM32F7 || MACH_STM32G0 || MACH_STM32H7 + select HAVE_GPIO_HARD_PWM if MACH_STM32F042 || MACH_STM32F070 || MACH_STM32F072 || MACH_STM32F1 || MACH_STM32F4 || MACH_STM32F7 || MACH_STM32G0 || MACH_STM32H7 select HAVE_STRICT_TIMING select HAVE_CHIPID select HAVE_STEPPER_OPTIMIZED_BOTH_EDGE if !MACH_STM32H7 diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index 6ed27a305..0729d47b1 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -21,6 +21,9 @@ struct gpio_pwm_info { static const struct gpio_pwm_info pwm_regs[] = { #if CONFIG_MACH_STM32F0 + #if CONFIG_MACH_STM32F042 + {TIM3, GPIO('B', 4), 1, GPIO_FUNCTION(1)}, + #endif #if CONFIG_MACH_STM32F070 {TIM15, GPIO('A', 2), 1, GPIO_FUNCTION(0)}, {TIM15, GPIO('A', 3), 2, GPIO_FUNCTION(0)}, From ec0eb4a8bf69249a4ab8aa88a23796eb8e76a090 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 15 Oct 2025 04:25:43 +0200 Subject: [PATCH 290/411] stm32: hard pwm allow scale PWM frequency To support the cartographer, it is required to output 24 MHz. With current defaults max output frequency is: 48 MHz/256 = 187.5 KHz Adjusting the PWM scale allows for ramping up the frequency. To not mess up with existing PWM users, define the STM32-specific command. Signed-off-by: Timofey Titovets --- src/stm32/hard_pwm.c | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index 0729d47b1..8a9dcbb4b 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -300,7 +300,8 @@ static const struct gpio_pwm_info pwm_regs[] = { }; struct gpio_pwm -gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) +gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, + int is_clock_out) { // Find pin in pwm_regs table const struct gpio_pwm_info* p = pwm_regs; @@ -316,7 +317,18 @@ gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) uint32_t pclock_div = CONFIG_CLOCK_FREQ / pclk; if (pclock_div > 1) pclock_div /= 2; // Timers run at twice the normal pclock frequency - uint32_t prescaler = cycle_time / (pclock_div * (MAX_PWM - 1)); + uint32_t max_pwm = MAX_PWM; + uint32_t pcycle_time = cycle_time / pclock_div; + uint32_t prescaler = pcycle_time / (max_pwm - 1); + // CLK output + if (is_clock_out) { + prescaler = 1; + while (pcycle_time > UINT16_MAX) { + prescaler = prescaler * 2; + pcycle_time /= 2; + } + max_pwm = pcycle_time; + } if (prescaler > UINT16_MAX) { prescaler = UINT16_MAX; } else if (prescaler > 0) { @@ -334,9 +346,12 @@ gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) if (p->timer->PSC != (uint16_t) prescaler) { shutdown("PWM already programmed at different speed"); } + if (p->timer->ARR != (uint16_t) max_pwm - 1) { + shutdown("PWM already programmed with different pulse duration"); + } } else { p->timer->PSC = (uint16_t) prescaler; - p->timer->ARR = MAX_PWM - 1; + p->timer->ARR = max_pwm - 1; p->timer->EGR |= TIM_EGR_UG; } @@ -393,6 +408,19 @@ gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) return channel; } +struct gpio_pwm +gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) { + return gpio_timer_setup(pin, cycle_time, val, 0); +} + +void +command_stm32_timer_output(uint32_t *args) +{ + gpio_timer_setup(args[0], args[1], args[2], 1); +} +DECL_COMMAND(command_stm32_timer_output, + "stm32_timer_output pin=%u cycle_ticks=%u on_ticks=%hu"); + void gpio_pwm_write(struct gpio_pwm g, uint32_t val) { *(volatile uint32_t*) g.reg = val; From 1fdf0ebaf4da3f4149c6d50ce2561f3bbc60ad56 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 16 Oct 2025 19:28:19 +0200 Subject: [PATCH 291/411] static_pwm_clock: define module for stm32 Signed-off-by: Timofey Titovets --- klippy/extras/static_pwm_clock.py | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 klippy/extras/static_pwm_clock.py diff --git a/klippy/extras/static_pwm_clock.py b/klippy/extras/static_pwm_clock.py new file mode 100644 index 000000000..db62af88b --- /dev/null +++ b/klippy/extras/static_pwm_clock.py @@ -0,0 +1,47 @@ +# Define GPIO as clock output +# +# Copyright (C) 2025 Timofey Titovets +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import logging + +class PrinterClockOutputPin: + def __init__(self, config): + self.name = config.get_name() + self.printer = config.get_printer() + ppins = self.printer.lookup_object('pins') + mcu_pin = ppins.lookup_pin(config.get('pin'), can_invert=True) + self.mcu = mcu_pin["chip"] + self.pin = mcu_pin["pin"] + self.invert = mcu_pin["invert"] + self.frequency = config.getfloat('frequency', 100, above=(1/0.3), + maxval=520000000) + self.mcu.register_config_callback(self._build_config) + def _build_config(self): + self.mcu.lookup_command( + "stm32_timer_output pin=%u cycle_ticks=%u on_ticks=%hu") + mcu_freq = self.mcu.seconds_to_clock(1.0) + cycle_ticks = int(mcu_freq // self.frequency) + # validate frequency + mcu_freq_rev = int(cycle_ticks * self.frequency) + if mcu_freq_rev != mcu_freq: + msg = """ + # Frequency output must be without remainder, %i != %i + [%s] + frequency = %f + """ % (mcu_freq, mcu_freq_rev, self.name, self.frequency) + raise self.printer.config_error(msg) + value = int(0.5 * cycle_ticks) + if self.invert: + value = cycle_ticks - value + if value/cycle_ticks < 0.4: + logging.warning("[%s] pulse width < 40%%" % (self.name)) + if value/cycle_ticks > 0.6: + logging.warning("[%s] pulse width > 60%%" % (self.name)) + self.mcu.add_config_cmd( + "stm32_timer_output pin=%s cycle_ticks=%d on_ticks=%d" + % (self.pin, cycle_ticks, value)) + +def load_config_prefix(config): + return PrinterClockOutputPin(config) From 3d5f352e242e47a398f07aa7c215dd0c0dc06f52 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 16 Oct 2025 22:52:51 +0200 Subject: [PATCH 292/411] docs: Describe static_pwm_clock section Signed-off-by: Timofey Titovets --- docs/Config_Reference.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 528465e64..b01360adf 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3616,6 +3616,20 @@ pin: # These options are deprecated and should no longer be specified. ``` +### [static_pwm_clock] + +Static configurable output pin (one may define any number of +sections with an "static_pwm_clock" prefix). +Pins configured here will be set up as clock output pins. +Generally used to provide clock input to other hardware on the board. +``` +[static_pwm_clock my_pin] +pin: +# The pin to configure as an output. This parameter must be provided. +#frequency: 100 +# Target output frequency. +``` + ### [pwm_tool] Pulse width modulation digital output pins capable of high speed From 7377da63b7b1ca4b62fb7d843b57224f332b37c7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Dec 2025 18:16:40 -0500 Subject: [PATCH 293/411] stm32: Declare gpio_timer_setup() as static in hard_pwm.c Signed-off-by: Kevin O'Connor --- src/stm32/hard_pwm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index 8a9dcbb4b..72c7685bd 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -299,7 +299,7 @@ static const struct gpio_pwm_info pwm_regs[] = { #endif }; -struct gpio_pwm +static struct gpio_pwm gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, int is_clock_out) { From db35e99ea19b8e9b978898f3c19afea97c7b9032 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 29 Dec 2025 13:40:57 -0500 Subject: [PATCH 294/411] tmc: Group code startup functions together in TMCCommandHelper() Code movement only; no code changes. Signed-off-by: Kevin O'Connor --- klippy/extras/tmc.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index aad19cf4d..944bc3e1f 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -323,14 +323,20 @@ class TMCCommandHelper: self.name = config.get_name().split()[-1] self.mcu_tmc = mcu_tmc self.current_helper = current_helper + self.fields = mcu_tmc.get_fields() + self.stepper = None + # Stepper phase tracking + self.mcu_phase_offset = None + # Stepper enable/disable tracking + self.toff = None + self.stepper_enable = self.printer.load_object(config, "stepper_enable") + # DUMP_TMC support + self.read_registers = self.read_translate = None + # Common tmc helpers self.echeck_helper = TMCErrorCheck(config, mcu_tmc) self.record_helper = TMCStallguardDump(config, mcu_tmc) - self.fields = mcu_tmc.get_fields() - self.read_registers = self.read_translate = None - self.toff = None - self.mcu_phase_offset = None - self.stepper = None - self.stepper_enable = self.printer.load_object(config, "stepper_enable") + TMCMicrostepHelper(config, mcu_tmc) + # Register callbacks self.printer.register_event_handler("stepper:sync_mcu_position", self._handle_sync_mcu_pos) self.printer.register_event_handler("stepper:set_sdir_inverted", @@ -339,8 +345,6 @@ class TMCCommandHelper: self._handle_mcu_identify) self.printer.register_event_handler("klippy:connect", self._handle_connect) - # Set microstep config options - TMCMicrostepHelper(config, mcu_tmc) # Register commands gcode = self.printer.lookup_object("gcode") gcode.register_mux_command("SET_TMC_FIELD", "STEPPER", self.name, @@ -468,18 +472,19 @@ class TMCCommandHelper: self.echeck_helper.stop_checks() except self.printer.command_error as e: self.printer.invoke_shutdown(str(e)) - def _handle_mcu_identify(self): - # Lookup stepper object - force_move = self.printer.lookup_object("force_move") - self.stepper = force_move.lookup_stepper(self.stepper_name) - # Note pulse duration and step_both_edge optimizations available - self.stepper.setup_default_pulse_duration(.000000100, True) def _handle_stepper_enable(self, print_time, is_enable): if is_enable: cb = (lambda ev: self._do_enable(print_time)) else: cb = (lambda ev: self._do_disable(print_time)) self.printer.get_reactor().register_callback(cb) + # Initial startup handling + def _handle_mcu_identify(self): + # Lookup stepper object + force_move = self.printer.lookup_object("force_move") + self.stepper = force_move.lookup_stepper(self.stepper_name) + # Note pulse duration and step_both_edge optimizations available + self.stepper.setup_default_pulse_duration(.000000100, True) def _handle_connect(self): # Check if using step on both edges optimization pulse_duration, step_both_edge = self.stepper.get_pulse_duration() From d5ef9247510717f7485fc6bcde41b6178e7d5f87 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 29 Dec 2025 14:03:47 -0500 Subject: [PATCH 295/411] tmc: Simplify TMCCommandHelper() error checking Move shutdown checking from _do_enable() and _dos_disable() to new enable_disable_cb(). Signed-off-by: Kevin O'Connor --- klippy/extras/tmc.py | 62 +++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 944bc3e1f..298b71728 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -442,42 +442,40 @@ class TMCCommandHelper: self.mcu_phase_offset = moff # Stepper enable/disable tracking def _do_enable(self, print_time): - try: - if self.toff is not None: - # Shared enable via comms handling - self.fields.set_field("toff", self.toff) - self._init_registers() - did_reset = self.echeck_helper.start_checks() - if did_reset: - self.mcu_phase_offset = None - # Calculate phase offset + if self.toff is not None: + # Shared enable via comms handling + self.fields.set_field("toff", self.toff) + self._init_registers() + did_reset = self.echeck_helper.start_checks() + if did_reset: + self.mcu_phase_offset = None + # Calculate phase offset + if self.mcu_phase_offset is not None: + return + gcode = self.printer.lookup_object("gcode") + with gcode.get_mutex(): if self.mcu_phase_offset is not None: return - gcode = self.printer.lookup_object("gcode") - with gcode.get_mutex(): - if self.mcu_phase_offset is not None: - return - logging.info("Pausing toolhead to calculate %s phase offset", - self.stepper_name) - self.printer.lookup_object('toolhead').wait_moves() - self._handle_sync_mcu_pos(self.stepper) - except self.printer.command_error as e: - self.printer.invoke_shutdown(str(e)) + logging.info("Pausing toolhead to calculate %s phase offset", + self.stepper_name) + self.printer.lookup_object('toolhead').wait_moves() + self._handle_sync_mcu_pos(self.stepper) def _do_disable(self, print_time): - try: - if self.toff is not None: - val = self.fields.set_field("toff", 0) - reg_name = self.fields.lookup_register("toff") - self.mcu_tmc.set_register(reg_name, val, print_time) - self.echeck_helper.stop_checks() - except self.printer.command_error as e: - self.printer.invoke_shutdown(str(e)) + if self.toff is not None: + val = self.fields.set_field("toff", 0) + reg_name = self.fields.lookup_register("toff") + self.mcu_tmc.set_register(reg_name, val, print_time) + self.echeck_helper.stop_checks() def _handle_stepper_enable(self, print_time, is_enable): - if is_enable: - cb = (lambda ev: self._do_enable(print_time)) - else: - cb = (lambda ev: self._do_disable(print_time)) - self.printer.get_reactor().register_callback(cb) + def enable_disable_cb(eventtime): + try: + if is_enable: + self._do_enable(print_time) + else: + self._do_disable(print_time) + except self.printer.command_error as e: + self.printer.invoke_shutdown(str(e)) + self.printer.get_reactor().register_callback(enable_disable_cb) # Initial startup handling def _handle_mcu_identify(self): # Lookup stepper object From 8ea7be5dd7139215a4b3e06b2512d2c3f88c9b5b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 29 Dec 2025 14:09:38 -0500 Subject: [PATCH 296/411] tmc: Hold a mutex during enable/disable checking It's possible for a motor disable request to occur while processing a previous motor enable. Use a reactor mutex to ensure the two events are processed serially. Signed-off-by: Kevin O'Connor --- klippy/extras/tmc.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 298b71728..536308d97 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -330,6 +330,7 @@ class TMCCommandHelper: # Stepper enable/disable tracking self.toff = None self.stepper_enable = self.printer.load_object(config, "stepper_enable") + self.enable_mutex = self.printer.get_reactor().mutex() # DUMP_TMC support self.read_registers = self.read_translate = None # Common tmc helpers @@ -469,10 +470,11 @@ class TMCCommandHelper: def _handle_stepper_enable(self, print_time, is_enable): def enable_disable_cb(eventtime): try: - if is_enable: - self._do_enable(print_time) - else: - self._do_disable(print_time) + with self.enable_mutex: + if is_enable: + self._do_enable(print_time) + else: + self._do_disable(print_time) except self.printer.command_error as e: self.printer.invoke_shutdown(str(e)) self.printer.get_reactor().register_callback(enable_disable_cb) From 51dcb09d1274d1f1b9fb8fae3787f774ce16fbe2 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 25 Oct 2025 21:17:57 +0200 Subject: [PATCH 297/411] probe_eddy_current: reload z_offset probe helper Currently, there is no way to adjust the calibration curve. The existing z_offset infrastructure is not applicable or disabled here. To make it possible to fine tune calibration curve. Reload Z_OFFSET helper for probe_eddy_current. Signed-off-by: Timofey Titovets --- klippy/extras/probe.py | 5 ++++- klippy/extras/probe_eddy_current.py | 22 ++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index bfeb33cea..d8f086265 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -36,7 +36,8 @@ def calc_probe_z_average(positions, method='average'): # Helper to implement common probing commands class ProbeCommandHelper: - def __init__(self, config, probe, query_endstop=None): + def __init__(self, config, probe, query_endstop=None, + replace_z_offset=False): self.printer = config.get_printer() self.probe = probe self.query_endstop = query_endstop @@ -57,6 +58,8 @@ class ProbeCommandHelper: # Other commands gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, desc=self.cmd_PROBE_ACCURACY_help) + if replace_z_offset: + return gcode.register_command('Z_OFFSET_APPLY_PROBE', self.cmd_Z_OFFSET_APPLY_PROBE, desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 779a904fb..b5c4b2897 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -31,6 +31,9 @@ class EddyCalibration: gcode.register_mux_command("PROBE_EDDY_CURRENT_CALIBRATE", "CHIP", cname, self.cmd_EDDY_CALIBRATE, desc=self.cmd_EDDY_CALIBRATE_help) + gcode.register_command('Z_OFFSET_APPLY_PROBE', + self.cmd_Z_OFFSET_APPLY_PROBE, + desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) def is_calibrated(self): return len(self.cal_freqs) > 2 def load_calibration(self, cal): @@ -236,13 +239,16 @@ class EddyCalibration: if len(filtered) <= 8: raise self.printer.command_error( "Failed calibration - No usable data") + z_freq_pairs = [(pos, freq) for pos, freq, _, _ in filtered] + self._save_calibration(z_freq_pairs) + def _save_calibration(self, z_freq_pairs): gcode = self.printer.lookup_object("gcode") gcode.respond_info( "The SAVE_CONFIG command will update the printer config file\n" "and restart the printer.") # Save results cal_contents = [] - for i, (pos, freq, _, _) in enumerate(filtered): + for i, (pos, freq) in enumerate(z_freq_pairs): if not i % 3: cal_contents.append('\n') cal_contents.append("%.6f:%.3f" % (pos, freq)) @@ -256,6 +262,17 @@ class EddyCalibration: # Start manual probe manual_probe.ManualProbeHelper(self.printer, gcmd, self.post_manual_probe) + cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset" + def cmd_Z_OFFSET_APPLY_PROBE(self, gcmd): + gcode_move = self.printer.lookup_object("gcode_move") + offset = gcode_move.get_status()['homing_origin'].z + if offset == 0: + gcmd.respond_info("Nothing to do: Z Offset is 0") + return + cal_zpos = [z - offset for z in self.cal_zpos] + z_freq_pairs = zip(cal_zpos, self.cal_freqs) + z_freq_pairs = sorted(z_freq_pairs) + self._save_calibration(z_freq_pairs) def register_drift_compensation(self, comp): self.drift_comp = comp @@ -519,7 +536,8 @@ class PrinterEddyProbe: self.param_helper = probe.ProbeParameterHelper(config) self.eddy_descend = EddyDescend( config, self.sensor_helper, self.calibration, self.param_helper) - self.cmd_helper = probe.ProbeCommandHelper(config, self) + self.cmd_helper = probe.ProbeCommandHelper(config, self, + replace_z_offset=True) self.probe_offsets = probe.ProbeOffsetsHelper(config) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.eddy_descend.start_probe_session) From dd625933f7b9bd53363fe015c62aaa874021fa9a Mon Sep 17 00:00:00 2001 From: Kyoungkyu Park Date: Fri, 19 Dec 2025 02:47:27 +0900 Subject: [PATCH 298/411] avr: add lgt8f328p support Signed-off-by: Kyoungkyu Park --- src/avr/Kconfig | 16 ++++++++++++++-- src/avr/Makefile | 2 +- src/avr/adc.c | 27 ++++++++++++++++++++++++++- src/avr/gpio.c | 6 ++++++ src/avr/hard_pwm.c | 3 ++- src/avr/i2c.c | 3 ++- src/avr/spi.c | 3 ++- test/configs/lgt8f328p.config | 15 +++++++++++++++ 8 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 test/configs/lgt8f328p.config diff --git a/src/avr/Kconfig b/src/avr/Kconfig index d4d78d279..74271ec82 100644 --- a/src/avr/Kconfig +++ b/src/avr/Kconfig @@ -11,7 +11,7 @@ config AVR_SELECT select HAVE_GPIO_I2C select HAVE_GPIO_HARD_PWM select HAVE_STRICT_TIMING - select HAVE_LIMITED_CODE_SIZE if MACH_atmega168 || MACH_atmega328 || MACH_atmega328p || MACH_atmega32u4 + select HAVE_LIMITED_CODE_SIZE if MACH_atmega168 || MACH_atmega328 || MACH_atmega328p || MACH_atmega32u4 || MACH_lgt8f328p config BOARD_DIRECTORY string @@ -39,6 +39,8 @@ choice bool "atmega328" config MACH_atmega168 bool "atmega168" + config MACH_lgt8f328p + bool "lgt8f328p" endchoice config MCU @@ -53,6 +55,12 @@ config MCU default "atmega32u4" if MACH_atmega32u4 default "atmega1280" if MACH_atmega1280 default "atmega2560" if MACH_atmega2560 + default "lgt8f328p" if MACH_lgt8f328p + +config AVR_BUILD_MCU + string + default MCU if !MACH_lgt8f328p + default "atmega328p" if MACH_lgt8f328p config AVRDUDE_PROTOCOL string @@ -62,6 +70,9 @@ config AVRDUDE_PROTOCOL choice prompt "Processor speed" if LOW_LEVEL_OPTIONS + config AVR_FREQ_32000000 + bool "32Mhz" + depends on MACH_lgt8f328p config AVR_FREQ_16000000 bool "16Mhz" config AVR_FREQ_20000000 @@ -75,11 +86,12 @@ config CLOCK_FREQ int default 8000000 if AVR_FREQ_8000000 default 20000000 if AVR_FREQ_20000000 + default 32000000 if AVR_FREQ_32000000 default 16000000 config CLEAR_PRESCALER bool "Manually clear the CPU prescaler field at startup" if LOW_LEVEL_OPTIONS - depends on MACH_at90usb1286 || MACH_at90usb646 || MACH_atmega32u4 + depends on MACH_at90usb1286 || MACH_at90usb646 || MACH_atmega32u4 || MACH_lgt8f328p default y help Some AVR chips ship with a "clock prescaler" that causes the diff --git a/src/avr/Makefile b/src/avr/Makefile index f667de3b7..7cf6d68f2 100644 --- a/src/avr/Makefile +++ b/src/avr/Makefile @@ -6,7 +6,7 @@ CROSS_PREFIX=avr- dirs-y += src/avr src/generic CFLAGS-$(CONFIG_HAVE_LIMITED_CODE_SIZE) += -Os -CFLAGS += $(CFLAGS-y) -mmcu=$(CONFIG_MCU) +CFLAGS += $(CFLAGS-y) -mmcu=$(CONFIG_AVR_BUILD_MCU) # Add avr source files src-y += avr/main.c avr/timer.c diff --git a/src/avr/adc.c b/src/avr/adc.c index 1d16368d0..99fd063f6 100644 --- a/src/avr/adc.c +++ b/src/avr/adc.c @@ -30,6 +30,10 @@ static const uint8_t adc_pins[] PROGMEM = { GPIO('F', 4), GPIO('F', 5), GPIO('F', 6), GPIO('F', 7), GPIO('K', 0), GPIO('K', 1), GPIO('K', 2), GPIO('K', 3), GPIO('K', 4), GPIO('K', 5), GPIO('K', 6), GPIO('K', 7), +#elif CONFIG_MACH_lgt8f328p + GPIO('C', 0), GPIO('C', 1), GPIO('C', 2), GPIO('C', 3), + GPIO('C', 4), GPIO('C', 5), GPIO('E', 1), GPIO('E', 3), + GPIO('C', 7), GPIO('F', 0), GPIO('E', 6), GPIO('E', 7), #endif }; @@ -41,7 +45,11 @@ DECL_ENUMERATION_RANGE("pin", "PE2", GPIO('E', 2), 2); enum { ADMUX_DEFAULT = 0x40 }; enum { ADC_ENABLE = (1<= 8) DIDR2 |= 1 << (chan & 0x07); else +#elif CONFIG_MACH_lgt8f328p + if (chan >= 8) + switch (chan) { + case 8: + DIDR1 |= (1 << 2); + break; + case 9: + DIDR1 |= (1 << 3); + break; + case 10: + DIDR1 |= (1 << 6); + break; + case 11: + DIDR1 |= (1 << 7); + break; + } + else #endif DIDR0 |= 1 << chan; diff --git a/src/avr/gpio.c b/src/avr/gpio.c index f52251770..6164a49d5 100644 --- a/src/avr/gpio.c +++ b/src/avr/gpio.c @@ -20,6 +20,9 @@ DECL_ENUMERATION_RANGE("pin", "PC0", GPIO('C', 0), 8); DECL_ENUMERATION_RANGE("pin", "PD0", GPIO('D', 0), 8); #if CONFIG_MACH_atmega328p DECL_ENUMERATION_RANGE("pin", "PE0", GPIO('E', 0), 8); +#elif CONFIG_MACH_lgt8f328p +DECL_ENUMERATION_RANGE("pin", "PE0", GPIO('E', 0), 8); +DECL_ENUMERATION_RANGE("pin", "PF0", GPIO('F', 0), 8); #endif #ifdef PINE DECL_ENUMERATION_RANGE("pin", "PE0", GPIO('E', 0), 8); @@ -42,6 +45,9 @@ volatile uint8_t * const digital_regs[] PROGMEM = { &PINB, &PINC, &PIND, #if CONFIG_MACH_atmega328p &_SFR_IO8(0x0C), // PINE on atmega328pb +#elif CONFIG_MACH_lgt8f328p + &_SFR_IO8(0x0C), // lgt8f328p have PINE and PINF + &_SFR_IO8(0x12) #endif #ifdef PINE &PINE, &PINF, diff --git a/src/avr/hard_pwm.c b/src/avr/hard_pwm.c index 01d0bd9e6..d7cf4c015 100644 --- a/src/avr/hard_pwm.c +++ b/src/avr/hard_pwm.c @@ -22,7 +22,8 @@ struct gpio_pwm_info { enum { GP_8BIT=1, GP_AFMT=2 }; static const struct gpio_pwm_info pwm_regs[] PROGMEM = { -#if CONFIG_MACH_atmega168 || CONFIG_MACH_atmega328 || CONFIG_MACH_atmega328p +#if CONFIG_MACH_atmega168 || CONFIG_MACH_atmega328 \ + || CONFIG_MACH_atmega328p || CONFIG_MACH_lgt8f328p { GPIO('D', 6), &OCR0A, &TCCR0A, &TCCR0B, 1< Date: Sun, 16 Nov 2025 17:14:31 -0500 Subject: [PATCH 299/411] stm32: Enable MOE bit in hard_pwm.c for all stm32 chips Always enable the MOE bit. Reported by @tt33415366. Signed-off-by: Kevin O'Connor --- src/stm32/hard_pwm.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index 72c7685bd..f4c8cdfde 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -400,11 +400,14 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, default: shutdown("Invalid PWM channel"); } + // Enable PWM output p->timer->CR1 |= TIM_CR1_CEN; -#if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G0 - p->timer->BDTR |= TIM_BDTR_MOE; -#endif + + // Advanced timers need MOE enabled. On standard timers this is a + // write to reserved memory, but that seems harmless in practice. + p->timer->BDTR = TIM_BDTR_MOE; + return channel; } From 05ce95645638c962a0d82fb8152d2819224a23af Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 31 Dec 2025 12:39:02 +0100 Subject: [PATCH 300/411] stm32: fix static clock output for faster chips If pcycle_time is scaled, the value should be scaled as well Signed-off-by: Timofey Titovets --- src/stm32/hard_pwm.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index f4c8cdfde..d6ea6dfca 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -323,9 +323,11 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, // CLK output if (is_clock_out) { prescaler = 1; + val = val / pclock_div; while (pcycle_time > UINT16_MAX) { prescaler = prescaler * 2; pcycle_time /= 2; + val /= 2; } max_pwm = pcycle_time; } From 8dd798ebb8ad37dcfd6d6d825ff7a7482f3ddafe Mon Sep 17 00:00:00 2001 From: MRX8024 <57844100+MRX8024@users.noreply.github.com> Date: Thu, 1 Jan 2026 00:30:16 +0200 Subject: [PATCH 301/411] tmc: Fix stepper:set_dir_inverted event handler name The event handler is registered with an incorrect event name, causing the handler to never be called. Signed-off-by: Maksim Bolgov maksim8024@gmail.com --- klippy/extras/tmc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 536308d97..23b05305e 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -340,7 +340,7 @@ class TMCCommandHelper: # Register callbacks self.printer.register_event_handler("stepper:sync_mcu_position", self._handle_sync_mcu_pos) - self.printer.register_event_handler("stepper:set_sdir_inverted", + self.printer.register_event_handler("stepper:set_dir_inverted", self._handle_sync_mcu_pos) self.printer.register_event_handler("klippy:mcu_identify", self._handle_mcu_identify) From 74a15f4834103956388650705cb88687781b974a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 31 Dec 2025 14:45:19 -0500 Subject: [PATCH 302/411] stm32: Minor comment and code organization changes to hard_pwm.c Signed-off-by: Kevin O'Connor --- src/stm32/hard_pwm.c | 50 +++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index d6ea6dfca..0b4ea6eb3 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -311,52 +311,58 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, if (p->pin == pin) break; } + gpio_peripheral(p->pin, p->function, 0); // Map cycle_time to pwm clock divisor uint32_t pclk = get_pclock_frequency((uint32_t)p->timer); uint32_t pclock_div = CONFIG_CLOCK_FREQ / pclk; if (pclock_div > 1) pclock_div /= 2; // Timers run at twice the normal pclock frequency - uint32_t max_pwm = MAX_PWM; uint32_t pcycle_time = cycle_time / pclock_div; - uint32_t prescaler = pcycle_time / (max_pwm - 1); - // CLK output - if (is_clock_out) { - prescaler = 1; + + // Convert requested cycle time (cycle_time/CLOCK_FREQ) to actual + // cycle time (hwpwm_ticks*prescaler*pclock_div/CLOCK_FREQ). + uint32_t hwpwm_ticks, prescaler; + if (!is_clock_out) { + // In normal mode, allow the pulse frequency (cycle_time) to change + // in order to maintain the requested duty ratio (val/MAX_PWM). + hwpwm_ticks = MAX_PWM; + prescaler = pcycle_time / (hwpwm_ticks - 1); + if (prescaler > UINT16_MAX + 1) + prescaler = UINT16_MAX + 1; + else if (prescaler < 1) + prescaler = 1; + } else { + // In clock output mode, allow the pulse width enable duration + // (val) to change in order to maintain the requested frequency. val = val / pclock_div; - while (pcycle_time > UINT16_MAX) { - prescaler = prescaler * 2; - pcycle_time /= 2; + hwpwm_ticks = pcycle_time; + prescaler = 1; + while (hwpwm_ticks > UINT16_MAX) { val /= 2; + hwpwm_ticks /= 2; + prescaler *= 2; } - max_pwm = pcycle_time; - } - if (prescaler > UINT16_MAX) { - prescaler = UINT16_MAX; - } else if (prescaler > 0) { - prescaler -= 1; } - gpio_peripheral(p->pin, p->function, 0); - - // Enable clock + // Enable requested pwm hardware block if (!is_enabled_pclock((uint32_t) p->timer)) { enable_pclock((uint32_t) p->timer); } - if (p->timer->CR1 & TIM_CR1_CEN) { - if (p->timer->PSC != (uint16_t) prescaler) { + if (p->timer->PSC != (uint16_t) (prescaler - 1)) { shutdown("PWM already programmed at different speed"); } - if (p->timer->ARR != (uint16_t) max_pwm - 1) { + if (p->timer->ARR != (uint16_t) (hwpwm_ticks - 1)) { shutdown("PWM already programmed with different pulse duration"); } } else { - p->timer->PSC = (uint16_t) prescaler; - p->timer->ARR = max_pwm - 1; + p->timer->PSC = prescaler - 1; + p->timer->ARR = hwpwm_ticks - 1; p->timer->EGR |= TIM_EGR_UG; } + // Enable requested channel of hardware pwm block struct gpio_pwm channel; switch (p->channel) { case 1: { From e60fe3d99b545d7e42ff2f5278efa5822668a57c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 31 Dec 2025 18:39:23 -0500 Subject: [PATCH 303/411] stm32: Fix off-by-one error in the prescaler calculation in hard_pwm.c Signed-off-by: Kevin O'Connor --- src/stm32/hard_pwm.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index 0b4ea6eb3..f34e62b00 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -11,7 +11,7 @@ #include "internal.h" // GPIO #include "sched.h" // sched_shutdown -#define MAX_PWM (256 + 1) +#define MAX_PWM 256 DECL_CONSTANT("PWM_MAX", MAX_PWM); struct gpio_pwm_info { @@ -327,7 +327,7 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, // In normal mode, allow the pulse frequency (cycle_time) to change // in order to maintain the requested duty ratio (val/MAX_PWM). hwpwm_ticks = MAX_PWM; - prescaler = pcycle_time / (hwpwm_ticks - 1); + prescaler = pcycle_time / MAX_PWM; if (prescaler > UINT16_MAX + 1) prescaler = UINT16_MAX + 1; else if (prescaler < 1) From abda66d6efafdcd12fb423e72cda1e936f6ac226 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 2 Jan 2026 23:44:43 +0100 Subject: [PATCH 304/411] ldc1612: enable frequency div to reduce noise BTT Eddy uses 12MHz clock in frequency. Coil is oscillating at 3+MHz. Which is out of spec for LDC1612 sensors. Division of coil frequency seems to reduce output noise. Signed-off-by: Timofey Titovets --- klippy/extras/ldc1612.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index 973556af1..fea4eba9c 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -87,8 +87,12 @@ class LDC1612: self.oid = oid = mcu.create_oid() self.query_ldc1612_cmd = None self.ldc1612_setup_home_cmd = self.query_ldc1612_home_state_cmd = None - self.frequency = config.getint("frequency", DEFAULT_LDC1612_FREQ, - 2000000, 40000000) + self.clock_freq = config.getint("frequency", DEFAULT_LDC1612_FREQ, + 2000000, 40000000) + # Coil frequency divider, assume 12MHz is BTT Eddy + # BTT Eddy's coil frequency is > 1/4 of reference clock + self.sensor_div = 1 if self.clock_freq != DEFAULT_LDC1612_FREQ else 2 + self.freq_conv = float(self.clock_freq * self.sensor_div) / (1<<28) if config.get('intb_pin', None) is not None: ppins = config.get_printer().lookup_object("pins") pin_params = ppins.lookup_pin(config.get('intb_pin')) @@ -143,7 +147,7 @@ class LDC1612: def setup_home(self, print_time, trigger_freq, trsync_oid, hit_reason, err_reason): clock = self.mcu.print_time_to_clock(print_time) - tfreq = int(trigger_freq * (1<<28) / float(self.frequency) + 0.5) + tfreq = int(trigger_freq / self.freq_conv + 0.5) self.ldc1612_setup_home_cmd.send( [self.oid, clock, tfreq, trsync_oid, hit_reason, err_reason]) def clear_home(self): @@ -155,7 +159,7 @@ class LDC1612: return self.mcu.clock_to_print_time(tclock) # Measurement decoding def _convert_samples(self, samples): - freq_conv = float(self.frequency) / (1<<28) + freq_conv = self.freq_conv count = 0 for ptime, val in samples: mv = val & 0x0fffffff @@ -176,11 +180,11 @@ class LDC1612: "(e.g. faulty wiring) or a faulty ldc1612 chip." % (manuf_id, dev_id, LDC1612_MANUF_ID, LDC1612_DEV_ID)) # Setup chip in requested query rate - rcount0 = self.frequency / (16. * self.data_rate) + rcount0 = self.clock_freq / (16. * self.data_rate) self.set_reg(REG_RCOUNT0, int(rcount0 + 0.5)) self.set_reg(REG_OFFSET0, 0) - self.set_reg(REG_SETTLECOUNT0, int(SETTLETIME*self.frequency/16. + .5)) - self.set_reg(REG_CLOCK_DIVIDERS0, (1 << 12) | 1) + self.set_reg(REG_SETTLECOUNT0, int(SETTLETIME*self.clock_freq/16. + .5)) + self.set_reg(REG_CLOCK_DIVIDERS0, (self.sensor_div << 12) | 1) self.set_reg(REG_ERROR_CONFIG, (0x1f << 11) | 1) self.set_reg(REG_MUX_CONFIG, 0x0208 | DEGLITCH) self.set_reg(REG_CONFIG, 0x001 | (1<<12) | (1<<10) | (1<<9)) From 8bca4cbcd94341010bda1f965cd464c8100a87dd Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 2 Jan 2026 18:33:22 -0500 Subject: [PATCH 305/411] static_pwm_clock: Don't rely on custom stm32_timer_output mcu code Use the regular hardware pwm interface instead of relying on a custom interface. Signed-off-by: Kevin O'Connor --- klippy/extras/static_pwm_clock.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/klippy/extras/static_pwm_clock.py b/klippy/extras/static_pwm_clock.py index db62af88b..e9c018c00 100644 --- a/klippy/extras/static_pwm_clock.py +++ b/klippy/extras/static_pwm_clock.py @@ -11,16 +11,15 @@ class PrinterClockOutputPin: self.name = config.get_name() self.printer = config.get_printer() ppins = self.printer.lookup_object('pins') - mcu_pin = ppins.lookup_pin(config.get('pin'), can_invert=True) - self.mcu = mcu_pin["chip"] - self.pin = mcu_pin["pin"] - self.invert = mcu_pin["invert"] + self.mcu_pin = ppins.setup_pin('pwm', config.get('pin')) + self.mcu = self.mcu_pin.get_mcu() self.frequency = config.getfloat('frequency', 100, above=(1/0.3), maxval=520000000) + self.mcu_pin.setup_cycle_time(1. / self.frequency, True) + self.mcu_pin.setup_max_duration(0.) + self.mcu_pin.setup_start_value(0.5, 0.5) self.mcu.register_config_callback(self._build_config) def _build_config(self): - self.mcu.lookup_command( - "stm32_timer_output pin=%u cycle_ticks=%u on_ticks=%hu") mcu_freq = self.mcu.seconds_to_clock(1.0) cycle_ticks = int(mcu_freq // self.frequency) # validate frequency @@ -33,15 +32,10 @@ class PrinterClockOutputPin: """ % (mcu_freq, mcu_freq_rev, self.name, self.frequency) raise self.printer.config_error(msg) value = int(0.5 * cycle_ticks) - if self.invert: - value = cycle_ticks - value if value/cycle_ticks < 0.4: logging.warning("[%s] pulse width < 40%%" % (self.name)) if value/cycle_ticks > 0.6: logging.warning("[%s] pulse width > 60%%" % (self.name)) - self.mcu.add_config_cmd( - "stm32_timer_output pin=%s cycle_ticks=%d on_ticks=%d" - % (self.pin, cycle_ticks, value)) def load_config_prefix(config): return PrinterClockOutputPin(config) From e605fd18560fbb5a7413ca12b72325ad4e18de16 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 2 Jan 2026 18:37:43 -0500 Subject: [PATCH 306/411] stm32: Improve accuracy of hardware pwm cycle time Use a different method of setting the hardware pwm registers so that the actual cycle_time is much closer to the requested cycle_time. Also, remove the now unused stm32_timer_output command, as the main hardware pwm interface provides the same accuracy. Signed-off-by: Kevin O'Connor --- src/stm32/gpio.h | 3 ++- src/stm32/hard_pwm.c | 56 ++++++++++++++------------------------------ 2 files changed, 19 insertions(+), 40 deletions(-) diff --git a/src/stm32/gpio.h b/src/stm32/gpio.h index 0bfffc4b4..d4c565d3c 100644 --- a/src/stm32/gpio.h +++ b/src/stm32/gpio.h @@ -25,7 +25,8 @@ void gpio_in_reset(struct gpio_in g, int32_t pull_up); uint8_t gpio_in_read(struct gpio_in g); struct gpio_pwm { - void *reg; + void *reg; + uint32_t hwpwm_ticks; }; struct gpio_pwm gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val); void gpio_pwm_write(struct gpio_pwm g, uint32_t val); diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index f34e62b00..08defce22 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -11,7 +11,7 @@ #include "internal.h" // GPIO #include "sched.h" // sched_shutdown -#define MAX_PWM 256 +#define MAX_PWM (1<<15) DECL_CONSTANT("PWM_MAX", MAX_PWM); struct gpio_pwm_info { @@ -299,9 +299,8 @@ static const struct gpio_pwm_info pwm_regs[] = { #endif }; -static struct gpio_pwm -gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, - int is_clock_out) +struct gpio_pwm +gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) { // Find pin in pwm_regs table const struct gpio_pwm_info* p = pwm_regs; @@ -322,28 +321,18 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, // Convert requested cycle time (cycle_time/CLOCK_FREQ) to actual // cycle time (hwpwm_ticks*prescaler*pclock_div/CLOCK_FREQ). - uint32_t hwpwm_ticks, prescaler; - if (!is_clock_out) { - // In normal mode, allow the pulse frequency (cycle_time) to change - // in order to maintain the requested duty ratio (val/MAX_PWM). - hwpwm_ticks = MAX_PWM; - prescaler = pcycle_time / MAX_PWM; - if (prescaler > UINT16_MAX + 1) - prescaler = UINT16_MAX + 1; - else if (prescaler < 1) - prescaler = 1; - } else { - // In clock output mode, allow the pulse width enable duration - // (val) to change in order to maintain the requested frequency. - val = val / pclock_div; - hwpwm_ticks = pcycle_time; - prescaler = 1; - while (hwpwm_ticks > UINT16_MAX) { - val /= 2; - hwpwm_ticks /= 2; - prescaler *= 2; - } + uint32_t hwpwm_ticks = pcycle_time, prescaler = 1, shift = 0; + while (hwpwm_ticks > UINT16_MAX) { + shift += 1; + hwpwm_ticks = (pcycle_time + (1 << (shift-1))) >> shift; + prescaler = 1 << shift; } + if (prescaler > UINT16_MAX + 1) { + prescaler = UINT16_MAX + 1; + hwpwm_ticks = UINT16_MAX; + } + if (hwpwm_ticks < 2) + hwpwm_ticks = 2; // Enable requested pwm hardware block if (!is_enabled_pclock((uint32_t) p->timer)) { @@ -364,6 +353,7 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, // Enable requested channel of hardware pwm block struct gpio_pwm channel; + channel.hwpwm_ticks = hwpwm_ticks; switch (p->channel) { case 1: { channel.reg = (void*) &p->timer->CCR1; @@ -419,20 +409,8 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, return channel; } -struct gpio_pwm -gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) { - return gpio_timer_setup(pin, cycle_time, val, 0); -} - -void -command_stm32_timer_output(uint32_t *args) -{ - gpio_timer_setup(args[0], args[1], args[2], 1); -} -DECL_COMMAND(command_stm32_timer_output, - "stm32_timer_output pin=%u cycle_ticks=%u on_ticks=%hu"); - void gpio_pwm_write(struct gpio_pwm g, uint32_t val) { - *(volatile uint32_t*) g.reg = val; + uint32_t r = DIV_ROUND_CLOSEST(val * g.hwpwm_ticks, MAX_PWM); + *(volatile uint32_t*) g.reg = r; } From f7ddb4003762f3f5baf4164a7125b3db582dab96 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 8 Feb 2025 16:03:27 +0100 Subject: [PATCH 307/411] ldc1612: handle i2c errors I2C error means we don't know the sensor status. Force data output to the host and cancel homing. Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/ldc1612.py | 19 ++++++ klippy/extras/probe_eddy_current.py | 4 +- src/sensor_ldc1612.c | 100 ++++++++++++++++++++++------ 3 files changed, 101 insertions(+), 22 deletions(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index fea4eba9c..66099b9f8 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -84,6 +84,7 @@ class LDC1612: default_addr=LDC1612_ADDR, default_speed=400000) self.mcu = mcu = self.i2c.get_mcu() + self._sensor_errors = {} self.oid = oid = mcu.create_oid() self.query_ldc1612_cmd = None self.ldc1612_setup_home_cmd = self.query_ldc1612_home_state_cmd = None @@ -132,6 +133,8 @@ class LDC1612: "query_ldc1612_home_state oid=%c", "ldc1612_home_state oid=%c homing=%c trigger_clock=%u", oid=self.oid, cq=cmdqueue) + errors = self.mcu.get_enumerations().get("ldc1612_error:", {}) + self._sensor_errors = {v: k for k, v in errors.items()} def get_mcu(self): return self.i2c.get_mcu() def read_reg(self, reg): @@ -157,16 +160,32 @@ class LDC1612: params = self.query_ldc1612_home_state_cmd.send([self.oid]) tclock = self.mcu.clock32_to_clock64(params['trigger_clock']) return self.mcu.clock_to_print_time(tclock) + def lookup_sensor_error(self, error): + return self._sensor_errors.get(error, "Unknown ldc1612 error") # Measurement decoding def _convert_samples(self, samples): freq_conv = self.freq_conv count = 0 + errors = {} + def log_once(msg): + if not errors.get(msg, 0): + errors[msg] = 0 + errors[msg] += 1 for ptime, val in samples: mv = val & 0x0fffffff if mv != val: self.last_error_count += 1 + if (val >> 16 & 0xffff) == 0xffff: + # Encoded error from sensor_ldc1612.c + log_once(self.lookup_sensor_error(val & 0xffff)) + continue + error_bits = (val >> 28) & 0x0f + log_once("Sensor reports error (%s)" % (bin(error_bits),)) samples[count] = (round(ptime, 6), round(freq_conv * mv, 3), 999.9) count += 1 + del samples[count:] + for msg in errors: + logging.error("%s: %s (%d)" % (self.name, msg, errors[msg])) # Start, stop, and process message batches def _start_measurements(self): # In case of miswiring, testing LDC1612 device ID prevents treating diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index b5c4b2897..c7b415d02 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -415,7 +415,9 @@ class EddyDescend: if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: raise self._printer.command_error( "Communication timeout during homing") - raise self._printer.command_error("Eddy current sensor error") + error_code = res - self.REASON_SENSOR_ERROR + error_msg = self._sensor_helper.lookup_sensor_error(error_code) + raise self._printer.command_error(error_msg) if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: return 0. if self._mcu.is_fileoutput(): diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 8b67884f1..e5ab60e3b 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -37,6 +37,16 @@ struct ldc1612 { static struct task_wake ldc1612_wake; +// Internal errors transmitted in sample reports (or trsync error) +enum { + SE_SENSOR_ERROR, SE_I2C_STATUS, SE_I2C_DATA, SE_INVALID_DATA +}; + +DECL_ENUMERATION("ldc1612_error:", "SENSOR_REPORTS_ERROR", SE_SENSOR_ERROR); +DECL_ENUMERATION("ldc1612_error:", "I2C_STATUS_ERROR", SE_I2C_STATUS); +DECL_ENUMERATION("ldc1612_error:", "I2C_DATA_ERROR", SE_I2C_DATA); +DECL_ENUMERATION("ldc1612_error:", "INVALID_READ_DATA", SE_INVALID_DATA); + // Check if the intb line is "asserted" static int check_intb_asserted(struct ldc1612 *ld) @@ -111,19 +121,34 @@ command_query_ldc1612_home_state(uint32_t *args) DECL_COMMAND(command_query_ldc1612_home_state, "query_ldc1612_home_state oid=%c"); +// Cancel homing due to an error +static void +cancel_homing(struct ldc1612 *ld, int error_code) +{ + if (!(ld->homing_flags & LH_CAN_TRIGGER)) + return; + ld->homing_flags = 0; + trsync_do_trigger(ld->ts, ld->error_reason + error_code); +} + +static int +check_data_bits(struct ldc1612 *ld, uint32_t raw_data) { + if (raw_data < 0x0fffffff) + return 0; + cancel_homing(ld, SE_SENSOR_ERROR); + return -1; +} + // Check if a sample should trigger a homing event static void -check_home(struct ldc1612 *ld, uint32_t data) +check_home(struct ldc1612 *ld, uint32_t raw_data) { uint8_t homing_flags = ld->homing_flags; if (!(homing_flags & LH_CAN_TRIGGER)) return; - if (data > 0x0fffffff) { - // Sensor reports an issue - cancel homing - ld->homing_flags = 0; - trsync_do_trigger(ld->ts, ld->error_reason); + if (check_data_bits(ld, raw_data)) return; - } + uint32_t data = raw_data & 0x0fffffff; uint32_t time = timer_read_time(); if ((homing_flags & LH_AWAIT_HOMING) && timer_is_before(time, ld->homing_clock)) @@ -143,41 +168,69 @@ check_home(struct ldc1612 *ld, uint32_t data) #define REG_STATUS 0x18 // Read a register on the ldc1612 -static void +static int read_reg(struct ldc1612 *ld, uint8_t reg, uint8_t *res) { - int ret = i2c_dev_read(ld->i2c, sizeof(reg), ®, 2, res); - i2c_shutdown_on_err(ret); + return i2c_dev_read(ld->i2c, sizeof(reg), ®, 2, res); } // Read the status register on the ldc1612 -static uint16_t -read_reg_status(struct ldc1612 *ld) +static int +read_reg_status(struct ldc1612 *ld, uint16_t *status) { uint8_t data_status[2]; - read_reg(ld, REG_STATUS, data_status); - return (data_status[0] << 8) | data_status[1]; + int ret = read_reg(ld, REG_STATUS, data_status); + *status = (data_status[0] << 8) | data_status[1]; + return ret; } +#define STATUS_UNREADCONV0 (1 << 3) #define BYTES_PER_SAMPLE 4 +static void +report_sample_error(struct ldc1612 *ld, int error_code) +{ + cancel_homing(ld, error_code); + + uint8_t *d = &ld->sb.data[ld->sb.data_count]; + d[0] = 0xff; + d[1] = 0xff; + d[2] = 0; + d[3] = error_code; +} + // Query ldc1612 data static void ldc1612_query(struct ldc1612 *ld, uint8_t oid) { // Check if data available (and clear INTB line) - uint16_t status = read_reg_status(ld); + uint16_t status; + int ret = read_reg_status(ld, &status); irq_disable(); ld->flags &= ~LDC_PENDING; irq_enable(); - if (!(status & 0x08)) + if (ret) { + report_sample_error(ld, SE_I2C_STATUS); + goto out; + } + if (!(status & STATUS_UNREADCONV0)) + // No data available return; // Read coil0 frequency uint8_t *d = &ld->sb.data[ld->sb.data_count]; - read_reg(ld, REG_DATA0_MSB, &d[0]); - read_reg(ld, REG_DATA0_LSB, &d[2]); - ld->sb.data_count += BYTES_PER_SAMPLE; + ret |= read_reg(ld, REG_DATA0_MSB, &d[0]); + ret |= read_reg(ld, REG_DATA0_LSB, &d[2]); + + if (ret) { + report_sample_error(ld, SE_I2C_DATA); + goto out; + } + if (d[0] == 0xff && d[1] == 0xff) { + // Invalid data from sensor (conflict with internal error indicator) + report_sample_error(ld, SE_INVALID_DATA); + goto out; + } // Check for endstop trigger uint32_t data = ((uint32_t)d[0] << 24) @@ -185,7 +238,8 @@ ldc1612_query(struct ldc1612 *ld, uint8_t oid) | ((uint32_t)d[2] << 8) | ((uint32_t)d[3]); check_home(ld, data); - +out: + ld->sb.data_count += BYTES_PER_SAMPLE; // Flush local buffer if needed if (ld->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(ld->sb.data)) sensor_bulk_report(&ld->sb, oid); @@ -228,11 +282,15 @@ command_query_status_ldc1612(uint32_t *args) } // Query sensor to see if a sample is pending + uint16_t status; uint32_t time1 = timer_read_time(); - uint16_t status = read_reg_status(ld); + int ret = read_reg_status(ld, &status); uint32_t time2 = timer_read_time(); - uint32_t fifo = status & 0x08 ? BYTES_PER_SAMPLE : 0; + if (ret) + // Query error - don't send response - host will retry + return; + uint32_t fifo = status & STATUS_UNREADCONV0 ? BYTES_PER_SAMPLE : 0; sensor_bulk_status(&ld->sb, args[0], time1, time2-time1, fifo); } DECL_COMMAND(command_query_status_ldc1612, "query_status_ldc1612 oid=%c"); From c6c76149726d1d7563db3dece7897109da292f43 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 27 Nov 2025 00:54:00 +0100 Subject: [PATCH 308/411] ldc1612: ignore amplitude errors during homing Amplitude errors are useful but often too aggressive. On some sensors, it is not possible to avoid them completely. Make them non-critical for homing. Signed-off-by: Timofey Titovets --- src/sensor_ldc1612.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index e5ab60e3b..7211c2c19 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -131,8 +131,12 @@ cancel_homing(struct ldc1612 *ld, int error_code) trsync_do_trigger(ld->ts, ld->error_reason + error_code); } +#define DATA_ERROR_AMPLITUDE (1L << 28) + static int check_data_bits(struct ldc1612 *ld, uint32_t raw_data) { + // Ignore amplitude errors + raw_data &= ~DATA_ERROR_AMPLITUDE; if (raw_data < 0x0fffffff) return 0; cancel_homing(ld, SE_SENSOR_ERROR); From 2e0d746172bc5ed961083659d2981df02d0a2a8b Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 19 Dec 2025 04:51:47 +0100 Subject: [PATCH 309/411] ldc1612: trigger error on high frequency If the sensor coil is disconnected, the frequency is equal to the reference. If the sensor is misconfigured or damaged, the coil frequency is greater than 1/4 of the reference frequency. Signed-off-by: Timofey Titovets --- src/sensor_ldc1612.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 7211c2c19..35c69a2bd 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -131,13 +131,15 @@ cancel_homing(struct ldc1612 *ld, int error_code) trsync_do_trigger(ld->ts, ld->error_reason + error_code); } +#define MAX_VALID_RAW_VALUE 0x03ffffff #define DATA_ERROR_AMPLITUDE (1L << 28) static int check_data_bits(struct ldc1612 *ld, uint32_t raw_data) { // Ignore amplitude errors raw_data &= ~DATA_ERROR_AMPLITUDE; - if (raw_data < 0x0fffffff) + // Datasheet define valid frequency input as < F_ref / 4 + if (raw_data < MAX_VALID_RAW_VALUE) return 0; cancel_homing(ld, SE_SENSOR_ERROR); return -1; From f1bd17d83df35744cb583ddb6633c4ba61824aad Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 27 Nov 2025 00:47:59 +0100 Subject: [PATCH 310/411] ldc1612: decode error flags Most errors, aside from amplitude, should never happen. Output them to the log to simplify later debugging. Count them to aggregate error metrics. Signed-off-by: Timofey Titovets --- klippy/extras/ldc1612.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index 66099b9f8..29c8cad2d 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -173,14 +173,22 @@ class LDC1612: errors[msg] += 1 for ptime, val in samples: mv = val & 0x0fffffff - if mv != val: + if val > 0x03ffffff or val == 0x0: self.last_error_count += 1 if (val >> 16 & 0xffff) == 0xffff: # Encoded error from sensor_ldc1612.c log_once(self.lookup_sensor_error(val & 0xffff)) continue error_bits = (val >> 28) & 0x0f - log_once("Sensor reports error (%s)" % (bin(error_bits),)) + if error_bits & 0x8 or mv == 0x0000000: + log_once("Frequency under valid range") + if error_bits & 0x4 or mv > 0x3ffffff: + type = "hard" if error_bits & 0x4 else "soft" + log_once("Frequency over valid %s range" % (type)) + if error_bits & 0x2: + log_once("Conversion Watchdog timeout") + if error_bits & 0x1: + log_once("Amplitude Low/High warning") samples[count] = (round(ptime, 6), round(freq_conv * mv, 3), 999.9) count += 1 del samples[count:] From 1fc9d81095a647401521d3e94bf34e8d4d3a363f Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 4 Jan 2026 22:48:23 +0100 Subject: [PATCH 311/411] docs: describe eddy error messages Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 8d81bd88b..11069c4bc 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -144,3 +144,38 @@ to perform thermal drift calibration: As one may conclude, the calibration process outlined above is more challenging and time consuming than most other procedures. It may require practice and several attempts to achieve an optimal calibration. + +## Errors description + +Possible homing errors and actionables: + +- Sensor error + - Check logs for detailed error +- Eddy I2C STATUS/DATA error. + - Check loose wiring. + - Try software I2C/decrease I2C rate +- Invalid read data + - Same as I2C + +Possible sensor errors and actionables: +- Frequency over valid hard range + - Check frequency configuration + - Hardware fault +- Frequency over valid soft range + - Check frequency configuration +- Conversion Watchdog timeout + - Hardware fault + +Amplitude Low/High warning messages can mean: +- Sensor close to the bed +- Sensor far from the bed +- Higher temperature than was at the current calibration +- Capacitor missing + +On some sensors, it is not possible to completely avoid amplitude +warning indicator. + +You can try to redo the `LDC_CALIBRATE_DRIVE_CURRENT` calibration at work +temperature or increase `reg_drive_current` by 1-2 from the calibrated value. + +Generally, it is like an engine check light. It may indicate an issue. From 2bd0acb6ca4d75e7035c20465a8f36d5e42e2b65 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Fri, 9 Jan 2026 10:57:47 +0100 Subject: [PATCH 312/411] exclude_object: Fixed object exclusion with changing GCode axes Signed-off-by: Dmitry Butyugin --- klippy/extras/exclude_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/exclude_object.py b/klippy/extras/exclude_object.py index c80dd000d..3ca6d53eb 100644 --- a/klippy/extras/exclude_object.py +++ b/klippy/extras/exclude_object.py @@ -89,7 +89,7 @@ class ExcludeObject: offset = [0.] * num_coord self.extrusion_offsets[ename] = offset if len(offset) < num_coord: - offset.extend([0.] * (len(num_coord) - len(offset))) + offset.extend([0.] * (num_coord - len(offset))) return offset def get_position(self): From 48f0b3cad6d4593746384bf49a39913dcb8cc796 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 4 Jan 2026 13:09:28 -0500 Subject: [PATCH 313/411] gcode_move: Export more than 4 components in gcode_position status Commit f04895f5 documented that "{printer.gcode_move.gcode_position}" may contain more than 4 components, however the code was not actually updated to export that additional information. (Commit ac6cab91 only made the change to "homing_origin" and "position".) Export the information in gcode_position as intended. Signed-off-by: Kevin O'Connor --- klippy/extras/gcode_move.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klippy/extras/gcode_move.py b/klippy/extras/gcode_move.py index c2b307663..3980f556e 100644 --- a/klippy/extras/gcode_move.py +++ b/klippy/extras/gcode_move.py @@ -94,7 +94,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[:4] + return p def _get_gcode_speed(self): return self.speed / self.speed_factor def _get_gcode_speed_override(self): @@ -191,7 +191,7 @@ class GCodeMove: def cmd_M114(self, gcmd): # Get Current Position p = self._get_gcode_position() - gcmd.respond_raw("X:%.3f Y:%.3f Z:%.3f E:%.3f" % tuple(p)) + gcmd.respond_raw("X:%.3f Y:%.3f Z:%.3f E:%.3f" % tuple(p[:4])) def cmd_M220(self, gcmd): # Set speed factor override percentage value = gcmd.get_float('S', 100., above=0.) / (60. * 100.) From e590bc87d8c6b279f24f592714b25e90673236e7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 20:27:02 -0500 Subject: [PATCH 314/411] spi_flash: Don't import mcu module Avoid using mcu.CommandQueryWrapper() and mcu.CommandWrapper() classes. Instead, implement local variants of these classes. This will make it easier to modify the mcu classes without fear of breaking the spi_flash code. Signed-off-by: Kevin O'Connor --- scripts/spi_flash/spi_flash.py | 46 ++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index e9394dbe5..af93b53e9 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -21,7 +21,6 @@ import util import reactor import serialhdl import clocksync -import mcu ########################################################### # @@ -143,11 +142,38 @@ class SPIFlashError(Exception): class MCUConfigError(SPIFlashError): pass +# Wrapper around query commands +class CommandQueryWrapper: + def __init__(self, serial, msgformat, respformat, oid=None): + self._serial = serial + self._cmd = serial.get_msgparser().lookup_command(msgformat) + serial.get_msgparser().lookup_command(respformat) + self._response = respformat.split()[0] + self._oid = oid + self._cmd_queue = serial.get_default_command_queue() + def send(self, data=(), minclock=0, reqclock=0, retry=True): + cmds = [self._cmd.encode(data)] + xh = serialhdl.SerialRetryCommand(self._serial, self._response, + self._oid) + reqclock = max(minclock, reqclock) + return xh.get_response(cmds, self._cmd_queue, minclock, reqclock, retry) + +# Wrapper around command sending +class CommandWrapper: + def __init__(self, serial, msgformat): + self._serial = serial + msgparser = serial.get_msgparser() + self._cmd = msgparser.lookup_command(msgformat) + self._cmd_queue = serial.get_default_command_queue() + def send(self, data=(), minclock=0, reqclock=0): + cmd = self._cmd.encode(data) + self._serial.raw_send(cmd, minclock, reqclock, self._cmd_queue) + class SPIDirect: def __init__(self, ser): self.oid = SPI_OID - self._spi_send_cmd = mcu.CommandWrapper(ser, SPI_SEND_CMD) - self._spi_transfer_cmd = mcu.CommandQueryWrapper( + self._spi_send_cmd = CommandWrapper(ser, SPI_SEND_CMD) + self._spi_transfer_cmd = CommandQueryWrapper( ser, SPI_XFER_CMD, SPI_XFER_RESPONSE, self.oid) def spi_send(self, data): @@ -159,18 +185,18 @@ class SPIDirect: class SDIODirect: def __init__(self, ser): self.oid = SDIO_OID - self._sdio_send_cmd = mcu.CommandQueryWrapper( + self._sdio_send_cmd = CommandQueryWrapper( ser, SDIO_SEND_CMD, SDIO_SEND_CMD_RESPONSE, self.oid) - self._sdio_read_data = mcu.CommandQueryWrapper( + self._sdio_read_data = CommandQueryWrapper( ser, SDIO_READ_DATA, SDIO_READ_DATA_RESPONSE, self.oid) - self._sdio_write_data = mcu.CommandQueryWrapper( + self._sdio_write_data = CommandQueryWrapper( ser, SDIO_WRITE_DATA, SDIO_WRITE_DATA_RESPONSE, self.oid) - self._sdio_read_data_buffer = mcu.CommandQueryWrapper( + self._sdio_read_data_buffer = CommandQueryWrapper( ser, SDIO_READ_DATA_BUFFER, SDIO_READ_DATA_BUFFER_RESPONSE, self.oid) - self._sdio_write_data_buffer = mcu.CommandWrapper(ser, + self._sdio_write_data_buffer = CommandWrapper(ser, SDIO_WRITE_DATA_BUFFER) - self._sdio_set_speed = mcu.CommandWrapper(ser, SDIO_SET_SPEED) + self._sdio_set_speed = CommandWrapper(ser, SDIO_SET_SPEED) def sdio_send_cmd(self, cmd, argument, wait): return self._sdio_send_cmd.send([self.oid, cmd, argument, wait]) @@ -1254,7 +1280,7 @@ class MCUConnection: # Iterate through backwards compatible response strings for response in GET_CFG_RESPONSES: try: - get_cfg_cmd = mcu.CommandQueryWrapper( + get_cfg_cmd = CommandQueryWrapper( self._serial, GET_CFG_CMD, response) break except Exception as err: From 1cde5e201869609f685a9e8dbd3e6e49b166193f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 20:40:40 -0500 Subject: [PATCH 315/411] mcu: Pass conn_helper to CommandWrapper and CommandQueryWrapper Pass the low-level MCUConnectHelper class to these helper classes. Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 507f77347..20ffc3fdf 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -60,14 +60,14 @@ class RetryAsyncCommand: # Wrapper around query commands class CommandQueryWrapper: - def __init__(self, serial, msgformat, respformat, oid=None, - cmd_queue=None, is_async=False, error=serialhdl.error): - self._serial = serial + def __init__(self, conn_helper, msgformat, respformat, oid=None, + cmd_queue=None, is_async=False): + self._serial = serial = conn_helper.get_serial() self._cmd = serial.get_msgparser().lookup_command(msgformat) serial.get_msgparser().lookup_command(respformat) self._response = respformat.split()[0] self._oid = oid - self._error = error + self._error = conn_helper.get_mcu().get_printer().command_error self._xmit_helper = serialhdl.SerialRetryCommand if is_async: self._xmit_helper = RetryAsyncCommand @@ -92,15 +92,15 @@ class CommandQueryWrapper: # Wrapper around command sending class CommandWrapper: - def __init__(self, serial, msgformat, cmd_queue=None, debugoutput=False): - self._serial = serial + def __init__(self, conn_helper, msgformat, cmd_queue=None): + self._serial = serial = conn_helper.get_serial() msgparser = serial.get_msgparser() self._cmd = msgparser.lookup_command(msgformat) if cmd_queue is None: cmd_queue = serial.get_default_command_queue() self._cmd_queue = cmd_queue self._msgtag = msgparser.lookup_msgid(msgformat) & 0xffffffff - if debugoutput: + if conn_helper.get_mcu().is_fileoutput(): # Can't use send_wait_ack when in debugging mode self.send_wait_ack = self.send def send(self, data=(), minclock=0, reqclock=0): @@ -1096,12 +1096,11 @@ class MCU: def max_nominal_duration(self): return MAX_NOMINAL_DURATION def lookup_command(self, msgformat, cq=None): - return CommandWrapper(self._serial, msgformat, cq, - debugoutput=self.is_fileoutput()) + return CommandWrapper(self._conn_helper, msgformat, cq) def lookup_query_command(self, msgformat, respformat, oid=None, cq=None, is_async=False): - return CommandQueryWrapper(self._serial, msgformat, respformat, oid, - cq, is_async, self._printer.command_error) + return CommandQueryWrapper(self._conn_helper, msgformat, respformat, + oid, cq, is_async) def try_lookup_command(self, msgformat): try: return self.lookup_command(msgformat) From 57b94520de9e2d21f9381adcf165b73f1fa90ca4 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 21:25:19 -0500 Subject: [PATCH 316/411] mcu: Generate a dummy response to query commands when in debugging mode Previously a querycmd.send() request would silently hang if the code is run in "file output" mode (that is, it is not communicating with a real micro-controller). This behavior makes it hard to implement regression tests and is generally confusing. Change the code to respond with a dummy response (typically all zeros and empty strings) instead. Signed-off-by: Kevin O'Connor --- klippy/extras/ads1220.py | 4 ++++ klippy/mcu.py | 18 +++++++++++++++++- klippy/msgproto.py | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index 16080dc72..5e9ef72ba 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -175,6 +175,8 @@ class ADS1220: # read startup register state and validate val = self.read_reg(0x0, 4) if val != RESET_STATE: + if self.mcu.is_fileoutput(): + return raise self.printer.command_error( "Invalid ads1220 reset state (got %s vs %s).\n" "This is generally indicative of connection problems\n" @@ -209,6 +211,8 @@ class ADS1220: self.spi.spi_send(write_command) stored_val = self.read_reg(reg, len(register_bytes)) if bytearray(register_bytes) != stored_val: + if self.mcu.is_fileoutput(): + return raise self.printer.command_error( "Failed to set ADS1220 register [0x%x] to %s: got %s. " "This may be a connection problem (e.g. faulty wiring)" % ( diff --git a/klippy/mcu.py b/klippy/mcu.py index 20ffc3fdf..909b8ddb5 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -22,6 +22,20 @@ MAX_NOMINAL_DURATION = 3.0 # Command transmit helper classes ###################################################################### +# Generate a dummy response to query commands when in debugging mode +class DummyResponse: + def __init__(self, serial, name, oid=None): + params = {} + if oid is not None: + params['oid'] = oid + msgparser = serial.get_msgparser() + resp = msgparser.create_dummy_response(name, params) + resp['#sent_time'] = 0. + resp['#receive_time'] = 0. + self._response = resp + def get_response(self, cmds, cmd_queue, minclock=0, reqclock=0, retry=True): + return dict(self._response) + # Class to retry sending of a query command until a given response is received class RetryAsyncCommand: TIMEOUT_TIME = 5.0 @@ -69,7 +83,9 @@ class CommandQueryWrapper: self._oid = oid self._error = conn_helper.get_mcu().get_printer().command_error self._xmit_helper = serialhdl.SerialRetryCommand - if is_async: + if conn_helper.get_mcu().is_fileoutput(): + self._xmit_helper = DummyResponse + elif is_async: self._xmit_helper = RetryAsyncCommand if cmd_queue is None: cmd_queue = serial.get_default_command_queue() diff --git a/klippy/msgproto.py b/klippy/msgproto.py index 25701df36..021a025a9 100644 --- a/klippy/msgproto.py +++ b/klippy/msgproto.py @@ -353,6 +353,27 @@ class MessageParser: #logging.exception("Unable to encode") self._error("Unable to encode: %s", msgname) return cmd + def create_dummy_response(self, msgname, params={}): + mp = self.messages_by_name.get(msgname) + if mp is None: + self._error("Unknown response: %s", msgname) + argparts = dict(params) + for name, t in mp.name_to_type.items(): + if name not in argparts: + tval = 0 + if t.is_dynamic_string: + tval = () + argparts[name] = tval + try: + msg = mp.encode_by_name(**argparts) + except error as e: + raise + except: + #logging.exception("Unable to encode") + self._error("Unable to encode: %s", msgname) + res, pos = mp.parse(msg, 0) + res['#name'] = msgname + return res def fill_enumerations(self, enumerations): for add_name, add_enums in enumerations.items(): enums = self.enumerations.setdefault(add_name, {}) From 8be004401e929d5c4e750880f257ab8a7c5a80be Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 21:53:27 -0500 Subject: [PATCH 317/411] probe_eddy_current: Implement regression test case Update the code to support simple regression test cases. Signed-off-by: Kevin O'Connor --- klippy/extras/ldc1612.py | 7 +-- klippy/extras/probe_eddy_current.py | 11 ++-- test/klippy/eddy.cfg | 84 +++++++++++++++++++++++++++++ test/klippy/eddy.test | 30 +++++++++++ 4 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 test/klippy/eddy.cfg create mode 100644 test/klippy/eddy.test diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index 29c8cad2d..db539c650 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -138,6 +138,8 @@ class LDC1612: def get_mcu(self): return self.i2c.get_mcu() def read_reg(self, reg): + if self.mcu.is_fileoutput(): + return 0 params = self.i2c.i2c_read([reg], 2) response = bytearray(params['response']) return (response[0] << 8) | response[1] @@ -155,8 +157,6 @@ class LDC1612: [self.oid, clock, tfreq, trsync_oid, hit_reason, err_reason]) def clear_home(self): self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0, 0]) - if self.mcu.is_fileoutput(): - return 0. params = self.query_ldc1612_home_state_cmd.send([self.oid]) tclock = self.mcu.clock32_to_clock64(params['trigger_clock']) return self.mcu.clock_to_print_time(tclock) @@ -200,7 +200,8 @@ class LDC1612: # noise or wrong signal as a correctly initialized device manuf_id = self.read_reg(REG_MANUFACTURER_ID) dev_id = self.read_reg(REG_DEVICE_ID) - if manuf_id != LDC1612_MANUF_ID or dev_id != LDC1612_DEV_ID: + if ((manuf_id != LDC1612_MANUF_ID or dev_id != LDC1612_DEV_ID) + and not self.mcu.is_fileoutput()): raise self.printer.command_error( "Invalid ldc1612 id (got %x,%x vs %x,%x).\n" "This is generally indicative of connection problems\n" diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index c7b415d02..b833f9ab9 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -313,6 +313,13 @@ class EddyGatherSamples: if est_print_time > end_time + 1.0: raise self._printer.command_error( "probe_eddy_current sensor outage") + if mcu.is_fileoutput(): + # In debugging mode + if pos_time is not None: + toolhead_pos = self._lookup_toolhead_pos(pos_time) + self._probe_results.append((toolhead_pos[2], toolhead_pos)) + self._probe_times.pop(0) + continue reactor.pause(systime + 0.010) def _pull_freq(self, start_time, end_time): # Find average sensor frequency between time range @@ -421,7 +428,7 @@ class EddyDescend: if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: return 0. if self._mcu.is_fileoutput(): - return home_end_time + trigger_time = home_end_time self._trigger_time = trigger_time return trigger_time # Probe session interface @@ -437,8 +444,6 @@ class EddyDescend: # Perform probing move phoming = self._printer.lookup_object('homing') trig_pos = phoming.probing_move(self, pos, speed) - if not self._trigger_time: - return trig_pos # Extract samples start_time = self._trigger_time + 0.050 end_time = start_time + 0.100 diff --git a/test/klippy/eddy.cfg b/test/klippy/eddy.cfg new file mode 100644 index 000000000..11946ebe4 --- /dev/null +++ b/test/klippy/eddy.cfg @@ -0,0 +1,84 @@ +# Test config for probe_eddy_current +[stepper_x] +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PE5 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PJ1 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: probe:z_virtual_endstop +position_max: 200 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[heater_bed] +heater_pin: PH5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +control: watermark +min_temp: 0 +max_temp: 130 + +[probe_eddy_current eddy] +z_offset: 0.4 +x_offset: -5 +y_offset: -4 +sensor_type: ldc1612 +speed: 10.0 +intb_pin: PK7 + +[bed_mesh] +mesh_min: 10,10 +mesh_max: 180,180 + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 + +# Dummy calibration data +[probe_eddy_current eddy] +calibrate = + 0.050000:3300000.000,1.000000:3200000.000,5.000000:3000000.000 diff --git a/test/klippy/eddy.test b/test/klippy/eddy.test new file mode 100644 index 000000000..5251be122 --- /dev/null +++ b/test/klippy/eddy.test @@ -0,0 +1,30 @@ +# Test case for probe_eddy_current support +CONFIG eddy.cfg +DICTIONARY atmega2560.dict + +# Start by homing the printer. +G28 +G1 F6000 + +# Z / X / Y moves +G1 Z1 +G1 X1 +G1 Y1 + +# Run bed_mesh_calibrate +BED_MESH_CALIBRATE + +G1 Z2 +BED_MESH_CALIBRATE METHOD=scan + +G1 Z2 +BED_MESH_CALIBRATE METHOD=rapid_scan + +# Move again +G1 Z5 X0 Y0 + +# Do regular probe +PROBE + +# Move again +G1 Z9 From bb963187259857211ca33bf798aef566b03c714b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 23:38:00 -0500 Subject: [PATCH 318/411] ci-install: Install scipy/numpy in github regression test case environment This is in preparation for enhanced load_cell test cases which require these packages. Signed-off-by: Kevin O'Connor --- scripts/ci-install.sh | 2 ++ scripts/tests-requirements.txt | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 scripts/tests-requirements.txt diff --git a/scripts/ci-install.sh b/scripts/ci-install.sh index 28f7b6540..88b346bc1 100755 --- a/scripts/ci-install.sh +++ b/scripts/ci-install.sh @@ -61,6 +61,7 @@ echo -e "\n\n=============== Install python3 virtualenv\n\n" cd ${MAIN_DIR} virtualenv -p python3 ${BUILD_DIR}/python-env ${BUILD_DIR}/python-env/bin/pip install -r ${MAIN_DIR}/scripts/klippy-requirements.txt +${BUILD_DIR}/python-env/bin/pip install -r ${MAIN_DIR}/scripts/tests-requirements.txt ###################################################################### @@ -71,3 +72,4 @@ echo -e "\n\n=============== Install python2 virtualenv\n\n" cd ${MAIN_DIR} virtualenv -p python2 ${BUILD_DIR}/python2-env ${BUILD_DIR}/python2-env/bin/pip install -r ${MAIN_DIR}/scripts/klippy-requirements.txt +${BUILD_DIR}/python2-env/bin/pip install -r ${MAIN_DIR}/scripts/tests-requirements.txt diff --git a/scripts/tests-requirements.txt b/scripts/tests-requirements.txt new file mode 100644 index 000000000..a3936f8ba --- /dev/null +++ b/scripts/tests-requirements.txt @@ -0,0 +1,7 @@ +# This file describes the Python virtualenv package requirements for +# the Klipper regression test cases. This is in addition to the +# package requirements listed in the klippy-requirements.txt file. +# Typically the packages listed here are installed via the command: +# pip install -r tests-requirements.txt +scipy==1.2.3 ; python_version < '3.0' +scipy==1.15.3 ; python_version >= '3.0' From 3c56eb7f6f9726ad46d10d404055a88118337ef5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 23:27:24 -0500 Subject: [PATCH 319/411] load_cell_probe: Enhance regression test case Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell.py | 4 ++ klippy/extras/load_cell_probe.py | 2 + test/klippy/load_cell.cfg | 88 +++++++++++++++++++++++++++++--- test/klippy/load_cell.test | 21 +++++++- 4 files changed, 108 insertions(+), 7 deletions(-) diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py index f370cad90..361c937fc 100644 --- a/klippy/extras/load_cell.py +++ b/klippy/extras/load_cell.py @@ -313,6 +313,8 @@ class LoadCellSampleCollector: self._errors = 0 overflows = self._overflows self._overflows = 0 + if self._mcu.is_fileoutput(): + samples = [(0., 0., 0.)] return samples, (errors, overflows) if errors or overflows else 0 def _collect_until(self, timeout): @@ -324,6 +326,8 @@ class LoadCellSampleCollector: raise self._printer.command_error( "LoadCellSampleCollector timed out! Errors: %i," " Overflows: %i" % (self._errors, self._overflows)) + if self._mcu.is_fileoutput(): + break self._reactor.pause(now + RETRY_DELAY) return self._finish_collecting() diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index db2f2a650..1e08090c2 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -454,6 +454,8 @@ class LoadCellProbingMove: res = self._dispatch.stop() # clear the homing state so it stops processing samples self._last_trigger_time = self._mcu_load_cell_probe.clear_home() + if self._mcu.is_fileoutput(): + self._last_trigger_time = home_end_time if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: error = "Load Cell Probe Error: unknown reason code %i" % (res,) if res in self.ERROR_MAP: diff --git a/test/klippy/load_cell.cfg b/test/klippy/load_cell.cfg index fa599d10e..48ceca3ca 100644 --- a/test/klippy/load_cell.cfg +++ b/test/klippy/load_cell.cfg @@ -1,11 +1,75 @@ -# Test config for load_cell +# Test config for load cells +[stepper_x] +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PE5 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PJ1 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: PK1 +position_endstop: 0.0 +position_max: 200 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[heater_bed] +heater_pin: PH5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +control: watermark +min_temp: 0 +max_temp: 130 + +[bed_mesh] +mesh_min: 10,10 +mesh_max: 180,180 + [mcu] serial: /dev/ttyACM0 [printer] -kinematics: none +kinematics: cartesian max_velocity: 300 max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 [load_cell my_ads1220] sensor_type: ads1220 @@ -14,10 +78,22 @@ data_ready_pin: PA1 [load_cell my_hx711] sensor_type: hx711 -sclk_pin: PA2 -dout_pin: PA3 +sclk_pin: PA3 +dout_pin: PA5 [load_cell my_hx717] sensor_type: hx717 -sclk_pin: PA4 -dout_pin: PA5 +sclk_pin: PA7 +dout_pin: PJ0 + +[load_cell_probe] +z_offset: 0 +sensor_type: ads1220 +speed: 10.0 +cs_pin: PJ2 +data_ready_pin: PJ3 +counts_per_gram: 100 +reference_tare_counts: 1000 +drift_filter_cutoff_frequency: 0.8 +buzz_filter_cutoff_frequency: 100.0 +notch_filter_frequencies: 50, 60 diff --git a/test/klippy/load_cell.test b/test/klippy/load_cell.test index 880f840aa..c22de2be7 100644 --- a/test/klippy/load_cell.test +++ b/test/klippy/load_cell.test @@ -2,4 +2,23 @@ DICTIONARY atmega2560.dict CONFIG load_cell.cfg -G4 P1000 +# Start by homing the printer. +G28 +G1 F6000 + +# Z / X / Y moves +G1 Z1 +G1 X1 +G1 Y1 + +# Run bed_mesh_calibrate +BED_MESH_CALIBRATE + +# Move again +G1 Z5 X0 Y0 + +# Do regular probe +PROBE + +# Move again +G1 Z9 From 9ccb4d96e90f2ce89f061d17ce89760826260769 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 20 Dec 2025 16:41:44 -0500 Subject: [PATCH 320/411] manual_probe: Report final probe results in new ProbeResult named tuple Return the manual probe results in a named tuple containing (bed_x, bed_y, bed_z, test_x, test_y, and test_z) components. For a manual probe the test_xyz will always be equal to bed_xyz, but these components may differ when using automated z probes. Signed-off-by: Kevin O'Connor --- klippy/extras/axis_twist_compensation.py | 6 ++--- klippy/extras/manual_probe.py | 30 ++++++++++++++++-------- klippy/extras/probe.py | 16 +++++++------ klippy/extras/probe_eddy_current.py | 6 ++--- klippy/extras/temperature_probe.py | 8 +++---- 5 files changed, 39 insertions(+), 27 deletions(-) diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index 908ac4dae..9f2e65d97 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -286,14 +286,14 @@ class Calibrater: # returns a callback function for the manual probe is_end = self.current_point_index == len(probe_points) - 1 - def callback(kin_pos): - if kin_pos is None: + def callback(mpresult): + if mpresult is None: # probe was cancelled self.gcmd.respond_info( "AXIS_TWIST_COMPENSATION_CALIBRATE: Probe cancelled, " "calibration aborted") return - z_offset = self.current_measured_z - kin_pos[2] + z_offset = self.current_measured_z - mpresult.bed_z self.results.append(z_offset) if is_end: # end of calibration diff --git a/klippy/extras/manual_probe.py b/klippy/extras/manual_probe.py index 44b2c719f..727526916 100644 --- a/klippy/extras/manual_probe.py +++ b/klippy/extras/manual_probe.py @@ -1,9 +1,18 @@ # Helper script for manual z height probing # -# Copyright (C) 2019 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, bisect +import logging, bisect, collections + +# Main probe results tuple. The probe estimates that if the toollhead +# is commanded to xy position (bed_x, bed_y) and then descends, the +# nozzle will contact the bed at a toolhead z position of bed_z. The +# probe test itself was performed while the toolhead was at xyz +# position (test_x, test_y, test_z). All coordinates are relative to +# the frame (the coordinate system used in the config file). +ProbeResult = collections.namedtuple('probe_result', [ + 'bed_x', 'bed_y', 'bed_z', 'test_x', 'test_y', 'test_z']) # Helper to lookup the Z stepper config section def lookup_z_endstop_config(config): @@ -62,9 +71,9 @@ class ManualProbe: self.cmd_Z_OFFSET_APPLY_DELTA_ENDSTOPS, desc=self.cmd_Z_OFFSET_APPLY_ENDSTOP_help) self.reset_status() - def manual_probe_finalize(self, kin_pos): - if kin_pos is not None: - self.gcode.respond_info("Z position is %.3f" % (kin_pos[2],)) + def manual_probe_finalize(self, mpresult): + if mpresult is not None: + self.gcode.respond_info("Z position is %.3f" % (mpresult.bed_z,)) def reset_status(self): self.status = { 'is_active': False, @@ -77,10 +86,10 @@ class ManualProbe: cmd_MANUAL_PROBE_help = "Start manual probe helper script" def cmd_MANUAL_PROBE(self, gcmd): ManualProbeHelper(self.printer, gcmd, self.manual_probe_finalize) - def z_endstop_finalize(self, kin_pos): - if kin_pos is None: + def z_endstop_finalize(self, mpresult): + if mpresult is None: return - z_pos = self.z_position_endstop - kin_pos[2] + z_pos = self.z_position_endstop - mpresult.bed_z self.gcode.respond_info( "%s: position_endstop: %.3f\n" "The SAVE_CONFIG command will update the printer config file\n" @@ -271,10 +280,11 @@ class ManualProbeHelper: self.gcode.register_command('NEXT', None) self.gcode.register_command('ABORT', None) self.gcode.register_command('TESTZ', None) - kin_pos = None + mpresult = None if success: kin_pos = self.get_kinematics_pos() - self.finalize_callback(kin_pos) + mpresult = ProbeResult(*(kin_pos[:3] + kin_pos[:3])) + self.finalize_callback(mpresult) def load_config(config): return ManualProbe(config) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index d8f086265..15c3db255 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -83,10 +83,10 @@ class ProbeCommandHelper: pos = run_single_probe(self.probe, gcmd) gcmd.respond_info("Result is z=%.6f" % (pos[2],)) self.last_z_result = pos[2] - def probe_calibrate_finalize(self, kin_pos): - if kin_pos is None: + def probe_calibrate_finalize(self, mpresult): + if mpresult is None: return - z_offset = self.probe_calibrate_z - kin_pos[2] + z_offset = self.probe_calibrate_z - mpresult.bed_z gcode = self.printer.lookup_object('gcode') gcode.respond_info( "%s: z_offset: %.3f\n" @@ -514,7 +514,9 @@ class ProbePointsHelper: def _manual_probe_start(self): self._raise_tool(not self.manual_results) if len(self.manual_results) >= len(self.probe_points): - done = self._invoke_callback(self.manual_results) + results = [[mpr.bed_x, mpr.bed_y, mpr.bed_z] + for mpr in self.manual_results] + done = self._invoke_callback(results) if done: return # Caller wants a "retry" - clear results and restart probing @@ -523,10 +525,10 @@ class ProbePointsHelper: gcmd = self.gcode.create_gcode_command("", "", {}) manual_probe.ManualProbeHelper(self.printer, gcmd, self._manual_probe_finalize) - def _manual_probe_finalize(self, kin_pos): - if kin_pos is None: + def _manual_probe_finalize(self, mpresult): + if mpresult is None: return - self.manual_results.append(kin_pos) + self.manual_results.append(mpresult) self._manual_probe_start() # Helper to obtain a single probe measurement diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index b833f9ab9..876728352 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -206,11 +206,11 @@ class EddyCalibration: pos, mad_mm, mad_hz) gcode.respond_info(msg) return filtered - def post_manual_probe(self, kin_pos): - if kin_pos is None: + def post_manual_probe(self, mpresult): + if mpresult is None: # Manual Probe was aborted return - curpos = list(kin_pos) + curpos = [mpresult.bed_x, mpresult.bed_y, mpresult.bed_z] move = self.printer.lookup_object('toolhead').manual_move # Move away from the bed probe_calibrate_z = curpos[2] diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py index aebb10764..d42d9e210 100644 --- a/klippy/extras/temperature_probe.py +++ b/klippy/extras/temperature_probe.py @@ -221,19 +221,19 @@ class TemperatureProbe: % (self.name, cnt, exp_cnt, last_temp, self.next_auto_temp) ) - def _manual_probe_finalize(self, kin_pos): - if kin_pos is None: + def _manual_probe_finalize(self, mpresult): + if mpresult is None: # Calibration aborted self._finalize_drift_cal(False) return if self.last_zero_pos is not None: - z_diff = self.last_zero_pos[2] - kin_pos[2] + z_diff = self.last_zero_pos - mpresult.bed_z self.total_expansion += z_diff logging.info( "Estimated Total Thermal Expansion: %.6f" % (self.total_expansion,) ) - self.last_zero_pos = kin_pos + self.last_zero_pos = mpresult.bed_z toolhead = self.printer.lookup_object("toolhead") tool_zero_z = toolhead.get_position()[2] try: From 2e0c2262e7c52cbc2ea4784e582fac2f1d3b9d67 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 21 Dec 2025 12:28:36 -0500 Subject: [PATCH 321/411] probe: Convert ProbePointsHelper to use ProbeResult Change the ProbePointsHelper class to return ProbeResult tuples. Callers of this class are also updated so that they use the tuple's bed_xyz parameters instead of manually calculating these values from the probe xyz offsets. Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 14 +++++++++++--- klippy/extras/bed_mesh.py | 22 +++++++++++++--------- klippy/extras/bed_tilt.py | 13 +++++-------- klippy/extras/delta_calibrate.py | 8 ++++---- klippy/extras/probe.py | 10 ++++++---- klippy/extras/quad_gantry_level.py | 20 ++++++++++---------- klippy/extras/screws_tilt_adjust.py | 8 ++++---- klippy/extras/z_tilt.py | 13 +++++-------- 8 files changed, 58 insertions(+), 50 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 6846e034d..76f455af5 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,9 +8,17 @@ All dates in this document are approximate. ## Changes -20251122: An option `axis` has been added to `[carriage ]` sections -for `generic_cartesian` kinematics, allowing arbitrary names for primary -carriages. Users are encouraged to explicitly specify `axis` option now. +20260109: The `[screws_tilt_adjust]` module now reports the status +variable `{printer.screws_tilt_adjust.result.screw1.z}` with the +probe's `z_offset` applied. That is, one would previously need to +subtract the probe's configured `z_offset` to find the absolute Z +deviation at the given screw location and now one must not apply the +`z_offset`. + +20251122: An option `axis` has been added to `[carriage ]` +sections for `generic_cartesian` kinematics, allowing arbitrary names +for primary carriages. Users are encouraged to explicitly specify +`axis` option now. 20251106: The status fields `{printer.toolhead.position}`, `{printer.gcode_move.position}`, diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index a8e5764c0..f752980e0 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -651,9 +651,9 @@ class BedMeshCalibrate: except BedMeshError as e: raise gcmd.error(str(e)) self.probe_mgr.start_probe(gcmd) - def probe_finalize(self, offsets, positions): - z_offset = offsets[2] - positions = [[round(p[0], 2), round(p[1], 2), p[2]] + def probe_finalize(self, positions): + z_offset = 0. + positions = [[round(p.bed_x, 2), round(p.bed_y, 2), p.bed_z] for p in positions] if self.probe_mgr.get_zero_ref_mode() == ZrefMode.PROBE: ref_pos = positions.pop() @@ -682,7 +682,7 @@ class BedMeshCalibrate: idx_offset = 0 start_idx = 0 for i, pts in substitutes.items(): - fpt = [p - o for p, o in zip(base_points[i], offsets[:2])] + fpt = base_points[i][:2] # offset the index to account for additional samples idx = i + idx_offset # Add "normal" points @@ -702,7 +702,7 @@ class BedMeshCalibrate: # validate length of result if len(base_points) != len(positions): - self._dump_points(probed_pts, positions, offsets) + self._dump_points(probed_pts, positions) raise self.gcode.error( "bed_mesh: invalid position list size, " "generated count: %d, probed count: %d" @@ -713,7 +713,7 @@ class BedMeshCalibrate: row = [] prev_pos = base_points[0] for pos, result in zip(base_points, positions): - offset_pos = [p - o for p, o in zip(pos, offsets[:2])] + offset_pos = pos[:2] if ( not isclose(offset_pos[0], result[0], abs_tol=.5) or not isclose(offset_pos[1], result[1], abs_tol=.5) @@ -786,7 +786,7 @@ class BedMeshCalibrate: self.gcode.respond_info("Mesh Bed Leveling Complete") if self._profile_name is not None: self.bedmesh.save_profile(self._profile_name) - def _dump_points(self, probed_pts, corrected_pts, offsets): + def _dump_points(self, probed_pts, corrected_pts): # logs generated points with offset applied, points received # from the finalize callback, and the list of corrected points points = self.probe_mgr.get_base_points() @@ -797,7 +797,7 @@ class BedMeshCalibrate: for i in list(range(max_len)): gen_pt = probed_pt = corr_pt = "" if i < len(points): - off_pt = [p - o for p, o in zip(points[i], offsets[:2])] + off_pt = points[i][:2] gen_pt = "(%.2f, %.2f)" % tuple(off_pt) if i < len(probed_pts): probed_pt = "(%.2f, %.2f, %.4f)" % tuple(probed_pts[i]) @@ -1220,8 +1220,12 @@ class RapidScanHelper: if is_probe_pt: probe_session.run_probe(gcmd) results = probe_session.pull_probed_results() + import manual_probe # XXX + results = [manual_probe.ProbeResult( + r[0]+offsets[0], r[1]+offsets[1], r[2]-offsets[2], r[0], r[1], r[2]) + for r in results] toolhead.get_last_move_time() - self.finalize_callback(offsets, results) + self.finalize_callback(results) probe_session.end_probe_session() def _raise_tool(self, gcmd, scan_height): diff --git a/klippy/extras/bed_tilt.py b/klippy/extras/bed_tilt.py index e5686cbeb..feb924997 100644 --- a/klippy/extras/bed_tilt.py +++ b/klippy/extras/bed_tilt.py @@ -58,19 +58,17 @@ class BedTiltCalibrate: cmd_BED_TILT_CALIBRATE_help = "Bed tilt calibration script" def cmd_BED_TILT_CALIBRATE(self, gcmd): self.probe_helper.start_probe(gcmd) - def probe_finalize(self, offsets, positions): + def probe_finalize(self, positions): # Setup for coordinate descent analysis - z_offset = offsets[2] logging.info("Calculating bed_tilt with: %s", positions) params = { 'x_adjust': self.bedtilt.x_adjust, 'y_adjust': self.bedtilt.y_adjust, - 'z_adjust': z_offset } + 'z_adjust': 0. } logging.info("Initial bed_tilt parameters: %s", params) # Perform coordinate descent def adjusted_height(pos, params): - x, y, z = pos - return (z - x*params['x_adjust'] - y*params['y_adjust'] - - params['z_adjust']) + return (pos.bed_z - pos.bed_x*params['x_adjust'] + - pos.bed_y*params['y_adjust'] - params['z_adjust']) def errorfunc(params): total_error = 0. for pos in positions: @@ -81,8 +79,7 @@ class BedTiltCalibrate: # Update current bed_tilt calculations x_adjust = new_params['x_adjust'] y_adjust = new_params['y_adjust'] - z_adjust = (new_params['z_adjust'] - z_offset - - x_adjust * offsets[0] - y_adjust * offsets[1]) + z_adjust = new_params['z_adjust'] self.bedtilt.update_adjust(x_adjust, y_adjust, z_adjust) # Log and report results logging.info("Calculated bed_tilt parameters: %s", new_params) diff --git a/klippy/extras/delta_calibrate.py b/klippy/extras/delta_calibrate.py index 4054e2310..df7f39356 100644 --- a/klippy/extras/delta_calibrate.py +++ b/klippy/extras/delta_calibrate.py @@ -152,12 +152,12 @@ class DeltaCalibrate: "%.3f,%.3f,%.3f" % tuple(spos1)) configfile.set(section, "distance%d_pos2" % (i,), "%.3f,%.3f,%.3f" % tuple(spos2)) - def probe_finalize(self, offsets, positions): + def probe_finalize(self, positions): # Convert positions into (z_offset, stable_position) pairs - z_offset = offsets[2] kin = self.printer.lookup_object('toolhead').get_kinematics() - delta_params = kin.get_calibration() - probe_positions = [(z_offset, delta_params.calc_stable_position(p)) + csp = kin.get_calibration().calc_stable_position + probe_positions = [(p.test_z - p.bed_z, + csp([p.test_x, p.test_y, p.test_z])) for p in positions] # Perform analysis self.calculate_params(probe_positions, self.last_distances) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 15c3db255..600d64e47 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -466,7 +466,7 @@ class ProbePointsHelper: toolhead = self.printer.lookup_object('toolhead') toolhead.get_last_move_time() # Invoke callback - res = self.finalize_callback(self.probe_offsets, results) + res = self.finalize_callback(results) return res != "retry" def _move_next(self, probe_num): # Move to next XY probe point @@ -502,6 +502,10 @@ class ProbePointsHelper: self._raise_tool(not probe_num) if probe_num >= len(self.probe_points): results = probe_session.pull_probed_results() + results = [manual_probe.ProbeResult( + r[0] + self.probe_offsets[0], r[1] + self.probe_offsets[1], + r[2] - self.probe_offsets[2], r[0], r[1], r[2]) + for r in results] done = self._invoke_callback(results) if done: break @@ -514,9 +518,7 @@ class ProbePointsHelper: def _manual_probe_start(self): self._raise_tool(not self.manual_results) if len(self.manual_results) >= len(self.probe_points): - results = [[mpr.bed_x, mpr.bed_y, mpr.bed_z] - for mpr in self.manual_results] - done = self._invoke_callback(results) + done = self._invoke_callback(self.manual_results) if done: return # Caller wants a "retry" - clear results and restart probing diff --git a/klippy/extras/quad_gantry_level.py b/klippy/extras/quad_gantry_level.py index 98cd53c5a..5556d5e1d 100644 --- a/klippy/extras/quad_gantry_level.py +++ b/klippy/extras/quad_gantry_level.py @@ -51,34 +51,34 @@ class QuadGantryLevel: self.z_status.reset() self.retry_helper.start(gcmd) self.probe_helper.start_probe(gcmd) - def probe_finalize(self, offsets, positions): + def probe_finalize(self, positions): # Mirror our perspective so the adjustments make sense # from the perspective of the gantry - z_positions = [self.horizontal_move_z - p[2] for p in positions] + z_positions = [self.horizontal_move_z - p.bed_z for p in positions] points_message = "Gantry-relative probe points:\n%s\n" % ( " ".join(["%s: %.6f" % (z_id, z_positions[z_id]) for z_id in range(len(z_positions))])) self.gcode.respond_info(points_message) # Calculate slope along X axis between probe point 0 and 3 - ppx0 = [positions[0][0] + offsets[0], z_positions[0]] - ppx3 = [positions[3][0] + offsets[0], z_positions[3]] + ppx0 = [positions[0].bed_x, z_positions[0]] + ppx3 = [positions[3].bed_x, z_positions[3]] slope_x_pp03 = self.linefit(ppx0, ppx3) # Calculate slope along X axis between probe point 1 and 2 - ppx1 = [positions[1][0] + offsets[0], z_positions[1]] - ppx2 = [positions[2][0] + offsets[0], z_positions[2]] + ppx1 = [positions[1].bed_x, z_positions[1]] + ppx2 = [positions[2].bed_x, z_positions[2]] slope_x_pp12 = self.linefit(ppx1, ppx2) logging.info("quad_gantry_level f1: %s, f2: %s" % (slope_x_pp03, slope_x_pp12)) # Calculate gantry slope along Y axis between stepper 0 and 1 - a1 = [positions[0][1] + offsets[1], + a1 = [positions[0].bed_y, self.plot(slope_x_pp03, self.gantry_corners[0][0])] - a2 = [positions[1][1] + offsets[1], + a2 = [positions[1].bed_y, self.plot(slope_x_pp12, self.gantry_corners[0][0])] slope_y_s01 = self.linefit(a1, a2) # Calculate gantry slope along Y axis between stepper 2 and 3 - b1 = [positions[0][1] + offsets[1], + b1 = [positions[0].bed_y, self.plot(slope_x_pp03, self.gantry_corners[1][0])] - b2 = [positions[1][1] + offsets[1], + b2 = [positions[1].bed_y, self.plot(slope_x_pp12, self.gantry_corners[1][0])] slope_y_s23 = self.linefit(b1, b2) logging.info("quad_gantry_level af: %s, bf: %s" diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index b988c7cef..b3327ba49 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -63,7 +63,7 @@ class ScrewsTiltAdjust: 'max_deviation': self.max_diff, 'results': self.results} - def probe_finalize(self, offsets, positions): + def probe_finalize(self, positions): self.results = {} self.max_diff_error = False # Factors used for CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, CW-M6 @@ -79,15 +79,15 @@ class ScrewsTiltAdjust: or (not is_clockwise_thread and self.direction == 'CCW')) min_or_max = max if use_max else min i_base, z_base = min_or_max( - enumerate([pos[2] for pos in positions]), key=lambda v: v[1]) + enumerate([pos.bed_z for pos in positions]), key=lambda v: v[1]) else: # First screw is the base position used for comparison - i_base, z_base = 0, positions[0][2] + i_base, z_base = 0, positions[0].bed_z # Provide the user some information on how to read the results self.gcode.respond_info("01:20 means 1 full turn and 20 minutes, " "CW=clockwise, CCW=counter-clockwise") for i, screw in enumerate(self.screws): - z = positions[i][2] + z = positions[i].bed_z coord, name = screw if i == i_base: # Show the results diff --git a/klippy/extras/z_tilt.py b/klippy/extras/z_tilt.py index 0316ee721..28763f1f0 100644 --- a/klippy/extras/z_tilt.py +++ b/klippy/extras/z_tilt.py @@ -143,16 +143,14 @@ class ZTilt: self.z_status.reset() self.retry_helper.start(gcmd) self.probe_helper.start_probe(gcmd) - def probe_finalize(self, offsets, positions): + def probe_finalize(self, positions): # Setup for coordinate descent analysis - z_offset = offsets[2] logging.info("Calculating bed tilt with: %s", positions) - params = { 'x_adjust': 0., 'y_adjust': 0., 'z_adjust': z_offset } + params = { 'x_adjust': 0., 'y_adjust': 0., 'z_adjust': 0. } # Perform coordinate descent def adjusted_height(pos, params): - x, y, z = pos - return (z - x*params['x_adjust'] - y*params['y_adjust'] - - params['z_adjust']) + return (pos.bed_z - pos.bed_x*params['x_adjust'] + - pos.bed_y*params['y_adjust'] - params['z_adjust']) def errorfunc(params): total_error = 0. for pos in positions: @@ -165,8 +163,7 @@ class ZTilt: logging.info("Calculated bed tilt parameters: %s", new_params) x_adjust = new_params['x_adjust'] y_adjust = new_params['y_adjust'] - z_adjust = (new_params['z_adjust'] - z_offset - - x_adjust * offsets[0] - y_adjust * offsets[1]) + z_adjust = new_params['z_adjust'] adjustments = [x*x_adjust + y*y_adjust + z_adjust for x, y in self.z_positions] self.z_helper.adjust_steppers(adjustments, speed) From 252fc18c121b7f17b6070767eeccc6d99f1b740c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 11:00:16 -0500 Subject: [PATCH 322/411] probe: Pass probe_offsets to HomingViaProbeHelper() class Signed-off-by: Kevin O'Connor --- klippy/extras/bltouch.py | 4 ++-- klippy/extras/probe.py | 7 ++++--- klippy/extras/probe_eddy_current.py | 3 ++- klippy/extras/smart_effector.py | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 2bcb9cc10..e39f70dbb 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -65,8 +65,8 @@ class BLTouchProbe: config, self, self.mcu_endstop.query_endstop) self.probe_offsets = probe.ProbeOffsetsHelper(config) self.param_helper = probe.ProbeParameterHelper(config) - self.homing_helper = probe.HomingViaProbeHelper(config, self, - self.param_helper) + self.homing_helper = probe.HomingViaProbeHelper( + config, self, self.probe_offsets, self.param_helper) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.homing_helper.start_probe_session) # Register BLTOUCH_DEBUG command diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 600d64e47..d09337223 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -198,9 +198,10 @@ class LookupZSteppers: # Homing via probe:z_virtual_endstop class HomingViaProbeHelper: - def __init__(self, config, mcu_probe, param_helper): + def __init__(self, config, mcu_probe, probe_offsets, param_helper): self.printer = config.get_printer() self.mcu_probe = mcu_probe + self.probe_offsets = probe_offsets self.param_helper = param_helper self.multi_probe_pending = False self.z_min_position = lookup_minimum_z(config) @@ -613,8 +614,8 @@ class PrinterProbe: self.mcu_probe.query_endstop) self.probe_offsets = ProbeOffsetsHelper(config) self.param_helper = ProbeParameterHelper(config) - self.homing_helper = HomingViaProbeHelper(config, self.mcu_probe, - self.param_helper) + self.homing_helper = HomingViaProbeHelper( + config, self.mcu_probe, self.probe_offsets, self.param_helper) self.probe_session = ProbeSessionHelper( config, self.param_helper, self.homing_helper.start_probe_session) def get_probe_params(self, gcmd=None): diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 876728352..3e10f2a0f 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -549,7 +549,8 @@ class PrinterEddyProbe: self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.eddy_descend.start_probe_session) mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) - probe.HomingViaProbeHelper(config, mcu_probe, self.param_helper) + probe.HomingViaProbeHelper( + config, mcu_probe, self.probe_offsets, self.param_helper) self.printer.add_object('probe', self) def add_client(self, cb): self.sensor_helper.add_client(cb) diff --git a/klippy/extras/smart_effector.py b/klippy/extras/smart_effector.py index 3caa4e6b7..87369f4f3 100644 --- a/klippy/extras/smart_effector.py +++ b/klippy/extras/smart_effector.py @@ -70,8 +70,8 @@ class SmartEffectorProbe: config, self, self.probe_wrapper.query_endstop) self.probe_offsets = probe.ProbeOffsetsHelper(config) self.param_helper = probe.ProbeParameterHelper(config) - self.homing_helper = probe.HomingViaProbeHelper(config, self, - self.param_helper) + self.homing_helper = probe.HomingViaProbeHelper( + config, self, self.probe_offsets, self.param_helper) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.homing_helper.start_probe_session) # SmartEffector control From 8c2c90b8d6aeea18bb062a919d704b944f65fb4e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 11:20:26 -0500 Subject: [PATCH 323/411] probe_eddy_current: Pass probe_offsets class to EddyDescend Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 36 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 3e10f2a0f..a8d68dbdb 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -278,11 +278,11 @@ class EddyCalibration: # Tool to gather samples and convert them to probe positions class EddyGatherSamples: - def __init__(self, printer, sensor_helper, calibration, z_offset): + def __init__(self, printer, sensor_helper, calibration, offsets): self._printer = printer self._sensor_helper = sensor_helper self._calibration = calibration - self._z_offset = z_offset + self._offsets = offsets # Results storage self._samples = [] self._probe_times = [] @@ -375,8 +375,9 @@ class EddyGatherSamples: raise self._printer.command_error( "probe_eddy_current sensor not in valid range") # Callers expect position relative to z_offset, so recalculate + z_offset = self._offsets[2] bed_deviation = toolhead_pos[2] - sensor_z - toolhead_pos[2] = self._z_offset + bed_deviation + toolhead_pos[2] = z_offset + bed_deviation results.append(toolhead_pos) del self._probe_results[:] return results @@ -390,14 +391,15 @@ class EddyGatherSamples: # Helper for implementing PROBE style commands (descend until trigger) class EddyDescend: REASON_SENSOR_ERROR = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 - def __init__(self, config, sensor_helper, calibration, param_helper): + def __init__(self, config, sensor_helper, calibration, + probe_offsets, param_helper): self._printer = config.get_printer() self._sensor_helper = sensor_helper self._mcu = sensor_helper.get_mcu() self._calibration = calibration + self._probe_offsets = probe_offsets self._param_helper = param_helper self._z_min_position = probe.lookup_minimum_z(config) - self._z_offset = config.getfloat('z_offset', minval=0.) self._dispatch = mcu.TriggerDispatch(self._mcu) self._trigger_time = 0. self._gather = None @@ -408,7 +410,8 @@ class EddyDescend: def home_start(self, print_time, sample_time, sample_count, rest_time, triggered=True): self._trigger_time = 0. - trigger_freq = self._calibration.height_to_freq(self._z_offset) + z_offset = self._probe_offsets.get_offsets()[2] + trigger_freq = self._calibration.height_to_freq(z_offset) trigger_completion = self._dispatch.start(print_time) self._sensor_helper.setup_home( print_time, trigger_freq, self._dispatch.get_oid(), @@ -433,8 +436,9 @@ class EddyDescend: return trigger_time # Probe session interface def start_probe_session(self, gcmd): + offsets = self._probe_offsets.get_offsets() self._gather = EddyGatherSamples(self._printer, self._sensor_helper, - self._calibration, self._z_offset) + self._calibration, offsets) return self def run_probe(self, gcmd): toolhead = self._printer.lookup_object('toolhead') @@ -488,17 +492,19 @@ class EddyEndstopWrapper: def probe_finish(self, hmove): pass def get_position_endstop(self): - return self._eddy_descend._z_offset + z_offset = self._eddy_descend._probe_offsets.get_offsets()[2] + return z_offset # Implementing probing with "METHOD=scan" class EddyScanningProbe: - def __init__(self, printer, sensor_helper, calibration, z_offset, gcmd): + def __init__(self, printer, sensor_helper, calibration, probe_offsets, + gcmd): self._printer = printer self._sensor_helper = sensor_helper self._calibration = calibration - self._z_offset = z_offset + offsets = probe_offsets.get_offsets() self._gather = EddyGatherSamples(printer, sensor_helper, - calibration, z_offset) + calibration, offsets) self._sample_time_delay = 0.050 self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' @@ -540,12 +546,13 @@ class PrinterEddyProbe: sensor_type = config.getchoice('sensor_type', {s: s for s in sensors}) self.sensor_helper = sensors[sensor_type](config, self.calibration) # Probe interface + self.probe_offsets = probe.ProbeOffsetsHelper(config) self.param_helper = probe.ProbeParameterHelper(config) self.eddy_descend = EddyDescend( - config, self.sensor_helper, self.calibration, self.param_helper) + config, self.sensor_helper, self.calibration, self.probe_offsets, + self.param_helper) self.cmd_helper = probe.ProbeCommandHelper(config, self, replace_z_offset=True) - self.probe_offsets = probe.ProbeOffsetsHelper(config) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.eddy_descend.start_probe_session) mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) @@ -563,9 +570,8 @@ class PrinterEddyProbe: def start_probe_session(self, gcmd): method = gcmd.get('METHOD', 'automatic').lower() if method in ('scan', 'rapid_scan'): - z_offset = self.get_offsets()[2] return EddyScanningProbe(self.printer, self.sensor_helper, - self.calibration, z_offset, gcmd) + self.calibration, self.probe_offsets, gcmd) return self.probe_session.start_probe_session(gcmd) def register_drift_compensation(self, comp): self.calibration.register_drift_compensation(comp) From f33c76da2274b745f1051de09a52f484b0bff8ed Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 11:25:56 -0500 Subject: [PATCH 324/411] load_cell_probe: Pass probe_offsets to TapSession() Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 1e08090c2..df12c9fc3 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -544,9 +544,11 @@ class TappingMove: # ProbeSession that implements Tap logic class TapSession: - def __init__(self, config, tapping_move, probe_params_helper): + def __init__(self, config, tapping_move, + probe_offsets, probe_params_helper): self._printer = config.get_printer() self._tapping_move = tapping_move + self._probe_offsets = probe_offsets self._probe_params_helper = probe_params_helper # Session state self._results = [] @@ -632,7 +634,8 @@ class LoadCellPrinterProbe: continuous_tare_filter_helper, config_helper) self._tapping_move = TappingMove(config, load_cell_probing_move, config_helper) - tap_session = TapSession(config, self._tapping_move, self._param_helper) + tap_session = TapSession(config, self._tapping_move, + self._probe_offsets, self._param_helper) self._probe_session = probe.ProbeSessionHelper(config, self._param_helper, tap_session.start_probe_session) # printer integration From 2a1027ce4104f4f10ce17a143514fc3fcad9a6ab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 22 Dec 2025 22:17:39 -0500 Subject: [PATCH 325/411] probe: Convert pull_probed_results() to return ProbeResult Change the low-level probe code to return ProbeResult tuples from probe_session.pull_probed_results(). Also update callers to use the calculated bed_xyz values found in the tuple instead of calculating them from the probe's xyz offsets. Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 7 +++ klippy/extras/axis_twist_compensation.py | 18 ++++--- klippy/extras/bed_mesh.py | 4 -- klippy/extras/load_cell_probe.py | 3 +- klippy/extras/probe.py | 65 +++++++++++++----------- klippy/extras/probe_eddy_current.py | 12 ++--- 6 files changed, 63 insertions(+), 46 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 76f455af5..384908727 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,13 @@ All dates in this document are approximate. ## Changes +20260109: The g-code console text output from the `PROBE`, +`PROBE_ACCURACY`, and similar commands has changed. Now Z heights are +reported relative to the nominal bed Z position instead of relative to +the probe's configured `z_offset`. Similarly, intermediate probe x and +y console reports will also have the probe's configured `x_offset` and +`y_offset` applied. + 20260109: The `[screws_tilt_adjust]` module now reports the status variable `{printer.screws_tilt_adjust.result.screw1.z}` with the probe's `z_offset` applied. That is, one would previously need to diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index 9f2e65d97..28f01d2d6 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -51,21 +51,27 @@ class AxisTwistCompensation: self.printer.register_event_handler("probe:update_results", self._update_z_compensation_value) - def _update_z_compensation_value(self, pos): + def _update_z_compensation_value(self, poslist): + pos = poslist[0] + zo = 0. if self.z_compensations: - pos[2] += self._get_interpolated_z_compensation( - pos[0], self.z_compensations, + zo += self._get_interpolated_z_compensation( + pos.test_x, self.z_compensations, self.compensation_start_x, self.compensation_end_x ) if self.zy_compensations: - pos[2] += self._get_interpolated_z_compensation( - pos[1], self.zy_compensations, + zo += self._get_interpolated_z_compensation( + pos.test_y, self.zy_compensations, self.compensation_start_y, self.compensation_end_y ) + pos = manual_probe.ProbeResult(pos.bed_x, pos.bed_y, pos.bed_z + zo, + pos.test_x, pos.test_y, pos.test_z) + poslist[0] = pos + def _get_interpolated_z_compensation( self, coord, z_compensations, comp_start, @@ -267,7 +273,7 @@ class Calibrater: # probe the point pos = probe.run_single_probe(self.probe, self.gcmd) - self.current_measured_z = pos[2] + self.current_measured_z = pos.bed_z # horizontal_move_z (to prevent probe trigger or hitting bed) self._move_helper((None, None, self.horizontal_move_z)) diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index f752980e0..822d43efd 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -1220,10 +1220,6 @@ class RapidScanHelper: if is_probe_pt: probe_session.run_probe(gcmd) results = probe_session.pull_probed_results() - import manual_probe # XXX - results = [manual_probe.ProbeResult( - r[0]+offsets[0], r[1]+offsets[1], r[2]-offsets[2], r[0], r[1], r[2]) - for r in results] toolhead.get_last_move_time() self.finalize_callback(results) probe_session.end_probe_session() diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index df12c9fc3..1bfe738fa 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -562,7 +562,8 @@ class TapSession: # probe until a single good sample is returned or retries are exhausted def run_probe(self, gcmd): epos, is_good = self._tapping_move.run_tap(gcmd) - self._results.append(epos) + res = self._probe_offsets.create_probe_result(epos) + self._results.append(res) def pull_probed_results(self): res = self._results diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index d09337223..fcf8cc697 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -17,11 +17,12 @@ can travel further (the Z minimum position can be negative). def calc_probe_z_average(positions, method='average'): if method != 'median': # Use mean average - count = float(len(positions)) - return [sum([pos[i] for pos in positions]) / count - for i in range(3)] + inv_count = 1. / float(len(positions)) + return manual_probe.ProbeResult( + *[sum([pos[i] for pos in positions]) * inv_count + for i in range(len(positions[0]))]) # Use median - z_sorted = sorted(positions, key=(lambda p: p[2])) + z_sorted = sorted(positions, key=(lambda p: p.bed_z)) middle = len(positions) // 2 if (len(positions) & 1) == 1: # odd number of samples @@ -52,7 +53,7 @@ class ProbeCommandHelper: gcode.register_command('PROBE', self.cmd_PROBE, desc=self.cmd_PROBE_help) # PROBE_CALIBRATE command - self.probe_calibrate_z = 0. + self.probe_calibrate_pos = None gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, desc=self.cmd_PROBE_CALIBRATE_help) # Other commands @@ -81,12 +82,15 @@ class ProbeCommandHelper: cmd_PROBE_help = "Probe Z-height at current XY position" def cmd_PROBE(self, gcmd): pos = run_single_probe(self.probe, gcmd) - gcmd.respond_info("Result is z=%.6f" % (pos[2],)) - self.last_z_result = pos[2] + gcmd.respond_info("Result: at %.3f,%.3f estimate contact at z=%.6f" + % (pos.bed_x, pos.bed_y, pos.bed_z)) + x_offset, y_offset, z_offset = self.probe.get_offsets() + self.last_z_result = pos.bed_z + z_offset # XXX def probe_calibrate_finalize(self, mpresult): if mpresult is None: return - z_offset = self.probe_calibrate_z - mpresult.bed_z + x_offset, y_offset, z_offset = self.probe.get_offsets() + z_offset += mpresult.bed_z - self.probe_calibrate_pos.bed_z gcode = self.printer.lookup_object('gcode') gcode.respond_info( "%s: z_offset: %.3f\n" @@ -99,17 +103,17 @@ class ProbeCommandHelper: manual_probe.verify_no_manual_probe(self.printer) params = self.probe.get_probe_params(gcmd) # Perform initial probe - curpos = run_single_probe(self.probe, gcmd) + pos = run_single_probe(self.probe, gcmd) # Move away from the bed - self.probe_calibrate_z = curpos[2] + curpos = self.printer.lookup_object('toolhead').get_position() curpos[2] += 5. self._move(curpos, params['lift_speed']) # Move the nozzle over the probe point - x_offset, y_offset, z_offset = self.probe.get_offsets() - curpos[0] += x_offset - curpos[1] += y_offset + curpos[0] = pos.bed_x + curpos[1] = pos.bed_y self._move(curpos, params['probe_speed']) # Start manual probe + self.probe_calibrate_pos = pos manual_probe.ManualProbeHelper(self.printer, gcmd, self.probe_calibrate_finalize) cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position" @@ -143,15 +147,15 @@ class ProbeCommandHelper: positions = probe_session.pull_probed_results() probe_session.end_probe_session() # Calculate maximum, minimum and average values - max_value = max([p[2] for p in positions]) - min_value = min([p[2] for p in positions]) + max_value = max([p.bed_z for p in positions]) + min_value = min([p.bed_z for p in positions]) range_value = max_value - min_value - avg_value = calc_probe_z_average(positions, 'average')[2] - median = calc_probe_z_average(positions, 'median')[2] + avg_value = calc_probe_z_average(positions, 'average').bed_z + median = calc_probe_z_average(positions, 'median').bed_z # calculate the standard deviation deviation_sum = 0 for i in range(len(positions)): - deviation_sum += pow(positions[i][2] - avg_value, 2.) + deviation_sum += pow(positions[i].bed_z - avg_value, 2.) sigma = (deviation_sum / len(positions)) ** 0.5 # Show information gcmd.respond_info( @@ -260,7 +264,9 @@ class HomingViaProbeHelper: pos[2] = self.z_min_position speed = self.param_helper.get_probe_params(gcmd)['probe_speed'] phoming = self.printer.lookup_object('homing') - self.results.append(phoming.probing_move(self.mcu_probe, pos, speed)) + ppos = phoming.probing_move(self.mcu_probe, pos, speed) + res = self.probe_offsets.create_probe_result(ppos) + self.results.append(res) def pull_probed_results(self): res = self.results self.results = [] @@ -368,12 +374,12 @@ class ProbeSessionHelper: reason += HINT_TIMEOUT raise self.printer.command_error(reason) # Allow axis_twist_compensation to update results - self.printer.send_event("probe:update_results", epos) + self.printer.send_event("probe:update_results", [epos]) # Report results gcode = self.printer.lookup_object('gcode') - gcode.respond_info("probe at %.3f,%.3f is z=%.6f" - % (epos[0], epos[1], epos[2])) - return epos[:3] + gcode.respond_info("probe: at %.3f,%.3f bed will contact at z=%.6f" + % (epos.bed_x, epos.bed_y, epos.bed_z)) + return epos def run_probe(self, gcmd): if self.hw_probe_session is None: self._probe_state_error() @@ -388,7 +394,7 @@ class ProbeSessionHelper: pos = self._probe(gcmd) positions.append(pos) # Check samples tolerance - z_positions = [p[2] for p in positions] + z_positions = [p.bed_z for p in positions] if max(z_positions)-min(z_positions) > params['samples_tolerance']: if retries >= params['samples_tolerance_retries']: raise gcmd.error("Probe samples exceed samples_tolerance") @@ -397,8 +403,9 @@ class ProbeSessionHelper: positions = [] # Retract if len(positions) < sample_count: + cur_z = toolhead.get_position()[2] toolhead.manual_move( - probexy + [pos[2] + params['sample_retract_dist']], + probexy + [cur_z + params['sample_retract_dist']], params['lift_speed']) # Calculate result epos = calc_probe_z_average(positions, params['samples_result']) @@ -416,6 +423,10 @@ class ProbeOffsetsHelper: self.z_offset = config.getfloat('z_offset') def get_offsets(self): return self.x_offset, self.y_offset, self.z_offset + def create_probe_result(self, test_pos): + return manual_probe.ProbeResult( + test_pos[0]+self.x_offset, test_pos[1]+self.y_offset, + test_pos[2]-self.z_offset, test_pos[0], test_pos[1], test_pos[2]) ###################################################################### @@ -503,10 +514,6 @@ class ProbePointsHelper: self._raise_tool(not probe_num) if probe_num >= len(self.probe_points): results = probe_session.pull_probed_results() - results = [manual_probe.ProbeResult( - r[0] + self.probe_offsets[0], r[1] + self.probe_offsets[1], - r[2] - self.probe_offsets[2], r[0], r[1], r[2]) - for r in results] done = self._invoke_callback(results) if done: break diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index a8d68dbdb..161b675b0 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -374,11 +374,11 @@ class EddyGatherSamples: if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: raise self._printer.command_error( "probe_eddy_current sensor not in valid range") - # Callers expect position relative to z_offset, so recalculate - z_offset = self._offsets[2] - bed_deviation = toolhead_pos[2] - sensor_z - toolhead_pos[2] = z_offset + bed_deviation - results.append(toolhead_pos) + res = manual_probe.ProbeResult( + toolhead_pos[0]+self._offsets[0], + toolhead_pos[1]+self._offsets[1], toolhead_pos[2]-sensor_z, + toolhead_pos[0], toolhead_pos[1], toolhead_pos[2]) + results.append(res) del self._probe_results[:] return results def note_probe(self, start_time, end_time, toolhead_pos): @@ -530,7 +530,7 @@ class EddyScanningProbe: results = self._gather.pull_probed() # Allow axis_twist_compensation to update results for epos in results: - self._printer.send_event("probe:update_results", epos) + self._printer.send_event("probe:update_results", [epos]) return results def end_probe_session(self): self._gather.finish() From 32a5f2b042ba32b071892b3268d06e6756b0c15d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 26 Dec 2025 21:28:41 -0500 Subject: [PATCH 326/411] probe: Deprecate last_z_result and add new last_probe_position Deprecate the PROBE command's exported value `{printer.probe.last_z_result}`. This value effectively returns the toolhead Z position when the probe triggers and user's then need to adjust the result using the probe's configured z_offset. Introduce a new `{printer.probe.last_probe_position}` as a replacement. This replacement has an easier to understand behavior - it states that the probe hardware estimates that if the toolhead is commanded to last_probe_position.x, last_probe_position.y and descends then the tip of the toolhead should first make contact at a Z height of last_probe_position.z . That is, the new exported value already takes into account the probe's configured xyz offsets. Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 5 +++++ docs/Load_Cell.md | 4 ++-- docs/Status_Reference.md | 16 ++++++++++++---- klippy/extras/probe.py | 7 ++++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 384908727..cf2d53fd9 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,11 @@ All dates in this document are approximate. ## Changes +20260109: The status value `{printer.probe.last_z_result}` is +deprecated; it will be removed in the near future. Use +`{printer.probe.last_probe_position}` instead, and note that this new +value already has the probe's configured xyz offsets applied. + 20260109: The g-code console text output from the `PROBE`, `PROBE_ACCURACY`, and similar commands has changed. Now Z heights are reported relative to the nominal bed Z position instead of relative to diff --git a/docs/Load_Cell.md b/docs/Load_Cell.md index 8345cae49..c6cf9ce49 100644 --- a/docs/Load_Cell.md +++ b/docs/Load_Cell.md @@ -252,12 +252,12 @@ macro. This requires setting up Here is a simple macro that can accomplish this. Note that the `_HOME_Z_FROM_LAST_PROBE` macro has to be separate because of the way macros work. The sub-call is needed so that the `_HOME_Z_FROM_LAST_PROBE` macro can -see the result of the probe in `printer.probe.last_z_result`. +see the result of the probe in `printer.probe.last_probe_position`. ```gcode [gcode_macro _HOME_Z_FROM_LAST_PROBE] gcode: - {% set z_probed = printer.probe.last_z_result %} + {% set z_probed = printer.probe.last_probe_position.z %} {% set z_position = printer.toolhead.position[2] %} {% set z_actual = z_position - z_probed %} SET_KINEMATIC_POSITION Z={z_actual} diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index 4e4ed5c76..1f6704acc 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -419,10 +419,18 @@ is defined): during the last QUERY_PROBE command. Note, if this is used in a macro, due to the order of template expansion, the QUERY_PROBE command must be run prior to the macro containing this reference. -- `last_z_result`: Returns the Z result value of the last PROBE - command. Note, if this is used in a macro, due to the order of - template expansion, the PROBE (or similar) command must be run prior - to the macro containing this reference. +- `last_probe_position`: The results of the last `PROBE` command. This + value is encoded as a [coordinate](#accessing-coordinates). The + probe hardware estimates that if one were to command the toolhead to + XY position `last_probe_position.x`,`last_probe_position.y` and + descend then the tip of the toolhead would first contact the bed at + a Z height of `last_probe_position.z`. These coordinates are + relative to the frame (that is, they use the coordinate system + specified in the config file). Note, if this is used in a macro, + due to the order of template expansion, the `PROBE` command must be + run prior to the macro containing this reference. +- `last_z_result`: This value is deprecated; it will be removed in the + near future. ## pwm_cycle_time diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index fcf8cc697..e6471219e 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -49,6 +49,7 @@ class ProbeCommandHelper: gcode.register_command('QUERY_PROBE', self.cmd_QUERY_PROBE, desc=self.cmd_QUERY_PROBE_help) # PROBE command + self.last_probe_position = gcode.Coord((0., 0., 0.)) self.last_z_result = 0. gcode.register_command('PROBE', self.cmd_PROBE, desc=self.cmd_PROBE_help) @@ -69,6 +70,7 @@ class ProbeCommandHelper: def get_status(self, eventtime): return {'name': self.name, 'last_query': self.last_state, + 'last_probe_position': self.last_probe_position, 'last_z_result': self.last_z_result} cmd_QUERY_PROBE_help = "Return the status of the z-probe" def cmd_QUERY_PROBE(self, gcmd): @@ -84,8 +86,11 @@ class ProbeCommandHelper: pos = run_single_probe(self.probe, gcmd) gcmd.respond_info("Result: at %.3f,%.3f estimate contact at z=%.6f" % (pos.bed_x, pos.bed_y, pos.bed_z)) + gcode = self.printer.lookup_object('gcode') + self.last_probe_position = gcode.Coord((pos.bed_x, pos.bed_y, + pos.bed_z)) x_offset, y_offset, z_offset = self.probe.get_offsets() - self.last_z_result = pos.bed_z + z_offset # XXX + self.last_z_result = pos.bed_z + z_offset # Deprecated def probe_calibrate_finalize(self, mpresult): if mpresult is None: return From a353efa5b617817988128fbd08b2e294624b0531 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 5 Jan 2026 12:27:03 -0500 Subject: [PATCH 327/411] probe: Return to start XY position on each attempt in PROBE_ACCURACY Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index e6471219e..4f87b8210 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -126,11 +126,11 @@ class ProbeCommandHelper: params = self.probe.get_probe_params(gcmd) sample_count = gcmd.get_int("SAMPLES", 10, minval=1) toolhead = self.printer.lookup_object('toolhead') - pos = toolhead.get_position() + start_pos = toolhead.get_position() gcmd.respond_info("PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f" " (samples=%d retract=%.3f" " speed=%.1f lift_speed=%.1f)\n" - % (pos[0], pos[1], pos[2], + % (start_pos[0], start_pos[1], start_pos[2], sample_count, params['sample_retract_dist'], params['probe_speed'], params['lift_speed'])) # Create dummy gcmd with SAMPLES=1 @@ -146,8 +146,8 @@ class ProbeCommandHelper: probe_session.run_probe(fo_gcmd) probe_num += 1 # Retract - pos = toolhead.get_position() - liftpos = [None, None, pos[2] + params['sample_retract_dist']] + lift_z = toolhead.get_position()[2] + params['sample_retract_dist'] + liftpos = [start_pos[0], start_pos[1], lift_z] self._move(liftpos, params['lift_speed']) positions = probe_session.pull_probed_results() probe_session.end_probe_session() From 2956c1e223f14c43da74e501bf28facff9431b70 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 22 Jan 2026 11:22:05 -0500 Subject: [PATCH 328/411] probe: Support passing gcmd to probe.get_offsets() Make it possible for the probe's get_offsets() code to depend on the parameters of the probe request. This is in preparation for eddy "tap" support. Signed-off-by: Kevin O'Connor --- klippy/extras/axis_twist_compensation.py | 10 ++++----- klippy/extras/bed_mesh.py | 4 ++-- klippy/extras/bltouch.py | 4 ++-- klippy/extras/load_cell_probe.py | 4 ++-- klippy/extras/probe.py | 26 ++++++++++++------------ klippy/extras/probe_eddy_current.py | 4 ++-- klippy/extras/smart_effector.py | 4 ++-- 7 files changed, 27 insertions(+), 29 deletions(-) diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index 28f01d2d6..081af9269 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -107,8 +107,7 @@ class Calibrater: self.gcode = self.printer.lookup_object('gcode') self.probe = None # probe settings are set to none, until they are available - self.lift_speed, self.probe_x_offset, self.probe_y_offset, _ = \ - None, None, None, None + self.lift_speed = None self.printer.register_event_handler("klippy:connect", self._handle_connect) self.speed = compensation.speed @@ -135,8 +134,6 @@ class Calibrater: raise self.printer.config_error( "AXIS_TWIST_COMPENSATION requires [probe] to be defined") self.lift_speed = self.probe.get_probe_params()['lift_speed'] - self.probe_x_offset, self.probe_y_offset, _ = \ - self.probe.get_offsets() def _register_gcode_handlers(self): # register gcode handlers @@ -154,6 +151,7 @@ class Calibrater: def cmd_AXIS_TWIST_COMPENSATION_CALIBRATE(self, gcmd): self.gcmd = gcmd + probe_x_offset, probe_y_offset, _ = self.probe.get_offsets(gcmd) sample_count = gcmd.get_int('SAMPLE_COUNT', DEFAULT_SAMPLE_COUNT) axis = gcmd.get('AXIS', 'X') @@ -225,7 +223,7 @@ class Calibrater: "Invalid axis.") probe_points = self._calculate_probe_points( - nozzle_points, self.probe_x_offset, self.probe_y_offset) + nozzle_points, probe_x_offset, probe_y_offset) # verify no other manual probe is in progress manual_probe.verify_no_manual_probe(self.printer) @@ -237,7 +235,7 @@ class Calibrater: self._calibration(probe_points, nozzle_points, interval_dist) def _calculate_probe_points(self, nozzle_points, - probe_x_offset, probe_y_offset): + probe_x_offset, probe_y_offset): # calculate the points to put the nozzle at # returned as a list of tuples probe_points = [] diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 822d43efd..9e5b206e9 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -308,7 +308,7 @@ class BedMesh: result["calibration"] = self.bmc.dump_calibration(gcmd) else: result["calibration"] = self.bmc.dump_calibration() - offsets = [0, 0, 0] if prb is None else prb.get_offsets() + offsets = [0, 0, 0] if prb is None else prb.get_offsets(gcmd) result["probe_offsets"] = offsets result["axis_minimum"] = th_sts["axis_minimum"] result["axis_maximum"] = th_sts["axis_maximum"] @@ -1209,7 +1209,7 @@ class RapidScanHelper: gcmd_params["SAMPLE_TIME"] = half_window * 2 self._raise_tool(gcmd, scan_height) probe_session = pprobe.start_probe_session(gcmd) - offsets = pprobe.get_offsets() + offsets = pprobe.get_offsets(gcmd) initial_move = True for pos, is_probe_pt in self.probe_manager.iter_rapid_path(): pos = self._apply_offsets(pos[:2], offsets) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index e39f70dbb..85fd03e9a 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -80,8 +80,8 @@ class BLTouchProbe: self.handle_connect) def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) - def get_offsets(self): - return self.probe_offsets.get_offsets() + def get_offsets(self, gcmd=None): + return self.probe_offsets.get_offsets(gcmd) def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 1bfe738fa..9e5b0475e 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -647,8 +647,8 @@ class LoadCellPrinterProbe: def get_probe_params(self, gcmd=None): return self._param_helper.get_probe_params(gcmd) - def get_offsets(self): - return self._probe_offsets.get_offsets() + def get_offsets(self, gcmd=None): + return self._probe_offsets.get_offsets(gcmd) def start_probe_session(self, gcmd): return self._probe_session.start_probe_session(gcmd) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 4f87b8210..31e5b71e3 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -54,7 +54,7 @@ class ProbeCommandHelper: gcode.register_command('PROBE', self.cmd_PROBE, desc=self.cmd_PROBE_help) # PROBE_CALIBRATE command - self.probe_calibrate_pos = None + self.probe_calibrate_info = None gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, desc=self.cmd_PROBE_CALIBRATE_help) # Other commands @@ -89,13 +89,13 @@ class ProbeCommandHelper: gcode = self.printer.lookup_object('gcode') self.last_probe_position = gcode.Coord((pos.bed_x, pos.bed_y, pos.bed_z)) - x_offset, y_offset, z_offset = self.probe.get_offsets() + x_offset, y_offset, z_offset = self.probe.get_offsets(gcmd) self.last_z_result = pos.bed_z + z_offset # Deprecated def probe_calibrate_finalize(self, mpresult): if mpresult is None: return - x_offset, y_offset, z_offset = self.probe.get_offsets() - z_offset += mpresult.bed_z - self.probe_calibrate_pos.bed_z + ppos, offsets = self.probe_calibrate_info + z_offset = offsets[2] + mpresult.bed_z - ppos.bed_z gcode = self.printer.lookup_object('gcode') gcode.respond_info( "%s: z_offset: %.3f\n" @@ -108,17 +108,17 @@ class ProbeCommandHelper: manual_probe.verify_no_manual_probe(self.printer) params = self.probe.get_probe_params(gcmd) # Perform initial probe - pos = run_single_probe(self.probe, gcmd) + ppos = run_single_probe(self.probe, gcmd) # Move away from the bed curpos = self.printer.lookup_object('toolhead').get_position() curpos[2] += 5. self._move(curpos, params['lift_speed']) # Move the nozzle over the probe point - curpos[0] = pos.bed_x - curpos[1] = pos.bed_y + curpos[0] = ppos.bed_x + curpos[1] = ppos.bed_y self._move(curpos, params['probe_speed']) # Start manual probe - self.probe_calibrate_pos = pos + self.probe_calibrate_info = (ppos, self.probe.get_offsets(gcmd)) manual_probe.ManualProbeHelper(self.printer, gcmd, self.probe_calibrate_finalize) cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position" @@ -174,7 +174,7 @@ class ProbeCommandHelper: if offset == 0: gcmd.respond_info("Nothing to do: Z Offset is 0") return - z_offset = self.probe.get_offsets()[2] + z_offset = self.probe.get_offsets(gcmd)[2] new_calibrate = z_offset - offset gcmd.respond_info( "%s: z_offset: %.3f\n" @@ -426,7 +426,7 @@ class ProbeOffsetsHelper: self.x_offset = config.getfloat('x_offset', 0.) self.y_offset = config.getfloat('y_offset', 0.) self.z_offset = config.getfloat('z_offset') - def get_offsets(self): + def get_offsets(self, gcmd=None): return self.x_offset, self.y_offset, self.z_offset def create_probe_result(self, test_pos): return manual_probe.ProbeResult( @@ -509,7 +509,7 @@ class ProbePointsHelper: return # Perform automatic probing self.lift_speed = probe.get_probe_params(gcmd)['lift_speed'] - self.probe_offsets = probe.get_offsets() + self.probe_offsets = probe.get_offsets(gcmd) if self.horizontal_move_z < self.probe_offsets[2]: raise gcmd.error("horizontal_move_z can't be less than" " probe's z_offset") @@ -632,8 +632,8 @@ class PrinterProbe: config, self.param_helper, self.homing_helper.start_probe_session) def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) - def get_offsets(self): - return self.probe_offsets.get_offsets() + def get_offsets(self, gcmd=None): + return self.probe_offsets.get_offsets(gcmd) def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 161b675b0..4d97d46d6 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -563,8 +563,8 @@ class PrinterEddyProbe: self.sensor_helper.add_client(cb) def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) - def get_offsets(self): - return self.probe_offsets.get_offsets() + def get_offsets(self, gcmd=None): + return self.probe_offsets.get_offsets(gcmd) def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): diff --git a/klippy/extras/smart_effector.py b/klippy/extras/smart_effector.py index 87369f4f3..3422dc247 100644 --- a/klippy/extras/smart_effector.py +++ b/klippy/extras/smart_effector.py @@ -90,8 +90,8 @@ class SmartEffectorProbe: desc=self.cmd_SET_SMART_EFFECTOR_help) def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) - def get_offsets(self): - return self.probe_offsets.get_offsets() + def get_offsets(self, gcmd=None): + return self.probe_offsets.get_offsets(gcmd) def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): From ec08ff5a1e95e3f8fd332362679a1395ac176d08 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 7 Jan 2026 18:37:35 -0500 Subject: [PATCH 329/411] trigger_analog: Rename load_cell_probe.c to trigger_analog.c Rename the mcu based load_cell_probe code to trigger_analog. This is a rename of the C code files, struct names, and command names. There is no change in behavior (other than naming) with this change. This is in preparation for using the load_cell_probe functionality with other sensors. Signed-off-by: Kevin O'Connor --- klippy/extras/ads1220.py | 6 +- klippy/extras/hx71x.py | 6 +- klippy/extras/load_cell_probe.py | 52 +++--- src/Kconfig | 4 +- src/Makefile | 2 +- src/load_cell_probe.c | 298 ------------------------------- src/load_cell_probe.h | 10 -- src/sensor_ads1220.c | 20 +-- src/sensor_hx71x.c | 22 +-- src/trigger_analog.c | 294 ++++++++++++++++++++++++++++++ src/trigger_analog.h | 9 + 11 files changed, 359 insertions(+), 364 deletions(-) delete mode 100644 src/load_cell_probe.c delete mode 100644 src/load_cell_probe.h create mode 100644 src/trigger_analog.c create mode 100644 src/trigger_analog.h diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index 5e9ef72ba..fda584ac3 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -110,7 +110,7 @@ class ADS1220: self.query_ads1220_cmd = self.mcu.lookup_command( "query_ads1220 oid=%c rest_ticks=%u", cq=cmdqueue) self.attach_probe_cmd = self.mcu.lookup_command( - "ads1220_attach_load_cell_probe oid=%c load_cell_probe_oid=%c") + "ads1220_attach_trigger_analog oid=%c trigger_analog_oid=%c") self.ffreader.setup_query_command("query_ads1220_status oid=%c", oid=self.oid, cq=cmdqueue) @@ -129,8 +129,8 @@ class ADS1220: def add_client(self, callback): self.batch_bulk.add_client(callback) - def attach_load_cell_probe(self, load_cell_probe_oid): - self.attach_probe_cmd.send([self.oid, load_cell_probe_oid]) + def attach_trigger_analog(self, trigger_analog_oid): + self.attach_probe_cmd.send([self.oid, trigger_analog_oid]) # Measurement decoding def _convert_samples(self, samples): diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index a7f49f8ad..60fe6047c 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -66,7 +66,7 @@ class HX71xBase: self.query_hx71x_cmd = self.mcu.lookup_command( "query_hx71x oid=%c rest_ticks=%u") self.attach_probe_cmd = self.mcu.lookup_command( - "hx71x_attach_load_cell_probe oid=%c load_cell_probe_oid=%c") + "hx71x_attach_trigger_analog oid=%c trigger_analog_oid=%c") self.ffreader.setup_query_command("query_hx71x_status oid=%c", oid=self.oid, cq=self.mcu.alloc_command_queue()) @@ -87,8 +87,8 @@ class HX71xBase: def add_client(self, callback): self.batch_bulk.add_client(callback) - def attach_load_cell_probe(self, load_cell_probe_oid): - self.attach_probe_cmd.send([self.oid, load_cell_probe_oid]) + def attach_trigger_analog(self, trigger_analog_oid): + self.attach_probe_cmd.send([self.oid, trigger_analog_oid]) # Measurement decoding def _convert_samples(self, samples): diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 9e5b0475e..9414ffc54 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -295,9 +295,9 @@ class LoadCellProbeConfigHelper: return sos_filter.to_fixed_32((1. / counts_per_gram), Q2_INT_BITS) -# McuLoadCellProbe is the interface to `load_cell_probe` on the MCU +# MCU_trigger_analog is the interface to `trigger_analog` on the MCU # This also manages the SosFilter so all commands use one command queue -class McuLoadCellProbe: +class MCU_trigger_analog: WATCHDOG_MAX = 3 ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 @@ -325,27 +325,27 @@ class McuLoadCellProbe: def _config_commands(self): self._sos_filter.create_filter() self._mcu.add_config_cmd( - "config_load_cell_probe oid=%d sos_filter_oid=%d" % ( + "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) def _build_config(self): # Lookup commands self._query_cmd = self._mcu.lookup_query_command( - "load_cell_probe_query_state oid=%c", - "load_cell_probe_state oid=%c is_homing_trigger=%c " + "trigger_analog_query_state oid=%c", + "trigger_analog_state oid=%c is_homing_trigger=%c " "trigger_ticks=%u", oid=self._oid, cq=self._cmd_queue) self._set_range_cmd = self._mcu.lookup_command( - "load_cell_probe_set_range" + "trigger_analog_set_range" " oid=%c safety_counts_min=%i safety_counts_max=%i tare_counts=%i" " trigger_grams=%u grams_per_count=%i", cq=self._cmd_queue) self._home_cmd = self._mcu.lookup_command( - "load_cell_probe_home oid=%c trsync_oid=%c trigger_reason=%c" + "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" " error_reason=%c clock=%u rest_ticks=%u timeout=%u", cq=self._cmd_queue) # the sensor data stream is connected on the MCU at the ready event def _on_connect(self): - self._sensor.attach_load_cell_probe(self._oid) + self._sensor.attach_trigger_analog(self._oid) def get_oid(self): return self._oid @@ -387,30 +387,30 @@ class McuLoadCellProbe: return self._mcu.clock_to_print_time(trigger_ticks) -# Execute probing moves using the McuLoadCellProbe +# Execute probing moves using the MCU_trigger_analog class LoadCellProbingMove: ERROR_MAP = { mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " "homing", - McuLoadCellProbe.ERROR_SAFETY_RANGE: "Load Cell Probe Error: load " - "exceeds safety limit", - McuLoadCellProbe.ERROR_OVERFLOW: "Load Cell Probe Error: fixed point " - "math overflow", - McuLoadCellProbe.ERROR_WATCHDOG: "Load Cell Probe Error: timed out " - "waiting for sensor data" + MCU_trigger_analog.ERROR_SAFETY_RANGE: "Load Cell Probe Error: load " + "exceeds safety limit", + MCU_trigger_analog.ERROR_OVERFLOW: "Load Cell Probe Error: fixed point " + "math overflow", + MCU_trigger_analog.ERROR_WATCHDOG: "Load Cell Probe Error: timed out " + "waiting for sensor data" } - def __init__(self, config, mcu_load_cell_probe, param_helper, + def __init__(self, config, mcu_trigger_analog, param_helper, continuous_tare_filter_helper, config_helper): self._printer = config.get_printer() - self._mcu_load_cell_probe = mcu_load_cell_probe + self._mcu_trigger_analog = mcu_trigger_analog self._param_helper = param_helper self._continuous_tare_filter_helper = continuous_tare_filter_helper self._config_helper = config_helper - self._mcu = mcu_load_cell_probe.get_mcu() - self._load_cell = mcu_load_cell_probe.get_load_cell() + self._mcu = mcu_trigger_analog.get_mcu() + self._load_cell = mcu_trigger_analog.get_load_cell() self._z_min_position = probe.lookup_minimum_z(config) - self._dispatch = mcu_load_cell_probe.get_dispatch() + self._dispatch = mcu_trigger_analog.get_dispatch() probe.LookupZSteppers(config, self._dispatch.add_stepper) # internal state tracking self._tare_counts = 0 @@ -436,12 +436,12 @@ class LoadCellProbingMove: tare_counts = np.average(np.array(tare_samples)[:, 2].astype(float)) # update sos_filter with any gcode parameter changes self._continuous_tare_filter_helper.update_from_command(gcmd) - self._mcu_load_cell_probe.set_endstop_range(tare_counts, gcmd) + self._mcu_trigger_analog.set_endstop_range(tare_counts, gcmd) def _home_start(self, print_time): # start trsync trigger_completion = self._dispatch.start(print_time) - self._mcu_load_cell_probe.home_start(print_time) + self._mcu_trigger_analog.home_start(print_time) return trigger_completion def home_start(self, print_time, sample_time, sample_count, rest_time, @@ -453,7 +453,7 @@ class LoadCellProbingMove: # trigger has happened, now to find out why... res = self._dispatch.stop() # clear the homing state so it stops processing samples - self._last_trigger_time = self._mcu_load_cell_probe.clear_home() + self._last_trigger_time = self._mcu_trigger_analog.clear_home() if self._mcu.is_fileoutput(): self._last_trigger_time = home_end_time if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: @@ -468,7 +468,7 @@ class LoadCellProbingMove: def get_steppers(self): return self._dispatch.get_steppers() - # Probe towards z_min until the load_cell_probe on the MCU triggers + # Probe towards z_min until the trigger_analog on the MCU triggers def probing_move(self, gcmd): # do not permit probing if the load cell is not calibrated if not self._load_cell.is_calibrated(): @@ -627,11 +627,11 @@ class LoadCellPrinterProbe: self._param_helper = probe.ProbeParameterHelper(config) self._cmd_helper = probe.ProbeCommandHelper(config, self) self._probe_offsets = probe.ProbeOffsetsHelper(config) - self._mcu_load_cell_probe = McuLoadCellProbe(config, self._load_cell, + self._mcu_trigger_analog = MCU_trigger_analog(config, self._load_cell, continuous_tare_filter_helper.get_sos_filter(), config_helper, trigger_dispatch) load_cell_probing_move = LoadCellProbingMove(config, - self._mcu_load_cell_probe, self._param_helper, + self._mcu_trigger_analog, self._param_helper, continuous_tare_filter_helper, config_helper) self._tapping_move = TappingMove(config, load_cell_probing_move, config_helper) diff --git a/src/Kconfig b/src/Kconfig index 6740946d8..8013b40f0 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -177,13 +177,13 @@ config NEED_SENSOR_BULK depends on WANT_ADXL345 || WANT_LIS2DW || WANT_MPU9250 || WANT_ICM20948 \ || WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 || WANT_SENSOR_ANGLE default y -config WANT_LOAD_CELL_PROBE +config WANT_TRIGGER_ANALOG bool depends on WANT_HX71X || WANT_ADS1220 default y config NEED_SOS_FILTER bool - depends on WANT_LOAD_CELL_PROBE + depends on WANT_TRIGGER_ANALOG default y menu "Optional features (to reduce code size)" depends on HAVE_LIMITED_CODE_SIZE diff --git a/src/Makefile b/src/Makefile index 974204bc5..dc6d4db52 100644 --- a/src/Makefile +++ b/src/Makefile @@ -28,4 +28,4 @@ src-$(CONFIG_WANT_LDC1612) += sensor_ldc1612.c src-$(CONFIG_WANT_SENSOR_ANGLE) += sensor_angle.c src-$(CONFIG_NEED_SENSOR_BULK) += sensor_bulk.c src-$(CONFIG_NEED_SOS_FILTER) += sos_filter.c -src-$(CONFIG_WANT_LOAD_CELL_PROBE) += load_cell_probe.c +src-$(CONFIG_WANT_TRIGGER_ANALOG) += trigger_analog.c diff --git a/src/load_cell_probe.c b/src/load_cell_probe.c deleted file mode 100644 index 48da77937..000000000 --- a/src/load_cell_probe.c +++ /dev/null @@ -1,298 +0,0 @@ -// Load Cell based end stops. -// -// Copyright (C) 2025 Gareth Farrington -// -// This file may be distributed under the terms of the GNU GPLv3 license. - -#include "basecmd.h" // oid_alloc -#include "command.h" // DECL_COMMAND -#include "sched.h" // shutdown -#include "trsync.h" // trsync_do_trigger -#include "board/misc.h" // timer_read_time -#include "sos_filter.h" // fixedQ12_t -#include "load_cell_probe.h" //load_cell_probe_report_sample -#include // int32_t -#include // abs - -// Q2.29 -typedef int32_t fixedQ2_t; -#define FIXEDQ2 2 -#define FIXEDQ2_FRAC_BITS ((32 - FIXEDQ2) - 1) - -// Q32.29 - a Q2.29 value stored in int64 -typedef int64_t fixedQ32_t; -#define FIXEDQ32_FRAC_BITS FIXEDQ2_FRAC_BITS - -// Q16.15 -typedef int32_t fixedQ16_t; -#define FIXEDQ16 16 -#define FIXEDQ16_FRAC_BITS ((32 - FIXEDQ16) - 1) - -// Q48.15 - a Q16.15 value stored in int64 -typedef int64_t fixedQ48_t; -#define FIXEDQ48_FRAC_BITS FIXEDQ16_FRAC_BITS - -#define MAX_TRIGGER_GRAMS ((1L << FIXEDQ16) - 1) -#define ERROR_SAFETY_RANGE 0 -#define ERROR_OVERFLOW 1 -#define ERROR_WATCHDOG 2 - -// Flags -enum {FLAG_IS_HOMING = 1 << 0 - , FLAG_IS_HOMING_TRIGGER = 1 << 1 - , FLAG_AWAIT_HOMING = 1 << 2 - }; - -// Endstop Structure -struct load_cell_probe { - struct timer time; - uint32_t trigger_grams, trigger_ticks, last_sample_ticks, rest_ticks; - uint32_t homing_start_time; - struct trsync *ts; - int32_t safety_counts_min, safety_counts_max, tare_counts; - uint8_t flags, trigger_reason, error_reason, watchdog_max - , watchdog_count; - fixedQ16_t trigger_grams_fixed; - fixedQ2_t grams_per_count; - struct sos_filter *sf; -}; - -static inline uint8_t -overflows_int32(int64_t value) { - return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; -} - -// returns the integer part of a fixedQ48_t -static inline int64_t -round_fixedQ48(const int64_t fixed_value) { - return fixed_value >> FIXEDQ48_FRAC_BITS; -} - -// Convert sensor counts to grams -static inline fixedQ48_t -counts_to_grams(struct load_cell_probe *lce, const int32_t counts) { - // tearing ensures readings are referenced to 0.0g - const int32_t delta = counts - lce->tare_counts; - // convert sensor counts to grams by multiplication: 124 * 0.051 = 6.324 - // this optimizes to single cycle SMULL instruction - const fixedQ32_t product = (int64_t)delta * (int64_t)lce->grams_per_count; - // after multiplication there are 30 fraction bits, reduce to 15 - // caller verifies this wont overflow a 32bit int when truncated - const fixedQ48_t grams = product >> - (FIXEDQ32_FRAC_BITS - FIXEDQ48_FRAC_BITS); - return grams; -} - -static inline uint8_t -is_flag_set(const uint8_t mask, struct load_cell_probe *lce) -{ - return !!(mask & lce->flags); -} - -static inline void -set_flag(uint8_t mask, struct load_cell_probe *lce) -{ - lce->flags |= mask; -} - -static inline void -clear_flag(uint8_t mask, struct load_cell_probe *lce) -{ - lce->flags &= ~mask; -} - -void -try_trigger(struct load_cell_probe *lce, uint32_t ticks) -{ - uint8_t is_homing_triggered = is_flag_set(FLAG_IS_HOMING_TRIGGER, lce); - if (!is_homing_triggered) { - // the first triggering sample when homing sets the trigger time - lce->trigger_ticks = ticks; - // this flag latches until a reset, disabling further triggering - set_flag(FLAG_IS_HOMING_TRIGGER, lce); - trsync_do_trigger(lce->ts, lce->trigger_reason); - } -} - -void -trigger_error(struct load_cell_probe *lce, uint8_t error_code) -{ - trsync_do_trigger(lce->ts, lce->error_reason + error_code); -} - -// Used by Sensors to report new raw ADC sample -void -load_cell_probe_report_sample(struct load_cell_probe *lce - , const int32_t sample) -{ - // only process samples when homing - uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, lce); - if (!is_homing) { - return; - } - - // save new sample - uint32_t ticks = timer_read_time(); - lce->last_sample_ticks = ticks; - lce->watchdog_count = 0; - - // do not trigger before homing start time - uint8_t await_homing = is_flag_set(FLAG_AWAIT_HOMING, lce); - if (await_homing && timer_is_before(ticks, lce->homing_start_time)) { - return; - } - clear_flag(FLAG_AWAIT_HOMING, lce); - - // check for safety limit violations - const uint8_t is_safety_trigger = sample <= lce->safety_counts_min - || sample >= lce->safety_counts_max; - // too much force, this is an error while homing - if (is_safety_trigger) { - trigger_error(lce, ERROR_SAFETY_RANGE); - return; - } - - // convert sample to grams - const fixedQ48_t raw_grams = counts_to_grams(lce, sample); - if (overflows_int32(raw_grams)) { - trigger_error(lce, ERROR_OVERFLOW); - return; - } - - // perform filtering - const fixedQ16_t filtered_grams = sosfilt(lce->sf, (fixedQ16_t)raw_grams); - - // update trigger state - if (abs(filtered_grams) >= lce->trigger_grams_fixed) { - try_trigger(lce, lce->last_sample_ticks); - } -} - -// Timer callback that monitors for timeouts -static uint_fast8_t -watchdog_event(struct timer *t) -{ - struct load_cell_probe *lce = container_of(t, struct load_cell_probe - , time); - uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, lce); - uint8_t is_homing_trigger = is_flag_set(FLAG_IS_HOMING_TRIGGER, lce); - // the watchdog stops when not homing or when trsync becomes triggered - if (!is_homing || is_homing_trigger) { - return SF_DONE; - } - - if (lce->watchdog_count > lce->watchdog_max) { - trigger_error(lce, ERROR_WATCHDOG); - } - lce->watchdog_count += 1; - - // A sample was recently delivered, continue monitoring - lce->time.waketime += lce->rest_ticks; - return SF_RESCHEDULE; -} - -static void -set_endstop_range(struct load_cell_probe *lce - , int32_t safety_counts_min, int32_t safety_counts_max - , int32_t tare_counts, uint32_t trigger_grams - , fixedQ2_t grams_per_count) -{ - if (!(safety_counts_max >= safety_counts_min)) { - shutdown("Safety range reversed"); - } - if (trigger_grams > MAX_TRIGGER_GRAMS) { - shutdown("trigger_grams too large"); - } - // grams_per_count must be a positive fraction in Q2 format - const fixedQ2_t one = 1L << FIXEDQ2_FRAC_BITS; - if (grams_per_count < 0 || grams_per_count >= one) { - shutdown("grams_per_count is invalid"); - } - lce->safety_counts_min = safety_counts_min; - lce->safety_counts_max = safety_counts_max; - lce->tare_counts = tare_counts; - lce->trigger_grams = trigger_grams; - lce->trigger_grams_fixed = trigger_grams << FIXEDQ16_FRAC_BITS; - lce->grams_per_count = grams_per_count; -} - -// Create a load_cell_probe -void -command_config_load_cell_probe(uint32_t *args) -{ - struct load_cell_probe *lce = oid_alloc(args[0] - , command_config_load_cell_probe, sizeof(*lce)); - lce->flags = 0; - lce->trigger_ticks = 0; - lce->watchdog_max = 0; - lce->watchdog_count = 0; - lce->sf = sos_filter_oid_lookup(args[1]); - set_endstop_range(lce, 0, 0, 0, 0, 0); -} -DECL_COMMAND(command_config_load_cell_probe, "config_load_cell_probe" - " oid=%c sos_filter_oid=%c"); - -// Lookup a load_cell_probe -struct load_cell_probe * -load_cell_probe_oid_lookup(uint8_t oid) -{ - return oid_lookup(oid, command_config_load_cell_probe); -} - -// Set the triggering range and tare value -void -command_load_cell_probe_set_range(uint32_t *args) -{ - struct load_cell_probe *lce = load_cell_probe_oid_lookup(args[0]); - set_endstop_range(lce, args[1], args[2], args[3], args[4] - , (fixedQ16_t)args[5]); -} -DECL_COMMAND(command_load_cell_probe_set_range, "load_cell_probe_set_range" - " oid=%c safety_counts_min=%i safety_counts_max=%i tare_counts=%i" - " trigger_grams=%u grams_per_count=%i"); - -// Home an axis -void -command_load_cell_probe_home(uint32_t *args) -{ - struct load_cell_probe *lce = load_cell_probe_oid_lookup(args[0]); - sched_del_timer(&lce->time); - // clear the homing trigger flag - clear_flag(FLAG_IS_HOMING_TRIGGER, lce); - clear_flag(FLAG_IS_HOMING, lce); - lce->trigger_ticks = 0; - lce->ts = NULL; - // 0 samples indicates homing is finished - if (args[3] == 0) { - // Disable end stop checking - return; - } - lce->ts = trsync_oid_lookup(args[1]); - lce->trigger_reason = args[2]; - lce->error_reason = args[3]; - lce->time.waketime = args[4]; - lce->homing_start_time = args[4]; - lce->rest_ticks = args[5]; - lce->watchdog_max = args[6]; - lce->watchdog_count = 0; - lce->time.func = watchdog_event; - set_flag(FLAG_IS_HOMING, lce); - set_flag(FLAG_AWAIT_HOMING, lce); - sched_add_timer(&lce->time); -} -DECL_COMMAND(command_load_cell_probe_home, - "load_cell_probe_home oid=%c trsync_oid=%c trigger_reason=%c" - " error_reason=%c clock=%u rest_ticks=%u timeout=%u"); - -void -command_load_cell_probe_query_state(uint32_t *args) -{ - uint8_t oid = args[0]; - struct load_cell_probe *lce = load_cell_probe_oid_lookup(args[0]); - sendf("load_cell_probe_state oid=%c is_homing_trigger=%c trigger_ticks=%u" - , oid - , is_flag_set(FLAG_IS_HOMING_TRIGGER, lce) - , lce->trigger_ticks); -} -DECL_COMMAND(command_load_cell_probe_query_state - , "load_cell_probe_query_state oid=%c"); diff --git a/src/load_cell_probe.h b/src/load_cell_probe.h deleted file mode 100644 index e67c16e55..000000000 --- a/src/load_cell_probe.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef __LOAD_CELL_PROBE_H -#define __LOAD_CELL_PROBE_H - -#include // uint8_t - -struct load_cell_probe *load_cell_probe_oid_lookup(uint8_t oid); -void load_cell_probe_report_sample(struct load_cell_probe *lce - , int32_t sample); - -#endif // load_cell_probe.h diff --git a/src/sensor_ads1220.c b/src/sensor_ads1220.c index f51dc355a..93d52b6ae 100644 --- a/src/sensor_ads1220.c +++ b/src/sensor_ads1220.c @@ -4,16 +4,16 @@ // // This file may be distributed under the terms of the GNU GPLv3 license. +#include +#include "basecmd.h" // oid_alloc #include "board/irq.h" // irq_disable #include "board/gpio.h" // gpio_out_write #include "board/misc.h" // timer_read_time -#include "basecmd.h" // oid_alloc #include "command.h" // DECL_COMMAND #include "sched.h" // sched_add_timer #include "sensor_bulk.h" // sensor_bulk_report -#include "load_cell_probe.h" // load_cell_probe_report_sample #include "spicmds.h" // spidev_transfer -#include +#include "trigger_analog.h" // trigger_analog_update struct ads1220_adc { struct timer timer; @@ -22,7 +22,7 @@ struct ads1220_adc { struct spidev_s *spi; uint8_t pending_flag, data_count; struct sensor_bulk sb; - struct load_cell_probe *lce; + struct trigger_analog *ta; }; // Flag types @@ -97,8 +97,8 @@ ads1220_read_adc(struct ads1220_adc *ads1220, uint8_t oid) counts |= 0xFF000000; // endstop is optional, report if enabled and no errors - if (ads1220->lce) { - load_cell_probe_report_sample(ads1220->lce, counts); + if (ads1220->ta) { + trigger_analog_update(ads1220->ta, counts); } add_sample(ads1220, oid, counts); @@ -119,13 +119,13 @@ DECL_COMMAND(command_config_ads1220, "config_ads1220 oid=%c" " spi_oid=%c data_ready_pin=%u"); void -ads1220_attach_load_cell_probe(uint32_t *args) { +ads1220_attach_trigger_analog(uint32_t *args) { uint8_t oid = args[0]; struct ads1220_adc *ads1220 = oid_lookup(oid, command_config_ads1220); - ads1220->lce = load_cell_probe_oid_lookup(args[1]); + ads1220->ta = trigger_analog_oid_lookup(args[1]); } -DECL_COMMAND(ads1220_attach_load_cell_probe, - "ads1220_attach_load_cell_probe oid=%c load_cell_probe_oid=%c"); +DECL_COMMAND(ads1220_attach_trigger_analog, + "ads1220_attach_trigger_analog oid=%c trigger_analog_oid=%c"); // start/stop capturing ADC data void diff --git a/src/sensor_hx71x.c b/src/sensor_hx71x.c index 74575eec1..2c09e97af 100644 --- a/src/sensor_hx71x.c +++ b/src/sensor_hx71x.c @@ -4,17 +4,17 @@ // // This file may be distributed under the terms of the GNU GPLv3 license. +#include +#include #include "autoconf.h" // CONFIG_MACH_AVR +#include "basecmd.h" // oid_alloc #include "board/gpio.h" // gpio_out_write #include "board/irq.h" // irq_poll #include "board/misc.h" // timer_read_time -#include "basecmd.h" // oid_alloc #include "command.h" // DECL_COMMAND #include "sched.h" // sched_add_timer #include "sensor_bulk.h" // sensor_bulk_report -#include "load_cell_probe.h" // load_cell_probe_report_sample -#include -#include +#include "trigger_analog.h" // trigger_analog_update struct hx71x_adc { struct timer timer; @@ -25,7 +25,7 @@ struct hx71x_adc { struct gpio_in dout; // pin used to receive data from the hx71x struct gpio_out sclk; // pin used to generate clock for the hx71x struct sensor_bulk sb; - struct load_cell_probe *lce; + struct trigger_analog *ta; }; enum { @@ -178,8 +178,8 @@ hx71x_read_adc(struct hx71x_adc *hx71x, uint8_t oid) } // probe is optional, report if enabled - if (hx71x->last_error == 0 && hx71x->lce) { - load_cell_probe_report_sample(hx71x->lce, counts); + if (hx71x->last_error == 0 && hx71x->ta) { + trigger_analog_update(hx71x->ta, counts); } // Add measurement to buffer @@ -206,13 +206,13 @@ DECL_COMMAND(command_config_hx71x, "config_hx71x oid=%c gain_channel=%c" " dout_pin=%u sclk_pin=%u"); void -hx71x_attach_load_cell_probe(uint32_t *args) { +hx71x_attach_trigger_analog(uint32_t *args) { uint8_t oid = args[0]; struct hx71x_adc *hx71x = oid_lookup(oid, command_config_hx71x); - hx71x->lce = load_cell_probe_oid_lookup(args[1]); + hx71x->ta = trigger_analog_oid_lookup(args[1]); } -DECL_COMMAND(hx71x_attach_load_cell_probe, "hx71x_attach_load_cell_probe oid=%c" - " load_cell_probe_oid=%c"); +DECL_COMMAND(hx71x_attach_trigger_analog, "hx71x_attach_trigger_analog oid=%c" + " trigger_analog_oid=%c"); // start/stop capturing ADC data void diff --git a/src/trigger_analog.c b/src/trigger_analog.c new file mode 100644 index 000000000..9c0220e19 --- /dev/null +++ b/src/trigger_analog.c @@ -0,0 +1,294 @@ +// Support homing/probing "trigger" notification from analog sensors +// +// Copyright (C) 2025 Gareth Farrington +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include // abs +#include "basecmd.h" // oid_alloc +#include "board/misc.h" // timer_read_time +#include "command.h" // DECL_COMMAND +#include "sched.h" // shutdown +#include "sos_filter.h" // fixedQ12_t +#include "trigger_analog.h" // trigger_analog_update +#include "trsync.h" // trsync_do_trigger + +// Q2.29 +typedef int32_t fixedQ2_t; +#define FIXEDQ2 2 +#define FIXEDQ2_FRAC_BITS ((32 - FIXEDQ2) - 1) + +// Q32.29 - a Q2.29 value stored in int64 +typedef int64_t fixedQ32_t; +#define FIXEDQ32_FRAC_BITS FIXEDQ2_FRAC_BITS + +// Q16.15 +typedef int32_t fixedQ16_t; +#define FIXEDQ16 16 +#define FIXEDQ16_FRAC_BITS ((32 - FIXEDQ16) - 1) + +// Q48.15 - a Q16.15 value stored in int64 +typedef int64_t fixedQ48_t; +#define FIXEDQ48_FRAC_BITS FIXEDQ16_FRAC_BITS + +#define MAX_TRIGGER_GRAMS ((1L << FIXEDQ16) - 1) +#define ERROR_SAFETY_RANGE 0 +#define ERROR_OVERFLOW 1 +#define ERROR_WATCHDOG 2 + +// Flags +enum {FLAG_IS_HOMING = 1 << 0 + , FLAG_IS_HOMING_TRIGGER = 1 << 1 + , FLAG_AWAIT_HOMING = 1 << 2 +}; + +// Endstop Structure +struct trigger_analog { + struct timer time; + uint32_t trigger_grams, trigger_ticks, last_sample_ticks, rest_ticks; + uint32_t homing_start_time; + struct trsync *ts; + int32_t safety_counts_min, safety_counts_max, tare_counts; + uint8_t flags, trigger_reason, error_reason, watchdog_max, watchdog_count; + fixedQ16_t trigger_grams_fixed; + fixedQ2_t grams_per_count; + struct sos_filter *sf; +}; + +static inline uint8_t +overflows_int32(int64_t value) { + return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; +} + +// returns the integer part of a fixedQ48_t +static inline int64_t +round_fixedQ48(const int64_t fixed_value) { + return fixed_value >> FIXEDQ48_FRAC_BITS; +} + +// Convert sensor counts to grams +static inline fixedQ48_t +counts_to_grams(struct trigger_analog *ta, const int32_t counts) { + // tearing ensures readings are referenced to 0.0g + const int32_t delta = counts - ta->tare_counts; + // convert sensor counts to grams by multiplication: 124 * 0.051 = 6.324 + // this optimizes to single cycle SMULL instruction + const fixedQ32_t product = (int64_t)delta * (int64_t)ta->grams_per_count; + // after multiplication there are 30 fraction bits, reduce to 15 + // caller verifies this wont overflow a 32bit int when truncated + const fixedQ48_t grams = product >> + (FIXEDQ32_FRAC_BITS - FIXEDQ48_FRAC_BITS); + return grams; +} + +static inline uint8_t +is_flag_set(const uint8_t mask, struct trigger_analog *ta) +{ + return !!(mask & ta->flags); +} + +static inline void +set_flag(uint8_t mask, struct trigger_analog *ta) +{ + ta->flags |= mask; +} + +static inline void +clear_flag(uint8_t mask, struct trigger_analog *ta) +{ + ta->flags &= ~mask; +} + +void +try_trigger(struct trigger_analog *ta, uint32_t ticks) +{ + uint8_t is_homing_triggered = is_flag_set(FLAG_IS_HOMING_TRIGGER, ta); + if (!is_homing_triggered) { + // the first triggering sample when homing sets the trigger time + ta->trigger_ticks = ticks; + // this flag latches until a reset, disabling further triggering + set_flag(FLAG_IS_HOMING_TRIGGER, ta); + trsync_do_trigger(ta->ts, ta->trigger_reason); + } +} + +void +trigger_error(struct trigger_analog *ta, uint8_t error_code) +{ + trsync_do_trigger(ta->ts, ta->error_reason + error_code); +} + +// Used by Sensors to report new raw ADC sample +void +trigger_analog_update(struct trigger_analog *ta, const int32_t sample) +{ + // only process samples when homing + uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); + if (!is_homing) { + return; + } + + // save new sample + uint32_t ticks = timer_read_time(); + ta->last_sample_ticks = ticks; + ta->watchdog_count = 0; + + // do not trigger before homing start time + uint8_t await_homing = is_flag_set(FLAG_AWAIT_HOMING, ta); + if (await_homing && timer_is_before(ticks, ta->homing_start_time)) { + return; + } + clear_flag(FLAG_AWAIT_HOMING, ta); + + // check for safety limit violations + const uint8_t is_safety_trigger = sample <= ta->safety_counts_min + || sample >= ta->safety_counts_max; + // too much force, this is an error while homing + if (is_safety_trigger) { + trigger_error(ta, ERROR_SAFETY_RANGE); + return; + } + + // convert sample to grams + const fixedQ48_t raw_grams = counts_to_grams(ta, sample); + if (overflows_int32(raw_grams)) { + trigger_error(ta, ERROR_OVERFLOW); + return; + } + + // perform filtering + const fixedQ16_t filtered_grams = sosfilt(ta->sf, (fixedQ16_t)raw_grams); + + // update trigger state + if (abs(filtered_grams) >= ta->trigger_grams_fixed) { + try_trigger(ta, ta->last_sample_ticks); + } +} + +// Timer callback that monitors for timeouts +static uint_fast8_t +watchdog_event(struct timer *t) +{ + struct trigger_analog *ta = container_of(t, struct trigger_analog, time); + uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); + uint8_t is_homing_trigger = is_flag_set(FLAG_IS_HOMING_TRIGGER, ta); + // the watchdog stops when not homing or when trsync becomes triggered + if (!is_homing || is_homing_trigger) { + return SF_DONE; + } + + if (ta->watchdog_count > ta->watchdog_max) { + trigger_error(ta, ERROR_WATCHDOG); + } + ta->watchdog_count += 1; + + // A sample was recently delivered, continue monitoring + ta->time.waketime += ta->rest_ticks; + return SF_RESCHEDULE; +} + +static void +set_endstop_range(struct trigger_analog *ta + , int32_t safety_counts_min, int32_t safety_counts_max + , int32_t tare_counts, uint32_t trigger_grams + , fixedQ2_t grams_per_count) +{ + if (!(safety_counts_max >= safety_counts_min)) { + shutdown("Safety range reversed"); + } + if (trigger_grams > MAX_TRIGGER_GRAMS) { + shutdown("trigger_grams too large"); + } + // grams_per_count must be a positive fraction in Q2 format + const fixedQ2_t one = 1L << FIXEDQ2_FRAC_BITS; + if (grams_per_count < 0 || grams_per_count >= one) { + shutdown("grams_per_count is invalid"); + } + ta->safety_counts_min = safety_counts_min; + ta->safety_counts_max = safety_counts_max; + ta->tare_counts = tare_counts; + ta->trigger_grams = trigger_grams; + ta->trigger_grams_fixed = trigger_grams << FIXEDQ16_FRAC_BITS; + ta->grams_per_count = grams_per_count; +} + +// Create a trigger_analog +void +command_config_trigger_analog(uint32_t *args) +{ + struct trigger_analog *ta = oid_alloc( + args[0], command_config_trigger_analog, sizeof(*ta)); + ta->flags = 0; + ta->trigger_ticks = 0; + ta->watchdog_max = 0; + ta->watchdog_count = 0; + ta->sf = sos_filter_oid_lookup(args[1]); + set_endstop_range(ta, 0, 0, 0, 0, 0); +} +DECL_COMMAND(command_config_trigger_analog + , "config_trigger_analog oid=%c sos_filter_oid=%c"); + +// Lookup a trigger_analog +struct trigger_analog * +trigger_analog_oid_lookup(uint8_t oid) +{ + return oid_lookup(oid, command_config_trigger_analog); +} + +// Set the triggering range and tare value +void +command_trigger_analog_set_range(uint32_t *args) +{ + struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); + set_endstop_range(ta, args[1], args[2], args[3], args[4] + , (fixedQ16_t)args[5]); +} +DECL_COMMAND(command_trigger_analog_set_range, "trigger_analog_set_range" + " oid=%c safety_counts_min=%i safety_counts_max=%i tare_counts=%i" + " trigger_grams=%u grams_per_count=%i"); + +// Home an axis +void +command_trigger_analog_home(uint32_t *args) +{ + struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); + sched_del_timer(&ta->time); + // clear the homing trigger flag + clear_flag(FLAG_IS_HOMING_TRIGGER, ta); + clear_flag(FLAG_IS_HOMING, ta); + ta->trigger_ticks = 0; + ta->ts = NULL; + // 0 samples indicates homing is finished + if (args[3] == 0) { + // Disable end stop checking + return; + } + ta->ts = trsync_oid_lookup(args[1]); + ta->trigger_reason = args[2]; + ta->error_reason = args[3]; + ta->time.waketime = args[4]; + ta->homing_start_time = args[4]; + ta->rest_ticks = args[5]; + ta->watchdog_max = args[6]; + ta->watchdog_count = 0; + ta->time.func = watchdog_event; + set_flag(FLAG_IS_HOMING, ta); + set_flag(FLAG_AWAIT_HOMING, ta); + sched_add_timer(&ta->time); +} +DECL_COMMAND(command_trigger_analog_home, + "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" + " error_reason=%c clock=%u rest_ticks=%u timeout=%u"); + +void +command_trigger_analog_query_state(uint32_t *args) +{ + uint8_t oid = args[0]; + struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); + sendf("trigger_analog_state oid=%c is_homing_trigger=%c trigger_ticks=%u" + , oid + , is_flag_set(FLAG_IS_HOMING_TRIGGER, ta) + , ta->trigger_ticks); +} +DECL_COMMAND(command_trigger_analog_query_state + , "trigger_analog_query_state oid=%c"); diff --git a/src/trigger_analog.h b/src/trigger_analog.h new file mode 100644 index 000000000..9867095e8 --- /dev/null +++ b/src/trigger_analog.h @@ -0,0 +1,9 @@ +#ifndef __TRIGGER_ANALOG_H +#define __TRIGGER_ANALOG_H + +#include // uint8_t + +struct trigger_analog *trigger_analog_oid_lookup(uint8_t oid); +void trigger_analog_update(struct trigger_analog *ta, int32_t sample); + +#endif // trigger_analog.h From 667c57e444fceacfa3381e0a4558efab6d86d9ce Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 13:33:04 -0500 Subject: [PATCH 330/411] sos_filter: Remove unnecessary is_active flag It's reasonable to deactivate the filter by setting n_sections=0, and this makes the code a little easier to use when the host doesn't actually need any filtering. Signed-off-by: Kevin O'Connor --- src/sos_filter.c | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/sos_filter.c b/src/sos_filter.c index 6e64bc0a2..2ed2b85a2 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -23,7 +23,7 @@ struct sos_filter_section { }; struct sos_filter { - uint8_t max_sections, n_sections, coeff_frac_bits, is_active; + uint8_t max_sections, n_sections, coeff_frac_bits; uint32_t coeff_rounding; // filter composed of second order sections struct sos_filter_section filter[0]; @@ -56,15 +56,6 @@ fixed_mul(struct sos_filter *sf, const fixedQ_coeff_t coeff // returns the fixedQ_value_t filtered value int32_t sosfilt(struct sos_filter *sf, const int32_t unfiltered_value) { - if (!sf->is_active) { - shutdown("sos_filter not property initialized"); - } - - // an empty filter performs no filtering - if (sf->n_sections == 0) { - return unfiltered_value; - } - fixedQ_value_t cur_val = unfiltered_value; // foreach section for (int section_idx = 0; section_idx < sf->n_sections; section_idx++) { @@ -92,7 +83,6 @@ command_config_sos_filter(uint32_t *args) struct sos_filter *sf = oid_alloc(args[0] , command_config_sos_filter, size); sf->max_sections = max_sections; - sf->is_active = 0; } DECL_COMMAND(command_config_sos_filter, "config_sos_filter oid=%c" " max_sections=%c"); @@ -118,7 +108,7 @@ command_sos_filter_set_section(uint32_t *args) { struct sos_filter *sf = sos_filter_oid_lookup(args[0]); // setting a section marks the filter as inactive - sf->is_active = 0; + sf->n_sections = 0; uint8_t section_idx = args[1]; validate_section_index(sf, section_idx); // copy section data @@ -137,7 +127,7 @@ command_sos_filter_set_state(uint32_t *args) { struct sos_filter *sf = sos_filter_oid_lookup(args[0]); // setting a section's state marks the filter as inactive - sf->is_active = 0; + sf->n_sections = 0; // copy state data uint8_t section_idx = args[1]; validate_section_index(sf, section_idx); @@ -160,8 +150,6 @@ command_sos_filter_activate(uint32_t *args) const uint8_t coeff_int_bits = args[2]; sf->coeff_frac_bits = (31 - coeff_int_bits); sf->coeff_rounding = (1 << (sf->coeff_frac_bits - 1)); - // mark filter as ready to use - sf->is_active = 1; } DECL_COMMAND(command_sos_filter_activate , "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c"); From fd195ff4ce0646403846efb1890541cec4d87d37 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 13:35:13 -0500 Subject: [PATCH 331/411] sos_filter: Remove unnecessary "const" declarations A "const" declaration on a local integer does not do anything in C code. Signed-off-by: Kevin O'Connor --- src/sos_filter.c | 16 +++++++++------- src/sos_filter.h | 3 +-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/sos_filter.c b/src/sos_filter.c index 2ed2b85a2..96281e282 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -30,14 +30,15 @@ struct sos_filter { }; static inline uint8_t -overflows_int32(int64_t value) { +overflows_int32(int64_t value) +{ return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; } // Multiply a coefficient in fixedQ_coeff_t by a value fixedQ_value_t static inline fixedQ_value_t -fixed_mul(struct sos_filter *sf, const fixedQ_coeff_t coeff - , const fixedQ_value_t value) { +fixed_mul(struct sos_filter *sf, fixedQ_coeff_t coeff, fixedQ_value_t value) +{ // This optimizes to single cycle SMULL on Arm Coretex M0+ int64_t product = (int64_t)coeff * (int64_t)value; // round up at the last bit to be shifted away @@ -55,7 +56,8 @@ fixed_mul(struct sos_filter *sf, const fixedQ_coeff_t coeff // Apply the sosfilt algorithm to a new datapoint // returns the fixedQ_value_t filtered value int32_t -sosfilt(struct sos_filter *sf, const int32_t unfiltered_value) { +sosfilt(struct sos_filter *sf, int32_t unfiltered_value) +{ fixedQ_value_t cur_val = unfiltered_value; // foreach section for (int section_idx = 0; section_idx < sf->n_sections; section_idx++) { @@ -112,7 +114,7 @@ command_sos_filter_set_section(uint32_t *args) uint8_t section_idx = args[1]; validate_section_index(sf, section_idx); // copy section data - const uint8_t arg_base = 2; + uint8_t arg_base = 2; for (uint8_t i = 0; i < SECTION_WIDTH; i++) { sf->filter[section_idx].coeff[i] = args[i + arg_base]; } @@ -131,7 +133,7 @@ command_sos_filter_set_state(uint32_t *args) // copy state data uint8_t section_idx = args[1]; validate_section_index(sf, section_idx); - const uint8_t arg_base = 2; + uint8_t arg_base = 2; sf->filter[section_idx].state[0] = args[0 + arg_base]; sf->filter[section_idx].state[1] = args[1 + arg_base]; } @@ -147,7 +149,7 @@ command_sos_filter_activate(uint32_t *args) if (n_sections > sf->max_sections) shutdown("Filter section index larger than max_sections"); sf->n_sections = n_sections; - const uint8_t coeff_int_bits = args[2]; + uint8_t coeff_int_bits = args[2]; sf->coeff_frac_bits = (31 - coeff_int_bits); sf->coeff_rounding = (1 << (sf->coeff_frac_bits - 1)); } diff --git a/src/sos_filter.h b/src/sos_filter.h index 6c215dda3..f235cb427 100644 --- a/src/sos_filter.h +++ b/src/sos_filter.h @@ -5,8 +5,7 @@ struct sos_filter; -int32_t sosfilt(struct sos_filter *sf - , const int32_t unfiltered_value); +int32_t sosfilt(struct sos_filter *sf, int32_t unfiltered_value); struct sos_filter *sos_filter_oid_lookup(uint8_t oid); #endif // sos_filter.h From 40242b2e3394dbc5964a13ed1e0b9798c327e65e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 16:29:45 -0500 Subject: [PATCH 332/411] sos_filter: Propagate overflow errors instead of a shutdown Pass an overflow error back to the caller instead of invoking a shutdown(). Signed-off-by: Kevin O'Connor --- src/sos_filter.c | 52 +++++++++++++++++++++++--------------------- src/sos_filter.h | 3 +-- src/trigger_analog.c | 9 ++++++-- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/sos_filter.c b/src/sos_filter.c index 96281e282..036c50ce5 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -9,17 +9,14 @@ #include "sched.h" // shutdown #include "sos_filter.h" // sos_filter -typedef int32_t fixedQ_coeff_t; -typedef int32_t fixedQ_value_t; - // filter strucutre sizes #define SECTION_WIDTH 5 #define STATE_WIDTH 2 struct sos_filter_section { // filter composed of second order sections - fixedQ_coeff_t coeff[SECTION_WIDTH]; // aka sos - fixedQ_value_t state[STATE_WIDTH]; // aka zi + int32_t coeff[SECTION_WIDTH]; // aka sos + int32_t state[STATE_WIDTH]; // aka zi }; struct sos_filter { @@ -29,15 +26,15 @@ struct sos_filter { struct sos_filter_section filter[0]; }; -static inline uint8_t +static inline int overflows_int32(int64_t value) { return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; } -// Multiply a coefficient in fixedQ_coeff_t by a value fixedQ_value_t -static inline fixedQ_value_t -fixed_mul(struct sos_filter *sf, fixedQ_coeff_t coeff, fixedQ_value_t value) +// Multiply a coeff*value and shift result by coeff_frac_bits +static inline int +fixed_mul(struct sos_filter *sf, int32_t coeff, int32_t value, int32_t *res) { // This optimizes to single cycle SMULL on Arm Coretex M0+ int64_t product = (int64_t)coeff * (int64_t)value; @@ -45,35 +42,40 @@ fixed_mul(struct sos_filter *sf, fixedQ_coeff_t coeff, fixedQ_value_t value) product += sf->coeff_rounding; // shift the decimal right to discard the coefficient fractional bits int64_t result = product >> sf->coeff_frac_bits; - // check for overflow of int32_t - if (overflows_int32(result)) { - shutdown("fixed_mul: overflow"); - } // truncate significant 32 bits - return (fixedQ_value_t)result; + *res = (int32_t)result; + // check for overflow of int32_t + if (overflows_int32(result)) + return -1; + return 0; } // Apply the sosfilt algorithm to a new datapoint -// returns the fixedQ_value_t filtered value -int32_t -sosfilt(struct sos_filter *sf, int32_t unfiltered_value) +int +sos_filter_apply(struct sos_filter *sf, int32_t *pvalue) { - fixedQ_value_t cur_val = unfiltered_value; + int32_t cur_val = *pvalue; // foreach section for (int section_idx = 0; section_idx < sf->n_sections; section_idx++) { struct sos_filter_section *section = &(sf->filter[section_idx]); // apply the section's filter coefficients to input - fixedQ_value_t next_val = fixed_mul(sf, section->coeff[0], cur_val); + int32_t next_val, c1_cur, c2_cur, c3_next, c4_next; + int ret = fixed_mul(sf, section->coeff[0], cur_val, &next_val); next_val += section->state[0]; - section->state[0] = fixed_mul(sf, section->coeff[1], cur_val) - - fixed_mul(sf, section->coeff[3], next_val) - + (section->state[1]); - section->state[1] = fixed_mul(sf, section->coeff[2], cur_val) - - fixed_mul(sf, section->coeff[4], next_val); + ret |= fixed_mul(sf, section->coeff[1], cur_val, &c1_cur); + ret |= fixed_mul(sf, section->coeff[3], next_val, &c3_next); + ret |= fixed_mul(sf, section->coeff[2], cur_val, &c2_cur); + ret |= fixed_mul(sf, section->coeff[4], next_val, &c4_next); + if (ret) + // Overflow + return -1; + section->state[0] = c1_cur - c3_next + section->state[1]; + section->state[1] = c2_cur - c4_next; cur_val = next_val; } - return (int32_t)cur_val; + *pvalue = cur_val; + return 0; } // Create an sos_filter diff --git a/src/sos_filter.h b/src/sos_filter.h index f235cb427..b697a2686 100644 --- a/src/sos_filter.h +++ b/src/sos_filter.h @@ -4,8 +4,7 @@ #include struct sos_filter; - -int32_t sosfilt(struct sos_filter *sf, int32_t unfiltered_value); +int sos_filter_apply(struct sos_filter *sf, int32_t *pvalue); struct sos_filter *sos_filter_oid_lookup(uint8_t oid); #endif // sos_filter.h diff --git a/src/trigger_analog.c b/src/trigger_analog.c index 9c0220e19..4a8419d3b 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -9,7 +9,7 @@ #include "board/misc.h" // timer_read_time #include "command.h" // DECL_COMMAND #include "sched.h" // shutdown -#include "sos_filter.h" // fixedQ12_t +#include "sos_filter.h" // sos_filter_apply #include "trigger_analog.h" // trigger_analog_update #include "trsync.h" // trsync_do_trigger @@ -157,7 +157,12 @@ trigger_analog_update(struct trigger_analog *ta, const int32_t sample) } // perform filtering - const fixedQ16_t filtered_grams = sosfilt(ta->sf, (fixedQ16_t)raw_grams); + int32_t filtered_grams = raw_grams; + int ret = sos_filter_apply(ta->sf, &filtered_grams); + if (ret) { + trigger_error(ta, ERROR_OVERFLOW); + return; + } // update trigger state if (abs(filtered_grams) >= ta->trigger_grams_fixed) { From 64133997842991287e510c0a4d04e52465580618 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 18:30:09 -0500 Subject: [PATCH 333/411] sos_filter: No need to support "is_init" in MCU_SosFilter Rename the SosFilter class to MCU_SosFilter. Automatically reload the filter coefficients on a reset_filter() call, so there is no need to support loading of the filter at init time. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 5 +- klippy/extras/sos_filter.py | 101 +++++++++++-------------------- 2 files changed, 36 insertions(+), 70 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 9414ffc54..0ee7b77ea 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -215,8 +215,8 @@ class ContinuousTareFilterHelper: buzz_delay, notches, notch_quality) def _create_filter(self, fixed_filter, cmd_queue): - return sos_filter.SosFilter(self._sensor.get_mcu(), cmd_queue, - fixed_filter, 4) + return sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, + fixed_filter, 4) def update_from_command(self, gcmd, cq=None): gcmd_filter = self._build_filter(gcmd) @@ -323,7 +323,6 @@ class MCU_trigger_analog: self._printer.register_event_handler("klippy:connect", self._on_connect) def _config_commands(self): - self._sos_filter.create_filter() self._mcu.add_config_cmd( "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index cbd6c51c9..dd9725d06 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -141,7 +141,7 @@ class FixedPointSosFilter: # Control an `sos_filter` object on the MCU -class SosFilter: +class MCU_SosFilter: # fixed_point_filter should be an FixedPointSosFilter instance. A filter of # size 0 will create a passthrough filter. # max_sections should be the largest number of sections you expect @@ -154,81 +154,48 @@ class SosFilter: self._max_sections = max_sections if self._max_sections is None: self._max_sections = self._filter.get_num_sections() - self._cmd_set_section = [ - "sos_filter_set_section oid=%d section_idx=%d" - " sos0=%i sos1=%i sos2=%i sos3=%i sos4=%i", - "sos_filter_set_section oid=%c section_idx=%c" - " sos0=%i sos1=%i sos2=%i sos3=%i sos4=%i"] - self._cmd_config_state = [ - "sos_filter_set_state oid=%d section_idx=%d state0=%i state1=%i", - "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i"] - self._cmd_activate = [ - "sos_filter_set_active oid=%d n_sections=%d coeff_int_bits=%d", - "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c"] + self._set_section_cmd = self._set_state_cmd = self._set_active_cmd =None + self._last_sent_coeffs = [None] * self._max_sections + self._mcu.add_config_cmd("config_sos_filter oid=%d max_sections=%d" + % (self._oid, self._max_sections)) self._mcu.register_config_callback(self._build_config) def _build_config(self): - cmds = [self._cmd_set_section, self._cmd_config_state, - self._cmd_activate] - for cmd in cmds: - cmd.append(self._mcu.lookup_command(cmd[1], cq=self._cmd_queue)) + self._set_section_cmd = self._mcu.lookup_command( + "sos_filter_set_section oid=%c section_idx=%c" + " sos0=%i sos1=%i sos2=%i sos3=%i sos4=%i", cq=self._cmd_queue) + self._set_state_cmd = self._mcu.lookup_command( + "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i", + cq=self._cmd_queue) + self._set_active_cmd = self._mcu.lookup_command( + "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c", + cq=self._cmd_queue) def get_oid(self): return self._oid - # create an uninitialized filter object - def create_filter(self): - self._mcu.add_config_cmd("config_sos_filter oid=%d max_sections=%d" - % (self._oid, self._max_sections)) - self._configure_filter(is_init=True) + # Change the filter coefficients and state at runtime + # fixed_point_filter should be an FixedPointSosFilter instance + def change_filter(self, fixed_point_filter): + self._filter = fixed_point_filter - # either setup an init command or send the command based on a flag - def _cmd(self, command, args, is_init=False): - if is_init: - self._mcu.add_config_cmd(command[0] % args, is_init=True) - else: - command[2].send(args) - - def _set_filter_sections(self, is_init=False): - for i, section in enumerate(self._filter.get_filter_sections()): - args = (self._oid, i, section[0], section[1], section[2], - section[3], section[4]) - self._cmd(self._cmd_set_section, args, is_init) - - def _set_filter_state(self, is_init=False): - for i, state in enumerate(self._filter.get_initial_state()): - args = (self._oid, i, state[0], state[1]) - self._cmd(self._cmd_config_state, args, is_init) - - def _activate_filter(self, is_init=False): - args = (self._oid, self._filter.get_num_sections(), - self._filter.get_coeff_int_bits()) - self._cmd(self._cmd_activate, args, is_init) - - # configure the filter sections on the mcu - # filters should be an array of filter sections in SciPi SOS format - # sos_filter_state should be an array of zi filter state elements - def _configure_filter(self, is_init=False): + # Resets the filter state back to initial conditions at runtime + def reset_filter(self): num_sections = self._filter.get_num_sections() if num_sections > self._max_sections: raise ValueError("Too many filter sections: %i, The max is %i" % (num_sections, self._max_sections,)) - # convert to fixed point to find errors - # no errors, state is accepted - # configure MCU filter and activate - self._set_filter_sections(is_init) - self._set_filter_state(is_init,) - self._activate_filter(is_init) - - # Change the filter coefficients and state at runtime - # fixed_point_filter should be an FixedPointSosFilter instance - # cq is an optional command queue to for command sequencing - def change_filter(self, fixed_point_filter): - self._filter = fixed_point_filter - self._configure_filter(False) - - # Resets the filter state back to initial conditions at runtime - # cq is an optional command queue to for command sequencing - def reset_filter(self): - self._set_filter_state(False) - self._activate_filter(False) + # Send section coefficients (if they have changed) + for i, section in enumerate(self._filter.get_filter_sections()): + args = (self._oid, i, section[0], section[1], section[2], + section[3], section[4]) + if args == self._last_sent_coeffs[i]: + continue + self._set_section_cmd.send(args) + self._last_sent_coeffs[i] = args + # Send section initial states + for i, state in enumerate(self._filter.get_initial_state()): + self._set_state_cmd.send([self._oid, i, state[0], state[1]]) + # Activate filter + self._set_active_cmd.send([self._oid, num_sections, + self._filter.get_coeff_int_bits()]) From 3b5045ed9e0217c93ca984d56d866c2826c564c6 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 19:44:15 -0500 Subject: [PATCH 334/411] sos_filter: Handle fixed point conversion within MCU_SosFilter Merge the logic from the FixedPointSosFilter class into the MCU_SosFilter class. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 18 ++--- klippy/extras/sos_filter.py | 131 +++++++++++++------------------ 2 files changed, 61 insertions(+), 88 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 0ee7b77ea..3b08b4510 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -163,13 +163,9 @@ class ContinuousTareFilter: # create a filter design from the parameters def design_filter(self, error_func): - design = sos_filter.DigitalFilter(self.sps, error_func, self.drift, + return sos_filter.DigitalFilter(self.sps, error_func, self.drift, self.drift_delay, self.buzz, self.buzz_delay, self.notches, self.notch_quality) - fixed_filter = sos_filter.FixedPointSosFilter( - design.get_filter_sections(), design.get_initial_state(), - Q2_INT_BITS, Q16_INT_BITS) - return fixed_filter # Combine ContinuousTareFilter and SosFilter into an easy-to-use class @@ -214,9 +210,11 @@ class ContinuousTareFilterHelper: return ContinuousTareFilter(self._sps, drift, drift_delay, buzz, buzz_delay, notches, notch_quality) - def _create_filter(self, fixed_filter, cmd_queue): - return sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, - fixed_filter, 4) + def _create_filter(self, design, cmd_queue): + sf = sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, + Q2_INT_BITS, Q16_INT_BITS) + sf.set_filter_design(design) + return sf def update_from_command(self, gcmd, cq=None): gcmd_filter = self._build_filter(gcmd) @@ -224,8 +222,8 @@ class ContinuousTareFilterHelper: if self._active_design == gcmd_filter: return # update MCU filter from GCode command - self._sos_filter.change_filter( - self._active_design.design_filter(gcmd.error)) + design = self._active_design.design_filter(gcmd.error) + self._sos_filter.set_filter_design(design) def get_sos_filter(self): return self._sos_filter diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index dd9725d06..9c9cda499 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -63,41 +63,25 @@ class DigitalFilter: def get_initial_state(self): return self.initial_state -# container that accepts SciPy formatted SOS filter data and converts it to a -# selected fixed point representation. This data could come from DigitalFilter, -# static data, config etc. -class FixedPointSosFilter: - # filter_sections is an array of SciPy formatted SOS filter sections (sos) - # initial_state is an array of SciPy formatted SOS state sections (zi) - def __init__(self, filter_sections=None, initial_state=None, - coeff_int_bits=2, value_int_bits=15): - filter_sections = [] if filter_sections is None else filter_sections - initial_state = [] if initial_state is None else initial_state - num_sections = len(filter_sections) - num_state = len(initial_state) - if num_state != num_sections: - raise ValueError("The number of filter sections (%i) and state " - "sections (%i) must be equal" % ( - num_sections, num_state)) - self._coeff_int_bits = self._validate_int_bits(coeff_int_bits) - self._value_int_bits = self._validate_int_bits(value_int_bits) - self._filter = self._convert_filter(filter_sections) - self._state = self._convert_state(initial_state) - def get_filter_sections(self): - return self._filter - - def get_initial_state(self): - return self._state - - def get_coeff_int_bits(self): - return self._coeff_int_bits - - def get_value_int_bits(self): - return self._value_int_bits - - def get_num_sections(self): - return len(self._filter) +# Control an `sos_filter` object on the MCU +class MCU_SosFilter: + # max_sections should be the largest number of sections you expect + # to use at runtime. + def __init__(self, mcu, cmd_queue, max_sections, + coeff_int_bits=2, value_int_bits=15): + self._mcu = mcu + self._cmd_queue = cmd_queue + self._oid = self._mcu.create_oid() + self._max_sections = max_sections + self._coeff_int_bits = coeff_int_bits + self._value_int_bits = value_int_bits + self._design = None + self._set_section_cmd = self._set_state_cmd = self._set_active_cmd =None + self._last_sent_coeffs = [None] * self._max_sections + self._mcu.add_config_cmd("config_sos_filter oid=%d max_sections=%d" + % (self._oid, self._max_sections)) + self._mcu.register_config_callback(self._build_config) def _validate_int_bits(self, int_bits): if int_bits < 1 or int_bits > 30: @@ -105,8 +89,25 @@ class FixedPointSosFilter: " value between 1 and 30" % (int_bits,)) return int_bits + def _build_config(self): + self._set_section_cmd = self._mcu.lookup_command( + "sos_filter_set_section oid=%c section_idx=%c" + " sos0=%i sos1=%i sos2=%i sos3=%i sos4=%i", cq=self._cmd_queue) + self._set_state_cmd = self._mcu.lookup_command( + "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i", + cq=self._cmd_queue) + self._set_active_cmd = self._mcu.lookup_command( + "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c", + cq=self._cmd_queue) + + def get_oid(self): + return self._oid + # convert the SciPi SOS filters to fixed point format - def _convert_filter(self, filter_sections): + def _convert_filter(self): + if self._design is None: + return [] + filter_sections = self._design.get_filter_sections() sos_fixed = [] for section in filter_sections: nun_coeff = len(section) @@ -125,7 +126,10 @@ class FixedPointSosFilter: return sos_fixed # convert the SOS filter state matrix (zi) to fixed point format - def _convert_state(self, filter_state): + def _convert_state(self): + if self._design is None: + return [] + filter_state = self._design.get_initial_state() sos_state = [] for section in filter_state: nun_states = len(section) @@ -139,54 +143,25 @@ class FixedPointSosFilter: sos_state.append(fixed_state) return sos_state - -# Control an `sos_filter` object on the MCU -class MCU_SosFilter: - # fixed_point_filter should be an FixedPointSosFilter instance. A filter of - # size 0 will create a passthrough filter. - # max_sections should be the largest number of sections you expect - # to use at runtime. The default is the size of the fixed_point_filter. - def __init__(self, mcu, cmd_queue, fixed_point_filter, max_sections=None): - self._mcu = mcu - self._cmd_queue = cmd_queue - self._oid = self._mcu.create_oid() - self._filter = fixed_point_filter - self._max_sections = max_sections - if self._max_sections is None: - self._max_sections = self._filter.get_num_sections() - self._set_section_cmd = self._set_state_cmd = self._set_active_cmd =None - self._last_sent_coeffs = [None] * self._max_sections - self._mcu.add_config_cmd("config_sos_filter oid=%d max_sections=%d" - % (self._oid, self._max_sections)) - self._mcu.register_config_callback(self._build_config) - - def _build_config(self): - self._set_section_cmd = self._mcu.lookup_command( - "sos_filter_set_section oid=%c section_idx=%c" - " sos0=%i sos1=%i sos2=%i sos3=%i sos4=%i", cq=self._cmd_queue) - self._set_state_cmd = self._mcu.lookup_command( - "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i", - cq=self._cmd_queue) - self._set_active_cmd = self._mcu.lookup_command( - "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c", - cq=self._cmd_queue) - - def get_oid(self): - return self._oid - # Change the filter coefficients and state at runtime - # fixed_point_filter should be an FixedPointSosFilter instance - def change_filter(self, fixed_point_filter): - self._filter = fixed_point_filter + def set_filter_design(self, design): + self._design = design # Resets the filter state back to initial conditions at runtime def reset_filter(self): - num_sections = self._filter.get_num_sections() + # Generate filter parameters + sos_fixed = self._convert_filter() + sos_state = self._convert_state() + num_sections = len(sos_fixed) if num_sections > self._max_sections: raise ValueError("Too many filter sections: %i, The max is %i" % (num_sections, self._max_sections,)) + if len(sos_state) != num_sections: + raise ValueError("The number of filter sections (%i) and state " + "sections (%i) must be equal" + % (num_sections, len(sos_state))) # Send section coefficients (if they have changed) - for i, section in enumerate(self._filter.get_filter_sections()): + for i, section in enumerate(sos_fixed): args = (self._oid, i, section[0], section[1], section[2], section[3], section[4]) if args == self._last_sent_coeffs[i]: @@ -194,8 +169,8 @@ class MCU_SosFilter: self._set_section_cmd.send(args) self._last_sent_coeffs[i] = args # Send section initial states - for i, state in enumerate(self._filter.get_initial_state()): + for i, state in enumerate(sos_state): self._set_state_cmd.send([self._oid, i, state[0], state[1]]) # Activate filter self._set_active_cmd.send([self._oid, num_sections, - self._filter.get_coeff_int_bits()]) + self._coeff_int_bits]) From 109f13c7977381dc0ce926e5842621260a5d2e1b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 10 Jan 2026 19:17:05 -0500 Subject: [PATCH 335/411] sos_filter: Consistently use "frac_bits" instead of "int_bits" Internally describe the Qx.y format using the number of fractional bits. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 18 +++++++--------- klippy/extras/sos_filter.py | 37 ++++++++++++++++---------------- src/sos_filter.c | 34 ++++++++++++++--------------- 3 files changed, 43 insertions(+), 46 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 3b08b4510..56992e9f8 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -10,10 +10,8 @@ from . import probe, sos_filter, load_cell, hx71x, ads1220 np = None # delay NumPy import until configuration time # constants for fixed point numbers -Q2_INT_BITS = 2 -Q2_FRAC_BITS = (32 - (1 + Q2_INT_BITS)) -Q16_INT_BITS = 16 -Q16_FRAC_BITS = (32 - (1 + Q16_INT_BITS)) +Q2_29_FRAC_BITS = 29 +Q16_15_FRAC_BITS = 15 class TapAnalysis: @@ -212,7 +210,7 @@ class ContinuousTareFilterHelper: def _create_filter(self, design, cmd_queue): sf = sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, - Q2_INT_BITS, Q16_INT_BITS) + Q2_29_FRAC_BITS, Q16_15_FRAC_BITS) sf.set_filter_design(design) return sf @@ -282,15 +280,15 @@ class LoadCellProbeConfigHelper: raise cmd_err("Load cell force_safety_limit exceeds sensor range!") return safety_min, safety_max - # calculate 1/counts_per_gram in Q2 fixed point + # calculate 1/counts_per_gram in Q2.29 fixed point def get_grams_per_count(self): counts_per_gram = self._load_cell.get_counts_per_gram() # The counts_per_gram could be so large that it becomes 0.0 when - # converted to Q2 format. This would mean the ADC range only measures a - # few grams which seems very unlikely. Treat this as an error: - if counts_per_gram >= 2**Q2_FRAC_BITS: + # converted to Q2.29 format. This would mean the ADC range only measures + # a few grams which seems very unlikely. Treat this as an error: + if counts_per_gram >= 2**Q2_29_FRAC_BITS: raise OverflowError("counts_per_gram value is too large to filter") - return sos_filter.to_fixed_32((1. / counts_per_gram), Q2_INT_BITS) + return sos_filter.to_fixed_32((1. / counts_per_gram), Q2_29_FRAC_BITS) # MCU_trigger_analog is the interface to `trigger_analog` on the MCU diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index 9c9cda499..021dfff28 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -6,18 +6,17 @@ MAX_INT32 = (2 ** 31) MIN_INT32 = -(2 ** 31) - 1 -def assert_is_int32(value, error): +def assert_is_int32(value, frac_bits): if value > MAX_INT32 or value < MIN_INT32: - raise OverflowError(error) + raise OverflowError("Fixed point Q%d.%d overflow" + % (31-frac_bits, frac_bits)) return value # convert a floating point value to a 32 bit fixed point representation # checks for overflow -def to_fixed_32(value, int_bits): - fractional_bits = (32 - (1 + int_bits)) - fixed_val = int(value * (2 ** fractional_bits)) - return assert_is_int32(fixed_val, "Fixed point Q%i overflow" - % (int_bits,)) +def to_fixed_32(value, frac_bits): + fixed_val = int(value * (2**frac_bits)) + return assert_is_int32(fixed_val, frac_bits) # Digital filter designer and container @@ -69,13 +68,13 @@ class MCU_SosFilter: # max_sections should be the largest number of sections you expect # to use at runtime. def __init__(self, mcu, cmd_queue, max_sections, - coeff_int_bits=2, value_int_bits=15): + coeff_frac_bits=29, value_frac_bits=16): self._mcu = mcu self._cmd_queue = cmd_queue self._oid = self._mcu.create_oid() self._max_sections = max_sections - self._coeff_int_bits = coeff_int_bits - self._value_int_bits = value_int_bits + self._coeff_frac_bits = coeff_frac_bits + self._value_frac_bits = value_frac_bits self._design = None self._set_section_cmd = self._set_state_cmd = self._set_active_cmd =None self._last_sent_coeffs = [None] * self._max_sections @@ -83,11 +82,11 @@ class MCU_SosFilter: % (self._oid, self._max_sections)) self._mcu.register_config_callback(self._build_config) - def _validate_int_bits(self, int_bits): - if int_bits < 1 or int_bits > 30: - raise ValueError("The number of integer bits (%i) must be a" - " value between 1 and 30" % (int_bits,)) - return int_bits + def _validate_frac_bits(self, frac_bits): + if frac_bits < 0 or frac_bits > 31: + raise ValueError("The number of fractional bits (%i) must be a" + " value between 0 and 31" % (frac_bits,)) + return frac_bits def _build_config(self): self._set_section_cmd = self._mcu.lookup_command( @@ -97,7 +96,7 @@ class MCU_SosFilter: "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i", cq=self._cmd_queue) self._set_active_cmd = self._mcu.lookup_command( - "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c", + "sos_filter_set_active oid=%c n_sections=%c coeff_frac_bits=%c", cq=self._cmd_queue) def get_oid(self): @@ -117,7 +116,7 @@ class MCU_SosFilter: fixed_section = [] for col, coeff in enumerate(section): if col != 3: # omit column 3 - fixed_coeff = to_fixed_32(coeff, self._coeff_int_bits) + fixed_coeff = to_fixed_32(coeff, self._coeff_frac_bits) fixed_section.append(fixed_coeff) elif coeff != 1.0: # double check column 3 is always 1.0 raise ValueError("Coefficient 3 is expected to be 1.0" @@ -139,7 +138,7 @@ class MCU_SosFilter: % (nun_states,)) fixed_state = [] for col, value in enumerate(section): - fixed_state.append(to_fixed_32(value, self._value_int_bits)) + fixed_state.append(to_fixed_32(value, self._value_frac_bits)) sos_state.append(fixed_state) return sos_state @@ -173,4 +172,4 @@ class MCU_SosFilter: self._set_state_cmd.send([self._oid, i, state[0], state[1]]) # Activate filter self._set_active_cmd.send([self._oid, num_sections, - self._coeff_int_bits]) + self._coeff_frac_bits]) diff --git a/src/sos_filter.c b/src/sos_filter.c index 036c50ce5..60c6abf4c 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -21,7 +21,6 @@ struct sos_filter_section { struct sos_filter { uint8_t max_sections, n_sections, coeff_frac_bits; - uint32_t coeff_rounding; // filter composed of second order sections struct sos_filter_section filter[0]; }; @@ -33,15 +32,17 @@ overflows_int32(int64_t value) } // Multiply a coeff*value and shift result by coeff_frac_bits -static inline int -fixed_mul(struct sos_filter *sf, int32_t coeff, int32_t value, int32_t *res) +static int +fixed_mul(int32_t coeff, int32_t value, uint_fast8_t frac_bits, int32_t *res) { // This optimizes to single cycle SMULL on Arm Coretex M0+ - int64_t product = (int64_t)coeff * (int64_t)value; - // round up at the last bit to be shifted away - product += sf->coeff_rounding; - // shift the decimal right to discard the coefficient fractional bits - int64_t result = product >> sf->coeff_frac_bits; + int64_t result = (int64_t)coeff * (int64_t)value; + if (frac_bits) { + // round up at the last bit to be shifted away + result += 1 << (frac_bits - 1); + // shift the decimal right to discard the coefficient fractional bits + result >>= frac_bits; + } // truncate significant 32 bits *res = (int32_t)result; // check for overflow of int32_t @@ -55,17 +56,18 @@ int sos_filter_apply(struct sos_filter *sf, int32_t *pvalue) { int32_t cur_val = *pvalue; + uint_fast8_t cfb = sf->coeff_frac_bits; // foreach section for (int section_idx = 0; section_idx < sf->n_sections; section_idx++) { struct sos_filter_section *section = &(sf->filter[section_idx]); // apply the section's filter coefficients to input int32_t next_val, c1_cur, c2_cur, c3_next, c4_next; - int ret = fixed_mul(sf, section->coeff[0], cur_val, &next_val); + int ret = fixed_mul(section->coeff[0], cur_val, cfb, &next_val); next_val += section->state[0]; - ret |= fixed_mul(sf, section->coeff[1], cur_val, &c1_cur); - ret |= fixed_mul(sf, section->coeff[3], next_val, &c3_next); - ret |= fixed_mul(sf, section->coeff[2], cur_val, &c2_cur); - ret |= fixed_mul(sf, section->coeff[4], next_val, &c4_next); + ret |= fixed_mul(section->coeff[1], cur_val, cfb, &c1_cur); + ret |= fixed_mul(section->coeff[3], next_val, cfb, &c3_next); + ret |= fixed_mul(section->coeff[2], cur_val, cfb, &c2_cur); + ret |= fixed_mul(section->coeff[4], next_val, cfb, &c4_next); if (ret) // Overflow return -1; @@ -151,9 +153,7 @@ command_sos_filter_activate(uint32_t *args) if (n_sections > sf->max_sections) shutdown("Filter section index larger than max_sections"); sf->n_sections = n_sections; - uint8_t coeff_int_bits = args[2]; - sf->coeff_frac_bits = (31 - coeff_int_bits); - sf->coeff_rounding = (1 << (sf->coeff_frac_bits - 1)); + sf->coeff_frac_bits = args[2] & 0x3f; } DECL_COMMAND(command_sos_filter_activate - , "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c"); + , "sos_filter_set_active oid=%c n_sections=%c coeff_frac_bits=%c"); From 7ec82baca3f37bf3193c9316e538b538d8075e6f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 14:01:18 -0500 Subject: [PATCH 336/411] sos_filter: Move offset/scale support from trigger_analog.c to sos_filter.c Support offsetting and scaling the initial raw value prior to processing in the sos_filter. Remove that support from trigger_analog.c . Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 22 ++++--- klippy/extras/sos_filter.py | 28 +++++++-- src/sos_filter.c | 29 ++++++++- src/trigger_analog.c | 103 +++---------------------------- 4 files changed, 72 insertions(+), 110 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 56992e9f8..2d5fce81e 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -210,7 +210,7 @@ class ContinuousTareFilterHelper: def _create_filter(self, design, cmd_queue): sf = sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, - Q2_29_FRAC_BITS, Q16_15_FRAC_BITS) + Q2_29_FRAC_BITS) sf.set_filter_design(design) return sf @@ -280,7 +280,7 @@ class LoadCellProbeConfigHelper: raise cmd_err("Load cell force_safety_limit exceeds sensor range!") return safety_min, safety_max - # calculate 1/counts_per_gram in Q2.29 fixed point + # calculate 1/counts_per_gram def get_grams_per_count(self): counts_per_gram = self._load_cell.get_counts_per_gram() # The counts_per_gram could be so large that it becomes 0.0 when @@ -288,7 +288,7 @@ class LoadCellProbeConfigHelper: # a few grams which seems very unlikely. Treat this as an error: if counts_per_gram >= 2**Q2_29_FRAC_BITS: raise OverflowError("counts_per_gram value is too large to filter") - return sos_filter.to_fixed_32((1. / counts_per_gram), Q2_29_FRAC_BITS) + return 1. / counts_per_gram # MCU_trigger_analog is the interface to `trigger_analog` on the MCU @@ -330,9 +330,8 @@ class MCU_trigger_analog: "trigger_analog_state oid=%c is_homing_trigger=%c " "trigger_ticks=%u", oid=self._oid, cq=self._cmd_queue) self._set_range_cmd = self._mcu.lookup_command( - "trigger_analog_set_range" - " oid=%c safety_counts_min=%i safety_counts_max=%i tare_counts=%i" - " trigger_grams=%u grams_per_count=%i", cq=self._cmd_queue) + "trigger_analog_set_range oid=%c safety_counts_min=%i" + " safety_counts_max=%i trigger_value=%i", cq=self._cmd_queue) self._home_cmd = self._mcu.lookup_command( "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" " error_reason=%c clock=%u rest_ticks=%u timeout=%u", @@ -359,10 +358,13 @@ class MCU_trigger_analog: self._load_cell.tare(tare_counts) # update internal tare value safety_min, safety_max = self._config_helper.get_safety_range(gcmd) - args = [self._oid, safety_min, safety_max, int(tare_counts), - self._config_helper.get_trigger_force_grams(gcmd), - self._config_helper.get_grams_per_count()] - self._set_range_cmd.send(args) + trigger_val = self._config_helper.get_trigger_force_grams(gcmd) + tval32 = sos_filter.to_fixed_32(trigger_val, Q16_15_FRAC_BITS) + self._set_range_cmd.send([self._oid, safety_min, safety_max, tval32]) + gpc = self._config_helper.get_grams_per_count() + Q17_14_FRAC_BITS = 14 + self._sos_filter.set_offset_scale(int(-tare_counts), gpc, + Q17_14_FRAC_BITS, Q16_15_FRAC_BITS) self._sos_filter.reset_filter() def home_start(self, print_time): diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index 021dfff28..6456eaa5c 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -67,17 +67,20 @@ class DigitalFilter: class MCU_SosFilter: # max_sections should be the largest number of sections you expect # to use at runtime. - def __init__(self, mcu, cmd_queue, max_sections, - coeff_frac_bits=29, value_frac_bits=16): + def __init__(self, mcu, cmd_queue, max_sections, coeff_frac_bits=29): self._mcu = mcu self._cmd_queue = cmd_queue self._oid = self._mcu.create_oid() self._max_sections = max_sections self._coeff_frac_bits = coeff_frac_bits - self._value_frac_bits = value_frac_bits + self._value_frac_bits = self._scale_frac_bits = 0 self._design = None - self._set_section_cmd = self._set_state_cmd = self._set_active_cmd =None + self._offset = 0 + self._scale = 1 + self._set_section_cmd = self._set_state_cmd = None + self._set_active_cmd = self._set_offset_scale_cmd = None self._last_sent_coeffs = [None] * self._max_sections + self._last_sent_offset_scale = None self._mcu.add_config_cmd("config_sos_filter oid=%d max_sections=%d" % (self._oid, self._max_sections)) self._mcu.register_config_callback(self._build_config) @@ -95,6 +98,9 @@ class MCU_SosFilter: self._set_state_cmd = self._mcu.lookup_command( "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i", cq=self._cmd_queue) + self._set_offset_scale_cmd = self._mcu.lookup_command( + "sos_filter_set_offset_scale oid=%c offset=%i" + " scale=%i scale_frac_bits=%c", cq=self._cmd_queue) self._set_active_cmd = self._mcu.lookup_command( "sos_filter_set_active oid=%c n_sections=%c coeff_frac_bits=%c", cq=self._cmd_queue) @@ -142,6 +148,15 @@ class MCU_SosFilter: sos_state.append(fixed_state) return sos_state + # Set conversion of a raw value 1 to a 1.0 value processed by sos filter + def set_offset_scale(self, offset, scale, scale_frac_bits=0, + value_frac_bits=0): + self._offset = offset + self._value_frac_bits = value_frac_bits + scale_mult = scale * float(1 << value_frac_bits) + self._scale = to_fixed_32(scale_mult, scale_frac_bits) + self._scale_frac_bits = scale_frac_bits + # Change the filter coefficients and state at runtime def set_filter_design(self, design): self._design = design @@ -170,6 +185,11 @@ class MCU_SosFilter: # Send section initial states for i, state in enumerate(sos_state): self._set_state_cmd.send([self._oid, i, state[0], state[1]]) + # Send offset/scale (if they have changed) + args = (self._oid, self._offset, self._scale, self._scale_frac_bits) + if args != self._last_sent_offset_scale: + self._set_offset_scale_cmd.send(args) + self._last_sent_offset_scale = args # Activate filter self._set_active_cmd.send([self._oid, num_sections, self._coeff_frac_bits]) diff --git a/src/sos_filter.c b/src/sos_filter.c index 60c6abf4c..c11ae3df8 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -20,7 +20,8 @@ struct sos_filter_section { }; struct sos_filter { - uint8_t max_sections, n_sections, coeff_frac_bits; + uint8_t max_sections, n_sections, coeff_frac_bits, scale_frac_bits; + int32_t offset, scale; // filter composed of second order sections struct sos_filter_section filter[0]; }; @@ -55,9 +56,19 @@ fixed_mul(int32_t coeff, int32_t value, uint_fast8_t frac_bits, int32_t *res) int sos_filter_apply(struct sos_filter *sf, int32_t *pvalue) { - int32_t cur_val = *pvalue; - uint_fast8_t cfb = sf->coeff_frac_bits; + int32_t raw_val = *pvalue; + + // Apply offset and scale + int32_t offset = sf->offset, offset_val = raw_val + offset, cur_val; + if ((offset >= 0) != (offset_val >= raw_val)) + // Overflow + return -1; + int ret = fixed_mul(sf->scale, offset_val, sf->scale_frac_bits, &cur_val); + if (ret) + return -1; + // foreach section + uint_fast8_t cfb = sf->coeff_frac_bits; for (int section_idx = 0; section_idx < sf->n_sections; section_idx++) { struct sos_filter_section *section = &(sf->filter[section_idx]); // apply the section's filter coefficients to input @@ -144,6 +155,18 @@ command_sos_filter_set_state(uint32_t *args) DECL_COMMAND(command_sos_filter_set_state , "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i"); +// Set incoming sample offset/scaling +void +command_trigger_analog_set_offset_scale(uint32_t *args) +{ + struct sos_filter *sf = sos_filter_oid_lookup(args[0]); + sf->offset = args[1]; + sf->scale = args[2]; + sf->scale_frac_bits = args[3] & 0x3f; +} +DECL_COMMAND(command_trigger_analog_set_offset_scale, + "sos_filter_set_offset_scale oid=%c offset=%i scale=%i scale_frac_bits=%c"); + // Set one section of the filter void command_sos_filter_activate(uint32_t *args) diff --git a/src/trigger_analog.c b/src/trigger_analog.c index 4a8419d3b..df64a4f40 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -13,25 +13,6 @@ #include "trigger_analog.h" // trigger_analog_update #include "trsync.h" // trsync_do_trigger -// Q2.29 -typedef int32_t fixedQ2_t; -#define FIXEDQ2 2 -#define FIXEDQ2_FRAC_BITS ((32 - FIXEDQ2) - 1) - -// Q32.29 - a Q2.29 value stored in int64 -typedef int64_t fixedQ32_t; -#define FIXEDQ32_FRAC_BITS FIXEDQ2_FRAC_BITS - -// Q16.15 -typedef int32_t fixedQ16_t; -#define FIXEDQ16 16 -#define FIXEDQ16_FRAC_BITS ((32 - FIXEDQ16) - 1) - -// Q48.15 - a Q16.15 value stored in int64 -typedef int64_t fixedQ48_t; -#define FIXEDQ48_FRAC_BITS FIXEDQ16_FRAC_BITS - -#define MAX_TRIGGER_GRAMS ((1L << FIXEDQ16) - 1) #define ERROR_SAFETY_RANGE 0 #define ERROR_OVERFLOW 1 #define ERROR_WATCHDOG 2 @@ -45,42 +26,15 @@ enum {FLAG_IS_HOMING = 1 << 0 // Endstop Structure struct trigger_analog { struct timer time; - uint32_t trigger_grams, trigger_ticks, last_sample_ticks, rest_ticks; + uint32_t trigger_ticks, last_sample_ticks, rest_ticks; uint32_t homing_start_time; struct trsync *ts; - int32_t safety_counts_min, safety_counts_max, tare_counts; + int32_t safety_counts_min, safety_counts_max; uint8_t flags, trigger_reason, error_reason, watchdog_max, watchdog_count; - fixedQ16_t trigger_grams_fixed; - fixedQ2_t grams_per_count; + int32_t trigger_value; struct sos_filter *sf; }; -static inline uint8_t -overflows_int32(int64_t value) { - return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; -} - -// returns the integer part of a fixedQ48_t -static inline int64_t -round_fixedQ48(const int64_t fixed_value) { - return fixed_value >> FIXEDQ48_FRAC_BITS; -} - -// Convert sensor counts to grams -static inline fixedQ48_t -counts_to_grams(struct trigger_analog *ta, const int32_t counts) { - // tearing ensures readings are referenced to 0.0g - const int32_t delta = counts - ta->tare_counts; - // convert sensor counts to grams by multiplication: 124 * 0.051 = 6.324 - // this optimizes to single cycle SMULL instruction - const fixedQ32_t product = (int64_t)delta * (int64_t)ta->grams_per_count; - // after multiplication there are 30 fraction bits, reduce to 15 - // caller verifies this wont overflow a 32bit int when truncated - const fixedQ48_t grams = product >> - (FIXEDQ32_FRAC_BITS - FIXEDQ48_FRAC_BITS); - return grams; -} - static inline uint8_t is_flag_set(const uint8_t mask, struct trigger_analog *ta) { @@ -149,23 +103,16 @@ trigger_analog_update(struct trigger_analog *ta, const int32_t sample) return; } - // convert sample to grams - const fixedQ48_t raw_grams = counts_to_grams(ta, sample); - if (overflows_int32(raw_grams)) { - trigger_error(ta, ERROR_OVERFLOW); - return; - } - // perform filtering - int32_t filtered_grams = raw_grams; - int ret = sos_filter_apply(ta->sf, &filtered_grams); + int32_t filtered_value = sample; + int ret = sos_filter_apply(ta->sf, &filtered_value); if (ret) { trigger_error(ta, ERROR_OVERFLOW); return; } // update trigger state - if (abs(filtered_grams) >= ta->trigger_grams_fixed) { + if (abs(filtered_value) >= ta->trigger_value) { try_trigger(ta, ta->last_sample_ticks); } } @@ -192,43 +139,13 @@ watchdog_event(struct timer *t) return SF_RESCHEDULE; } -static void -set_endstop_range(struct trigger_analog *ta - , int32_t safety_counts_min, int32_t safety_counts_max - , int32_t tare_counts, uint32_t trigger_grams - , fixedQ2_t grams_per_count) -{ - if (!(safety_counts_max >= safety_counts_min)) { - shutdown("Safety range reversed"); - } - if (trigger_grams > MAX_TRIGGER_GRAMS) { - shutdown("trigger_grams too large"); - } - // grams_per_count must be a positive fraction in Q2 format - const fixedQ2_t one = 1L << FIXEDQ2_FRAC_BITS; - if (grams_per_count < 0 || grams_per_count >= one) { - shutdown("grams_per_count is invalid"); - } - ta->safety_counts_min = safety_counts_min; - ta->safety_counts_max = safety_counts_max; - ta->tare_counts = tare_counts; - ta->trigger_grams = trigger_grams; - ta->trigger_grams_fixed = trigger_grams << FIXEDQ16_FRAC_BITS; - ta->grams_per_count = grams_per_count; -} - // Create a trigger_analog void command_config_trigger_analog(uint32_t *args) { struct trigger_analog *ta = oid_alloc( args[0], command_config_trigger_analog, sizeof(*ta)); - ta->flags = 0; - ta->trigger_ticks = 0; - ta->watchdog_max = 0; - ta->watchdog_count = 0; ta->sf = sos_filter_oid_lookup(args[1]); - set_endstop_range(ta, 0, 0, 0, 0, 0); } DECL_COMMAND(command_config_trigger_analog , "config_trigger_analog oid=%c sos_filter_oid=%c"); @@ -245,12 +162,12 @@ void command_trigger_analog_set_range(uint32_t *args) { struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); - set_endstop_range(ta, args[1], args[2], args[3], args[4] - , (fixedQ16_t)args[5]); + ta->safety_counts_min = args[1]; + ta->safety_counts_max = args[2]; + ta->trigger_value = args[3]; } DECL_COMMAND(command_trigger_analog_set_range, "trigger_analog_set_range" - " oid=%c safety_counts_min=%i safety_counts_max=%i tare_counts=%i" - " trigger_grams=%u grams_per_count=%i"); + " oid=%c safety_counts_min=%i safety_counts_max=%i trigger_value=%i"); // Home an axis void From 067539e0a394f99c2a874a5815f6dcb9d106017f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 12 Jan 2026 13:14:38 -0500 Subject: [PATCH 337/411] load_cell_probe: Move set_endstop_range() code to LoadCellProbingMove Move this load cell specific code from MCU_trigger_analog class to LoadCellProbingMove class. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 62 ++++++++++++++++++++------------ klippy/extras/sos_filter.py | 3 ++ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 2d5fce81e..4daeec2a2 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -299,18 +299,20 @@ class MCU_trigger_analog: ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 - def __init__(self, config, load_cell_inst, sos_filter_inst, config_helper, + def __init__(self, config, sensor_inst, sos_filter_inst, config_helper, trigger_dispatch): self._printer = config.get_printer() - self._load_cell = load_cell_inst self._sos_filter = sos_filter_inst self._config_helper = config_helper - self._sensor = load_cell_inst.get_sensor() + self._sensor = sensor_inst self._mcu = self._sensor.get_mcu() # configure MCU objects self._dispatch = trigger_dispatch self._cmd_queue = self._dispatch.get_command_queue() self._oid = self._mcu.create_oid() + self._raw_min = self._raw_max = 0 + self._last_range_args = None + self._trigger_value = 0. self._config_commands() self._home_cmd = None self._query_cmd = None @@ -347,24 +349,27 @@ class MCU_trigger_analog: def get_mcu(self): return self._mcu - def get_load_cell(self): - return self._load_cell + def get_sos_filter(self): + return self._sos_filter def get_dispatch(self): return self._dispatch - def set_endstop_range(self, tare_counts, gcmd=None): - # update the load cell so it reflects the new tare value - self._load_cell.tare(tare_counts) - # update internal tare value - safety_min, safety_max = self._config_helper.get_safety_range(gcmd) - trigger_val = self._config_helper.get_trigger_force_grams(gcmd) - tval32 = sos_filter.to_fixed_32(trigger_val, Q16_15_FRAC_BITS) - self._set_range_cmd.send([self._oid, safety_min, safety_max, tval32]) - gpc = self._config_helper.get_grams_per_count() - Q17_14_FRAC_BITS = 14 - self._sos_filter.set_offset_scale(int(-tare_counts), gpc, - Q17_14_FRAC_BITS, Q16_15_FRAC_BITS) + def set_trigger_value(self, trigger_value): + self._trigger_value = trigger_value + + def set_raw_range(self, raw_min, raw_max): + self._raw_min = raw_min + self._raw_max = raw_max + + def reset_filter(self): + # Update parameters in mcu (if they have changed) + tval32 = self._sos_filter.convert_value(self._trigger_value) + args = [self._oid, self._raw_min, self._raw_max, tval32] + if args != self._last_range_args: + self._set_range_cmd.send(args) + self._last_range_args = args + # Update sos filter in mcu self._sos_filter.reset_filter() def home_start(self, print_time): @@ -397,15 +402,15 @@ class LoadCellProbingMove: "waiting for sensor data" } - def __init__(self, config, mcu_trigger_analog, param_helper, + def __init__(self, config, load_cell_inst, mcu_trigger_analog, param_helper, continuous_tare_filter_helper, config_helper): self._printer = config.get_printer() + self._load_cell = load_cell_inst self._mcu_trigger_analog = mcu_trigger_analog self._param_helper = param_helper self._continuous_tare_filter_helper = continuous_tare_filter_helper self._config_helper = config_helper self._mcu = mcu_trigger_analog.get_mcu() - self._load_cell = mcu_trigger_analog.get_load_cell() self._z_min_position = probe.lookup_minimum_z(config) self._dispatch = mcu_trigger_analog.get_dispatch() probe.LookupZSteppers(config, self._dispatch.add_stepper) @@ -433,7 +438,20 @@ class LoadCellProbingMove: tare_counts = np.average(np.array(tare_samples)[:, 2].astype(float)) # update sos_filter with any gcode parameter changes self._continuous_tare_filter_helper.update_from_command(gcmd) - self._mcu_trigger_analog.set_endstop_range(tare_counts, gcmd) + # update the load cell so it reflects the new tare value + self._load_cell.tare(tare_counts) + # update range and trigger + safety_min, safety_max = self._config_helper.get_safety_range(gcmd) + self._mcu_trigger_analog.set_raw_range(safety_min, safety_max) + trigger_val = self._config_helper.get_trigger_force_grams(gcmd) + self._mcu_trigger_analog.set_trigger_value(trigger_val) + # update internal tare value + gpc = self._config_helper.get_grams_per_count() + sos_filter = self._mcu_trigger_analog.get_sos_filter() + Q17_14_FRAC_BITS = 14 + sos_filter.set_offset_scale(int(-tare_counts), gpc, + Q17_14_FRAC_BITS, Q16_15_FRAC_BITS) + self._mcu_trigger_analog.reset_filter() def _home_start(self, print_time): # start trsync @@ -624,10 +642,10 @@ class LoadCellPrinterProbe: self._param_helper = probe.ProbeParameterHelper(config) self._cmd_helper = probe.ProbeCommandHelper(config, self) self._probe_offsets = probe.ProbeOffsetsHelper(config) - self._mcu_trigger_analog = MCU_trigger_analog(config, self._load_cell, + self._mcu_trigger_analog = MCU_trigger_analog(config, sensor, continuous_tare_filter_helper.get_sos_filter(), config_helper, trigger_dispatch) - load_cell_probing_move = LoadCellProbingMove(config, + load_cell_probing_move = LoadCellProbingMove(config, self._load_cell, self._mcu_trigger_analog, self._param_helper, continuous_tare_filter_helper, config_helper) self._tapping_move = TappingMove(config, load_cell_probing_move, diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index 6456eaa5c..01a394377 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -108,6 +108,9 @@ class MCU_SosFilter: def get_oid(self): return self._oid + def convert_value(self, val): + return to_fixed_32(val, self._value_frac_bits) + # convert the SciPi SOS filters to fixed point format def _convert_filter(self): if self._design is None: From 165fe1730d9d9ec902d8345b415fd50d3956f8ef Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 19:16:31 -0500 Subject: [PATCH 338/411] load_cell_probe: Move phoming.probing_move() interface to MCU_trigger_analog Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 127 ++++++++++++++----------------- 1 file changed, 58 insertions(+), 69 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 4daeec2a2..ea4aaf767 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -242,7 +242,6 @@ class LoadCellProbeConfigHelper: self._printer = config.get_printer() self._load_cell = load_cell_inst self._sensor = load_cell_inst.get_sensor() - self._rest_time = 1. / float(self._sensor.get_samples_per_second()) # Collect 4 x 60hz power cycles of data to average across power noise self._tare_time_param = floatParamHelper(config, 'tare_time', default=4. / 60., minval=0.01, maxval=1.0) @@ -263,9 +262,6 @@ class LoadCellProbeConfigHelper: def get_safety_limit_grams(self, gcmd=None): return self._force_safety_limit_param.get(gcmd) - def get_rest_time(self): - return self._rest_time - def get_safety_range(self, gcmd=None): counts_per_gram = self._load_cell.get_counts_per_gram() # calculate the safety band @@ -294,16 +290,21 @@ class LoadCellProbeConfigHelper: # MCU_trigger_analog is the interface to `trigger_analog` on the MCU # This also manages the SosFilter so all commands use one command queue class MCU_trigger_analog: - WATCHDOG_MAX = 3 + MONITOR_MAX = 3 ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 + ERROR_MAP = { + mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " + "homing", + ERROR_SAFETY_RANGE: "sensor exceeds safety limit", + ERROR_OVERFLOW: "fixed point math overflow", + ERROR_WATCHDOG: "timed out waiting for sensor data" + } - def __init__(self, config, sensor_inst, sos_filter_inst, config_helper, - trigger_dispatch): - self._printer = config.get_printer() + def __init__(self, sensor_inst, sos_filter_inst, trigger_dispatch): + self._printer = sensor_inst.get_mcu().get_printer() self._sos_filter = sos_filter_inst - self._config_helper = config_helper self._sensor = sensor_inst self._mcu = self._sensor.get_mcu() # configure MCU objects @@ -313,6 +314,7 @@ class MCU_trigger_analog: self._raw_min = self._raw_max = 0 self._last_range_args = None self._trigger_value = 0. + self._last_trigger_time = 0. self._config_commands() self._home_cmd = None self._query_cmd = None @@ -355,6 +357,9 @@ class MCU_trigger_analog: def get_dispatch(self): return self._dispatch + def get_last_trigger_time(self): + return self._last_trigger_time + def set_trigger_value(self, trigger_value): self._trigger_value = trigger_value @@ -362,7 +367,7 @@ class MCU_trigger_analog: self._raw_min = raw_min self._raw_max = raw_max - def reset_filter(self): + def _reset_filter(self): # Update parameters in mcu (if they have changed) tval32 = self._sos_filter.convert_value(self._trigger_value) args = [self._oid, self._raw_min, self._raw_max, tval32] @@ -372,15 +377,7 @@ class MCU_trigger_analog: # Update sos filter in mcu self._sos_filter.reset_filter() - def home_start(self, print_time): - clock = self._mcu.print_time_to_clock(print_time) - rest_time = self._config_helper.get_rest_time() - rest_ticks = self._mcu.seconds_to_clock(rest_time) - self._home_cmd.send([self._oid, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, - rest_ticks, self.WATCHDOG_MAX], reqclock=clock) - - def clear_home(self): + def _clear_home(self): params = self._query_cmd.send([self._oid]) # The time of the first sample that triggered is in "trigger_ticks" trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_ticks']) @@ -388,20 +385,43 @@ class MCU_trigger_analog: self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) return self._mcu.clock_to_print_time(trigger_ticks) + def get_steppers(self): + return self._dispatch.get_steppers() + + def home_start(self, print_time, sample_time, sample_count, rest_time, + triggered=True): + self._last_trigger_time = 0. + self._reset_filter() + trigger_completion = self._dispatch.start(print_time) + clock = self._mcu.print_time_to_clock(print_time) + sensor_update = 1. / self._sensor.get_samples_per_second() + sm_ticks = self._mcu.seconds_to_clock(sensor_update) + self._home_cmd.send([self._oid, self._dispatch.get_oid(), + mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, + sm_ticks, self.MONITOR_MAX], reqclock=clock) + return trigger_completion + + def home_wait(self, home_end_time): + self._dispatch.wait_end(home_end_time) + # trigger has happened, now to find out why... + res = self._dispatch.stop() + # clear the homing state so it stops processing samples + trigger_time = self._clear_home() + if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + defmsg = "unknown reason code %i" % (res,) + error_msg = self.ERROR_MAP.get(res, defmsg) + raise self._printer.command_error("Trigger analog error: %s" + % (error_msg,)) + if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: + return 0. + if self._mcu.is_fileoutput(): + trigger_time = home_end_time + self._last_trigger_time = trigger_time + return trigger_time + # Execute probing moves using the MCU_trigger_analog class LoadCellProbingMove: - ERROR_MAP = { - mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " - "homing", - MCU_trigger_analog.ERROR_SAFETY_RANGE: "Load Cell Probe Error: load " - "exceeds safety limit", - MCU_trigger_analog.ERROR_OVERFLOW: "Load Cell Probe Error: fixed point " - "math overflow", - MCU_trigger_analog.ERROR_WATCHDOG: "Load Cell Probe Error: timed out " - "waiting for sensor data" - } - def __init__(self, config, load_cell_inst, mcu_trigger_analog, param_helper, continuous_tare_filter_helper, config_helper): self._printer = config.get_printer() @@ -416,7 +436,6 @@ class LoadCellProbingMove: probe.LookupZSteppers(config, self._dispatch.add_stepper) # internal state tracking self._tare_counts = 0 - self._last_trigger_time = 0 def _start_collector(self): toolhead = self._printer.lookup_object('toolhead') @@ -451,37 +470,6 @@ class LoadCellProbingMove: Q17_14_FRAC_BITS = 14 sos_filter.set_offset_scale(int(-tare_counts), gpc, Q17_14_FRAC_BITS, Q16_15_FRAC_BITS) - self._mcu_trigger_analog.reset_filter() - - def _home_start(self, print_time): - # start trsync - trigger_completion = self._dispatch.start(print_time) - self._mcu_trigger_analog.home_start(print_time) - return trigger_completion - - def home_start(self, print_time, sample_time, sample_count, rest_time, - triggered=True): - return self._home_start(print_time) - - def home_wait(self, home_end_time): - self._dispatch.wait_end(home_end_time) - # trigger has happened, now to find out why... - res = self._dispatch.stop() - # clear the homing state so it stops processing samples - self._last_trigger_time = self._mcu_trigger_analog.clear_home() - if self._mcu.is_fileoutput(): - self._last_trigger_time = home_end_time - if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - error = "Load Cell Probe Error: unknown reason code %i" % (res,) - if res in self.ERROR_MAP: - error = self.ERROR_MAP[res] - raise self._printer.command_error(error) - if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: - return 0. - return self._last_trigger_time - - def get_steppers(self): - return self._dispatch.get_steppers() # Probe towards z_min until the trigger_analog on the MCU triggers def probing_move(self, gcmd): @@ -499,20 +487,22 @@ class LoadCellProbingMove: # start collector after tare samples are consumed collector = self._start_collector() # do homing move - return phoming.probing_move(self, pos, speed), collector + epos = phoming.probing_move(self._mcu_trigger_analog, pos, speed) + return epos, collector # Wait for the MCU to trigger with no movement def probing_test(self, gcmd, timeout): self._pause_and_tare(gcmd) toolhead = self._printer.lookup_object('toolhead') print_time = toolhead.get_last_move_time() - self._home_start(print_time) - return self.home_wait(print_time + timeout) + self._mcu_trigger_analog.home_start(print_time, 0., 0, 0.) + return self._mcu_trigger_analog.home_wait(print_time + timeout) def get_status(self, eventtime): + trig_time = self._mcu_trigger_analog.get_last_trigger_time() return { 'tare_counts': self._tare_counts, - 'last_trigger_time': self._last_trigger_time, + 'last_trigger_time': trig_time, } @@ -642,9 +632,8 @@ class LoadCellPrinterProbe: self._param_helper = probe.ProbeParameterHelper(config) self._cmd_helper = probe.ProbeCommandHelper(config, self) self._probe_offsets = probe.ProbeOffsetsHelper(config) - self._mcu_trigger_analog = MCU_trigger_analog(config, sensor, - continuous_tare_filter_helper.get_sos_filter(), config_helper, - trigger_dispatch) + self._mcu_trigger_analog = MCU_trigger_analog(sensor, + continuous_tare_filter_helper.get_sos_filter(), trigger_dispatch) load_cell_probing_move = LoadCellProbingMove(config, self._load_cell, self._mcu_trigger_analog, self._param_helper, continuous_tare_filter_helper, config_helper) From 402303aa2287e9f07fe363f500bdf064476358e3 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 18:05:08 -0500 Subject: [PATCH 339/411] trigger_analog: New trigger_analog.py file Rename sos_filter.py to trigger_analog.py and copy MCU_trigger_analog class from load_cell_probe.py to this new file. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 143 +---------------- .../{sos_filter.py => trigger_analog.py} | 145 +++++++++++++++++- 2 files changed, 149 insertions(+), 139 deletions(-) rename klippy/extras/{sos_filter.py => trigger_analog.py} (58%) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index ea4aaf767..4d53a320a 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -5,7 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging, math import mcu -from . import probe, sos_filter, load_cell, hx71x, ads1220 +from . import probe, trigger_analog, load_cell, hx71x, ads1220 np = None # delay NumPy import until configuration time @@ -161,7 +161,7 @@ class ContinuousTareFilter: # create a filter design from the parameters def design_filter(self, error_func): - return sos_filter.DigitalFilter(self.sps, error_func, self.drift, + return trigger_analog.DigitalFilter(self.sps, error_func, self.drift, self.drift_delay, self.buzz, self.buzz_delay, self.notches, self.notch_quality) @@ -209,8 +209,8 @@ class ContinuousTareFilterHelper: buzz_delay, notches, notch_quality) def _create_filter(self, design, cmd_queue): - sf = sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, - Q2_29_FRAC_BITS) + sf = trigger_analog.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, + Q2_29_FRAC_BITS) sf.set_filter_design(design) return sf @@ -287,139 +287,6 @@ class LoadCellProbeConfigHelper: return 1. / counts_per_gram -# MCU_trigger_analog is the interface to `trigger_analog` on the MCU -# This also manages the SosFilter so all commands use one command queue -class MCU_trigger_analog: - MONITOR_MAX = 3 - ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 - ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 - ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 - ERROR_MAP = { - mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " - "homing", - ERROR_SAFETY_RANGE: "sensor exceeds safety limit", - ERROR_OVERFLOW: "fixed point math overflow", - ERROR_WATCHDOG: "timed out waiting for sensor data" - } - - def __init__(self, sensor_inst, sos_filter_inst, trigger_dispatch): - self._printer = sensor_inst.get_mcu().get_printer() - self._sos_filter = sos_filter_inst - self._sensor = sensor_inst - self._mcu = self._sensor.get_mcu() - # configure MCU objects - self._dispatch = trigger_dispatch - self._cmd_queue = self._dispatch.get_command_queue() - self._oid = self._mcu.create_oid() - self._raw_min = self._raw_max = 0 - self._last_range_args = None - self._trigger_value = 0. - self._last_trigger_time = 0. - self._config_commands() - self._home_cmd = None - self._query_cmd = None - self._set_range_cmd = None - self._mcu.register_config_callback(self._build_config) - self._printer.register_event_handler("klippy:connect", self._on_connect) - - def _config_commands(self): - self._mcu.add_config_cmd( - "config_trigger_analog oid=%d sos_filter_oid=%d" % ( - self._oid, self._sos_filter.get_oid())) - - def _build_config(self): - # Lookup commands - self._query_cmd = self._mcu.lookup_query_command( - "trigger_analog_query_state oid=%c", - "trigger_analog_state oid=%c is_homing_trigger=%c " - "trigger_ticks=%u", oid=self._oid, cq=self._cmd_queue) - self._set_range_cmd = self._mcu.lookup_command( - "trigger_analog_set_range oid=%c safety_counts_min=%i" - " safety_counts_max=%i trigger_value=%i", cq=self._cmd_queue) - self._home_cmd = self._mcu.lookup_command( - "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" - " error_reason=%c clock=%u rest_ticks=%u timeout=%u", - cq=self._cmd_queue) - - # the sensor data stream is connected on the MCU at the ready event - def _on_connect(self): - self._sensor.attach_trigger_analog(self._oid) - - def get_oid(self): - return self._oid - - def get_mcu(self): - return self._mcu - - def get_sos_filter(self): - return self._sos_filter - - def get_dispatch(self): - return self._dispatch - - def get_last_trigger_time(self): - return self._last_trigger_time - - def set_trigger_value(self, trigger_value): - self._trigger_value = trigger_value - - def set_raw_range(self, raw_min, raw_max): - self._raw_min = raw_min - self._raw_max = raw_max - - def _reset_filter(self): - # Update parameters in mcu (if they have changed) - tval32 = self._sos_filter.convert_value(self._trigger_value) - args = [self._oid, self._raw_min, self._raw_max, tval32] - if args != self._last_range_args: - self._set_range_cmd.send(args) - self._last_range_args = args - # Update sos filter in mcu - self._sos_filter.reset_filter() - - def _clear_home(self): - params = self._query_cmd.send([self._oid]) - # The time of the first sample that triggered is in "trigger_ticks" - trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_ticks']) - # clear trsync from load_cell_endstop - self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) - return self._mcu.clock_to_print_time(trigger_ticks) - - def get_steppers(self): - return self._dispatch.get_steppers() - - def home_start(self, print_time, sample_time, sample_count, rest_time, - triggered=True): - self._last_trigger_time = 0. - self._reset_filter() - trigger_completion = self._dispatch.start(print_time) - clock = self._mcu.print_time_to_clock(print_time) - sensor_update = 1. / self._sensor.get_samples_per_second() - sm_ticks = self._mcu.seconds_to_clock(sensor_update) - self._home_cmd.send([self._oid, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, - sm_ticks, self.MONITOR_MAX], reqclock=clock) - return trigger_completion - - def home_wait(self, home_end_time): - self._dispatch.wait_end(home_end_time) - # trigger has happened, now to find out why... - res = self._dispatch.stop() - # clear the homing state so it stops processing samples - trigger_time = self._clear_home() - if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - defmsg = "unknown reason code %i" % (res,) - error_msg = self.ERROR_MAP.get(res, defmsg) - raise self._printer.command_error("Trigger analog error: %s" - % (error_msg,)) - if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: - return 0. - if self._mcu.is_fileoutput(): - trigger_time = home_end_time - self._last_trigger_time = trigger_time - return trigger_time - - # Execute probing moves using the MCU_trigger_analog class LoadCellProbingMove: def __init__(self, config, load_cell_inst, mcu_trigger_analog, param_helper, @@ -632,7 +499,7 @@ class LoadCellPrinterProbe: self._param_helper = probe.ProbeParameterHelper(config) self._cmd_helper = probe.ProbeCommandHelper(config, self) self._probe_offsets = probe.ProbeOffsetsHelper(config) - self._mcu_trigger_analog = MCU_trigger_analog(sensor, + self._mcu_trigger_analog = trigger_analog.MCU_trigger_analog(sensor, continuous_tare_filter_helper.get_sos_filter(), trigger_dispatch) load_cell_probing_move = LoadCellProbingMove(config, self._load_cell, self._mcu_trigger_analog, self._param_helper, diff --git a/klippy/extras/sos_filter.py b/klippy/extras/trigger_analog.py similarity index 58% rename from klippy/extras/sos_filter.py rename to klippy/extras/trigger_analog.py index 01a394377..482fa6272 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/trigger_analog.py @@ -1,8 +1,15 @@ -# Second Order Sections Filter +# Wrapper around mcu trigger_analog objects # # Copyright (C) 2025 Gareth Farrington +# Copyright (C) 2026 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. +import mcu + + +###################################################################### +# SOS filters (Second Order Sectional) +###################################################################### MAX_INT32 = (2 ** 31) MIN_INT32 = -(2 ** 31) - 1 @@ -196,3 +203,139 @@ class MCU_SosFilter: # Activate filter self._set_active_cmd.send([self._oid, num_sections, self._coeff_frac_bits]) + + +###################################################################### +# Trigger Analog +###################################################################### + +# MCU_trigger_analog is the interface to `trigger_analog` on the MCU +class MCU_trigger_analog: + MONITOR_MAX = 3 + ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 + ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 + ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 + ERROR_MAP = { + mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " + "homing", + ERROR_SAFETY_RANGE: "sensor exceeds safety limit", + ERROR_OVERFLOW: "fixed point math overflow", + ERROR_WATCHDOG: "timed out waiting for sensor data" + } + + def __init__(self, sensor_inst, sos_filter_inst, trigger_dispatch): + self._printer = sensor_inst.get_mcu().get_printer() + self._sos_filter = sos_filter_inst + self._sensor = sensor_inst + self._mcu = self._sensor.get_mcu() + # configure MCU objects + self._dispatch = trigger_dispatch + self._cmd_queue = self._dispatch.get_command_queue() + self._oid = self._mcu.create_oid() + self._raw_min = self._raw_max = 0 + self._last_range_args = None + self._trigger_value = 0. + self._last_trigger_time = 0. + self._config_commands() + self._home_cmd = None + self._query_cmd = None + self._set_range_cmd = None + self._mcu.register_config_callback(self._build_config) + self._printer.register_event_handler("klippy:connect", self._on_connect) + + def _config_commands(self): + self._mcu.add_config_cmd( + "config_trigger_analog oid=%d sos_filter_oid=%d" % ( + self._oid, self._sos_filter.get_oid())) + + def _build_config(self): + # Lookup commands + self._query_cmd = self._mcu.lookup_query_command( + "trigger_analog_query_state oid=%c", + "trigger_analog_state oid=%c is_homing_trigger=%c " + "trigger_ticks=%u", oid=self._oid, cq=self._cmd_queue) + self._set_range_cmd = self._mcu.lookup_command( + "trigger_analog_set_range oid=%c safety_counts_min=%i" + " safety_counts_max=%i trigger_value=%i", cq=self._cmd_queue) + self._home_cmd = self._mcu.lookup_command( + "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" + " error_reason=%c clock=%u rest_ticks=%u timeout=%u", + cq=self._cmd_queue) + + # the sensor data stream is connected on the MCU at the ready event + def _on_connect(self): + self._sensor.attach_trigger_analog(self._oid) + + def get_oid(self): + return self._oid + + def get_mcu(self): + return self._mcu + + def get_sos_filter(self): + return self._sos_filter + + def get_dispatch(self): + return self._dispatch + + def get_last_trigger_time(self): + return self._last_trigger_time + + def set_trigger_value(self, trigger_value): + self._trigger_value = trigger_value + + def set_raw_range(self, raw_min, raw_max): + self._raw_min = raw_min + self._raw_max = raw_max + + def _reset_filter(self): + # Update parameters in mcu (if they have changed) + tval32 = self._sos_filter.convert_value(self._trigger_value) + args = [self._oid, self._raw_min, self._raw_max, tval32] + if args != self._last_range_args: + self._set_range_cmd.send(args) + self._last_range_args = args + # Update sos filter in mcu + self._sos_filter.reset_filter() + + def _clear_home(self): + params = self._query_cmd.send([self._oid]) + # The time of the first sample that triggered is in "trigger_ticks" + trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_ticks']) + # clear trsync from load_cell_endstop + self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) + return self._mcu.clock_to_print_time(trigger_ticks) + + def get_steppers(self): + return self._dispatch.get_steppers() + + def home_start(self, print_time, sample_time, sample_count, rest_time, + triggered=True): + self._last_trigger_time = 0. + self._reset_filter() + trigger_completion = self._dispatch.start(print_time) + clock = self._mcu.print_time_to_clock(print_time) + sensor_update = 1. / self._sensor.get_samples_per_second() + sm_ticks = self._mcu.seconds_to_clock(sensor_update) + self._home_cmd.send([self._oid, self._dispatch.get_oid(), + mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, + sm_ticks, self.MONITOR_MAX], reqclock=clock) + return trigger_completion + + def home_wait(self, home_end_time): + self._dispatch.wait_end(home_end_time) + # trigger has happened, now to find out why... + res = self._dispatch.stop() + # clear the homing state so it stops processing samples + trigger_time = self._clear_home() + if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + defmsg = "unknown reason code %i" % (res,) + error_msg = self.ERROR_MAP.get(res, defmsg) + raise self._printer.command_error("Trigger analog error: %s" + % (error_msg,)) + if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: + return 0. + if self._mcu.is_fileoutput(): + trigger_time = home_end_time + self._last_trigger_time = trigger_time + return trigger_time From f9b1b9c1b5775d8a7a2468c837a65ab9ac994f33 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 19:41:43 -0500 Subject: [PATCH 340/411] trigger_analog: Create trigger_dispatch within MCU_trigger_analog Don't require callers of MCU_trigger_analog to create the mcu.TriggerDispatch() instance - instead, create it within the MCU_trigger_analog() class. Also, make it easier to use MCU_trigger_analog without an MCU_SosFilter - the MCU_trigger_analog can automatically create an empty filter if needed. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 32 +++++++++++++------------------ klippy/extras/trigger_analog.py | 33 ++++++++++++++++---------------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 4d53a320a..c15a57fd7 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -168,8 +168,9 @@ class ContinuousTareFilter: # Combine ContinuousTareFilter and SosFilter into an easy-to-use class class ContinuousTareFilterHelper: - def __init__(self, config, sensor, cmd_queue): + def __init__(self, config, sensor, sos_filter): self._sensor = sensor + self._sos_filter = sos_filter self._sps = self._sensor.get_samples_per_second() max_filter_frequency = math.floor(self._sps / 2.) # setup filter parameters @@ -194,8 +195,8 @@ class ContinuousTareFilterHelper: self._config_design = self._build_filter() # filter design currently inside the MCU self._active_design = self._config_design - self._sos_filter = self._create_filter( - self._active_design.design_filter(config.error), cmd_queue) + design = self._active_design.design_filter(config.error) + self._sos_filter.set_filter_design(design) def _build_filter(self, gcmd=None): drift = self._drift_param.get(gcmd) @@ -208,12 +209,6 @@ class ContinuousTareFilterHelper: return ContinuousTareFilter(self._sps, drift, drift_delay, buzz, buzz_delay, notches, notch_quality) - def _create_filter(self, design, cmd_queue): - sf = trigger_analog.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, - Q2_29_FRAC_BITS) - sf.set_filter_design(design) - return sf - def update_from_command(self, gcmd, cq=None): gcmd_filter = self._build_filter(gcmd) # if filters are identical, no change required @@ -223,9 +218,6 @@ class ContinuousTareFilterHelper: design = self._active_design.design_filter(gcmd.error) self._sos_filter.set_filter_design(design) - def get_sos_filter(self): - return self._sos_filter - # check results from the collector for errors and raise an exception is found def check_sensor_errors(results, printer): @@ -299,8 +291,8 @@ class LoadCellProbingMove: self._config_helper = config_helper self._mcu = mcu_trigger_analog.get_mcu() self._z_min_position = probe.lookup_minimum_z(config) - self._dispatch = mcu_trigger_analog.get_dispatch() - probe.LookupZSteppers(config, self._dispatch.add_stepper) + dispatch = mcu_trigger_analog.get_dispatch() + probe.LookupZSteppers(config, dispatch.add_stepper) # internal state tracking self._tare_counts = 0 @@ -492,15 +484,17 @@ class LoadCellPrinterProbe: # Read all user configuration and build modules config_helper = LoadCellProbeConfigHelper(config, self._load_cell) self._mcu = self._load_cell.get_sensor().get_mcu() - trigger_dispatch = mcu.TriggerDispatch(self._mcu) - continuous_tare_filter_helper = ContinuousTareFilterHelper(config, - sensor, trigger_dispatch.get_command_queue()) + self._mcu_trigger_analog = trigger_analog.MCU_trigger_analog(sensor) + cmd_queue = self._mcu_trigger_analog.get_dispatch().get_command_queue() + sos_filter = trigger_analog.MCU_SosFilter(self._mcu, cmd_queue, 4, + Q2_29_FRAC_BITS) + self._mcu_trigger_analog.setup_sos_filter(sos_filter) + continuous_tare_filter_helper = ContinuousTareFilterHelper( + config, sensor, sos_filter) # Probe Interface self._param_helper = probe.ProbeParameterHelper(config) self._cmd_helper = probe.ProbeCommandHelper(config, self) self._probe_offsets = probe.ProbeOffsetsHelper(config) - self._mcu_trigger_analog = trigger_analog.MCU_trigger_analog(sensor, - continuous_tare_filter_helper.get_sos_filter(), trigger_dispatch) load_cell_probing_move = LoadCellProbingMove(config, self._load_cell, self._mcu_trigger_analog, self._param_helper, continuous_tare_filter_helper, config_helper) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 482fa6272..78c4e7c70 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -201,8 +201,9 @@ class MCU_SosFilter: self._set_offset_scale_cmd.send(args) self._last_sent_offset_scale = args # Activate filter - self._set_active_cmd.send([self._oid, num_sections, - self._coeff_frac_bits]) + if self._max_sections: + self._set_active_cmd.send([self._oid, num_sections, + self._coeff_frac_bits]) ###################################################################### @@ -223,44 +224,44 @@ class MCU_trigger_analog: ERROR_WATCHDOG: "timed out waiting for sensor data" } - def __init__(self, sensor_inst, sos_filter_inst, trigger_dispatch): + def __init__(self, sensor_inst): self._printer = sensor_inst.get_mcu().get_printer() - self._sos_filter = sos_filter_inst self._sensor = sensor_inst self._mcu = self._sensor.get_mcu() + self._sos_filter = None # configure MCU objects - self._dispatch = trigger_dispatch - self._cmd_queue = self._dispatch.get_command_queue() + self._dispatch = mcu.TriggerDispatch(self._mcu) self._oid = self._mcu.create_oid() self._raw_min = self._raw_max = 0 self._last_range_args = None self._trigger_value = 0. self._last_trigger_time = 0. - self._config_commands() - self._home_cmd = None - self._query_cmd = None - self._set_range_cmd = None + self._home_cmd = self._query_cmd = self._set_range_cmd = None self._mcu.register_config_callback(self._build_config) self._printer.register_event_handler("klippy:connect", self._on_connect) - def _config_commands(self): + def setup_sos_filter(self, sos_filter): + self._sos_filter = sos_filter + + def _build_config(self): + cmd_queue = self._dispatch.get_command_queue() + if self._sos_filter is None: + self.setup_sos_filter(MCU_SosFilter(self._mcu, cmd_queue, 0, 0)) self._mcu.add_config_cmd( "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) - - def _build_config(self): # Lookup commands self._query_cmd = self._mcu.lookup_query_command( "trigger_analog_query_state oid=%c", "trigger_analog_state oid=%c is_homing_trigger=%c " - "trigger_ticks=%u", oid=self._oid, cq=self._cmd_queue) + "trigger_ticks=%u", oid=self._oid, cq=cmd_queue) self._set_range_cmd = self._mcu.lookup_command( "trigger_analog_set_range oid=%c safety_counts_min=%i" - " safety_counts_max=%i trigger_value=%i", cq=self._cmd_queue) + " safety_counts_max=%i trigger_value=%i", cq=cmd_queue) self._home_cmd = self._mcu.lookup_command( "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" " error_reason=%c clock=%u rest_ticks=%u timeout=%u", - cq=self._cmd_queue) + cq=cmd_queue) # the sensor data stream is connected on the MCU at the ready event def _on_connect(self): From 5bd791d96e705c7506cb48ca1be157c01911bfc8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 20:02:21 -0500 Subject: [PATCH 341/411] hx71x: Make sure to use the same cmd_queue for all commands Signed-off-by: Kevin O'Connor --- klippy/extras/hx71x.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index 60fe6047c..f0d904a89 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -63,13 +63,14 @@ class HX71xBase: mcu.register_config_callback(self._build_config) def _build_config(self): + cmd_queue = self.mcu.alloc_command_queue() self.query_hx71x_cmd = self.mcu.lookup_command( - "query_hx71x oid=%c rest_ticks=%u") + "query_hx71x oid=%c rest_ticks=%u", cq=cmd_queue) self.attach_probe_cmd = self.mcu.lookup_command( - "hx71x_attach_trigger_analog oid=%c trigger_analog_oid=%c") + "hx71x_attach_trigger_analog oid=%c trigger_analog_oid=%c", + cq=cmd_queue) self.ffreader.setup_query_command("query_hx71x_status oid=%c", - oid=self.oid, - cq=self.mcu.alloc_command_queue()) + oid=self.oid, cq=cmd_queue) def get_mcu(self): From 87c8b505a7f77d03a8b6b15e733628ebe504fe08 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 20:10:47 -0500 Subject: [PATCH 342/411] trigger_analog: Attach trigger_analog to sensor during initialization Avoid setting up a "connect" callback - just register the association using mcu.add_config_cmd() . Signed-off-by: Kevin O'Connor --- klippy/extras/ads1220.py | 11 +++++------ klippy/extras/hx71x.py | 12 +++++------- klippy/extras/trigger_analog.py | 6 +----- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index fda584ac3..809c44435 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -96,7 +96,6 @@ class ADS1220: self.printer, self._process_batch, self._start_measurements, self._finish_measurements, UPDATE_INTERVAL) # Command Configuration - self.attach_probe_cmd = None mcu.add_config_cmd( "config_ads1220 oid=%d spi_oid=%d data_ready_pin=%s" % (self.oid, self.spi.get_oid(), self.data_ready_pin)) @@ -105,12 +104,15 @@ class ADS1220: mcu.register_config_callback(self._build_config) self.query_ads1220_cmd = None + def setup_trigger_analog(self, trigger_analog_oid): + self.mcu.add_config_cmd( + "ads1220_attach_trigger_analog oid=%d trigger_analog_oid=%d" + % (self.oid, trigger_analog_oid), is_init=True) + def _build_config(self): cmdqueue = self.spi.get_command_queue() self.query_ads1220_cmd = self.mcu.lookup_command( "query_ads1220 oid=%c rest_ticks=%u", cq=cmdqueue) - self.attach_probe_cmd = self.mcu.lookup_command( - "ads1220_attach_trigger_analog oid=%c trigger_analog_oid=%c") self.ffreader.setup_query_command("query_ads1220_status oid=%c", oid=self.oid, cq=cmdqueue) @@ -129,9 +131,6 @@ class ADS1220: def add_client(self, callback): self.batch_bulk.add_client(callback) - def attach_trigger_analog(self, trigger_analog_oid): - self.attach_probe_cmd.send([self.oid, trigger_analog_oid]) - # Measurement decoding def _convert_samples(self, samples): adc_factor = 1. / (1 << 23) diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index f0d904a89..e9d7bf605 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -53,7 +53,6 @@ class HX71xBase: self._finish_measurements, UPDATE_INTERVAL) # Command Configuration self.query_hx71x_cmd = None - self.attach_probe_cmd = None mcu.add_config_cmd( "config_hx71x oid=%d gain_channel=%d dout_pin=%s sclk_pin=%s" % (self.oid, self.gain_channel, self.dout_pin, self.sclk_pin)) @@ -62,13 +61,15 @@ class HX71xBase: mcu.register_config_callback(self._build_config) + def setup_trigger_analog(self, trigger_analog_oid): + self.mcu.add_config_cmd( + "hx71x_attach_trigger_analog oid=%d trigger_analog_oid=%d" + % (self.oid, trigger_analog_oid), is_init=True) + def _build_config(self): cmd_queue = self.mcu.alloc_command_queue() self.query_hx71x_cmd = self.mcu.lookup_command( "query_hx71x oid=%c rest_ticks=%u", cq=cmd_queue) - self.attach_probe_cmd = self.mcu.lookup_command( - "hx71x_attach_trigger_analog oid=%c trigger_analog_oid=%c", - cq=cmd_queue) self.ffreader.setup_query_command("query_hx71x_status oid=%c", oid=self.oid, cq=cmd_queue) @@ -88,9 +89,6 @@ class HX71xBase: def add_client(self, callback): self.batch_bulk.add_client(callback) - def attach_trigger_analog(self, trigger_analog_oid): - self.attach_probe_cmd.send([self.oid, trigger_analog_oid]) - # Measurement decoding def _convert_samples(self, samples): adc_factor = 1. / (1 << 23) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 78c4e7c70..4e149cdf0 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -238,12 +238,12 @@ class MCU_trigger_analog: self._last_trigger_time = 0. self._home_cmd = self._query_cmd = self._set_range_cmd = None self._mcu.register_config_callback(self._build_config) - self._printer.register_event_handler("klippy:connect", self._on_connect) def setup_sos_filter(self, sos_filter): self._sos_filter = sos_filter def _build_config(self): + self._sensor.setup_trigger_analog(self._oid) cmd_queue = self._dispatch.get_command_queue() if self._sos_filter is None: self.setup_sos_filter(MCU_SosFilter(self._mcu, cmd_queue, 0, 0)) @@ -263,10 +263,6 @@ class MCU_trigger_analog: " error_reason=%c clock=%u rest_ticks=%u timeout=%u", cq=cmd_queue) - # the sensor data stream is connected on the MCU at the ready event - def _on_connect(self): - self._sensor.attach_trigger_analog(self._oid) - def get_oid(self): return self._oid From 73a6184407996fe3553daf70182f5e116f0b8a54 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 21:17:59 -0500 Subject: [PATCH 343/411] trigger_analog: Check if trigger_analog is allocated in trigger_analog_update() Check if the trigger_analog struct has been allocated in trigger_analog_update() itself. This makes the code easier to use in the sensor code. Signed-off-by: Kevin O'Connor --- src/sensor_ads1220.c | 4 +--- src/sensor_hx71x.c | 2 +- src/trigger_analog.c | 3 +++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sensor_ads1220.c b/src/sensor_ads1220.c index 93d52b6ae..fdf770cf6 100644 --- a/src/sensor_ads1220.c +++ b/src/sensor_ads1220.c @@ -97,9 +97,7 @@ ads1220_read_adc(struct ads1220_adc *ads1220, uint8_t oid) counts |= 0xFF000000; // endstop is optional, report if enabled and no errors - if (ads1220->ta) { - trigger_analog_update(ads1220->ta, counts); - } + trigger_analog_update(ads1220->ta, counts); add_sample(ads1220, oid, counts); } diff --git a/src/sensor_hx71x.c b/src/sensor_hx71x.c index 2c09e97af..7d869d9d5 100644 --- a/src/sensor_hx71x.c +++ b/src/sensor_hx71x.c @@ -178,7 +178,7 @@ hx71x_read_adc(struct hx71x_adc *hx71x, uint8_t oid) } // probe is optional, report if enabled - if (hx71x->last_error == 0 && hx71x->ta) { + if (hx71x->last_error == 0) { trigger_analog_update(hx71x->ta, counts); } diff --git a/src/trigger_analog.c b/src/trigger_analog.c index df64a4f40..fafc63fc6 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -76,6 +76,9 @@ trigger_error(struct trigger_analog *ta, uint8_t error_code) void trigger_analog_update(struct trigger_analog *ta, const int32_t sample) { + if (!ta) + return; + // only process samples when homing uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); if (!is_homing) { From 147022dee2e18b1b98ab478a5f00e390bcb58b30 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 22:16:22 -0500 Subject: [PATCH 344/411] trigger_analog: Update to support generic trigger types Rework the trigger_analog code to support different "trigger" conditions. This merges in features of ldc1612.c into trigger_analog.c, such as error code reporting in the MCU. This is in preparation for using trigger_analog with ldc1612. Signed-off-by: Kevin O'Connor --- klippy/extras/ads1220.py | 3 + klippy/extras/hx71x.py | 3 + klippy/extras/load_cell_probe.py | 2 +- klippy/extras/trigger_analog.py | 87 ++++++---- src/trigger_analog.c | 265 +++++++++++++++++-------------- src/trigger_analog.h | 1 + 6 files changed, 202 insertions(+), 159 deletions(-) diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index 809c44435..891783922 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -122,6 +122,9 @@ class ADS1220: def get_samples_per_second(self): return self.sps + def lookup_sensor_error(self, error_code): + return "Unknown ads1220 error" % (error_code,) + # returns a tuple of the minimum and maximum value of the sensor, used to # detect if a data value is saturated def get_range(self): diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index e9d7bf605..a1bc20529 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -80,6 +80,9 @@ class HX71xBase: def get_samples_per_second(self): return self.sps + def lookup_sensor_error(self, error_code): + return "Unknown hx71x error %d" % (error_code,) + # returns a tuple of the minimum and maximum value of the sensor, used to # detect if a data value is saturated def get_range(self): diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index c15a57fd7..5f2a3c111 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -322,7 +322,7 @@ class LoadCellProbingMove: safety_min, safety_max = self._config_helper.get_safety_range(gcmd) self._mcu_trigger_analog.set_raw_range(safety_min, safety_max) trigger_val = self._config_helper.get_trigger_force_grams(gcmd) - self._mcu_trigger_analog.set_trigger_value(trigger_val) + self._mcu_trigger_analog.set_trigger("abs_ge", trigger_val) # update internal tare value gpc = self._config_helper.get_grams_per_count() sos_filter = self._mcu_trigger_analog.get_sos_filter() diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 4e149cdf0..2a6952ccb 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -213,30 +213,28 @@ class MCU_SosFilter: # MCU_trigger_analog is the interface to `trigger_analog` on the MCU class MCU_trigger_analog: MONITOR_MAX = 3 - ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 - ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 - ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 - ERROR_MAP = { - mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " - "homing", - ERROR_SAFETY_RANGE: "sensor exceeds safety limit", - ERROR_OVERFLOW: "fixed point math overflow", - ERROR_WATCHDOG: "timed out waiting for sensor data" - } - + REASON_TRIGGER_ANALOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 def __init__(self, sensor_inst): self._printer = sensor_inst.get_mcu().get_printer() self._sensor = sensor_inst self._mcu = self._sensor.get_mcu() self._sos_filter = None - # configure MCU objects self._dispatch = mcu.TriggerDispatch(self._mcu) - self._oid = self._mcu.create_oid() + self._last_trigger_time = 0. + # Raw range checking self._raw_min = self._raw_max = 0 self._last_range_args = None + # Trigger type + self._trigger_type = "unspecified" self._trigger_value = 0. - self._last_trigger_time = 0. - self._home_cmd = self._query_cmd = self._set_range_cmd = None + self._last_trigger_args = None + # Error codes from MCU + self._error_map = {} + self._sensor_specific_error = 0 + # Configure MCU objects + self._oid = self._mcu.create_oid() + self._home_cmd = self._query_state_cmd = None + self._set_raw_range_cmd = self._set_trigger_cmd = None self._mcu.register_config_callback(self._build_config) def setup_sos_filter(self, sos_filter): @@ -251,17 +249,24 @@ class MCU_trigger_analog: "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) # Lookup commands - self._query_cmd = self._mcu.lookup_query_command( + self._query_state_cmd = self._mcu.lookup_query_command( "trigger_analog_query_state oid=%c", - "trigger_analog_state oid=%c is_homing_trigger=%c " - "trigger_ticks=%u", oid=self._oid, cq=cmd_queue) - self._set_range_cmd = self._mcu.lookup_command( - "trigger_analog_set_range oid=%c safety_counts_min=%i" - " safety_counts_max=%i trigger_value=%i", cq=cmd_queue) + "trigger_analog_state oid=%c homing=%c trigger_clock=%u", + oid=self._oid, cq=cmd_queue) + self._set_raw_range_cmd = self._mcu.lookup_command( + "trigger_analog_set_raw_range oid=%c raw_min=%i raw_max=%i", + cq=cmd_queue) + self._set_trigger_cmd = self._mcu.lookup_command( + "trigger_analog_set_trigger oid=%c trigger_analog_type=%c" + " trigger_value=%i", cq=cmd_queue) self._home_cmd = self._mcu.lookup_command( "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" - " error_reason=%c clock=%u rest_ticks=%u timeout=%u", + " error_reason=%c clock=%u monitor_ticks=%u monitor_max=%u", cq=cmd_queue) + # Load errors from mcu + errors = self._mcu.get_enumerations().get("trigger_analog_error:", {}) + self._error_map = {v: k for k, v in errors.items()} + self._sensor_specific_error = errors.get("SENSOR_SPECIFIC", 0) def get_oid(self): return self._oid @@ -278,7 +283,8 @@ class MCU_trigger_analog: def get_last_trigger_time(self): return self._last_trigger_time - def set_trigger_value(self, trigger_value): + def set_trigger(self, trigger_type, trigger_value): + self._trigger_type = trigger_type self._trigger_value = trigger_value def set_raw_range(self, raw_min, raw_max): @@ -286,21 +292,24 @@ class MCU_trigger_analog: self._raw_max = raw_max def _reset_filter(self): - # Update parameters in mcu (if they have changed) - tval32 = self._sos_filter.convert_value(self._trigger_value) - args = [self._oid, self._raw_min, self._raw_max, tval32] + # Update raw range parameters in mcu (if they have changed) + args = [self._oid, self._raw_min, self._raw_max] if args != self._last_range_args: - self._set_range_cmd.send(args) + self._set_raw_range_cmd.send(args) self._last_range_args = args + # Update trigger in mcu (if it has changed) + tval32 = self._sos_filter.convert_value(self._trigger_value) + args = [self._oid, self._trigger_type, tval32] + if args != self._last_trigger_args: + self._set_trigger_cmd.send(args) + self._last_trigger_args = args # Update sos filter in mcu self._sos_filter.reset_filter() def _clear_home(self): - params = self._query_cmd.send([self._oid]) - # The time of the first sample that triggered is in "trigger_ticks" - trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_ticks']) - # clear trsync from load_cell_endstop self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) + params = self._query_state_cmd.send([self._oid]) + trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_clock']) return self._mcu.clock_to_print_time(trigger_ticks) def get_steppers(self): @@ -315,8 +324,8 @@ class MCU_trigger_analog: sensor_update = 1. / self._sensor.get_samples_per_second() sm_ticks = self._mcu.seconds_to_clock(sensor_update) self._home_cmd.send([self._oid, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, - sm_ticks, self.MONITOR_MAX], reqclock=clock) + mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.REASON_TRIGGER_ANALOG, + clock, sm_ticks, self.MONITOR_MAX], reqclock=clock) return trigger_completion def home_wait(self, home_end_time): @@ -326,8 +335,16 @@ class MCU_trigger_analog: # clear the homing state so it stops processing samples trigger_time = self._clear_home() if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - defmsg = "unknown reason code %i" % (res,) - error_msg = self.ERROR_MAP.get(res, defmsg) + if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + raise self._printer.command_error( + "Communication timeout during homing") + error_code = res - self.REASON_TRIGGER_ANALOG + if error_code >= self._sensor_specific_error: + sensor_err = error_code - self._sensor_specific_error + error_msg = self._sensor.lookup_sensor_error(sensor_err) + else: + defmsg = "Unknown code %i" % (error_code,) + error_msg = self._error_map.get(error_code, defmsg) raise self._printer.command_error("Trigger analog error: %s" % (error_msg,)) if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: diff --git a/src/trigger_analog.c b/src/trigger_analog.c index fafc63fc6..f7ec477ec 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -1,11 +1,13 @@ // Support homing/probing "trigger" notification from analog sensors // // Copyright (C) 2025 Gareth Farrington +// Copyright (C) 2024-2026 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. #include // abs #include "basecmd.h" // oid_alloc +#include "board/io.h" // writeb #include "board/misc.h" // timer_read_time #include "command.h" // DECL_COMMAND #include "sched.h" // shutdown @@ -13,133 +15,149 @@ #include "trigger_analog.h" // trigger_analog_update #include "trsync.h" // trsync_do_trigger -#define ERROR_SAFETY_RANGE 0 -#define ERROR_OVERFLOW 1 -#define ERROR_WATCHDOG 2 - -// Flags -enum {FLAG_IS_HOMING = 1 << 0 - , FLAG_IS_HOMING_TRIGGER = 1 << 1 - , FLAG_AWAIT_HOMING = 1 << 2 -}; - -// Endstop Structure +// Main trigger_analog storage struct trigger_analog { - struct timer time; - uint32_t trigger_ticks, last_sample_ticks, rest_ticks; - uint32_t homing_start_time; - struct trsync *ts; - int32_t safety_counts_min, safety_counts_max; - uint8_t flags, trigger_reason, error_reason, watchdog_max, watchdog_count; - int32_t trigger_value; + // Raw value range check + int32_t raw_min, raw_max; + // Filtering struct sos_filter *sf; + // Trigger value checking + int32_t trigger_value; + uint8_t trigger_type; + // Trsync triggering + uint8_t flags, trigger_reason, error_reason; + struct trsync *ts; + uint32_t homing_clock, trigger_clock; + // Sensor activity monitoring + uint8_t monitor_max, monitor_count; + struct timer time; + uint32_t monitor_ticks; }; -static inline uint8_t -is_flag_set(const uint8_t mask, struct trigger_analog *ta) -{ - return !!(mask & ta->flags); -} +// Homing flags +enum { + TA_AWAIT_HOMING = 1<<1, TA_CAN_TRIGGER = 1<<2 +}; -static inline void -set_flag(uint8_t mask, struct trigger_analog *ta) -{ - ta->flags |= mask; -} +// Trigger types +enum { + TT_ABS_GE, TT_GT +}; +DECL_ENUMERATION("trigger_analog_type", "abs_ge", TT_ABS_GE); +DECL_ENUMERATION("trigger_analog_type", "gt", TT_GT); -static inline void -clear_flag(uint8_t mask, struct trigger_analog *ta) -{ - ta->flags &= ~mask; -} +// Sample errors sent via trsync error code +enum { + TE_RAW_RANGE, TE_OVERFLOW, TE_MONITOR, TE_SENSOR_SPECIFIC +}; +DECL_ENUMERATION("trigger_analog_error:", "RAW_RANGE", TE_RAW_RANGE); +DECL_ENUMERATION("trigger_analog_error:", "OVERFLOW", TE_OVERFLOW); +DECL_ENUMERATION("trigger_analog_error:", "MONITOR", TE_MONITOR); +DECL_ENUMERATION("trigger_analog_error:", "SENSOR_SPECIFIC" + , TE_SENSOR_SPECIFIC); -void -try_trigger(struct trigger_analog *ta, uint32_t ticks) +// Timer callback that monitors for sensor timeouts +static uint_fast8_t +monitor_event(struct timer *t) { - uint8_t is_homing_triggered = is_flag_set(FLAG_IS_HOMING_TRIGGER, ta); - if (!is_homing_triggered) { - // the first triggering sample when homing sets the trigger time - ta->trigger_ticks = ticks; - // this flag latches until a reset, disabling further triggering - set_flag(FLAG_IS_HOMING_TRIGGER, ta); - trsync_do_trigger(ta->ts, ta->trigger_reason); + struct trigger_analog *ta = container_of(t, struct trigger_analog, time); + + if (!(ta->flags & TA_CAN_TRIGGER)) + return SF_DONE; + + if (ta->monitor_count > ta->monitor_max) { + trsync_do_trigger(ta->ts, ta->error_reason + TE_MONITOR); + return SF_DONE; } + + // A sample was recently delivered, continue monitoring + ta->monitor_count++; + ta->time.waketime += ta->monitor_ticks; + return SF_RESCHEDULE; } -void -trigger_error(struct trigger_analog *ta, uint8_t error_code) +// Note recent activity +static void +monitor_note_activity(struct trigger_analog *ta) { + writeb(&ta->monitor_count, 0); +} + +// Check if a value should signal a "trigger" event +static int +check_trigger(struct trigger_analog *ta, int32_t value) +{ + switch (ta->trigger_type) { + case TT_ABS_GE: + return abs(value) >= ta->trigger_value; + case TT_GT: + return value > ta->trigger_value; + } + return 0; +} + +// Stop homing due to an error +static void +cancel_homing(struct trigger_analog *ta, uint8_t error_code) +{ + if (!(ta->flags & TA_CAN_TRIGGER)) + return; + ta->flags = 0; trsync_do_trigger(ta->ts, ta->error_reason + error_code); } +// Handle an error reported by the sensor +void +trigger_analog_note_error(struct trigger_analog *ta, uint8_t sensor_code) +{ + if (!ta) + return; + cancel_homing(ta, sensor_code + TE_SENSOR_SPECIFIC); +} + // Used by Sensors to report new raw ADC sample void -trigger_analog_update(struct trigger_analog *ta, const int32_t sample) +trigger_analog_update(struct trigger_analog *ta, int32_t sample) { + // Check homing is active if (!ta) return; + uint8_t flags = ta->flags; + if (!(flags & TA_CAN_TRIGGER)) + return; - // only process samples when homing - uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); - if (!is_homing) { + // Check if homing has started + uint32_t time = timer_read_time(); + if ((flags & TA_AWAIT_HOMING) && timer_is_before(time, ta->homing_clock)) + return; + flags &= ~TA_AWAIT_HOMING; + + // Reset the sensor timeout checking + monitor_note_activity(ta); + + // Check that raw value is in range + if (sample < ta->raw_min || sample > ta->raw_max) { + cancel_homing(ta, TE_RAW_RANGE); return; } - // save new sample - uint32_t ticks = timer_read_time(); - ta->last_sample_ticks = ticks; - ta->watchdog_count = 0; - - // do not trigger before homing start time - uint8_t await_homing = is_flag_set(FLAG_AWAIT_HOMING, ta); - if (await_homing && timer_is_before(ticks, ta->homing_start_time)) { - return; - } - clear_flag(FLAG_AWAIT_HOMING, ta); - - // check for safety limit violations - const uint8_t is_safety_trigger = sample <= ta->safety_counts_min - || sample >= ta->safety_counts_max; - // too much force, this is an error while homing - if (is_safety_trigger) { - trigger_error(ta, ERROR_SAFETY_RANGE); - return; - } - - // perform filtering + // Perform filtering int32_t filtered_value = sample; int ret = sos_filter_apply(ta->sf, &filtered_value); if (ret) { - trigger_error(ta, ERROR_OVERFLOW); + cancel_homing(ta, TE_OVERFLOW); return; } - // update trigger state - if (abs(filtered_value) >= ta->trigger_value) { - try_trigger(ta, ta->last_sample_ticks); - } -} - -// Timer callback that monitors for timeouts -static uint_fast8_t -watchdog_event(struct timer *t) -{ - struct trigger_analog *ta = container_of(t, struct trigger_analog, time); - uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); - uint8_t is_homing_trigger = is_flag_set(FLAG_IS_HOMING_TRIGGER, ta); - // the watchdog stops when not homing or when trsync becomes triggered - if (!is_homing || is_homing_trigger) { - return SF_DONE; + // Check if this is a "trigger" + ret = check_trigger(ta, filtered_value); + if (ret) { + trsync_do_trigger(ta->ts, ta->trigger_reason); + ta->trigger_clock = time; + flags = 0; } - if (ta->watchdog_count > ta->watchdog_max) { - trigger_error(ta, ERROR_WATCHDOG); - } - ta->watchdog_count += 1; - - // A sample was recently delivered, continue monitoring - ta->time.waketime += ta->rest_ticks; - return SF_RESCHEDULE; + ta->flags = flags; } // Create a trigger_analog @@ -160,17 +178,27 @@ trigger_analog_oid_lookup(uint8_t oid) return oid_lookup(oid, command_config_trigger_analog); } -// Set the triggering range and tare value +// Set valid raw range void -command_trigger_analog_set_range(uint32_t *args) +command_trigger_analog_set_raw_range(uint32_t *args) { struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); - ta->safety_counts_min = args[1]; - ta->safety_counts_max = args[2]; - ta->trigger_value = args[3]; + ta->raw_min = args[1]; + ta->raw_max = args[2]; } -DECL_COMMAND(command_trigger_analog_set_range, "trigger_analog_set_range" - " oid=%c safety_counts_min=%i safety_counts_max=%i trigger_value=%i"); +DECL_COMMAND(command_trigger_analog_set_raw_range, + "trigger_analog_set_raw_range oid=%c raw_min=%i raw_max=%i"); + +// Set the triggering type and value +void +command_trigger_analog_set_trigger(uint32_t *args) +{ + struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); + ta->trigger_type = args[1]; + ta->trigger_value = args[2]; +} +DECL_COMMAND(command_trigger_analog_set_trigger, "trigger_analog_set_trigger" + " oid=%c trigger_analog_type=%c trigger_value=%i"); // Home an axis void @@ -178,42 +206,33 @@ command_trigger_analog_home(uint32_t *args) { struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); sched_del_timer(&ta->time); - // clear the homing trigger flag - clear_flag(FLAG_IS_HOMING_TRIGGER, ta); - clear_flag(FLAG_IS_HOMING, ta); - ta->trigger_ticks = 0; - ta->ts = NULL; - // 0 samples indicates homing is finished - if (args[3] == 0) { - // Disable end stop checking + ta->monitor_ticks = args[5]; + if (!ta->monitor_ticks) { + ta->flags = 0; + ta->ts = NULL; return; } ta->ts = trsync_oid_lookup(args[1]); ta->trigger_reason = args[2]; ta->error_reason = args[3]; - ta->time.waketime = args[4]; - ta->homing_start_time = args[4]; - ta->rest_ticks = args[5]; - ta->watchdog_max = args[6]; - ta->watchdog_count = 0; - ta->time.func = watchdog_event; - set_flag(FLAG_IS_HOMING, ta); - set_flag(FLAG_AWAIT_HOMING, ta); + ta->time.waketime = ta->homing_clock = args[4]; + ta->monitor_max = args[6]; + ta->monitor_count = 0; + ta->time.func = monitor_event; + ta->flags = TA_AWAIT_HOMING | TA_CAN_TRIGGER; sched_add_timer(&ta->time); } DECL_COMMAND(command_trigger_analog_home, "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" - " error_reason=%c clock=%u rest_ticks=%u timeout=%u"); + " error_reason=%c clock=%u monitor_ticks=%u monitor_max=%u"); void command_trigger_analog_query_state(uint32_t *args) { uint8_t oid = args[0]; struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); - sendf("trigger_analog_state oid=%c is_homing_trigger=%c trigger_ticks=%u" - , oid - , is_flag_set(FLAG_IS_HOMING_TRIGGER, ta) - , ta->trigger_ticks); + sendf("trigger_analog_state oid=%c homing=%c trigger_clock=%u" + , oid, !!(ta->flags & TA_CAN_TRIGGER), ta->trigger_clock); } DECL_COMMAND(command_trigger_analog_query_state , "trigger_analog_query_state oid=%c"); diff --git a/src/trigger_analog.h b/src/trigger_analog.h index 9867095e8..53b36ce63 100644 --- a/src/trigger_analog.h +++ b/src/trigger_analog.h @@ -4,6 +4,7 @@ #include // uint8_t struct trigger_analog *trigger_analog_oid_lookup(uint8_t oid); +void trigger_analog_note_error(struct trigger_analog *ta, uint8_t sensor_code); void trigger_analog_update(struct trigger_analog *ta, int32_t sample); #endif // trigger_analog.h From 58cc059e31d186797dc08e4cb35e5fae0d191bb5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 22:56:29 -0500 Subject: [PATCH 345/411] sensor_ldc1612: Convert homing code to use trigger_analog system Remove the homing code from sensor_ldc1612.c and utilize the generic homing support found in trigger_analog.c . Signed-off-by: Kevin O'Connor --- klippy/extras/ldc1612.py | 28 +++----- klippy/extras/probe_eddy_current.py | 54 +++++---------- src/Kconfig | 2 +- src/sensor_ldc1612.c | 102 ++++------------------------ 4 files changed, 40 insertions(+), 146 deletions(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index db539c650..085dc2a55 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -87,7 +87,6 @@ class LDC1612: self._sensor_errors = {} self.oid = oid = mcu.create_oid() self.query_ldc1612_cmd = None - self.ldc1612_setup_home_cmd = self.query_ldc1612_home_state_cmd = None self.clock_freq = config.getint("frequency", DEFAULT_LDC1612_FREQ, 2000000, 40000000) # Coil frequency divider, assume 12MHz is BTT Eddy @@ -120,23 +119,22 @@ class LDC1612: hdr = ('time', 'frequency', 'z') self.batch_bulk.add_mux_endpoint("ldc1612/dump_ldc1612", "sensor", self.name, {'header': hdr}) + def setup_trigger_analog(self, trigger_analog_oid): + self.mcu.add_config_cmd( + "ldc1612_attach_trigger_analog oid=%d trigger_analog_oid=%d" + % (self.oid, trigger_analog_oid), is_init=True) def _build_config(self): cmdqueue = self.i2c.get_command_queue() self.query_ldc1612_cmd = self.mcu.lookup_command( "query_ldc1612 oid=%c rest_ticks=%u", cq=cmdqueue) self.ffreader.setup_query_command("query_status_ldc1612 oid=%c", oid=self.oid, cq=cmdqueue) - self.ldc1612_setup_home_cmd = self.mcu.lookup_command( - "ldc1612_setup_home oid=%c clock=%u threshold=%u" - " trsync_oid=%c trigger_reason=%c error_reason=%c", cq=cmdqueue) - self.query_ldc1612_home_state_cmd = self.mcu.lookup_query_command( - "query_ldc1612_home_state oid=%c", - "ldc1612_home_state oid=%c homing=%c trigger_clock=%u", - oid=self.oid, cq=cmdqueue) errors = self.mcu.get_enumerations().get("ldc1612_error:", {}) self._sensor_errors = {v: k for k, v in errors.items()} def get_mcu(self): return self.i2c.get_mcu() + def get_samples_per_second(self): + return self.data_rate def read_reg(self, reg): if self.mcu.is_fileoutput(): return 0 @@ -148,20 +146,10 @@ class LDC1612: minclock=minclock) def add_client(self, cb): self.batch_bulk.add_client(cb) - # Homing - def setup_home(self, print_time, trigger_freq, - trsync_oid, hit_reason, err_reason): - clock = self.mcu.print_time_to_clock(print_time) - tfreq = int(trigger_freq / self.freq_conv + 0.5) - self.ldc1612_setup_home_cmd.send( - [self.oid, clock, tfreq, trsync_oid, hit_reason, err_reason]) - def clear_home(self): - self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0, 0]) - params = self.query_ldc1612_home_state_cmd.send([self.oid]) - tclock = self.mcu.clock32_to_clock64(params['trigger_clock']) - return self.mcu.clock_to_print_time(tclock) def lookup_sensor_error(self, error): return self._sensor_errors.get(error, "Unknown ldc1612 error") + def convert_frequency(self, freq): + return int(freq / self.freq_conv + 0.5) # Measurement decoding def _convert_samples(self, samples): freq_conv = self.freq_conv diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 4d97d46d6..d30c7d404 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -5,7 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging, math, bisect import mcu -from . import ldc1612, probe, manual_probe +from . import ldc1612, trigger_analog, probe, manual_probe OUT_OF_RANGE = 99.9 @@ -388,9 +388,10 @@ class EddyGatherSamples: self._probe_times.append((start_time, end_time, pos_time, None)) self._check_samples() +MAX_VALID_RAW_VALUE=0x03ffffff + # Helper for implementing PROBE style commands (descend until trigger) class EddyDescend: - REASON_SENSOR_ERROR = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 def __init__(self, config, sensor_helper, calibration, probe_offsets, param_helper): self._printer = config.get_printer() @@ -399,43 +400,20 @@ class EddyDescend: self._calibration = calibration self._probe_offsets = probe_offsets self._param_helper = param_helper + self._trigger_analog = trigger_analog.MCU_trigger_analog(sensor_helper) self._z_min_position = probe.lookup_minimum_z(config) - self._dispatch = mcu.TriggerDispatch(self._mcu) - self._trigger_time = 0. self._gather = None - probe.LookupZSteppers(config, self._dispatch.add_stepper) - # Interface for phoming.probing_move() - def get_steppers(self): - return self._dispatch.get_steppers() - def home_start(self, print_time, sample_time, sample_count, rest_time, - triggered=True): - self._trigger_time = 0. + dispatch = self._trigger_analog.get_dispatch() + probe.LookupZSteppers(config, dispatch.add_stepper) + def _prep_trigger_analog(self): + self._trigger_analog.set_raw_range(0, MAX_VALID_RAW_VALUE) z_offset = self._probe_offsets.get_offsets()[2] trigger_freq = self._calibration.height_to_freq(z_offset) - trigger_completion = self._dispatch.start(print_time) - self._sensor_helper.setup_home( - print_time, trigger_freq, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.REASON_SENSOR_ERROR) - return trigger_completion - def home_wait(self, home_end_time): - self._dispatch.wait_end(home_end_time) - trigger_time = self._sensor_helper.clear_home() - res = self._dispatch.stop() - if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - raise self._printer.command_error( - "Communication timeout during homing") - error_code = res - self.REASON_SENSOR_ERROR - error_msg = self._sensor_helper.lookup_sensor_error(error_code) - raise self._printer.command_error(error_msg) - if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: - return 0. - if self._mcu.is_fileoutput(): - trigger_time = home_end_time - self._trigger_time = trigger_time - return trigger_time + conv_freq = self._sensor_helper.convert_frequency(trigger_freq) + self._trigger_analog.set_trigger('gt', conv_freq) # Probe session interface def start_probe_session(self, gcmd): + self._prep_trigger_analog() offsets = self._probe_offsets.get_offsets() self._gather = EddyGatherSamples(self._printer, self._sensor_helper, self._calibration, offsets) @@ -447,9 +425,9 @@ class EddyDescend: speed = self._param_helper.get_probe_params(gcmd)['probe_speed'] # Perform probing move phoming = self._printer.lookup_object('homing') - trig_pos = phoming.probing_move(self, pos, speed) + trig_pos = phoming.probing_move(self._trigger_analog, pos, speed) # Extract samples - start_time = self._trigger_time + 0.050 + start_time = self._trigger_analog.get_last_trigger_time() + 0.050 end_time = start_time + 0.100 toolhead_pos = toolhead.get_position() self._gather.note_probe(start_time, end_time, toolhead_pos) @@ -472,13 +450,13 @@ class EddyEndstopWrapper: def add_stepper(self, stepper): pass def get_steppers(self): - return self._eddy_descend.get_steppers() + return self._eddy_descend._trigger_analog.get_steppers() def home_start(self, print_time, sample_time, sample_count, rest_time, triggered=True): - return self._eddy_descend.home_start( + return self._eddy_descend._trigger_analog.home_start( print_time, sample_time, sample_count, rest_time, triggered) def home_wait(self, home_end_time): - return self._eddy_descend.home_wait(home_end_time) + return self._eddy_descend._trigger_analog.home_wait(home_end_time) def query_endstop(self, print_time): return False # XXX # Interface for HomingViaProbeHelper diff --git a/src/Kconfig b/src/Kconfig index 8013b40f0..bf5149380 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -179,7 +179,7 @@ config NEED_SENSOR_BULK default y config WANT_TRIGGER_ANALOG bool - depends on WANT_HX71X || WANT_ADS1220 + depends on WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 default y config NEED_SOS_FILTER bool diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 35c69a2bd..2c794fafe 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -13,11 +13,10 @@ #include "i2ccmds.h" // i2cdev_oid_lookup #include "sched.h" // DECL_TASK #include "sensor_bulk.h" // sensor_bulk_report -#include "trsync.h" // trsync_do_trigger +#include "trigger_analog.h" // trigger_analog_update enum { LDC_PENDING = 1<<0, LDC_HAVE_INTB = 1<<1, - LH_AWAIT_HOMING = 1<<1, LH_CAN_TRIGGER = 1<<2 }; struct ldc1612 { @@ -27,12 +26,7 @@ struct ldc1612 { uint8_t flags; struct sensor_bulk sb; struct gpio_in intb_pin; - // homing - struct trsync *ts; - uint8_t homing_flags; - uint8_t trigger_reason, error_reason; - uint32_t trigger_threshold; - uint32_t homing_clock; + struct trigger_analog *ta; }; static struct task_wake ldc1612_wake; @@ -91,83 +85,15 @@ DECL_COMMAND(command_config_ldc1612_with_intb, "config_ldc1612_with_intb oid=%c i2c_oid=%c intb_pin=%c"); void -command_ldc1612_setup_home(uint32_t *args) -{ +ldc1612_attach_trigger_analog(uint32_t *args) { struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); - - ld->trigger_threshold = args[2]; - if (!ld->trigger_threshold) { - ld->ts = NULL; - ld->homing_flags = 0; - return; - } - ld->homing_clock = args[1]; - ld->ts = trsync_oid_lookup(args[3]); - ld->trigger_reason = args[4]; - ld->error_reason = args[5]; - ld->homing_flags = LH_AWAIT_HOMING | LH_CAN_TRIGGER; + ld->ta = trigger_analog_oid_lookup(args[1]); } -DECL_COMMAND(command_ldc1612_setup_home, - "ldc1612_setup_home oid=%c clock=%u threshold=%u" - " trsync_oid=%c trigger_reason=%c error_reason=%c"); +DECL_COMMAND(ldc1612_attach_trigger_analog, + "ldc1612_attach_trigger_analog oid=%c trigger_analog_oid=%c"); -void -command_query_ldc1612_home_state(uint32_t *args) -{ - struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); - sendf("ldc1612_home_state oid=%c homing=%c trigger_clock=%u" - , args[0], !!(ld->homing_flags & LH_CAN_TRIGGER), ld->homing_clock); -} -DECL_COMMAND(command_query_ldc1612_home_state, - "query_ldc1612_home_state oid=%c"); - -// Cancel homing due to an error -static void -cancel_homing(struct ldc1612 *ld, int error_code) -{ - if (!(ld->homing_flags & LH_CAN_TRIGGER)) - return; - ld->homing_flags = 0; - trsync_do_trigger(ld->ts, ld->error_reason + error_code); -} - -#define MAX_VALID_RAW_VALUE 0x03ffffff #define DATA_ERROR_AMPLITUDE (1L << 28) -static int -check_data_bits(struct ldc1612 *ld, uint32_t raw_data) { - // Ignore amplitude errors - raw_data &= ~DATA_ERROR_AMPLITUDE; - // Datasheet define valid frequency input as < F_ref / 4 - if (raw_data < MAX_VALID_RAW_VALUE) - return 0; - cancel_homing(ld, SE_SENSOR_ERROR); - return -1; -} - -// Check if a sample should trigger a homing event -static void -check_home(struct ldc1612 *ld, uint32_t raw_data) -{ - uint8_t homing_flags = ld->homing_flags; - if (!(homing_flags & LH_CAN_TRIGGER)) - return; - if (check_data_bits(ld, raw_data)) - return; - uint32_t data = raw_data & 0x0fffffff; - uint32_t time = timer_read_time(); - if ((homing_flags & LH_AWAIT_HOMING) - && timer_is_before(time, ld->homing_clock)) - return; - homing_flags &= ~LH_AWAIT_HOMING; - if (data > ld->trigger_threshold) { - homing_flags = 0; - ld->homing_clock = time; - trsync_do_trigger(ld->ts, ld->trigger_reason); - } - ld->homing_flags = homing_flags; -} - // Chip registers #define REG_DATA0_MSB 0x00 #define REG_DATA0_LSB 0x01 @@ -196,7 +122,7 @@ read_reg_status(struct ldc1612 *ld, uint16_t *status) static void report_sample_error(struct ldc1612 *ld, int error_code) { - cancel_homing(ld, error_code); + trigger_analog_note_error(ld->ta, error_code); uint8_t *d = &ld->sb.data[ld->sb.data_count]; d[0] = 0xff; @@ -238,12 +164,14 @@ ldc1612_query(struct ldc1612 *ld, uint8_t oid) goto out; } - // Check for endstop trigger - uint32_t data = ((uint32_t)d[0] << 24) - | ((uint32_t)d[1] << 16) - | ((uint32_t)d[2] << 8) - | ((uint32_t)d[3]); - check_home(ld, data); + // Check for homing trigger + uint32_t raw_data = (((uint32_t)d[0] << 24) | ((uint32_t)d[1] << 16) + | ((uint32_t)d[2] << 8) | ((uint32_t)d[3])); + if (raw_data & 0xe0000000) + trigger_analog_note_error(ld->ta, SE_SENSOR_ERROR); + else + trigger_analog_update(ld->ta, raw_data & 0x0fffffff); + out: ld->sb.data_count += BYTES_PER_SAMPLE; // Flush local buffer if needed From 16755481ccd0f08e24e1a552e6b3e961a2c906e8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 23 Jan 2026 18:38:12 -0500 Subject: [PATCH 346/411] trigger_analog: Support scaling the filter's initial start state Add a new set_start_state() method to MCU_SosFilter that can arrange for the filter to better handle a non-zero initial starting state. Also, this removes the previous 1.0 gram initial start state for load cells as it tares the initial value. Signed-off-by: Kevin O'Connor --- klippy/extras/trigger_analog.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 2a6952ccb..133eabea7 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -82,6 +82,7 @@ class MCU_SosFilter: self._coeff_frac_bits = coeff_frac_bits self._value_frac_bits = self._scale_frac_bits = 0 self._design = None + self._start_value = 0. self._offset = 0 self._scale = 1 self._set_section_cmd = self._set_state_cmd = None @@ -154,10 +155,16 @@ class MCU_SosFilter: % (nun_states,)) fixed_state = [] for col, value in enumerate(section): - fixed_state.append(to_fixed_32(value, self._value_frac_bits)) + adjval = value * self._start_value + fixed_state.append(to_fixed_32(adjval, self._value_frac_bits)) sos_state.append(fixed_state) return sos_state + # Set expected state when filter first starts (avoids filter + # "ringing" if sensor data has a known static offset) + def set_start_state(self, start_value): + self._start_value = start_value + # Set conversion of a raw value 1 to a 1.0 value processed by sos filter def set_offset_scale(self, offset, scale, scale_frac_bits=0, value_frac_bits=0): From 36b98981176a2429cbf1d811ac71f8a79f1588b1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 24 Jan 2026 18:39:14 -0500 Subject: [PATCH 347/411] trigger_analog: Set the MCU_SosFilter coeff_frac_bits in set_filter_design() This parameter is rarely changed and is directly tied to the coefficients in the filter "design". Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 3 +-- klippy/extras/trigger_analog.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 5f2a3c111..f36d46124 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -486,8 +486,7 @@ class LoadCellPrinterProbe: self._mcu = self._load_cell.get_sensor().get_mcu() self._mcu_trigger_analog = trigger_analog.MCU_trigger_analog(sensor) cmd_queue = self._mcu_trigger_analog.get_dispatch().get_command_queue() - sos_filter = trigger_analog.MCU_SosFilter(self._mcu, cmd_queue, 4, - Q2_29_FRAC_BITS) + sos_filter = trigger_analog.MCU_SosFilter(self._mcu, cmd_queue, 4) self._mcu_trigger_analog.setup_sos_filter(sos_filter) continuous_tare_filter_helper = ContinuousTareFilterHelper( config, sensor, sos_filter) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 133eabea7..279e542f1 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -74,17 +74,20 @@ class DigitalFilter: class MCU_SosFilter: # max_sections should be the largest number of sections you expect # to use at runtime. - def __init__(self, mcu, cmd_queue, max_sections, coeff_frac_bits=29): + def __init__(self, mcu, cmd_queue, max_sections): self._mcu = mcu self._cmd_queue = cmd_queue - self._oid = self._mcu.create_oid() self._max_sections = max_sections - self._coeff_frac_bits = coeff_frac_bits - self._value_frac_bits = self._scale_frac_bits = 0 + # SOS filter "design" self._design = None + self._coeff_frac_bits = 0 self._start_value = 0. + # Offset and scaling self._offset = 0 self._scale = 1 + self._value_frac_bits = self._scale_frac_bits = 0 + # MCU commands + self._oid = self._mcu.create_oid() self._set_section_cmd = self._set_state_cmd = None self._set_active_cmd = self._set_offset_scale_cmd = None self._last_sent_coeffs = [None] * self._max_sections @@ -175,8 +178,9 @@ class MCU_SosFilter: self._scale_frac_bits = scale_frac_bits # Change the filter coefficients and state at runtime - def set_filter_design(self, design): + def set_filter_design(self, design, coeff_frac_bits=29): self._design = design + self._coeff_frac_bits = coeff_frac_bits # Resets the filter state back to initial conditions at runtime def reset_filter(self): @@ -251,7 +255,7 @@ class MCU_trigger_analog: self._sensor.setup_trigger_analog(self._oid) cmd_queue = self._dispatch.get_command_queue() if self._sos_filter is None: - self.setup_sos_filter(MCU_SosFilter(self._mcu, cmd_queue, 0, 0)) + self.setup_sos_filter(MCU_SosFilter(self._mcu, cmd_queue, 0)) self._mcu.add_config_cmd( "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) From 30720d29b51c5c70de732b24e180c0a9233b775c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 24 Jan 2026 20:05:12 -0500 Subject: [PATCH 348/411] trigger_analog: Avoid storing value_frac_bits in MCU_SosFilter It is simpler for callers to convert values to their desired format. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 22 +++++++++++----------- klippy/extras/trigger_analog.py | 30 +++++++++--------------------- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index f36d46124..1e361d5d1 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -9,9 +9,8 @@ from . import probe, trigger_analog, load_cell, hx71x, ads1220 np = None # delay NumPy import until configuration time -# constants for fixed point numbers -Q2_29_FRAC_BITS = 29 -Q16_15_FRAC_BITS = 15 +# MCU SOS filter scaled to "fractional grams" for consistent sensor precision +FRAC_GRAMS_CONV = 32768.0 class TapAnalysis: @@ -272,9 +271,9 @@ class LoadCellProbeConfigHelper: def get_grams_per_count(self): counts_per_gram = self._load_cell.get_counts_per_gram() # The counts_per_gram could be so large that it becomes 0.0 when - # converted to Q2.29 format. This would mean the ADC range only measures + # sent to the mcu. This would mean the ADC range only measures # a few grams which seems very unlikely. Treat this as an error: - if counts_per_gram >= 2**Q2_29_FRAC_BITS: + if counts_per_gram >= (1<<29): raise OverflowError("counts_per_gram value is too large to filter") return 1. / counts_per_gram @@ -318,17 +317,18 @@ class LoadCellProbingMove: self._continuous_tare_filter_helper.update_from_command(gcmd) # update the load cell so it reflects the new tare value self._load_cell.tare(tare_counts) - # update range and trigger + # update raw range safety_min, safety_max = self._config_helper.get_safety_range(gcmd) self._mcu_trigger_analog.set_raw_range(safety_min, safety_max) - trigger_val = self._config_helper.get_trigger_force_grams(gcmd) - self._mcu_trigger_analog.set_trigger("abs_ge", trigger_val) # update internal tare value - gpc = self._config_helper.get_grams_per_count() + gpc = self._config_helper.get_grams_per_count() * FRAC_GRAMS_CONV sos_filter = self._mcu_trigger_analog.get_sos_filter() Q17_14_FRAC_BITS = 14 - sos_filter.set_offset_scale(int(-tare_counts), gpc, - Q17_14_FRAC_BITS, Q16_15_FRAC_BITS) + sos_filter.set_offset_scale(int(-tare_counts), gpc, Q17_14_FRAC_BITS) + # update trigger + trigger_val = self._config_helper.get_trigger_force_grams(gcmd) + trigger_frac_grams = trigger_val * FRAC_GRAMS_CONV + self._mcu_trigger_analog.set_trigger("abs_ge", trigger_frac_grams) # Probe towards z_min until the trigger_analog on the MCU triggers def probing_move(self, gcmd): diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 279e542f1..27af6777e 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -21,7 +21,7 @@ def assert_is_int32(value, frac_bits): # convert a floating point value to a 32 bit fixed point representation # checks for overflow -def to_fixed_32(value, frac_bits): +def to_fixed_32(value, frac_bits=0): fixed_val = int(value * (2**frac_bits)) return assert_is_int32(fixed_val, frac_bits) @@ -84,8 +84,8 @@ class MCU_SosFilter: self._start_value = 0. # Offset and scaling self._offset = 0 - self._scale = 1 - self._value_frac_bits = self._scale_frac_bits = 0 + self._scale = 1. + self._scale_frac_bits = 0 # MCU commands self._oid = self._mcu.create_oid() self._set_section_cmd = self._set_state_cmd = None @@ -96,12 +96,6 @@ class MCU_SosFilter: % (self._oid, self._max_sections)) self._mcu.register_config_callback(self._build_config) - def _validate_frac_bits(self, frac_bits): - if frac_bits < 0 or frac_bits > 31: - raise ValueError("The number of fractional bits (%i) must be a" - " value between 0 and 31" % (frac_bits,)) - return frac_bits - def _build_config(self): self._set_section_cmd = self._mcu.lookup_command( "sos_filter_set_section oid=%c section_idx=%c" @@ -119,9 +113,6 @@ class MCU_SosFilter: def get_oid(self): return self._oid - def convert_value(self, val): - return to_fixed_32(val, self._value_frac_bits) - # convert the SciPi SOS filters to fixed point format def _convert_filter(self): if self._design is None: @@ -159,7 +150,7 @@ class MCU_SosFilter: fixed_state = [] for col, value in enumerate(section): adjval = value * self._start_value - fixed_state.append(to_fixed_32(adjval, self._value_frac_bits)) + fixed_state.append(to_fixed_32(adjval)) sos_state.append(fixed_state) return sos_state @@ -169,12 +160,9 @@ class MCU_SosFilter: self._start_value = start_value # Set conversion of a raw value 1 to a 1.0 value processed by sos filter - def set_offset_scale(self, offset, scale, scale_frac_bits=0, - value_frac_bits=0): + def set_offset_scale(self, offset, scale, scale_frac_bits=0): self._offset = offset - self._value_frac_bits = value_frac_bits - scale_mult = scale * float(1 << value_frac_bits) - self._scale = to_fixed_32(scale_mult, scale_frac_bits) + self._scale = scale self._scale_frac_bits = scale_frac_bits # Change the filter coefficients and state at runtime @@ -207,7 +195,8 @@ class MCU_SosFilter: for i, state in enumerate(sos_state): self._set_state_cmd.send([self._oid, i, state[0], state[1]]) # Send offset/scale (if they have changed) - args = (self._oid, self._offset, self._scale, self._scale_frac_bits) + su = to_fixed_32(self._scale, self._scale_frac_bits) + args = (self._oid, self._offset, su, self._scale_frac_bits) if args != self._last_sent_offset_scale: self._set_offset_scale_cmd.send(args) self._last_sent_offset_scale = args @@ -309,8 +298,7 @@ class MCU_trigger_analog: self._set_raw_range_cmd.send(args) self._last_range_args = args # Update trigger in mcu (if it has changed) - tval32 = self._sos_filter.convert_value(self._trigger_value) - args = [self._oid, self._trigger_type, tval32] + args = [self._oid, self._trigger_type, to_fixed_32(self._trigger_value)] if args != self._last_trigger_args: self._set_trigger_cmd.send(args) self._last_trigger_args = args From 9a97ade74f78dedbd0a3fe048004a4dfb9e3e2c1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 24 Jan 2026 12:12:39 -0500 Subject: [PATCH 349/411] sos_filter: Implement auto_offset feature Add a setting that will enable the mcu sos_filter code to automatically set an offset using the first read measurement. Signed-off-by: Kevin O'Connor --- klippy/extras/trigger_analog.py | 12 ++++++++---- src/sos_filter.c | 11 ++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 27af6777e..f053611d8 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -86,6 +86,7 @@ class MCU_SosFilter: self._offset = 0 self._scale = 1. self._scale_frac_bits = 0 + self._auto_offset = False # MCU commands self._oid = self._mcu.create_oid() self._set_section_cmd = self._set_state_cmd = None @@ -105,7 +106,7 @@ class MCU_SosFilter: cq=self._cmd_queue) self._set_offset_scale_cmd = self._mcu.lookup_command( "sos_filter_set_offset_scale oid=%c offset=%i" - " scale=%i scale_frac_bits=%c", cq=self._cmd_queue) + " scale=%i scale_frac_bits=%c auto_offset=%c", cq=self._cmd_queue) self._set_active_cmd = self._mcu.lookup_command( "sos_filter_set_active oid=%c n_sections=%c coeff_frac_bits=%c", cq=self._cmd_queue) @@ -160,10 +161,12 @@ class MCU_SosFilter: self._start_value = start_value # Set conversion of a raw value 1 to a 1.0 value processed by sos filter - def set_offset_scale(self, offset, scale, scale_frac_bits=0): + def set_offset_scale(self, offset=0, scale=1., scale_frac_bits=0, + auto_offset=False): self._offset = offset self._scale = scale self._scale_frac_bits = scale_frac_bits + self._auto_offset = auto_offset # Change the filter coefficients and state at runtime def set_filter_design(self, design, coeff_frac_bits=29): @@ -196,8 +199,9 @@ class MCU_SosFilter: self._set_state_cmd.send([self._oid, i, state[0], state[1]]) # Send offset/scale (if they have changed) su = to_fixed_32(self._scale, self._scale_frac_bits) - args = (self._oid, self._offset, su, self._scale_frac_bits) - if args != self._last_sent_offset_scale: + args = (self._oid, self._offset, su, self._scale_frac_bits, + self._auto_offset) + if args != self._last_sent_offset_scale or self._auto_offset: self._set_offset_scale_cmd.send(args) self._last_sent_offset_scale = args # Activate filter diff --git a/src/sos_filter.c b/src/sos_filter.c index c11ae3df8..00e8d9b09 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -21,6 +21,7 @@ struct sos_filter_section { struct sos_filter { uint8_t max_sections, n_sections, coeff_frac_bits, scale_frac_bits; + uint8_t auto_offset; int32_t offset, scale; // filter composed of second order sections struct sos_filter_section filter[0]; @@ -58,6 +59,12 @@ sos_filter_apply(struct sos_filter *sf, int32_t *pvalue) { int32_t raw_val = *pvalue; + // Automatically apply offset (if requested) + if (sf->auto_offset) { + sf->offset = -raw_val; + sf->auto_offset = 0; + } + // Apply offset and scale int32_t offset = sf->offset, offset_val = raw_val + offset, cur_val; if ((offset >= 0) != (offset_val >= raw_val)) @@ -163,9 +170,11 @@ command_trigger_analog_set_offset_scale(uint32_t *args) sf->offset = args[1]; sf->scale = args[2]; sf->scale_frac_bits = args[3] & 0x3f; + sf->auto_offset = args[4]; } DECL_COMMAND(command_trigger_analog_set_offset_scale, - "sos_filter_set_offset_scale oid=%c offset=%i scale=%i scale_frac_bits=%c"); + "sos_filter_set_offset_scale oid=%c offset=%i scale=%i scale_frac_bits=%c" + " auto_offset=%c"); // Set one section of the filter void From c9d904aa9d9de3ddb84a4c929f9d667c085329fc Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 22 Jan 2026 11:40:40 -0500 Subject: [PATCH 350/411] trigger_analog: Add initial support for detecting "tap" events Add a new "diff_peak_gt" trigger type. This will be useful with detecting ldc1612 "tap" events. Signed-off-by: Kevin O'Connor --- src/trigger_analog.c | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/trigger_analog.c b/src/trigger_analog.c index f7ec477ec..a77af68d7 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -22,12 +22,13 @@ struct trigger_analog { // Filtering struct sos_filter *sf; // Trigger value checking - int32_t trigger_value; + int32_t trigger_value, trigger_peak; + uint32_t trigger_clock; uint8_t trigger_type; // Trsync triggering uint8_t flags, trigger_reason, error_reason; struct trsync *ts; - uint32_t homing_clock, trigger_clock; + uint32_t homing_clock; // Sensor activity monitoring uint8_t monitor_max, monitor_count; struct timer time; @@ -41,10 +42,11 @@ enum { // Trigger types enum { - TT_ABS_GE, TT_GT + TT_ABS_GE, TT_GT, TT_DIFF_PEAK_GT }; DECL_ENUMERATION("trigger_analog_type", "abs_ge", TT_ABS_GE); DECL_ENUMERATION("trigger_analog_type", "gt", TT_GT); +DECL_ENUMERATION("trigger_analog_type", "diff_peak_gt", TT_DIFF_PEAK_GT); // Sample errors sent via trsync error code enum { @@ -85,17 +87,34 @@ monitor_note_activity(struct trigger_analog *ta) // Check if a value should signal a "trigger" event static int -check_trigger(struct trigger_analog *ta, int32_t value) +check_trigger(struct trigger_analog *ta, uint32_t time, int32_t value) { switch (ta->trigger_type) { case TT_ABS_GE: + ta->trigger_clock = time; return abs(value) >= ta->trigger_value; case TT_GT: + ta->trigger_clock = time; return value > ta->trigger_value; + case TT_DIFF_PEAK_GT: + if (value > ta->trigger_peak) { + ta->trigger_clock = time; + ta->trigger_peak = value; + return 0; + } + uint32_t delta = ta->trigger_peak - value; + return delta > ta->trigger_value; } return 0; } +// Reset fields associated with trigger checking +static void +trigger_reset(struct trigger_analog *ta) +{ + ta->trigger_peak = INT32_MIN; +} + // Stop homing due to an error static void cancel_homing(struct trigger_analog *ta, uint8_t error_code) @@ -150,10 +169,9 @@ trigger_analog_update(struct trigger_analog *ta, int32_t sample) } // Check if this is a "trigger" - ret = check_trigger(ta, filtered_value); + ret = check_trigger(ta, time, filtered_value); if (ret) { trsync_do_trigger(ta->ts, ta->trigger_reason); - ta->trigger_clock = time; flags = 0; } @@ -220,6 +238,7 @@ command_trigger_analog_home(uint32_t *args) ta->monitor_count = 0; ta->time.func = monitor_event; ta->flags = TA_AWAIT_HOMING | TA_CAN_TRIGGER; + trigger_reset(ta); sched_add_timer(&ta->time); } DECL_COMMAND(command_trigger_analog_home, From 7e394d6dd7cf1348342ff265385eb3afea4f1c59 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 26 Jan 2026 18:30:18 -0500 Subject: [PATCH 351/411] bed_mesh: Fix tuple vs list error Commit 2e0c2262e incorrectly changed the internal fpt variable from a list to a tuple. Reported by @nefelim4ag. Signed-off-by: Kevin O'Connor --- klippy/extras/bed_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 9e5b206e9..dbe4f331c 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -682,7 +682,7 @@ class BedMeshCalibrate: idx_offset = 0 start_idx = 0 for i, pts in substitutes.items(): - fpt = base_points[i][:2] + fpt = list(base_points[i][:2]) # offset the index to account for additional samples idx = i + idx_offset # Add "normal" points From 4d9d57c0bd811e63bc911f6fad4ce039348aa2be Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 26 Jan 2026 18:38:39 -0500 Subject: [PATCH 352/411] test: Add a bed_mesh test case Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- test/klippy/bed_mesh.cfg | 81 +++++++++++++++++++++++++++++++++++++++ test/klippy/bed_mesh.test | 13 +++++++ 2 files changed, 94 insertions(+) create mode 100644 test/klippy/bed_mesh.cfg create mode 100644 test/klippy/bed_mesh.test diff --git a/test/klippy/bed_mesh.cfg b/test/klippy/bed_mesh.cfg new file mode 100644 index 000000000..a3a56d92a --- /dev/null +++ b/test/klippy/bed_mesh.cfg @@ -0,0 +1,81 @@ +# Test config for bed_mesh +[stepper_x] +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PE5 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PJ1 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: probe:z_virtual_endstop +position_max: 200 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[heater_bed] +heater_pin: PH5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +control: watermark +min_temp: 0 +max_temp: 130 + +[probe] +pin: PC7 +z_offset: 1.15 + +[bed_mesh] +mesh_min: 10,10 +mesh_max: 180,180 +probe_count: 7, 7 +algorithm: bicubic +faulty_region_1_min: 21.422, 87.126 +faulty_region_1_max: 42.922, 129.126 +faulty_region_2_min: 54.172, 97.376 +faulty_region_2_max: 100.172, 150.876 + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 diff --git a/test/klippy/bed_mesh.test b/test/klippy/bed_mesh.test new file mode 100644 index 000000000..bf104755d --- /dev/null +++ b/test/klippy/bed_mesh.test @@ -0,0 +1,13 @@ +# Test case for bed_mesh tests +CONFIG bed_mesh.cfg +DICTIONARY atmega2560.dict + +# Start by homing the printer. +G28 +G1 F6000 +G1 X60 Y60 Z10 + +# Run bed_mesh_calibrate +BED_MESH_CALIBRATE + +G1 Z10 From 7f822f3a5c27743b3e49a8e0d7b9d81bc3176f17 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 27 Jan 2026 01:03:55 +0100 Subject: [PATCH 353/411] probe: fix sign inversion in probe calibrate (#7178) Commit 2a1027ce inadvertently flipped the signs in probe_calibrate_finalize(). Signed-off-by: Timofey Titovets --- klippy/extras/probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 31e5b71e3..73fd8927b 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -95,7 +95,7 @@ class ProbeCommandHelper: if mpresult is None: return ppos, offsets = self.probe_calibrate_info - z_offset = offsets[2] + mpresult.bed_z - ppos.bed_z + z_offset = offsets[2] - mpresult.bed_z + ppos.bed_z gcode = self.printer.lookup_object('gcode') gcode.respond_info( "%s: z_offset: %.3f\n" From 9957546ae0ebd10b0fc27de0950bd0b01341f082 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 22 Jan 2026 10:26:38 -0500 Subject: [PATCH 354/411] probe_eddy_current: Rework EddyGatherSamples() Rework the internal EddyGatherSamples() class with a goal of making it easier to add tap analysis in the future. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 217 +++++++++++++++------------- 1 file changed, 116 insertions(+), 101 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index d30c7d404..3668cd190 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -34,8 +34,12 @@ class EddyCalibration: gcode.register_command('Z_OFFSET_APPLY_PROBE', self.cmd_Z_OFFSET_APPLY_PROBE, desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) - def is_calibrated(self): - return len(self.cal_freqs) > 2 + def get_printer(self): + return self.printer + def verify_calibrated(self): + if len(self.cal_freqs) <= 2: + raise self.printer.command_error( + "Must calibrate probe_eddy_current first") def load_calibration(self, cal): cal = sorted([(c[1], c[0]) for c in cal]) self.cal_freqs = [c[0] for c in cal] @@ -278,56 +282,33 @@ class EddyCalibration: # Tool to gather samples and convert them to probe positions class EddyGatherSamples: - def __init__(self, printer, sensor_helper, calibration, offsets): + def __init__(self, printer, sensor_helper): self._printer = printer self._sensor_helper = sensor_helper - self._calibration = calibration - self._offsets = offsets - # Results storage - self._samples = [] - self._probe_times = [] - self._probe_results = [] + # Sensor reading + self._sensor_messages = [] self._need_stop = False + # Probe request and results storage + self._probe_requests = [] + self._analysis_results = [] # Start samples - if not self._calibration.is_calibrated(): - raise self._printer.command_error( - "Must calibrate probe_eddy_current first") - sensor_helper.add_client(self._add_measurement) - def _add_measurement(self, msg): + sensor_helper.add_client(self._add_sensor_message) + # Sensor reading and measurement extraction + def _add_sensor_message(self, msg): if self._need_stop: - del self._samples[:] + del self._sensor_messages[:] return False - self._samples.append(msg) - self._check_samples() + self._sensor_messages.append(msg) + self._check_sensor_messages() return True def finish(self): self._need_stop = True - def _await_samples(self): - # Make sure enough samples have been collected - reactor = self._printer.get_reactor() - mcu = self._sensor_helper.get_mcu() - while self._probe_times: - start_time, end_time, pos_time, toolhead_pos = self._probe_times[0] - systime = reactor.monotonic() - est_print_time = mcu.estimated_print_time(systime) - if est_print_time > end_time + 1.0: - raise self._printer.command_error( - "probe_eddy_current sensor outage") - if mcu.is_fileoutput(): - # In debugging mode - if pos_time is not None: - toolhead_pos = self._lookup_toolhead_pos(pos_time) - self._probe_results.append((toolhead_pos[2], toolhead_pos)) - self._probe_times.pop(0) - continue - reactor.pause(systime + 0.010) - def _pull_freq(self, start_time, end_time): - # Find average sensor frequency between time range + def _pull_measurements(self, start_time, end_time): + # Extract measurements from sensor messages for given time range + measures = [] msg_num = discard_msgs = 0 - samp_sum = 0. - samp_count = 0 - while msg_num < len(self._samples): - msg = self._samples[msg_num] + while msg_num < len(self._sensor_messages): + msg = self._sensor_messages[msg_num] msg_num += 1 data = msg['data'] if data[0][0] > end_time: @@ -335,58 +316,78 @@ class EddyGatherSamples: if data[-1][0] < start_time: discard_msgs = msg_num continue - for time, freq, z in data: - if time >= start_time and time <= end_time: - samp_sum += freq - samp_count += 1 - del self._samples[:discard_msgs] - if not samp_count: - # No sensor readings - raise error in pull_probed() - return 0. - return samp_sum / samp_count - def _lookup_toolhead_pos(self, pos_time): - toolhead = self._printer.lookup_object('toolhead') - kin = toolhead.get_kinematics() - kin_spos = {s.get_name(): s.mcu_to_commanded_position( - s.get_past_mcu_position(pos_time)) - for s in kin.get_steppers()} - return kin.calc_position(kin_spos) - def _check_samples(self): - while self._samples and self._probe_times: - start_time, end_time, pos_time, toolhead_pos = self._probe_times[0] - if self._samples[-1]['data'][-1][0] < end_time: + for measure in data: + time = measure[0] + if time < start_time: + continue + if time > end_time: + break + measures.append(measure) + del self._sensor_messages[:discard_msgs] + return measures + def _check_sensor_messages(self): + while self._sensor_messages and self._probe_requests: + cb, start_time, end_time, args = self._probe_requests[0] + if self._sensor_messages[-1]['data'][-1][0] < end_time: break - freq = self._pull_freq(start_time, end_time) - if pos_time is not None: - toolhead_pos = self._lookup_toolhead_pos(pos_time) - sensor_z = None - if freq: - sensor_z = self._calibration.freq_to_height(freq) - self._probe_results.append((sensor_z, toolhead_pos)) - self._probe_times.pop(0) + measures = self._pull_measurements(start_time, end_time) + errmsg = res = None + try: + # Call analysis callback to process measurements + res = cb(measures, *args) + except self._printer.command_error as e: + # Defer raising of errors to pull_probed() + errmsg = str(e) + self._analysis_results.append((res, errmsg)) + self._probe_requests.pop(0) + def add_probe_request(self, cb, start_time, end_time, *args): + self._probe_requests.append((cb, start_time, end_time, args)) + self._check_sensor_messages() + # Extract probe results + def _await_sensor_messages(self): + # Make sure enough samples have been collected + reactor = self._printer.get_reactor() + mcu = self._sensor_helper.get_mcu() + while self._probe_requests: + cb, start_time, end_time, args = self._probe_requests[0] + systime = reactor.monotonic() + est_print_time = mcu.estimated_print_time(systime) + if est_print_time > end_time + 1.0: + raise self._printer.command_error( + "probe_eddy_current sensor outage") + if mcu.is_fileoutput(): + # In debugging mode - just create dummy response + dummy_pr = manual_probe.ProbeResult(0., 0., 0., 0., 0., 0.) + self._analysis_results.append((dummy_pr, None)) + self._probe_requests.pop(0) + continue + reactor.pause(systime + 0.010) def pull_probed(self): - self._await_samples() + self._await_sensor_messages() results = [] - for sensor_z, toolhead_pos in self._probe_results: - if sensor_z is None: - raise self._printer.command_error( - "Unable to obtain probe_eddy_current sensor readings") - if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: - raise self._printer.command_error( - "probe_eddy_current sensor not in valid range") - res = manual_probe.ProbeResult( - toolhead_pos[0]+self._offsets[0], - toolhead_pos[1]+self._offsets[1], toolhead_pos[2]-sensor_z, - toolhead_pos[0], toolhead_pos[1], toolhead_pos[2]) + for res, errmsg in self._analysis_results: + if errmsg is not None: + raise self._printer.command_error(errmsg) results.append(res) - del self._probe_results[:] + del self._analysis_results[:] return results - def note_probe(self, start_time, end_time, toolhead_pos): - self._probe_times.append((start_time, end_time, None, toolhead_pos)) - self._check_samples() - def note_probe_and_position(self, start_time, end_time, pos_time): - self._probe_times.append((start_time, end_time, pos_time, None)) - self._check_samples() + +# Generate a ProbeResult from the average of a set of measurements +def probe_results_from_avg(measures, toolhead_pos, calibration, offsets): + cmderr = calibration.get_printer().command_error + if not measures: + raise cmderr("Unable to obtain probe_eddy_current sensor readings") + # Determine average of measurements + freq_sum = sum([m[1] for m in measures]) + freq_avg = freq_sum / len(measures) + # Determine height associated with frequency + sensor_z = calibration.freq_to_height(freq_avg) + if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: + raise cmderr("probe_eddy_current sensor not in valid range") + return manual_probe.ProbeResult( + toolhead_pos[0] + offsets[0], toolhead_pos[1] + offsets[1], + toolhead_pos[2] - sensor_z, + toolhead_pos[0], toolhead_pos[1], toolhead_pos[2]) MAX_VALID_RAW_VALUE=0x03ffffff @@ -396,7 +397,6 @@ class EddyDescend: probe_offsets, param_helper): self._printer = config.get_printer() self._sensor_helper = sensor_helper - self._mcu = sensor_helper.get_mcu() self._calibration = calibration self._probe_offsets = probe_offsets self._param_helper = param_helper @@ -413,10 +413,9 @@ class EddyDescend: self._trigger_analog.set_trigger('gt', conv_freq) # Probe session interface def start_probe_session(self, gcmd): + self._calibration.verify_calibrated() self._prep_trigger_analog() - offsets = self._probe_offsets.get_offsets() - self._gather = EddyGatherSamples(self._printer, self._sensor_helper, - self._calibration, offsets) + self._gather = EddyGatherSamples(self._printer, self._sensor_helper) return self def run_probe(self, gcmd): toolhead = self._printer.lookup_object('toolhead') @@ -430,7 +429,10 @@ class EddyDescend: start_time = self._trigger_analog.get_last_trigger_time() + 0.050 end_time = start_time + 0.100 toolhead_pos = toolhead.get_position() - self._gather.note_probe(start_time, end_time, toolhead_pos) + offsets = self._probe_offsets.get_offsets() + self._gather.add_probe_request(probe_results_from_avg, + start_time, end_time, + toolhead_pos, self._calibration, offsets) def pull_probed_results(self): return self._gather.pull_probed() def end_probe_session(self): @@ -477,19 +479,31 @@ class EddyEndstopWrapper: class EddyScanningProbe: def __init__(self, printer, sensor_helper, calibration, probe_offsets, gcmd): + calibration.verify_calibrated() self._printer = printer self._sensor_helper = sensor_helper self._calibration = calibration - offsets = probe_offsets.get_offsets() - self._gather = EddyGatherSamples(printer, sensor_helper, - calibration, offsets) + self._offsets = probe_offsets.get_offsets() + self._gather = EddyGatherSamples(printer, sensor_helper) self._sample_time_delay = 0.050 self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' + def _lookup_toolhead_pos(self, pos_time): + toolhead = self._printer.lookup_object('toolhead') + kin = toolhead.get_kinematics() + kin_spos = {s.get_name(): s.mcu_to_commanded_position( + s.get_past_mcu_position(pos_time)) + for s in kin.get_steppers()} + return kin.calc_position(kin_spos) + def _analyze_scan(self, measures, pos_time): + toolhead_pos = self._lookup_toolhead_pos(pos_time) + return probe_results_from_avg(measures, toolhead_pos, + self._calibration, self._offsets) def _rapid_lookahead_cb(self, printtime): start_time = printtime - self._sample_time / 2 - self._gather.note_probe_and_position( - start_time, start_time + self._sample_time, printtime) + end_time = start_time + self._sample_time + self._gather.add_probe_request(self._analyze_scan, start_time, end_time, + printtime) def run_probe(self, gcmd): toolhead = self._printer.lookup_object("toolhead") if self._is_rapid: @@ -498,8 +512,9 @@ class EddyScanningProbe: printtime = toolhead.get_last_move_time() toolhead.dwell(self._sample_time_delay + self._sample_time) start_time = printtime + self._sample_time_delay - self._gather.note_probe_and_position( - start_time, start_time + self._sample_time, start_time) + end_time = start_time + self._sample_time + self._gather.add_probe_request(self._analyze_scan, start_time, end_time, + start_time) def pull_probed_results(self): if self._is_rapid: # Flush lookahead (so all lookahead callbacks are invoked) From 8922481034c9c767610d5577711a6cd9fc18b183 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 26 Jan 2026 14:04:14 -0500 Subject: [PATCH 355/411] probe_eddy_current: Create trigger_analog instance in main PrinterEddyProbe Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 3668cd190..909a6cbcc 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -394,17 +394,15 @@ MAX_VALID_RAW_VALUE=0x03ffffff # Helper for implementing PROBE style commands (descend until trigger) class EddyDescend: def __init__(self, config, sensor_helper, calibration, - probe_offsets, param_helper): + probe_offsets, param_helper, trigger_analog): self._printer = config.get_printer() self._sensor_helper = sensor_helper self._calibration = calibration self._probe_offsets = probe_offsets self._param_helper = param_helper - self._trigger_analog = trigger_analog.MCU_trigger_analog(sensor_helper) + self._trigger_analog = trigger_analog self._z_min_position = probe.lookup_minimum_z(config) self._gather = None - dispatch = self._trigger_analog.get_dispatch() - probe.LookupZSteppers(config, dispatch.add_stepper) def _prep_trigger_analog(self): self._trigger_analog.set_raw_range(0, MAX_VALID_RAW_VALUE) z_offset = self._probe_offsets.get_offsets()[2] @@ -538,12 +536,15 @@ class PrinterEddyProbe: sensors = { "ldc1612": ldc1612.LDC1612 } sensor_type = config.getchoice('sensor_type', {s: s for s in sensors}) self.sensor_helper = sensors[sensor_type](config, self.calibration) + # Create trigger_analog interface + trig_analog = trigger_analog.MCU_trigger_analog(self.sensor_helper) + probe.LookupZSteppers(config, trig_analog.get_dispatch().add_stepper) # Probe interface self.probe_offsets = probe.ProbeOffsetsHelper(config) self.param_helper = probe.ProbeParameterHelper(config) self.eddy_descend = EddyDescend( config, self.sensor_helper, self.calibration, self.probe_offsets, - self.param_helper) + self.param_helper, trig_analog) self.cmd_helper = probe.ProbeCommandHelper(config, self, replace_z_offset=True) self.probe_session = probe.ProbeSessionHelper( From 08f4b65c7c8597fdd26c33a3488a3c282c50e8d0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 26 Jan 2026 14:28:17 -0500 Subject: [PATCH 356/411] probe_eddy_current: Make EddyScanningProbe a long lived class Create the class at the start of PrinterEddyProbe and call it as needed. This makes the class life-cycle similar to EddyDescend. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 909a6cbcc..4e23848c3 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -475,17 +475,15 @@ class EddyEndstopWrapper: # Implementing probing with "METHOD=scan" class EddyScanningProbe: - def __init__(self, printer, sensor_helper, calibration, probe_offsets, - gcmd): - calibration.verify_calibrated() - self._printer = printer + def __init__(self, config, sensor_helper, calibration, probe_offsets): + self._printer = config.get_printer() self._sensor_helper = sensor_helper self._calibration = calibration self._offsets = probe_offsets.get_offsets() - self._gather = EddyGatherSamples(printer, sensor_helper) + self._gather = None self._sample_time_delay = 0.050 - self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) - self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' + self._sample_time = 0. + self._is_rapid = False def _lookup_toolhead_pos(self, pos_time): toolhead = self._printer.lookup_object('toolhead') kin = toolhead.get_kinematics() @@ -502,6 +500,13 @@ class EddyScanningProbe: end_time = start_time + self._sample_time self._gather.add_probe_request(self._analyze_scan, start_time, end_time, printtime) + # Probe session interface + def start_probe_session(self, gcmd): + self._calibration.verify_calibrated() + self._gather = EddyGatherSamples(self._printer, self._sensor_helper) + self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) + self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' + return self def run_probe(self, gcmd): toolhead = self._printer.lookup_object("toolhead") if self._is_rapid: @@ -539,7 +544,7 @@ class PrinterEddyProbe: # Create trigger_analog interface trig_analog = trigger_analog.MCU_trigger_analog(self.sensor_helper) probe.LookupZSteppers(config, trig_analog.get_dispatch().add_stepper) - # Probe interface + # Basic probe requests self.probe_offsets = probe.ProbeOffsetsHelper(config) self.param_helper = probe.ProbeParameterHelper(config) self.eddy_descend = EddyDescend( @@ -549,9 +554,14 @@ class PrinterEddyProbe: replace_z_offset=True) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.eddy_descend.start_probe_session) + # Create wrapper to support Z homing with probe mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) probe.HomingViaProbeHelper( config, mcu_probe, self.probe_offsets, self.param_helper) + # Probing via "scan" and "rapid_scan" requests + self.eddy_scan = EddyScanningProbe(config, self.sensor_helper, + self.calibration, self.probe_offsets) + # Register with main probe interface self.printer.add_object('probe', self) def add_client(self, cb): self.sensor_helper.add_client(cb) @@ -564,8 +574,7 @@ class PrinterEddyProbe: def start_probe_session(self, gcmd): method = gcmd.get('METHOD', 'automatic').lower() if method in ('scan', 'rapid_scan'): - return EddyScanningProbe(self.printer, self.sensor_helper, - self.calibration, self.probe_offsets, gcmd) + return self.eddy_scan.start_probe_session(gcmd) return self.probe_session.start_probe_session(gcmd) def register_drift_compensation(self, comp): self.calibration.register_drift_compensation(comp) From 5c23f9296a42bd2ff8404e78f17ecf88d061ef23 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 22 Jan 2026 12:31:44 -0500 Subject: [PATCH 357/411] probe_eddy_current: implement tap support Use SOS filters + derivative filter to generate dF/dT on mcu. Feed that to the MCU's trigger_analog peak detection. Interpret peak time as a tap event Signed-off-by: Kevin O'Connor Signed-off-by: Timofey Titovets --- docs/Config_Reference.md | 3 ++ klippy/extras/probe_eddy_current.py | 83 ++++++++++++++++++++++++++++- klippy/extras/trigger_analog.py | 12 +++++ test/klippy/eddy.cfg | 1 + test/klippy/eddy.test | 4 ++ 5 files changed, 102 insertions(+), 1 deletion(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index b01360adf..8e7ed287a 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2293,6 +2293,9 @@ sensor_type: ldc1612 #samples_tolerance: #samples_tolerance_retries: # See the "probe" section for information on these parameters. +#tap_threshold: 0 +# Noise cutoff/stop trigger threshold delta Hz per sample +# See the Eddy_Probe.md for explanation ``` ### [axis_twist_compensation] diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 4e23848c3..509fe923f 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -404,6 +404,9 @@ class EddyDescend: self._z_min_position = probe.lookup_minimum_z(config) self._gather = None def _prep_trigger_analog(self): + sos_filter = self._trigger_analog.get_sos_filter() + sos_filter.set_filter_design(None) + sos_filter.set_offset_scale(0, 1.) self._trigger_analog.set_raw_range(0, MAX_VALID_RAW_VALUE) z_offset = self._probe_offsets.get_offsets()[2] trigger_freq = self._calibration.height_to_freq(z_offset) @@ -473,6 +476,74 @@ class EddyEndstopWrapper: z_offset = self._eddy_descend._probe_offsets.get_offsets()[2] return z_offset +# Probing helper for "tap" requests +class EddyTap: + def __init__(self, config, sensor_helper, calibration, + param_helper, trigger_analog): + self._printer = config.get_printer() + self._sensor_helper = sensor_helper + self._calibration = calibration + self._param_helper = param_helper + self._trigger_analog = trigger_analog + self._z_min_position = probe.lookup_minimum_z(config) + self._gather = None + self._filter_design = None + self._tap_threshold = config.getfloat('tap_threshold', 0., minval=0.) + if self._tap_threshold: + self._setup_tap() + # Setup for "tap" probe request + def _setup_tap(self): + # Create sos filter "design" + cfg_error = self._printer.config_error + sps = self._sensor_helper.get_samples_per_second() + design = trigger_analog.DigitalFilter(sps, cfg_error, + lowpass=25.0, lowpass_order=4) + # Create the derivative (sample to sample difference) post filter + self._filter_design = trigger_analog.DerivativeFilter(design) + # Create SOS filter + cmd_queue = self._trigger_analog.get_dispatch().get_command_queue() + mcu = self._sensor_helper.get_mcu() + sos_filter = trigger_analog.MCU_SosFilter(mcu, cmd_queue, 5) + self._trigger_analog.setup_sos_filter(sos_filter) + def _prep_trigger_analog_tap(self): + if not self._tap_threshold: + raise self._printer.command_error("Tap not configured") + sos_filter = self._trigger_analog.get_sos_filter() + sos_filter.set_filter_design(self._filter_design) + sos_filter.set_offset_scale(0, 1., auto_offset=True) + self._trigger_analog.set_raw_range(0, MAX_VALID_RAW_VALUE) + convert_frequency = self._sensor_helper.convert_frequency + raw_threshold = convert_frequency(self._tap_threshold) + self._trigger_analog.set_trigger('diff_peak_gt', raw_threshold) + # Measurement analysis to determine "tap" position + def _analyze_tap(self, measures, trig_pos): + # XXX - for now just use trigger position (this is not very accurate) + return manual_probe.ProbeResult(trig_pos[0], trig_pos[1], trig_pos[2], + trig_pos[0], trig_pos[1], trig_pos[2]) + # Probe session interface + def start_probe_session(self, gcmd): + self._prep_trigger_analog_tap() + self._gather = EddyGatherSamples(self._printer, self._sensor_helper) + return self + def run_probe(self, gcmd): + toolhead = self._printer.lookup_object('toolhead') + pos = toolhead.get_position() + pos[2] = self._z_min_position + speed = self._param_helper.get_probe_params(gcmd)['probe_speed'] + # Perform probing move + phoming = self._printer.lookup_object('homing') + trig_pos = phoming.probing_move(self._trigger_analog, pos, speed) + # Extract samples + start_time = self._trigger_analog.get_last_trigger_time() - 0.025 + end_time = start_time + 0.025 + self._gather.add_probe_request(self._analyze_tap, + start_time, end_time, trig_pos) + def pull_probed_results(self): + return self._gather.pull_probed() + def end_probe_session(self): + self._gather.finish() + self._gather = None + # Implementing probing with "METHOD=scan" class EddyScanningProbe: def __init__(self, config, sensor_helper, calibration, probe_offsets): @@ -553,11 +624,14 @@ class PrinterEddyProbe: self.cmd_helper = probe.ProbeCommandHelper(config, self, replace_z_offset=True) self.probe_session = probe.ProbeSessionHelper( - config, self.param_helper, self.eddy_descend.start_probe_session) + config, self.param_helper, self._start_descend_wrapper) # Create wrapper to support Z homing with probe mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) probe.HomingViaProbeHelper( config, mcu_probe, self.probe_offsets, self.param_helper) + # Probing via "tap" interface + self.eddy_tap = EddyTap(config, self.sensor_helper, self.calibration, + self.param_helper, trig_analog) # Probing via "scan" and "rapid_scan" requests self.eddy_scan = EddyScanningProbe(config, self.sensor_helper, self.calibration, self.probe_offsets) @@ -568,9 +642,16 @@ class PrinterEddyProbe: def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) def get_offsets(self, gcmd=None): + if gcmd is not None and gcmd.get('METHOD', '').lower() == "tap": + return (0., 0., 0.) return self.probe_offsets.get_offsets(gcmd) def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) + def _start_descend_wrapper(self, gcmd): + method = gcmd.get('METHOD', 'automatic').lower() + if method == "tap": + return self.eddy_tap.start_probe_session(gcmd) + return self.eddy_descend.start_probe_session(gcmd) def start_probe_session(self, gcmd): method = gcmd.get('METHOD', 'automatic').lower() if method in ('scan', 'rapid_scan'): diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index f053611d8..41bec07f7 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -69,6 +69,18 @@ class DigitalFilter: def get_initial_state(self): return self.initial_state +# Produce sample to sample difference (derivative) of a DigitalFilter +class DerivativeFilter: + def __init__(self, main_filter): + self._main_filter = main_filter + + def get_filter_sections(self): + s = list(self._main_filter.get_filter_sections()) + return s + [(1., -1., 0., 1., 0., 0.)] + + def get_initial_state(self): + s = list(self._main_filter.get_initial_state()) + return s + [(-1., 0.)] # Control an `sos_filter` object on the MCU class MCU_SosFilter: diff --git a/test/klippy/eddy.cfg b/test/klippy/eddy.cfg index 11946ebe4..6f26899ef 100644 --- a/test/klippy/eddy.cfg +++ b/test/klippy/eddy.cfg @@ -63,6 +63,7 @@ y_offset: -4 sensor_type: ldc1612 speed: 10.0 intb_pin: PK7 +tap_threshold: 30 [bed_mesh] mesh_min: 10,10 diff --git a/test/klippy/eddy.test b/test/klippy/eddy.test index 5251be122..beeacf949 100644 --- a/test/klippy/eddy.test +++ b/test/klippy/eddy.test @@ -23,7 +23,11 @@ BED_MESH_CALIBRATE METHOD=rapid_scan # Move again G1 Z5 X0 Y0 +# Do "tap" probe +PROBE METHOD=tap + # Do regular probe +G1 Z5 PROBE # Move again From a03eed71150e72d5abb6b536e3b91d5c941e4d87 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 9 Dec 2025 00:35:45 +0100 Subject: [PATCH 358/411] docs: describe tap calibration routine Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++ docs/G-Codes.md | 14 +++++---- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 11069c4bc..58b3abb7f 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -55,6 +55,78 @@ surface temperature or sensor hardware temperature can skew the results. It is important that calibration and probing is only done when the printer is at a stable temperature. +## Tap calibration + +The Eddy probe measures the resonance frequency of the coil. +By the absolute value of the frequency and the calibration curve from +`PROBE_EDDY_CURRENT_CALIBRATE`, it is therefore possible to detect +where the bed is without physical contact. + +By use of the same knowledge, we know that frequency changes with +the distance. It is possible to track that change in real time and +detect the time/position where contact happens - a change of frequency +starts to change in a different way. +For example, stopped to change because of the collision. + +Because eddy output is not perfect: there is sensor noise, +mechanical oscillation, thermal expansion and other discrepancies, +it is required to calibrate the stop threshold for your machine. +Practically, it ensures that the Eddy's output data absolute value +change per second (velocity) is high enough - higher than the noise level, +and that upon collision it always decreases by at least this value. + +``` +[probe_eddy_current my_probe] +# eddy probe configuration... +tap_threshold: 0 +``` + +The suggested calibration routine works as follows: +1. Home Z +2. Place the toolhead at the center of the bed. +3. Move the Z axis far away (30 mm, for example). +4. Run `PROBE METHOD=tap` +5. If it stops before colliding, increase the `tap_threshold`. + +Repeat until the nozzle softly touches the bed. +This is easier to do with a clean nozzle and +by visually inspecting the process. + +You can streamline the process by placing the toolhead in the center once. +Then, upon config restart, trick the machine into thinking that Z is homed. +``` +SET_KINEMATIC_POSITION X= Y= Z=0 +G0 Z5 # Optional retract +PROBE METHOD=tap +``` + +Here is an example sequence of threshold values to test: +``` +1 -> 5 -> 10 -> 20 -> 40 -> 80 -> 160 +160 -> 120 -> 100 +``` +Your value will normally be between those. +- Too high a value leaves a less safe margin for early collision - +if something is between the nozzle and the bed, or if the nozzle +is too close to the bed before the tap. +- Too low - can make the toolhead stop in mid-air +because of the noise. + +You can validate the tap precision by measuring the paper thickness +from the initial calibration guide. It is expected to be ~0.1mm. + +Tap precision is limited by the sampling frequency and +the speed of the descent. +If you take 24 photos per second of the moving train, you can only estimate +where the train was between photos. + +It is possible to reduce the descending speed. It may require decrease of +absolute `tap_threshold` value. + +It is possible to tap over non-conductive surfaces as long as there is metal +behind it within the sensor's sensitivity range. +Max distance can be approximated to be about 1.5x of the coil's narrowest part. + ## Thermal Drift Calibration As with all inductive probes, eddy current probes are subject to diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 893993e85..6869cbc62 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -1160,23 +1160,25 @@ The following commands are available when a see the [probe calibrate guide](Probe_Calibrate.md)). #### PROBE -`PROBE [PROBE_SPEED=] [LIFT_SPEED=] [SAMPLES=] -[SAMPLE_RETRACT_DIST=] [SAMPLES_TOLERANCE=] +`PROBE [METHOD=] [PROBE_SPEED=] [LIFT_SPEED=] +[SAMPLES=] [SAMPLE_RETRACT_DIST=] [SAMPLES_TOLERANCE=] [SAMPLES_TOLERANCE_RETRIES=] [SAMPLES_RESULT=median|average]`: Move the nozzle downwards until the probe triggers. If any of the optional parameters are provided they override their equivalent setting in the [probe config section](Config_Reference.md#probe). +The optional parameter `METHOD` is probe-specific. #### QUERY_PROBE `QUERY_PROBE`: Report the current status of the probe ("triggered" or "open"). #### PROBE_ACCURACY -`PROBE_ACCURACY [PROBE_SPEED=] [SAMPLES=] +`PROBE_ACCURACY [METHOD=] [PROBE_SPEED=] [SAMPLES=] [SAMPLE_RETRACT_DIST=]`: Calculate the maximum, minimum, average, median, and standard deviation of multiple probe samples. By default, 10 SAMPLES are taken. Otherwise the optional parameters default to their equivalent setting in the probe config section. +The optional parameter `METHOD` is probe-specific. #### PROBE_CALIBRATE `PROBE_CALIBRATE [SPEED=] [=]`: Run a @@ -1237,13 +1239,14 @@ The following commands are available when the is enabled. #### QUAD_GANTRY_LEVEL -`QUAD_GANTRY_LEVEL [RETRIES=] [RETRY_TOLERANCE=] +`QUAD_GANTRY_LEVEL [METHOD=] [RETRIES=] [RETRY_TOLERANCE=] [HORIZONTAL_MOVE_Z=] [=]`: This command will probe the points specified in the config and then make independent adjustments to each Z stepper to compensate for tilt. See the PROBE command for details on the optional probe parameters. The optional `RETRIES`, `RETRY_TOLERANCE`, and `HORIZONTAL_MOVE_Z` values override those options specified in the config file. +The optional parameter `METHOD` is probe-specific. ### [query_adc] @@ -1672,10 +1675,11 @@ The following commands are available when the [z_tilt config section](Config_Reference.md#z_tilt) is enabled. #### Z_TILT_ADJUST -`Z_TILT_ADJUST [RETRIES=] [RETRY_TOLERANCE=] +`Z_TILT_ADJUST [METHOD=] [RETRIES=] [RETRY_TOLERANCE=] [HORIZONTAL_MOVE_Z=] [=]`: This command will probe the points specified in the config and then make independent adjustments to each Z stepper to compensate for tilt. See the PROBE command for details on the optional probe parameters. The optional `RETRIES`, `RETRY_TOLERANCE`, and `HORIZONTAL_MOVE_Z` values override those options specified in the config file. +The optional parameter `METHOD` is probe-specific. From dfe6d3f066c468da8f5f380a79dcc80924447349 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 7 Jan 2026 17:16:48 +0100 Subject: [PATCH 359/411] docs: describe calibration output Add a hint about the connection between the calibration output and tap threshold. Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 58b3abb7f..d24aadf31 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -24,6 +24,7 @@ named `[probe_eddy_current my_eddy_probe]` then one would run complete in a few seconds. After it completes, issue a `SAVE_CONFIG` command to save the results to the printer.cfg and restart. +Eddy current is used as a proximity/distance sensor (similar to a laser ruler). The second step in calibration is to correlate the sensor readings to the corresponding Z heights. Home the printer and navigate the toolhead so that the nozzle is near the center of the bed. Then run a @@ -35,7 +36,17 @@ those steps are complete one can `ACCEPT` the position. The tool will then move the toolhead so that the sensor is above the point where the nozzle used to be and run a series of movements to correlate the sensor to Z positions. This will take a couple of minutes. After the -tool completes, issue a `SAVE_CONFIG` command to save the results to +tool completes it will output the sensor performance data: +``` +probe_eddy_current: noise 0.000642mm, MAD_Hz=11.314 in 2525 queries +Total frequency range: 45000.012 Hz +z_offset: 0.250 # noise 0.000200mm, MAD_Hz=11.000 +z_offset: 0.530 # noise 0.000300mm, MAD_Hz=12.000 +z_offset: 1.010 # noise 0.000400mm, MAD_Hz=14.000 +z_offset: 2.010 # noise 0.000600mm, MAD_Hz=12.000 +z_offset: 3.010 # noise 0.000700mm, MAD_Hz=9.000 +``` +issue a `SAVE_CONFIG` command to save the results to the printer.cfg and restart. After initial calibration it is a good idea to verify that the @@ -112,6 +123,19 @@ is too close to the bed before the tap. - Too low - can make the toolhead stop in mid-air because of the noise. +You can estimate the initial threshold value by analyzing your own +calibration routine output: +``` +probe_eddy_current: noise 0.000642mm, MAD_Hz=11.314 +... +z_offset: 1.010 # noise 0.000400mm, MAD_Hz=14.000 +``` +The estimation will be: +``` +MAD_Hz * 2 +11.314 * 2 = 22.628 +``` + You can validate the tap precision by measuring the paper thickness from the initial calibration guide. It is expected to be ~0.1mm. From 5fb9902dda78e0027a3110245e4815094e3fcfe0 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 24 Jan 2026 17:11:41 +0100 Subject: [PATCH 360/411] sos_filter: define filtfilt call To implement host-side analysis of tap data, we need a way to apply the same filtering as on the mcu. As bonus, it cancels the induced signal delay. Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/trigger_analog.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 41bec07f7..223f83768 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -38,6 +38,7 @@ class DigitalFilter: return try: import scipy.signal as signal + import numpy except: raise cfg_error("DigitalFilter require the SciPy module") if highpass: @@ -69,6 +70,12 @@ class DigitalFilter: def get_initial_state(self): return self.initial_state + def filtfilt(self, data): + import scipy.signal as signal + import numpy + data = numpy.array(data) + return signal.sosfiltfilt(self.filter_sections, data) + # Produce sample to sample difference (derivative) of a DigitalFilter class DerivativeFilter: def __init__(self, main_filter): From 0795fb014175386ef22bc2d9c42b745e0183b506 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 20 Nov 2025 00:53:14 +0100 Subject: [PATCH 361/411] probe_eddy_current: analyze tap data To cancel out any lag, filter data on the host Then to avoid derivatives lag, compute central difference. Assume that peak velocity is always the moment right before collision happens. Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 68 ++++++++++++++++++++++++++--- klippy/extras/trigger_analog.py | 3 ++ 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 509fe923f..e3dd7270e 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -516,8 +516,60 @@ class EddyTap: raw_threshold = convert_frequency(self._tap_threshold) self._trigger_analog.set_trigger('diff_peak_gt', raw_threshold) # Measurement analysis to determine "tap" position - def _analyze_tap(self, measures, trig_pos): - # XXX - for now just use trigger position (this is not very accurate) + def central_diff(self, times, values): + velocity = [0.0] * len(values) + for i in range(1, len(values) - 1): + delta_v = (values[i+1] - values[i-1]) + delta_t = (times[i+1] - times[i-1]) + velocity[i] = delta_v / delta_t + velocity[0] = (values[1] - values[0]) / (times[1] - times[0]) + velocity[-1] = (values[-1] - values[-2]) / (times[-1] - times[-2]) + return velocity + def validate_samples_time(self, timestamps): + sps = self._sensor_helper.get_samples_per_second() + cycle_time = 1.0 / sps + SYNC_SLACK = 0.001 + for i in range(1, len(timestamps)): + tdiff = timestamps[i] - timestamps[i-1] + if cycle_time + SYNC_SLACK < tdiff: + logging.error("Eddy: Gaps in the data: %.3f < %.3f" % ( + (cycle_time + SYNC_SLACK, tdiff) + )) + break + if cycle_time - SYNC_SLACK > tdiff: + logging.error( + "Eddy: CLKIN frequency too low: %.3f > %.3f" % ( + (cycle_time - SYNC_SLACK, tdiff) + )) + break + def _pull_tap_time(self, measures): + tap_time = [] + tap_value = [] + for time, freq, z in measures: + tap_time.append(time) + tap_value.append(freq) + # If samples have gaps this will not produce adequate data + self.validate_samples_time(tap_time) + # Do the same filtering as on the MCU but without induced lag + main_design = self._filter_design.get_main_filter() + try: + fvals = main_design.filtfilt(tap_value) + except ValueError as e: + raise self._printer.command_error(str(e)) + velocity = self.central_diff(tap_time, fvals) + peak_velocity = max(velocity) + i = velocity.index(peak_velocity) + return tap_time[i] + def _lookup_toolhead_pos(self, pos_time): + toolhead = self._printer.lookup_object('toolhead') + kin = toolhead.get_kinematics() + kin_spos = {s.get_name(): s.mcu_to_commanded_position( + s.get_past_mcu_position(pos_time)) + for s in kin.get_steppers()} + return kin.calc_position(kin_spos) + def _analyze_tap(self, measures): + pos_time = self._pull_tap_time(measures) + trig_pos = self._lookup_toolhead_pos(pos_time) return manual_probe.ProbeResult(trig_pos[0], trig_pos[1], trig_pos[2], trig_pos[0], trig_pos[1], trig_pos[2]) # Probe session interface @@ -530,14 +582,18 @@ class EddyTap: pos = toolhead.get_position() pos[2] = self._z_min_position speed = self._param_helper.get_probe_params(gcmd)['probe_speed'] + move_start_time = toolhead.get_last_move_time() # Perform probing move phoming = self._printer.lookup_object('homing') trig_pos = phoming.probing_move(self._trigger_analog, pos, speed) # Extract samples - start_time = self._trigger_analog.get_last_trigger_time() - 0.025 - end_time = start_time + 0.025 - self._gather.add_probe_request(self._analyze_tap, - start_time, end_time, trig_pos) + trigger_time = self._trigger_analog.get_last_trigger_time() + start_time = trigger_time - 0.250 + if start_time < move_start_time: + # Filter short move + start_time = move_start_time + end_time = trigger_time + self._gather.add_probe_request(self._analyze_tap, start_time, end_time) def pull_probed_results(self): return self._gather.pull_probed() def end_probe_session(self): diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 223f83768..6f79c0486 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -81,6 +81,9 @@ class DerivativeFilter: def __init__(self, main_filter): self._main_filter = main_filter + def get_main_filter(self): + return self._main_filter + def get_filter_sections(self): s = list(self._main_filter.get_filter_sections()) return s + [(1., -1., 0., 1., 0., 0.)] From 1fe9fb3ad414ddcf7ecc1dacd310bf8967b61e2c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 29 Jan 2026 14:53:11 -0500 Subject: [PATCH 362/411] trigger_analog: Don't report trigger time as the peak time There are some rare corner cases where reporting the peak time could cause hard to debug issues (for example, the peak time could theoretically be a significant time prior to the actual trigger time, which could possibly cause unexpected clock rollover issues). Now that the host code does not utilize the peak time for "tap" detection, it can be removed from the mcu code. Signed-off-by: Kevin O'Connor --- klippy/extras/trigger_analog.py | 4 ++-- src/trigger_analog.c | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 6f79c0486..d38bcd777 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -277,7 +277,7 @@ class MCU_trigger_analog: # Lookup commands self._query_state_cmd = self._mcu.lookup_query_command( "trigger_analog_query_state oid=%c", - "trigger_analog_state oid=%c homing=%c trigger_clock=%u", + "trigger_analog_state oid=%c homing=%c homing_clock=%u", oid=self._oid, cq=cmd_queue) self._set_raw_range_cmd = self._mcu.lookup_command( "trigger_analog_set_raw_range oid=%c raw_min=%i raw_max=%i", @@ -334,7 +334,7 @@ class MCU_trigger_analog: def _clear_home(self): self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) params = self._query_state_cmd.send([self._oid]) - trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_clock']) + trigger_ticks = self._mcu.clock32_to_clock64(params['homing_clock']) return self._mcu.clock_to_print_time(trigger_ticks) def get_steppers(self): diff --git a/src/trigger_analog.c b/src/trigger_analog.c index a77af68d7..ce9b998bf 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -23,7 +23,6 @@ struct trigger_analog { struct sos_filter *sf; // Trigger value checking int32_t trigger_value, trigger_peak; - uint32_t trigger_clock; uint8_t trigger_type; // Trsync triggering uint8_t flags, trigger_reason, error_reason; @@ -91,14 +90,11 @@ check_trigger(struct trigger_analog *ta, uint32_t time, int32_t value) { switch (ta->trigger_type) { case TT_ABS_GE: - ta->trigger_clock = time; return abs(value) >= ta->trigger_value; case TT_GT: - ta->trigger_clock = time; return value > ta->trigger_value; case TT_DIFF_PEAK_GT: if (value > ta->trigger_peak) { - ta->trigger_clock = time; ta->trigger_peak = value; return 0; } @@ -173,6 +169,7 @@ trigger_analog_update(struct trigger_analog *ta, int32_t sample) if (ret) { trsync_do_trigger(ta->ts, ta->trigger_reason); flags = 0; + ta->homing_clock = time; } ta->flags = flags; @@ -250,8 +247,8 @@ command_trigger_analog_query_state(uint32_t *args) { uint8_t oid = args[0]; struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); - sendf("trigger_analog_state oid=%c homing=%c trigger_clock=%u" - , oid, !!(ta->flags & TA_CAN_TRIGGER), ta->trigger_clock); + sendf("trigger_analog_state oid=%c homing=%c homing_clock=%u" + , oid, !!(ta->flags & TA_CAN_TRIGGER), ta->homing_clock); } DECL_COMMAND(command_trigger_analog_query_state , "trigger_analog_query_state oid=%c"); From 576d0ca13de8434d6a52e2a3cd474501f3c3eae6 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 30 Jan 2026 14:00:55 -0500 Subject: [PATCH 363/411] probe_eddy_current: Minor reorg to PrinterEddyProbe startup The EddyTap class doesn't need the calibration object. Add some code comments. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index e3dd7270e..1befaf6e9 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -478,11 +478,9 @@ class EddyEndstopWrapper: # Probing helper for "tap" requests class EddyTap: - def __init__(self, config, sensor_helper, calibration, - param_helper, trigger_analog): + def __init__(self, config, sensor_helper, param_helper, trigger_analog): self._printer = config.get_printer() self._sensor_helper = sensor_helper - self._calibration = calibration self._param_helper = param_helper self._trigger_analog = trigger_analog self._z_min_position = probe.lookup_minimum_z(config) @@ -677,21 +675,21 @@ class PrinterEddyProbe: self.eddy_descend = EddyDescend( config, self.sensor_helper, self.calibration, self.probe_offsets, self.param_helper, trig_analog) - self.cmd_helper = probe.ProbeCommandHelper(config, self, - replace_z_offset=True) - self.probe_session = probe.ProbeSessionHelper( - config, self.param_helper, self._start_descend_wrapper) # Create wrapper to support Z homing with probe mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) - probe.HomingViaProbeHelper( - config, mcu_probe, self.probe_offsets, self.param_helper) + probe.HomingViaProbeHelper(config, mcu_probe, + self.probe_offsets, self.param_helper) # Probing via "tap" interface - self.eddy_tap = EddyTap(config, self.sensor_helper, self.calibration, + self.eddy_tap = EddyTap(config, self.sensor_helper, self.param_helper, trig_analog) # Probing via "scan" and "rapid_scan" requests self.eddy_scan = EddyScanningProbe(config, self.sensor_helper, self.calibration, self.probe_offsets) # Register with main probe interface + self.cmd_helper = probe.ProbeCommandHelper(config, self, + replace_z_offset=True) + self.probe_session = probe.ProbeSessionHelper( + config, self.param_helper, self._start_descend_wrapper) self.printer.add_object('probe', self) def add_client(self, cb): self.sensor_helper.add_client(cb) @@ -712,6 +710,7 @@ class PrinterEddyProbe: method = gcmd.get('METHOD', 'automatic').lower() if method in ('scan', 'rapid_scan'): return self.eddy_scan.start_probe_session(gcmd) + # For "tap" and normal, probe_session can average multiple attempts return self.probe_session.start_probe_session(gcmd) def register_drift_compensation(self, comp): self.calibration.register_drift_compensation(comp) From 85ccd1d9df6ed6a67dc2bddfa52e86445b5d76d2 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 30 Jan 2026 23:10:14 +0100 Subject: [PATCH 364/411] temperature_probe: fix missing kin_pos Signed-off-by: Timofey Titovets --- klippy/extras/temperature_probe.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py index d42d9e210..f47c4e848 100644 --- a/klippy/extras/temperature_probe.py +++ b/klippy/extras/temperature_probe.py @@ -185,7 +185,7 @@ class TemperatureProbe: def get_temp(self, eventtime=None): return self.last_measurement[0], self.target_temp - def _collect_sample(self, kin_pos, tool_zero_z): + def _collect_sample(self, mpresult, tool_zero_z): probe = self._get_probe() x_offset, y_offset, _ = probe.get_offsets() speeds = self._get_speeds() @@ -198,7 +198,7 @@ class TemperatureProbe: cur_pos[0] -= x_offset cur_pos[1] -= y_offset toolhead.manual_move(cur_pos, move_speed) - return self.cal_helper.collect_sample(kin_pos, tool_zero_z, speeds) + return self.cal_helper.collect_sample(mpresult, tool_zero_z, speeds) def _prepare_next_sample(self, last_temp, tool_zero_z): # Register our own abort command now that the manual @@ -237,7 +237,7 @@ class TemperatureProbe: toolhead = self.printer.lookup_object("toolhead") tool_zero_z = toolhead.get_position()[2] try: - last_temp = self._collect_sample(kin_pos, tool_zero_z) + last_temp = self._collect_sample(mpresult, tool_zero_z) except Exception: self._finalize_drift_cal(False) raise @@ -562,7 +562,7 @@ class EddyDriftCompensation: % (self.name, self.cal_temp) ) - def collect_sample(self, kin_pos, tool_zero_z, speeds): + def collect_sample(self, mpresult, tool_zero_z, speeds): if self.calibration_samples is None: self.calibration_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)] move_times = [] @@ -616,7 +616,7 @@ class EddyDriftCompensation: zvals = [d[2] for d in data] avg_freq = sum(freqs) / len(freqs) avg_z = sum(zvals) / len(zvals) - kin_z = i * .5 + .05 + kin_pos[2] + kin_z = i * .5 + .05 + mpresult.bed_z logging.info( "Probe Values at Temp %.2fC, Z %.4fmm: Avg Freq = %.6f, " "Avg Measured Z = %.6f" From bc0862ddba4aacd70e5ff83084c68a78f250660c Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 31 Jan 2026 01:10:46 +0100 Subject: [PATCH 365/411] config: cartographer v3 example config Signed-off-by: Timofey Titovets --- config/sample-cartographer-v3.cfg | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 config/sample-cartographer-v3.cfg diff --git a/config/sample-cartographer-v3.cfg b/config/sample-cartographer-v3.cfg new file mode 100644 index 000000000..ee1a6fd41 --- /dev/null +++ b/config/sample-cartographer-v3.cfg @@ -0,0 +1,52 @@ +# This file contains common pin mappings for the Cartographer board V3 +# To use this config, the firmware should be compiled for the +# STM32F042 with "24 MHz crystal" and +# "USB (on PA9/PA10)" or "CAN bus (on PA9/PA10)". +# CAN bus requires PA1 GPIO pin to be set at micro-controller start-up +# The "carto" micro-controller will be used to control +# the components on the board. + +# See docs/Config_Reference.md for a description of parameters. + +[mcu carto] +serial: /dev/serial/by-id/usb-Klipper_stm32f042x6_29000380114330394D363620-if00 +#canbus_uuid: 92cf532ef122 + +#[adxl345 carto] +#cs_pin: carto:PA3 +#spi_bus: spi1_PA6_PA7_PA5 +#axes_map: x,y,z + +[thermistor 50k] +temperature1: 25 +resistance1: 50000 +temperature2: 50 +resistance2: 17940 +temperature3: 100 +resistance3: 3090 + +[temperature_probe carto] +pullup_resistor: 10000 +sensor_type: 50k +sensor_pin: carto:PA4 +min_temp: 0 +max_temp: 125 + +[led carto_led] +white_pin: carto:PB5 +initial_WHITE: 0.03 + +[output_pin _LDC1612_en] +pin: carto:PA15 +value: 0 # enable + +[static_pwm_clock ldc1612_clk_in] +pin: carto:PB4 +frequency: 24000000 + +[probe_eddy_current carto] +sensor_type: ldc1612 +frequency: 24000000 +i2c_address: 42 +i2c_mcu: carto +i2c_bus: i2c1_PB6_PB7 From 7047d80b0f4f6a635161241f25a9917c27636c73 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 31 Jan 2026 21:28:48 +0100 Subject: [PATCH 366/411] docs: eddy probe - tap requires install of scipy Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index d24aadf31..e2790a198 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -89,7 +89,13 @@ and that upon collision it always decreases by at least this value. ``` [probe_eddy_current my_probe] # eddy probe configuration... -tap_threshold: 0 +tap_threshold: 0 # 0 means tap is disabled +``` + +Before setting it to any other value, it is necessary to install `scipy`: + +```bash +~/klippy-env/bin/pip install scipy ``` The suggested calibration routine works as follows: From 4c62220491988bab772142f7932bb414eb36a484 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 31 Jan 2026 21:29:39 +0100 Subject: [PATCH 367/411] docs: eddy probe - tip about PROBE_ACCURACY Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index e2790a198..5d2b395d3 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -142,6 +142,11 @@ MAD_Hz * 2 11.314 * 2 = 22.628 ``` +To further fine tune threshold, one can use `PROBE_ACCURACY METHOD=tap`. +The range is expected to be about 0.02 mm, +with the default probe speed of 5 mm/s. +Elevated coil temperature may increase noise and may require additional tuning. + You can validate the tap precision by measuring the paper thickness from the initial calibration guide. It is expected to be ~0.1mm. From 9a04eb5aabbbc49ed97aaf735c1e96c8bee39d2b Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 1 Feb 2026 16:21:08 +0100 Subject: [PATCH 368/411] docs: eddy tap recommended samples/tolerance/retries Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 5d2b395d3..812df992e 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -89,6 +89,10 @@ and that upon collision it always decreases by at least this value. ``` [probe_eddy_current my_probe] # eddy probe configuration... +# Recommended starting values for the tap +#samples: 3 +#samples_tolerance: 0.025 +#samples_tolerance_retries: 3 tap_threshold: 0 # 0 means tap is disabled ``` From 29a494aa9e8f1c1458d587d8331adee6e8d44a5b Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 1 Feb 2026 17:00:07 +0100 Subject: [PATCH 369/411] docs: eddy homing correction macro Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 812df992e..8b229545e 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -4,8 +4,10 @@ This document describes how to use an [eddy current](https://en.wikipedia.org/wiki/Eddy_current) inductive probe in Klipper. -Currently, an eddy current probe can not be used for Z homing. The -sensor can only be used for Z probing. +Currently, an eddy current probe can not precisely home Z (i.e., `G28 Z`). +The sensor can precisely do Z probing (i.e., `PROBE ...`). +Look at the [homing correction](Eddy_Probe.md#homing-correction-macros) +for further details. Start by declaring a [probe_eddy_current config section](Config_Reference.md#probe_eddy_current) @@ -66,6 +68,34 @@ surface temperature or sensor hardware temperature can skew the results. It is important that calibration and probing is only done when the printer is at a stable temperature. +## Homing correction macros + +Because of current limitations, homing and probing +are implemented differently for the eddy sensors. +As a result, homing suffers from an offset error, +while probing handles this correctly. + +To correct the homing offset. +One can use the suggested macro inside the homing override or +inside the starting G-Code. + +[Force move](Config_Reference.md#force_move) section +have to be defined in the config. + +``` +[gcode_macro _RELOAD_Z_OFFSET_FROM_PROBE] +gcode: + {% set Z = printer.toolhead.position.z %} + SET_KINEMATIC_POSITION Z={Z - printer.probe.last_probe_position.z} + +[gcode_macro SET_Z_FROM_PROBE] +gcode: + {% set METHOD = params.METHOD | default("automatic") %} + PROBE METHOD={METHOD} + _RELOAD_Z_OFFSET_FROM_PROBE + G0 Z5 +``` + ## Tap calibration The Eddy probe measures the resonance frequency of the coil. From ca7d90084c9f8bdcdeba93a561c3047cbc08e080 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 25 Jan 2026 15:05:20 +0100 Subject: [PATCH 370/411] probe: disable PROBE_CALIBRATE for Eddy PROBE_CALIBRATE will try to adjust z_offset Which will produce a confusing outcome and will not do what it is supposed to do Signed-off-by: Timofey Titovets --- klippy/extras/probe.py | 16 ++++++++-------- klippy/extras/probe_eddy_current.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 73fd8927b..c57f9ef1d 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -38,7 +38,7 @@ def calc_probe_z_average(positions, method='average'): # Helper to implement common probing commands class ProbeCommandHelper: def __init__(self, config, probe, query_endstop=None, - replace_z_offset=False): + can_set_z_offset=True): self.printer = config.get_printer() self.probe = probe self.query_endstop = query_endstop @@ -55,16 +55,16 @@ class ProbeCommandHelper: desc=self.cmd_PROBE_help) # PROBE_CALIBRATE command self.probe_calibrate_info = None - gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, - desc=self.cmd_PROBE_CALIBRATE_help) + if can_set_z_offset: + gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, + desc=self.cmd_PROBE_CALIBRATE_help) # Other commands gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, desc=self.cmd_PROBE_ACCURACY_help) - if replace_z_offset: - return - gcode.register_command('Z_OFFSET_APPLY_PROBE', - self.cmd_Z_OFFSET_APPLY_PROBE, - desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) + if can_set_z_offset: + gcode.register_command('Z_OFFSET_APPLY_PROBE', + self.cmd_Z_OFFSET_APPLY_PROBE, + desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) def _move(self, coord, speed): self.printer.lookup_object('toolhead').manual_move(coord, speed) def get_status(self, eventtime): diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 1befaf6e9..6952480a3 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -687,7 +687,7 @@ class PrinterEddyProbe: self.calibration, self.probe_offsets) # Register with main probe interface self.cmd_helper = probe.ProbeCommandHelper(config, self, - replace_z_offset=True) + can_set_z_offset=False) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self._start_descend_wrapper) self.printer.add_object('probe', self) From 4c89f7f826743b897f4f759a4ca525fcd5bee376 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Wed, 4 Feb 2026 22:54:05 +0100 Subject: [PATCH 371/411] generic_cartesian: Full IQEX printers support (#7165) Signed-off-by: Dmitry Butyugin --- docs/Config_Reference.md | 19 +- docs/G-Codes.md | 5 +- klippy/kinematics/cartesian.py | 4 +- klippy/kinematics/generic_cartesian.py | 169 +++++++---- klippy/kinematics/hybrid_corexy.py | 4 +- klippy/kinematics/hybrid_corexz.py | 4 +- klippy/kinematics/idex_modes.py | 214 +++++++------ test/klippy/corexyuv.cfg | 2 + test/klippy/generic_cartesian_iqex.cfg | 386 ++++++++++++++++++++++++ test/klippy/generic_cartesian_iqex.test | 71 +++++ test/klippy/generic_cartesian_itex.cfg | 320 ++++++++++++++++++++ test/klippy/generic_cartesian_itex.test | 65 ++++ 12 files changed, 1099 insertions(+), 164 deletions(-) create mode 100644 test/klippy/generic_cartesian_iqex.cfg create mode 100644 test/klippy/generic_cartesian_iqex.test create mode 100644 test/klippy/generic_cartesian_itex.cfg create mode 100644 test/klippy/generic_cartesian_itex.test diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 8e7ed287a..8b17d9502 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -743,7 +743,7 @@ max_accel: ``` -Then a user must define three carriages for X, Y, and Z axes, e.g.: +Then a user must define three primary carriages for X, Y, and Z axes, e.g.: ``` [carriage carriage_x] axis: @@ -2455,10 +2455,16 @@ Please note that in this case the `[dual_carriage]` configuration deviates from the configuration described above: ``` [dual_carriage my_dc_carriage] -primary_carriage: -# Defines the matching primary carriage of this dual carriage and -# the corresponding IDEX axis. Must match a name of a defined `[carriage]`. -# This parameter must be provided. +#primary_carriage: +# Defines the matching carriage on the same gantry as this dual carriage and +# the corresponding dual axis. Must match a name of a defined `[carriage]` or +# another independent `[dual_carriage]`. If not set, which is a default, +# defines a dual carriage independent of a `[carriage]` with the same axis +# as this one (e.g. on a different gantry). +#axis: +# Axis of a carriage, either x or y. If 'primary_carriage' is defined, then +# this parameter defaults to the 'axis' parameter of that primary carriage, +# otherwise this parameter must be defined. #safe_distance: # The minimum distance (in mm) to enforce between the dual and the primary # carriages. If a G-Code command is executed that will bring the carriages @@ -2467,7 +2473,8 @@ primary_carriage: # position_min and position_max for the dual and primary carriages. If set # to 0 (or safe_distance is unset and position_min and position_max are # identical for the primary and dual carriages), the carriages proximity -# checks will be disabled. +# checks will be disabled. Only valid for a dual_carriage with a defined +# 'primary_carriage'. endstop_pin: #position_min: position_endstop: diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 6869cbc62..915537411 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -350,8 +350,9 @@ reference a defined primary or dual carriage for `generic_cartesian` kinematics or be 0 (for primary carriage) or 1 (for dual carriage) for all other kinematics supporting IDEX. Setting the mode to `PRIMARY` deactivates the other carriage and makes the specified carriage execute -subsequent G-Code commands as-is. `COPY` and `MIRROR` modes are supported -only for dual carriages. When set to either of these modes, dual carriage +subsequent G-Code commands as-is. Before activating `COPY` or `MIRROR` +mode for a carriage, a different one must be activated as `PRIMARY` on +the same axis. When set to either of these two modes, the carriage will then track the subsequent moves of its primary carriage and either copy relative movements of it (in `COPY` mode) or execute them in the opposite (mirror) direction (in `MIRROR` mode). diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index 5362e57d0..67858d0bb 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -32,8 +32,8 @@ class CartKinematics: self.dc_module = idex_modes.DualCarriages( self.printer, [self.rails[self.dual_carriage_axis]], [self.rails[3]], axes=[self.dual_carriage_axis], - safe_dist=dc_config.getfloat( - 'safe_distance', None, minval=0.)) + safe_dist=[dc_config.getfloat( + 'safe_distance', None, minval=0.)]) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) # Setup boundary checks diff --git a/klippy/kinematics/generic_cartesian.py b/klippy/kinematics/generic_cartesian.py index 230997be3..afb5d47ac 100644 --- a/klippy/kinematics/generic_cartesian.py +++ b/klippy/kinematics/generic_cartesian.py @@ -4,11 +4,13 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. -import copy, itertools, logging, math +import collections, copy, itertools, logging, math import gcode, mathutil, stepper from . import idex_modes from . import kinematic_stepper as ks +VALID_AXES = ['x', 'y', 'z'] + def mat_mul(a, b): if len(a[0]) != len(b): return None @@ -35,13 +37,11 @@ class MainCarriage: def __init__(self, config): self.rail = stepper.GenericPrinterRail(config) carriage_name = self.rail.get_name(short=True) - valid_axes = ['x', 'y', 'z'] - if carriage_name in valid_axes: - axis_name = config.getchoice('axis', valid_axes, carriage_name) + if carriage_name in VALID_AXES: + self.axis_name = config.getchoice('axis', VALID_AXES, carriage_name) else: - axis_name = config.getchoice('axis', valid_axes) - self.axis = ord(axis_name) - ord('x') - self.axis_name = axis_name + self.axis_name = config.getchoice('axis', VALID_AXES) + self.axis = ord(self.axis_name) - ord('x') self.dual_carriage = None def get_name(self): return self.rail.get_name(short=True) @@ -56,9 +56,7 @@ class MainCarriage: return True return self.dual_carriage.get_dc_module().is_active(self.rail) def set_dual_carriage(self, carriage): - old_dc = self.dual_carriage self.dual_carriage = carriage - return old_dc def get_dual_carriage(self): return self.dual_carriage @@ -78,23 +76,49 @@ class ExtraCarriage: self.endstop_pin, self.name) class DualCarriage: - def __init__(self, config, carriages): + def __init__(self, config): self.printer = config.get_printer() self.rail = stepper.GenericPrinterRail(config) - self.primary_carriage = config.getchoice('primary_carriage', carriages) - if self.primary_carriage.set_dual_carriage(self) is not None: - raise config.error( - "Redefinition of dual_carriage for carriage '%s'" % - self.primary_carriage.get_name()) + self.primary_carriage_name = config.get('primary_carriage', None) + if self.primary_carriage_name is None: + self.axis_name = config.getchoice('axis', VALID_AXES) + self.axis = ord(self.axis_name) - ord('x') + self.safe_dist = None + else: + self.axis_name = config.getchoice('axis', VALID_AXES + [None], None) + self.safe_dist = config.getfloat('safe_distance', None, minval=0.) + self.primary_carriage = self.dual_carriage = None + self.config_error = config.error + def resolve_primary_carriage(self, carriages): + if self.primary_carriage_name is None: + return + if self.primary_carriage_name not in carriages: + raise self.config_error( + "primary_carriage = '%s' for '%s' is not a valid choice" + % (self.primary_carriage_name, self.get_name())) + self.primary_carriage = carriages[self.primary_carriage_name] + axis_name = self.axis_name or self.primary_carriage.axis_name + if axis_name != self.primary_carriage.axis_name: + raise self.config_error("Mismatching axes between carriage '%s' " + "(axis=%s) and dual_carriage '%s' (axis=%s)" + % (self.primary_carriage.get_name(), + self.primary_carriage.axis_name, + self.get_name(), axis_name)) + self.axis = ord(axis_name) - ord('x') + if self.primary_carriage.get_dual_carriage(): + raise self.config_error( + "Multiple dual carriages ('%s', '%s') for carriage '%s'" % + (self.primary_carriage.get_dual_carriage().get_name(), + self.get_name(), self.primary_carriage.get_name())) + self.primary_carriage.set_dual_carriage(self) self.axis = self.primary_carriage.get_axis() if self.axis > 1: - raise config.error("Invalid axis '%s' for dual_carriage" % - "xyz"[self.axis]) - self.safe_dist = config.getfloat('safe_distance', None, minval=0.) + raise self.config_error("Invalid axis '%s' for dual_carriage '%s'" % + ("xyz"[self.axis], self.get_name())) def get_name(self): return self.rail.get_name(short=True) def get_axis(self): - return self.primary_carriage.get_axis() + return self.axis def get_rail(self): return self.rail def get_safe_dist(self): @@ -103,7 +127,13 @@ class DualCarriage: return self.printer.lookup_object('dual_carriage') def is_active(self): return self.get_dc_module().is_active(self.rail) + def set_dual_carriage(self, carriage): + self.dual_carriage = carriage def get_dual_carriage(self): + if self.dual_carriage is not None: + return self.dual_carriage + return self.primary_carriage + def get_primary_carriage(self): return self.primary_carriage def add_stepper(self, kin_stepper): self.rail.add_stepper(kin_stepper.get_stepper()) @@ -116,27 +146,38 @@ class GenericCartesianKinematics: s.set_trapq(toolhead.get_trapq()) self.dc_module = None if self.dc_carriages: - pcs = [dc.get_dual_carriage() for dc in self.dc_carriages] + dc_axes = set(dc.get_axis() for dc in self.dc_carriages) + pcs = ([pc for pc in self.primary_carriages + if pc.get_axis() in dc_axes] + + [dc for dc in self.dc_carriages + if dc.get_primary_carriage() is None]) + dcs = [pc.get_dual_carriage() for pc in pcs] primary_rails = [pc.get_rail() for pc in pcs] - dual_rails = [dc.get_rail() for dc in self.dc_carriages] - axes = [dc.get_axis() for dc in self.dc_carriages] - safe_dist = {dc.get_axis() : dc.get_safe_dist() - for dc in self.dc_carriages} + dual_rails = [dc.get_rail() if dc else None for dc in dcs] + axes = [pc.get_axis() for pc in pcs] + safe_dist = [dc.get_safe_dist() if dc else None for dc in dcs] self.dc_module = dc_module = idex_modes.DualCarriages( self.printer, primary_rails, dual_rails, axes, safe_dist) zero_pos = (0., 0.) - for acs in itertools.product(*zip(pcs, self.dc_carriages)): + for acs in itertools.product(*zip(pcs, dcs)): for c in acs: + if c is None: + continue dc_module.get_dc_rail_wrapper(c.get_rail()).activate( idex_modes.PRIMARY, zero_pos) - dc_rail = c.get_dual_carriage().get_rail() - dc_module.get_dc_rail_wrapper(dc_rail).inactivate(zero_pos) + dc = c.get_dual_carriage() + if dc is not None: + dc_module.get_dc_rail_wrapper(dc.get_rail()).inactivate( + zero_pos) self._check_kinematics(config.error) - for c in pcs: - dc_module.get_dc_rail_wrapper(c.get_rail()).activate( + for dc in self.dc_carriages: + dc_module.get_dc_rail_wrapper(dc.get_rail()).inactivate( + zero_pos) + for pc in self.primary_carriages: + if pc.get_axis() not in dc_axes: + continue + dc_module.get_dc_rail_wrapper(pc.get_rail()).activate( idex_modes.PRIMARY, zero_pos) - dc_rail = c.get_dual_carriage().get_rail() - dc_module.get_dc_rail_wrapper(dc_rail).inactivate(zero_pos) else: self._check_kinematics(config.error) # Setup boundary checks @@ -152,25 +193,32 @@ class GenericCartesianKinematics: self.cmd_SET_STEPPER_CARRIAGES, desc=self.cmd_SET_STEPPER_CARRIAGES_help) def _load_kinematics(self, config): - carriages = {} + primary_carriages = [] for mcconfig in config.get_prefix_sections('carriage '): - c = MainCarriage(mcconfig) - axis = c.get_axis() - dups = [mc for mc in carriages.values() if mc.get_axis() == axis] - if dups: + primary_carriages.append(MainCarriage(mcconfig)) + for axis, axis_name in enumerate(VALID_AXES): + dups = [pc.get_name() for pc in primary_carriages + if pc.get_axis() == axis] + if len(dups) > 1: raise config.error( - "Axis '%s' referenced by multiple carriages (%s, %s)" - % ("xyz"[axis], c.get_name(), dups[0].get_name())) - carriages[c.get_name()] = c + "Axis '%s' is set for multiple primary carriages (%s)" + % (axis_name, ', '.join(dups))) + elif not dups: + raise config.error( + "No carriage defined for axis '%s'" % axis_name) dc_carriages = [] for dcconfig in config.get_prefix_sections('dual_carriage '): - dc_carriages.append(DualCarriage(dcconfig, carriages)) - for dc in dc_carriages: - name = dc.get_name() + dc_carriages.append(DualCarriage(dcconfig)) + carriages = {} + for carriage in primary_carriages + dc_carriages: + name = carriage.get_name() if name in carriages: raise config.error("Redefinition of carriage %s" % name) - carriages[name] = dc + carriages[name] = carriage + for dc in dc_carriages: + dc.resolve_primary_carriage(carriages) self.carriages = dict(carriages) + self.primary_carriages = primary_carriages self.dc_carriages = dc_carriages ec_carriages = [] for ecconfig in config.get_prefix_sections('extra_carriage '): @@ -207,16 +255,19 @@ class GenericCartesianKinematics: def get_steppers(self): return [s.get_stepper() for s in self.kin_steppers] def get_primary_carriages(self): - carriages = [None] * 3 - for carriage in self.carriages.values(): - a = carriage.get_axis() - if carriage.get_dual_carriage() is not None: + carriages = [] + for a in range(3): + c = None + if self.dc_module is not None and a in self.dc_module.get_axes(): primary_rail = self.dc_module.get_primary_rail(a) for c in self.carriages.values(): if c.get_rail() == primary_rail: - carriages[a] = c + break else: - carriages[a] = carriage + for c in self.primary_carriages: + if c.get_axis() == a: + break + carriages.append(c) return carriages def _get_kinematics_coeffs(self): matr = {s.get_name() : list(s.get_kin_coeffs()) @@ -227,9 +278,9 @@ class GenericCartesianKinematics: [0. for s in self.kin_steppers]) axes = [dc.get_axis() for dc in self.dc_carriages] orig_matr = copy.deepcopy(matr) - for dc in self.dc_carriages: - axis = dc.get_axis() - for c in [dc.get_dual_carriage(), dc]: + for c in self.carriages.values(): + axis = c.get_axis() + if axis in self.dc_module.get_axes(): m, o = self.dc_module.get_transform(c.get_rail()) for s in c.get_rail().get_steppers(): matr[s.get_name()][axis] *= m @@ -289,16 +340,14 @@ class GenericCartesianKinematics: homing_state.home_rails([rail], forcepos, homepos) def home(self, homing_state): self._check_kinematics(self.printer.command_error) + primary_carriages = self.get_primary_carriages() # Each axis is homed independently and in order for axis in homing_state.get_axes(): - for carriage in self.carriages.values(): - if carriage.get_axis() != axis: - continue - if carriage.get_dual_carriage() != None: - self.dc_module.home(homing_state, axis) - else: - self.home_axis(homing_state, axis, carriage.get_rail()) - break + if self.dc_module is not None and axis in self.dc_module.get_axes(): + self.dc_module.home(homing_state, axis) + else: + carriage = primary_carriages[axis] + self.home_axis(homing_state, axis, carriage.get_rail()) def _check_endstops(self, move): end_pos = move.end_pos for i in (0, 1, 2): diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index bbe7bfa55..1cd2aa8bc 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -35,8 +35,8 @@ class HybridCoreXYKinematics: self.rails[3].setup_itersolve('corexy_stepper_alloc', b'+') self.dc_module = idex_modes.DualCarriages( self.printer, [self.rails[0]], [self.rails[3]], axes=[0], - safe_dist=dc_config.getfloat( - 'safe_distance', None, minval=0.)) + safe_dist=[dc_config.getfloat( + 'safe_distance', None, minval=0.)]) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) # Setup boundary checks diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index 3e2dd4788..03e889376 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -35,8 +35,8 @@ class HybridCoreXZKinematics: self.rails[3].setup_itersolve('corexz_stepper_alloc', b'+') self.dc_module = idex_modes.DualCarriages( self.printer, [self.rails[0]], [self.rails[3]], axes=[0], - safe_dist=dc_config.getfloat( - 'safe_distance', None, minval=0.)) + safe_dist=[dc_config.getfloat( + 'safe_distance', None, minval=0.)]) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) # Setup boundary checks diff --git a/klippy/kinematics/idex_modes.py b/klippy/kinematics/idex_modes.py index 46b0be08b..4fe6df830 100644 --- a/klippy/kinematics/idex_modes.py +++ b/klippy/kinematics/idex_modes.py @@ -14,36 +14,37 @@ MIRROR = 'MIRROR' class DualCarriages: VALID_MODES = [PRIMARY, COPY, MIRROR] - def __init__(self, printer, primary_rails, dual_rails, axes, - safe_dist={}): + def __init__(self, printer, primary_rails, dual_rails, axes, safe_dist): self.printer = printer - self.axes = axes self._init_steppers(primary_rails + dual_rails) - self.primary_rails = [ - DualCarriagesRail(printer, c, dual_rails[i], - axes[i], active=True) - for i, c in enumerate(primary_rails)] + safe_dist = list(safe_dist) + for i, dc in enumerate(dual_rails): + if dc is None or safe_dist[i] is not None: + continue + pc = primary_rails[i] + safe_dist[i] = min(abs(pc.position_min - dc.position_min), + abs(pc.position_max - dc.position_max)) + self.primary_mode_dcs = [None] * 3 + self.primary_rails = [] + for i, c in enumerate(primary_rails): + activate = self.primary_mode_dcs[axes[i]] is None + dc_rail = DualCarriagesRail( + printer, c, dual_rails[i], axes[i], safe_dist[i], + active=activate) + if activate: + self.primary_mode_dcs[axes[i]] = dc_rail + self.primary_rails.append(dc_rail) self.dual_rails = [ DualCarriagesRail(printer, c, primary_rails[i], - axes[i], active=False) + axes[i], safe_dist[i], active=False) + if c is not None else None for i, c in enumerate(dual_rails)] self.dc_rails = collections.OrderedDict( [(c.rail.get_name(short=True), c) - for c in self.primary_rails + self.dual_rails]) + for c in self.primary_rails + self.dual_rails + if c is not None]) self.saved_states = {} - self.safe_dist = {} - for i, dc in enumerate(dual_rails): - axis = axes[i] - if isinstance(safe_dist, dict): - if axis in safe_dist: - self.safe_dist[axis] = safe_dist[axis] - continue - elif safe_dist is not None: - self.safe_dist[axis] = safe_dist - continue - pc = primary_rails[i] - self.safe_dist[axis] = min(abs(pc.position_min - dc.position_min), - abs(pc.position_max - dc.position_max)) + self.axes = sorted(set(axes)) self.printer.add_object('dual_carriage', self) self.printer.register_event_handler("klippy:ready", self._handle_ready) gcode = self.printer.lookup_object('gcode') @@ -64,6 +65,8 @@ class DualCarriages: self.orig_stepper_kinematics = [] steppers = set() for rail in rails: + if rail is None: + continue c_steppers = rail.get_steppers() if not c_steppers: raise self.printer.config_error( @@ -80,10 +83,9 @@ class DualCarriages: def get_axes(self): return self.axes def get_primary_rail(self, axis): - for dc_rail in self.dc_rails.values(): - if dc_rail.mode == PRIMARY and dc_rail.axis == axis: - return dc_rail.rail - return None + if self.primary_mode_dcs[axis] is None: + return None + return self.primary_mode_dcs[axis].rail def get_dc_rail_wrapper(self, rail): for dc_rail in self.dc_rails.values(): if dc_rail.rail == rail: @@ -109,22 +111,27 @@ class DualCarriages: if target_dc.mode != PRIMARY: newpos = pos[:axis] + [target_dc.get_axis_position(pos)] \ + pos[axis+1:] + self.primary_mode_dcs[axis] = target_dc target_dc.activate(PRIMARY, newpos, old_position=pos) toolhead.set_position(newpos) kin.update_limits(axis, target_dc.rail.get_range()) def home(self, homing_state, axis): kin = self.printer.lookup_object('toolhead').get_kinematics() - dcs = [dc for dc in self.dc_rails.values() if dc.axis == axis] - if (self.get_dc_order(dcs[0], dcs[1]) > 0) != \ - dcs[0].rail.get_homing_info().positive_dir: - # The second carriage must home first, because the carriages home in - # the same direction and the first carriage homes on the second one - dcs.reverse() - for dc in dcs: - self.toggle_active_dc_rail(dc) - kin.home_axis(homing_state, axis, dc.rail) - # Restore the original rails ordering - self.activate_dc_mode(dcs[0], PRIMARY) + homing_rails = [r for r in self.primary_rails if r.axis == axis] + for dc_rail in homing_rails: + dcs = [dc for dc in self.dc_rails.values() + if dc_rail.rail in [dc.rail, dc.dual_rail]] + if len(dcs) > 1 and (self.get_dc_order(dcs[0], dcs[1]) > 0) != \ + dcs[0].rail.get_homing_info().positive_dir: + # The second carriage must home first, because the carriages + # home in the same direction and the first carriage homes on + # the second one, so reversing the oder + dcs.reverse() + for dc in dcs: + self.toggle_active_dc_rail(dc) + kin.home_axis(homing_state, dc.axis, dc.rail) + # Restore the first rail as primary after all homed + self.activate_dc_mode(homing_rails[0], PRIMARY) def get_status(self, eventtime=None): status = {'carriages' : {dc.get_name() : dc.mode for dc in self.dc_rails.values()}} @@ -132,46 +139,62 @@ class DualCarriages: status.update({('carriage_%d' % (i,)) : dc.mode for i, dc in enumerate(self.dc_rails.values())}) return status - def get_kin_range(self, toolhead, mode, axis): + def get_kin_range(self, toolhead, axis): pos = toolhead.get_position() - dcs = [dc for dc in self.dc_rails.values() if dc.axis == axis] - axes_pos = [dc.get_axis_position(pos) for dc in dcs] - dc0_rail = dcs[0].rail - dc1_rail = dcs[1].rail - if mode != PRIMARY or dcs[0].is_active(): - range_min = dc0_rail.position_min - range_max = dc0_rail.position_max - else: - range_min = dc1_rail.position_min - range_max = dc1_rail.position_max - safe_dist = self.safe_dist[axis] - if not safe_dist: - return (range_min, range_max) + primary_carriage = self.primary_mode_dcs[axis] + if primary_carriage is None: + return (1.0, -1.0) + primary_pos = primary_carriage.get_axis_position(pos) + range_min = primary_carriage.rail.position_min + range_max = primary_carriage.rail.position_max + for carriage in self.dc_rails.values(): + if carriage.axis != axis: + continue + dcs = [carriage] + [dc for dc in self.dc_rails.values() + if carriage.rail is dc.dual_rail] + axes_pos = [dc.get_axis_position(pos) for dc in dcs] + # Check how dcs[0] affects the motion range of primary_carriage + if not dcs[0].is_active(): + continue + elif dcs[0].mode == COPY: + range_min = max(range_min, primary_pos + + dcs[0].rail.position_min - axes_pos[0]) + range_max = min(range_max, primary_pos + + dcs[0].rail.position_max - axes_pos[0]) + elif dcs[0].mode == MIRROR: + range_min = max(range_min, primary_pos + + axes_pos[0] - dcs[0].rail.position_max) + range_max = min(range_max, primary_pos + + axes_pos[0] - dcs[0].rail.position_min) + safe_dist = dcs[0].safe_dist + if not safe_dist or len(dcs) == 1: + continue + if dcs[0].mode == dcs[1].mode or \ + set((dcs[0].mode, dcs[1].mode)) == set((PRIMARY, COPY)): + # dcs[0] and dcs[1] carriages move in the same direction and + # cannot collide with each other + continue - if mode == COPY: - range_min = max(range_min, - axes_pos[0] - axes_pos[1] + dc1_rail.position_min) - range_max = min(range_max, - axes_pos[0] - axes_pos[1] + dc1_rail.position_max) - elif mode == MIRROR: + # Compute how much dcs[0] can move towards dcs[1] + dcs_dist = axes_pos[1] - axes_pos[0] if self.get_dc_order(dcs[0], dcs[1]) > 0: - range_min = max(range_min, - 0.5 * (sum(axes_pos) + safe_dist)) - range_max = min(range_max, - sum(axes_pos) - dc1_rail.position_min) + safe_move_dist = dcs_dist + safe_dist else: - range_max = min(range_max, - 0.5 * (sum(axes_pos) - safe_dist)) - range_min = max(range_min, - sum(axes_pos) - dc1_rail.position_max) - else: - # mode == PRIMARY - active_idx = 1 if dcs[1].is_active() else 0 - inactive_idx = 1 - active_idx - if self.get_dc_order(dcs[active_idx], dcs[inactive_idx]) > 0: - range_min = max(range_min, axes_pos[inactive_idx] + safe_dist) - else: - range_max = min(range_max, axes_pos[inactive_idx] - safe_dist) + safe_move_dist = dcs_dist - safe_dist + if dcs[1].is_active(): + safe_move_dist *= 0.5 + + if dcs[0].mode in (PRIMARY, COPY): + if self.get_dc_order(dcs[0], dcs[1]) > 0: + range_min = max(range_min, primary_pos + safe_move_dist) + else: + range_max = min(range_max, primary_pos + safe_move_dist) + else: # dcs[0].mode == MIRROR + if self.get_dc_order(dcs[0], dcs[1]) > 0: + range_max = min(range_max, primary_pos - safe_move_dist) + else: + range_min = max(range_min, primary_pos - safe_move_dist) + if range_min > range_max: # During multi-MCU homing it is possible that the carriage # position will end up below position_min or above position_max @@ -208,12 +231,13 @@ class DualCarriages: axis = dc.axis if mode == INACTIVE: dc.inactivate(toolhead.get_position()) + if self.primary_mode_dcs[axis] is dc: + self.primary_mode_dcs[axis] = None elif mode == PRIMARY: self.toggle_active_dc_rail(dc) else: - self.toggle_active_dc_rail(self.get_dc_rail_wrapper(dc.dual_rail)) dc.activate(mode, toolhead.get_position()) - kin.update_limits(axis, self.get_kin_range(toolhead, mode, axis)) + kin.update_limits(axis, self.get_kin_range(toolhead, axis)) def _handle_ready(self): for dc_rail in self.dc_rails.values(): dc_rail.apply_transform() @@ -241,10 +265,9 @@ class DualCarriages: if mode not in self.VALID_MODES: raise gcmd.error("Invalid mode=%s specified" % (mode,)) if mode in [COPY, MIRROR]: - if dc_rail in self.primary_rails: + if self.primary_mode_dcs[dc_rail.axis] in [None, dc_rail]: raise gcmd.error( - "Mode=%s is not supported for carriage=%s" % ( - mode, dc_rail.get_name())) + "Must activate another carriage as PRIMARY first") curtime = self.printer.get_reactor().monotonic() kin = self.printer.lookup_object('toolhead').get_kinematics() axis = 'xyz'[dc_rail.axis] @@ -291,18 +314,21 @@ class DualCarriages: for i, dc in enumerate(dcs)] for axis in self.axes: dc_ind = [i for i, dc in enumerate(dcs) if dc.axis == axis] - if abs(dl[dc_ind[0]]) >= abs(dl[dc_ind[1]]): - primary_ind, secondary_ind = dc_ind[0], dc_ind[1] - else: - primary_ind, secondary_ind = dc_ind[1], dc_ind[0] + abs_dl = [abs(dl[i]) for i in dc_ind] + primary_ind = dc_ind[abs_dl.index(max(abs_dl))] primary_dc = dcs[primary_ind] self.toggle_active_dc_rail(primary_dc) move_pos[axis] = carriage_positions[primary_dc.get_name()] - dc_mode = INACTIVE if min(abs(dl[primary_ind]), - abs(dl[secondary_ind])) < .000000001 \ - else COPY if dl[primary_ind] * dl[secondary_ind] > 0 \ - else MIRROR - if dc_mode != INACTIVE: + for secondary_ind in dc_ind: + if secondary_ind == primary_ind: + continue + if min(abs(dl[primary_ind]), + abs(dl[secondary_ind])) < .000000001: + continue + if dl[primary_ind] * dl[secondary_ind] > 0: + dc_mode = COPY + else: + dc_mode = MIRROR dcs[secondary_ind].activate(dc_mode, cur_pos[primary_ind]) dcs[secondary_ind].override_axis_scaling( abs(dl[secondary_ind] / dl[primary_ind]), @@ -312,18 +338,26 @@ class DualCarriages: # Make sure the scaling coefficients are restored with the mode for dc in dcs: dc.inactivate(move_pos) + saved_modes = saved_state['carriage_modes'] + saved_primary_dcs = [dc for dc in self.dc_rails.values() + if saved_modes[dc.get_name()] == PRIMARY] + # First activate all primary carriages + for dc in saved_primary_dcs: + self.activate_dc_mode(dc, PRIMARY) + # Then set the modes the remaining carriages for dc in self.dc_rails.values(): - saved_mode = saved_state['carriage_modes'][dc.get_name()] - self.activate_dc_mode(dc, saved_mode) + if dc not in saved_primary_dcs: + self.activate_dc_mode(dc, saved_modes[dc.get_name()]) class DualCarriagesRail: ENC_AXES = [b'x', b'y'] - def __init__(self, printer, rail, dual_rail, axis, active): + def __init__(self, printer, rail, dual_rail, axis, safe_dist, active): self.printer = printer self.rail = rail self.dual_rail = dual_rail self.sks = [s.get_stepper_kinematics() for s in rail.get_steppers()] self.axis = axis + self.safe_dist = safe_dist self.mode = (INACTIVE, PRIMARY)[active] self.offset = 0. self.scale = 1. if active else 0. diff --git a/test/klippy/corexyuv.cfg b/test/klippy/corexyuv.cfg index e9b0cd02a..e6fd37006 100644 --- a/test/klippy/corexyuv.cfg +++ b/test/klippy/corexyuv.cfg @@ -24,6 +24,7 @@ primary_carriage: carriage_z endstop_pin: ^PD2 [dual_carriage carriage_u] +axis: x primary_carriage: carriage_x safe_distance: 70 position_endstop: 300 @@ -32,6 +33,7 @@ homing_speed: 50 endstop_pin: ^PE4 [dual_carriage carriage_v] +axis: y primary_carriage: carriage_y safe_distance: 50 position_endstop: 200 diff --git a/test/klippy/generic_cartesian_iqex.cfg b/test/klippy/generic_cartesian_iqex.cfg new file mode 100644 index 000000000..aa4c2f5cf --- /dev/null +++ b/test/klippy/generic_cartesian_iqex.cfg @@ -0,0 +1,386 @@ +# Test config for generic cartesian kinematics with quad independent extruders +[mcu] +serial: /dev/ttyACM0 + +[mcu extboard] +serial: /dev/ttyACM1 + +[carriage carriage_t0] +axis: x +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG6 + +[dual_carriage carriage_t1] +primary_carriage: carriage_t0 +safe_distance: 70 +position_endstop: 300 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG9 + +[dual_carriage carriage_t2] +axis: x +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG10 + +[dual_carriage carriage_t3] +primary_carriage: carriage_t2 +safe_distance: 70 +position_endstop: 300 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG11 + +[carriage carriage_gantry0_left] +axis: y +position_endstop: 0 +position_max: 200 +homing_speed: 50 +endstop_pin: PG6 + +[extra_carriage carriage_gantry0_right] +primary_carriage: carriage_gantry0_left +endstop_pin: PG9 + +[dual_carriage carriage_gantry1_left] +primary_carriage: carriage_gantry0_left +safe_distance: 50 +position_endstop: 200 +position_max: 200 +homing_speed: 50 +endstop_pin: PG10 + +[extra_carriage carriage_gantry1_right] +primary_carriage: carriage_gantry1_left +endstop_pin: PG11 + +[carriage carriage_z0] +axis: z +position_endstop: 0.5 +position_max: 100 +endstop_pin: PG12 + +[extra_carriage carriage_z1] +primary_carriage: carriage_z0 +endstop_pin: PG13 + +[extra_carriage carriage_z2] +primary_carriage: carriage_z0 +endstop_pin: PG14 + +[stepper stepper_t0_x] +carriages: carriage_t0 +step_pin: extboard:PF13 +dir_pin: extboard:PF12 +enable_pin: !extboard:PF14 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_t1_x] +carriages: carriage_t1 +step_pin: extboard:PG0 +dir_pin: extboard:PG1 +enable_pin: !extboard:PF15 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_t2_x] +carriages: carriage_t2 +step_pin: extboard:PF11 +dir_pin: extboard:PG3 +enable_pin: !extboard:PG5 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_t3_x] +carriages: carriage_t3 +step_pin: extboard:PG4 +dir_pin: extboard:PC1 +enable_pin: !extboard:PA2 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry0_left] +carriages: carriage_gantry0_left +step_pin: PF13 +dir_pin: PF12 +enable_pin: !PF14 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry0_right] +carriages: carriage_gantry0_right +step_pin: PG0 +dir_pin: PG1 +enable_pin: !PF15 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry1_left] +carriages: carriage_gantry1_left +step_pin: PF11 +dir_pin: PG3 +enable_pin: !PG5 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry1_right] +carriages: carriage_gantry1_right +step_pin: PG4 +dir_pin: PC1 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 40 + +[stepper z0] +carriages: carriage_z0 +step_pin: PF9 +dir_pin: PF10 +enable_pin: !PG2 +microsteps: 16 +rotation_distance: 8 + +[stepper z1] +carriages: carriage_z1 +step_pin: PC13 +dir_pin: PF0 +enable_pin: !PF1 +microsteps: 16 +rotation_distance: 8 + +[stepper z2] +carriages: carriage_z2 +step_pin: PE2 +dir_pin: PE3 +enable_pin: !PD4 +microsteps: 16 +rotation_distance: 8 + +[extruder] +step_pin: extboard:PF9 +dir_pin: extboard:PF10 +enable_pin: !extboard:PG2 +heater_pin: extboard:PA0 # HE0 +sensor_pin: extboard:PF4 # T0 +microsteps: 16 +rotation_distance: 33.500 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder1] +step_pin: extboard:PC13 +dir_pin: extboard:PF0 +enable_pin: !extboard:PF1 +heater_pin: extboard:PA3 # HE1 +sensor_pin: extboard:PF5 # T1 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder2] +step_pin: extboard:PE2 +dir_pin: extboard:PE3 +enable_pin: !extboard:PD4 +heater_pin: extboard:PB0 # HE2 +sensor_pin: extboard:PF6 # T2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder3] +step_pin: extboard:PE6 +dir_pin: extboard:PA14 +enable_pin: !extboard:PE0 +heater_pin: extboard:PB11 # HE3 +sensor_pin: extboard:PF7 # T3 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[gcode_macro PARK_EXTRUDERS] +gcode: + G90 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + G1 Y{printer.configfile.settings["dual_carriage carriage_gantry1_left"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 + G1 X{printer.configfile.settings["dual_carriage carriage_t3"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{printer.configfile.settings["dual_carriage carriage_t1"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + +[gcode_macro T0] +gcode: + PARK_EXTRUDERS + ACTIVATE_EXTRUDER EXTRUDER=extruder + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + +[gcode_macro T1] +gcode: + PARK_EXTRUDERS + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder1 + ACTIVATE_EXTRUDER EXTRUDER=extruder1 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + +[gcode_macro T2] +gcode: + PARK_EXTRUDERS + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder2 + ACTIVATE_EXTRUDER EXTRUDER=extruder2 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + +[gcode_macro T3] +gcode: + PARK_EXTRUDERS + SYNC_EXTRUDER_MOTION EXTRUDER=extruder3 MOTION_QUEUE=extruder3 + ACTIVATE_EXTRUDER EXTRUDER=extruder3 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + +[gcode_macro SET_COPY_MODE] +gcode: + G90 + {% set y_center = 0.5 * (printer.configfile.settings["dual_carriage carriage_gantry1_left"].position_max + printer.configfile.settings["carriage carriage_gantry0_left"].position_min) %} + {% set x_max = [printer.configfile.settings["dual_carriage carriage_t3"].position_max, printer.configfile.settings["dual_carriage carriage_t1"].position_max]|min %} + {% set x_min = [printer.configfile.settings["dual_carriage carriage_t2"].position_min, printer.configfile.settings["carriage carriage_t0"].position_min]|max %} + {% set x_center = 0.5 * (x_max + x_min) %} + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + G1 Y{y_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left MODE=COPY + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder3 MOTION_QUEUE=extruder + +[gcode_macro SET_MIRROR_MODE1] +gcode: + G90 + {% set y_center = 0.5 * (printer.configfile.settings["dual_carriage carriage_gantry1_left"].position_max + printer.configfile.settings["carriage carriage_gantry0_left"].position_min) %} + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + G1 Y{y_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 + G1 X{printer.configfile.settings["dual_carriage carriage_t3"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{printer.configfile.settings["dual_carriage carriage_t1"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left MODE=COPY + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder3 MOTION_QUEUE=extruder + +[gcode_macro SET_MIRROR_MODE2] +gcode: + G90 + {% set x_max = [printer.configfile.settings["dual_carriage carriage_t3"].position_max, printer.configfile.settings["dual_carriage carriage_t1"].position_max]|min %} + {% set x_min = [printer.configfile.settings["dual_carriage carriage_t2"].position_min, printer.configfile.settings["carriage carriage_t0"].position_min]|max %} + {% set x_center = 0.5 * (x_max + x_min) %} + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + G1 Y{printer.configfile.settings["dual_carriage carriage_gantry1_left"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left MODE=MIRROR + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder3 MOTION_QUEUE=extruder + +[heater_bed] +heater_pin: PA1 +sensor_pin: PF3 # TB +sensor_type: ATC Semitec 104GT-2 +control: watermark +min_temp: 0 +max_temp: 130 + +[fan] +pin: PA8 + +[printer] +kinematics: generic_cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 + +[input_shaper] diff --git a/test/klippy/generic_cartesian_iqex.test b/test/klippy/generic_cartesian_iqex.test new file mode 100644 index 000000000..b317dbf21 --- /dev/null +++ b/test/klippy/generic_cartesian_iqex.test @@ -0,0 +1,71 @@ +# Test cases on printers with quad independent extruders +DICTIONARY stm32h723.dict extboard=stm32h723.dict +CONFIG generic_cartesian_iqex.cfg + +# First home the printer +G90 +M83 +G28 + +# Perform a dummy move +G1 X10 Y20 Z10 F6000 +G1 X11 E0.1 F3000 + +# Test other tools +T1 +G1 X120 Y50 F6000 +G1 X119 E0.1 F3000 + +T2 +G1 X200 Y70 F6000 +G1 X199 E0.1 F3000 + +T3 +G1 X70 Y50 F6000 +G1 X71 E0.1 F3000 + +# Go back to main tool +T0 +G1 X20 Y100 F6000 + +# Save dual carriage state +SAVE_DUAL_CARRIAGE_STATE + +G1 Y100 F6000 + +T2 +# Activate the dual carriage on Y axis +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left +G1 X10 Y150 F6000 + +# Restore dual carriage state +RESTORE_DUAL_CARRIAGE_STATE + +QUERY_ENDSTOPS + +# Configure input shaper +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left +SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 +SET_INPUT_SHAPER SHAPER_TYPE_X=ei SHAPER_FREQ_X=50 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=80 +SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 +SET_INPUT_SHAPER SHAPER_TYPE_X=ei SHAPER_FREQ_X=45 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=80 +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left +SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 +SET_INPUT_SHAPER SHAPER_TYPE_X=mzv SHAPER_FREQ_X=50 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=70 +SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 +SET_INPUT_SHAPER SHAPER_TYPE_X=zvd SHAPER_FREQ_X=55 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=70 + +T0 +G1 X100 Y150 F6000 + +SET_COPY_MODE +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 + +SET_MIRROR_MODE1 +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 + +SET_MIRROR_MODE2 +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 diff --git a/test/klippy/generic_cartesian_itex.cfg b/test/klippy/generic_cartesian_itex.cfg new file mode 100644 index 000000000..dc0ae115a --- /dev/null +++ b/test/klippy/generic_cartesian_itex.cfg @@ -0,0 +1,320 @@ +# Test config for generic cartesian kinematics with triple independent extruders +[mcu] +serial: /dev/ttyACM0 + +[mcu extboard] +serial: /dev/ttyACM1 + +[carriage carriage_t0] +axis: x +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG6 + +[dual_carriage carriage_t1] +primary_carriage: carriage_t0 +safe_distance: 70 +position_endstop: 300 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG9 + +[dual_carriage carriage_t2] +axis: x +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG10 + +[carriage carriage_gantry0_left] +axis: y +position_endstop: 0 +position_max: 200 +homing_speed: 50 +endstop_pin: PG6 + +[extra_carriage carriage_gantry0_right] +primary_carriage: carriage_gantry0_left +endstop_pin: PG9 + +[dual_carriage carriage_gantry1] +primary_carriage: carriage_gantry0_left +safe_distance: 50 +position_endstop: 200 +position_max: 200 +homing_speed: 50 +endstop_pin: PG10 + +[carriage carriage_z0] +axis: z +position_endstop: 0.5 +position_max: 100 +endstop_pin: PG12 + +[extra_carriage carriage_z1] +primary_carriage: carriage_z0 +endstop_pin: PG13 + +[extra_carriage carriage_z2] +primary_carriage: carriage_z0 +endstop_pin: PG14 + +[stepper stepper_t0_x] +carriages: carriage_t0 +step_pin: extboard:PF13 +dir_pin: extboard:PF12 +enable_pin: !extboard:PF14 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_t1_x] +carriages: carriage_t1 +step_pin: extboard:PG0 +dir_pin: extboard:PG1 +enable_pin: !extboard:PF15 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry0_left] +carriages: carriage_gantry0_left +step_pin: PF13 +dir_pin: PF12 +enable_pin: !PF14 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry0_right] +carriages: carriage_gantry0_right +step_pin: PG0 +dir_pin: PG1 +enable_pin: !PF15 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry1_a] +carriages: carriage_t2-carriage_gantry1 +step_pin: PF11 +dir_pin: PG3 +enable_pin: !PG5 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry1_b] +carriages: carriage_t2+carriage_gantry1 +step_pin: PG4 +dir_pin: PC1 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 40 + +[stepper z0] +carriages: carriage_z0 +step_pin: PF9 +dir_pin: PF10 +enable_pin: !PG2 +microsteps: 16 +rotation_distance: 8 + +[stepper z1] +carriages: carriage_z1 +step_pin: PC13 +dir_pin: PF0 +enable_pin: !PF1 +microsteps: 16 +rotation_distance: 8 + +[stepper z2] +carriages: carriage_z2 +step_pin: PE2 +dir_pin: PE3 +enable_pin: !PD4 +microsteps: 16 +rotation_distance: 8 + +[extruder] +step_pin: extboard:PF9 +dir_pin: extboard:PF10 +enable_pin: !extboard:PG2 +heater_pin: extboard:PA0 # HE0 +sensor_pin: extboard:PF4 # T0 +microsteps: 16 +rotation_distance: 33.500 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder1] +step_pin: extboard:PC13 +dir_pin: extboard:PF0 +enable_pin: !extboard:PF1 +heater_pin: extboard:PA3 # HE1 +sensor_pin: extboard:PF5 # T1 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder2] +step_pin: extboard:PE2 +dir_pin: extboard:PE3 +enable_pin: !extboard:PD4 +heater_pin: extboard:PB0 # HE2 +sensor_pin: extboard:PF6 # T2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[gcode_macro PARK_EXTRUDERS] +gcode: + G90 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 + G1 Y{printer.configfile.settings["dual_carriage carriage_gantry1"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{printer.configfile.settings["dual_carriage carriage_t1"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + +[gcode_macro T0] +gcode: + PARK_EXTRUDERS + ACTIVATE_EXTRUDER EXTRUDER=extruder + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + +[gcode_macro T1] +gcode: + PARK_EXTRUDERS + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder1 + ACTIVATE_EXTRUDER EXTRUDER=extruder1 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + +[gcode_macro T2] +gcode: + PARK_EXTRUDERS + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder2 + ACTIVATE_EXTRUDER EXTRUDER=extruder2 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + +[gcode_macro SET_COPY_MODE] +gcode: + G90 + {% set y_center = 0.5 * (printer.configfile.settings["dual_carriage carriage_gantry1"].position_max + printer.configfile.settings["carriage carriage_gantry0_left"].position_min) %} + {% set x_max = printer.configfile.settings["dual_carriage carriage_t1"].position_max %} + {% set x_min = [printer.configfile.settings["dual_carriage carriage_t2"].position_min, printer.configfile.settings["carriage carriage_t0"].position_min]|max %} + {% set x_center = 0.5 * (x_max + x_min) %} + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 + G1 Y{y_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 MODE=COPY + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + +[gcode_macro SET_MIRROR_MODE1] +gcode: + G90 + {% set y_center = 0.5 * (printer.configfile.settings["dual_carriage carriage_gantry1"].position_max + printer.configfile.settings["carriage carriage_gantry0_left"].position_min) %} + {% set x_max = printer.configfile.settings["dual_carriage carriage_t1"].position_max %} + {% set x_min = [printer.configfile.settings["dual_carriage carriage_t2"].position_min, printer.configfile.settings["carriage carriage_t0"].position_min]|max %} + {% set x_center = 0.5 * (x_max + x_min) %} + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 + G1 Y{y_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 MODE=COPY + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + +[gcode_macro SET_MIRROR_MODE2] +gcode: + {% set y_center = 0.5 * (printer.configfile.settings["dual_carriage carriage_gantry1"].position_max + printer.configfile.settings["carriage carriage_gantry0_left"].position_min) %} + G90 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 + G1 Y{y_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{printer.configfile.settings["dual_carriage carriage_t1"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 MODE=COPY + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + +[heater_bed] +heater_pin: PA1 +sensor_pin: PF3 # TB +sensor_type: ATC Semitec 104GT-2 +control: watermark +min_temp: 0 +max_temp: 130 + +[fan] +pin: PA8 + +[printer] +kinematics: generic_cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 + +[input_shaper] diff --git a/test/klippy/generic_cartesian_itex.test b/test/klippy/generic_cartesian_itex.test new file mode 100644 index 000000000..bcf80771d --- /dev/null +++ b/test/klippy/generic_cartesian_itex.test @@ -0,0 +1,65 @@ +# Test cases on printers with triple independent extruders +DICTIONARY stm32h723.dict extboard=stm32h723.dict +CONFIG generic_cartesian_itex.cfg + +# First home the printer +G90 +M83 +G28 + +# Perform a dummy move +G1 X10 Y20 Z10 F6000 +G1 X11 E0.1 F3000 + +# Test other tools +T1 +G1 X120 Y50 F6000 +G1 X119 E0.1 F3000 + +T2 +G1 X200 Y70 F6000 +G1 X199 E0.1 F3000 + +# Go back to main tool +T0 +G1 X20 Y100 F6000 + +# Save dual carriage state +SAVE_DUAL_CARRIAGE_STATE + +G1 Y100 F6000 + +T2 +# Activate the dual carriage on Y axis +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 +G1 X10 Y150 F6000 + +# Restore dual carriage state +RESTORE_DUAL_CARRIAGE_STATE + +QUERY_ENDSTOPS + +# Configure input shaper +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 +SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 +SET_INPUT_SHAPER SHAPER_TYPE_X=ei SHAPER_FREQ_X=45 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=80 +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left +SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 +SET_INPUT_SHAPER SHAPER_TYPE_X=mzv SHAPER_FREQ_X=50 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=70 +SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 +SET_INPUT_SHAPER SHAPER_TYPE_X=zvd SHAPER_FREQ_X=55 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=70 + +T0 +G1 X100 Y150 F6000 + +SET_COPY_MODE +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 + +SET_MIRROR_MODE1 +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 + +SET_MIRROR_MODE2 +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 From 36d9a7758084f15e539995f00e48d60c2c14b6c5 Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Wed, 31 Dec 2025 14:19:08 +0800 Subject: [PATCH 372/411] temperature_combined: add additional sensor values(humidity, pressure, gas) Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- klippy/extras/temperature_combined.py | 45 +++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/klippy/extras/temperature_combined.py b/klippy/extras/temperature_combined.py index d032bce03..22b5f4c88 100644 --- a/klippy/extras/temperature_combined.py +++ b/klippy/extras/temperature_combined.py @@ -26,6 +26,7 @@ class PrinterSensorCombined: self.apply_mode = config.getchoice('combination_method', algos) # set default values self.last_temp = self.min_temp = self.max_temp = 0.0 + self.humidity = self.pressure = self.gas = None # add object self.printer.add_object("temperature_combined " + self.name, self) # time-controlled sensor update @@ -96,13 +97,53 @@ class PrinterSensorCombined: def get_temp(self, eventtime): return self.last_temp, 0. + def update_additional(self, eventtime): + values_humidity = [] + values_pressure = [] + values_gas = [] + for sensor in self.sensors: + sensor_status = sensor.get_status(eventtime) + if 'humidity' in sensor_status: + sensor_humidity = sensor_status['humidity'] + if sensor_humidity is not None: + values_humidity.append(sensor_humidity) + if 'pressure' in sensor_status: + sensor_pressure = sensor_status['pressure'] + if sensor_pressure is not None: + values_pressure.append(sensor_pressure) + if 'gas' in sensor_status: + sensor_gas = sensor_status['gas'] + if sensor_gas is not None: + values_gas.append(sensor_gas) + + humidity = self.apply_mode(values_humidity) + if humidity: + self.humidity = humidity + + pressure = self.apply_mode(values_pressure) + if pressure: + self.pressure = pressure + + gas = self.apply_mode(values_gas) + if gas: + self.gas = gas + def get_status(self, eventtime): - return {'temperature': round(self.last_temp, 2), - } + data = { + 'temperature': round(self.last_temp, 2), + } + if self.humidity is not None: + data['humidity'] = self.humidity + if self.pressure is not None: + data['pressure'] = self.pressure + if self.gas is not None: + data['gas'] = self.gas + return data def _temperature_update_event(self, eventtime): # update sensor value self.update_temp(eventtime) + self.update_additional(eventtime) # check min / max temp values if self.last_temp < self.min_temp: From 87ea2ff1cea4f1c1a05217e96a04dbe415f0b002 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 6 Feb 2026 02:53:47 +0100 Subject: [PATCH 373/411] temperature_combined: fix operations on empty list Signed-off-by: Timofey Titovets --- klippy/extras/temperature_combined.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/klippy/extras/temperature_combined.py b/klippy/extras/temperature_combined.py index 22b5f4c88..51468bc9b 100644 --- a/klippy/extras/temperature_combined.py +++ b/klippy/extras/temperature_combined.py @@ -116,17 +116,20 @@ class PrinterSensorCombined: if sensor_gas is not None: values_gas.append(sensor_gas) - humidity = self.apply_mode(values_humidity) - if humidity: - self.humidity = humidity + if values_humidity: + humidity = self.apply_mode(values_humidity) + if humidity: + self.humidity = humidity - pressure = self.apply_mode(values_pressure) - if pressure: - self.pressure = pressure + if values_pressure: + pressure = self.apply_mode(values_pressure) + if pressure: + self.pressure = pressure - gas = self.apply_mode(values_gas) - if gas: - self.gas = gas + if values_gas: + gas = self.apply_mode(values_gas) + if gas: + self.gas = gas def get_status(self, eventtime): data = { From 8965958a8b6cf5ad1c2be8dbddc8024b776691a2 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 13 Aug 2025 22:35:05 +0200 Subject: [PATCH 374/411] bus: raise I2C errors on the host side Signed-off-by: Timofey Titovets --- klippy/extras/bus.py | 50 +++++++++++++++++++++++++++++++++++++------- src/i2ccmds.c | 31 +++++++++++++++++---------- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 19e278418..922d2a1c5 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -180,10 +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 + self.i2c_transfer_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 = [] + # Host side I2C error handling + self._configured = False def _handle_connect(self): for data in self._to_write: self.i2c_write(data) @@ -208,21 +211,54 @@ class MCU_I2C: self.mcu.add_config_cmd(self.config_fmt) self.i2c_write_cmd = self.mcu.lookup_command( "i2c_write oid=%c data=%*s", cq=self.cmd_queue) - self.i2c_read_cmd = self.mcu.lookup_query_command( - "i2c_read oid=%c reg=%*s read_len=%u", - "i2c_read_response oid=%c response=%*s", oid=self.oid, - cq=self.cmd_queue) + if self.mcu.try_lookup_command("i2c_read oid=%c reg=%*s read_len=%u"): + self.i2c_read_cmd = self.mcu.lookup_query_command( + "i2c_read oid=%c reg=%*s read_len=%u", + "i2c_read_response oid=%c response=%*s", oid=self.oid, + cq=self.cmd_queue) + else: + self.i2c_transfer_cmd = self.mcu.lookup_query_command( + "i2c_transfer oid=%c write=%*s read_len=%u", + "i2c_response oid=%c i2c_bus_status=%c response=%*s", + oid=self.oid, cq=self.cmd_queue) + if self.mcu.is_fileoutput(): + self.i2c_transfer_cmd = self.mcu.lookup_command( + "i2c_transfer oid=%c write=%*s read_len=%u", + cq=self.cmd_queue) + self._configured = True def i2c_write_noack(self, data, minclock=0, reqclock=0): self.i2c_write_cmd.send([self.oid, data], minclock=minclock, reqclock=reqclock) - def i2c_write(self, data, minclock=0, reqclock=0): - if self.i2c_write_cmd is None: + def i2c_write(self, data, minclock=0, reqclock=0, retry=True): + if not self._configured: self._to_write.append(data) return + if self.i2c_transfer_cmd is not None: + self.i2c_transfer(data, minclock=minclock, reqclock=reqclock, + retry=retry) + return 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) + if self.i2c_transfer_cmd is not None: + return self.i2c_transfer(write, read_len, retry=retry) + return self.i2c_read_cmd.send([self.oid, write, read_len], + retry=retry) + def i2c_transfer(self, write, read_len=0, minclock=0, reqclock=0, + retry=True): + cmd = self.i2c_transfer_cmd + if self.mcu.is_fileoutput(): + cmd.send([self.oid, write, read_len], + minclock=minclock, reqclock=reqclock) + return + param = cmd.send([self.oid, write, read_len], + minclock=minclock, reqclock=reqclock, retry=retry) + status = param["i2c_bus_status"] + if status == "SUCCESS": + return param + err_msg = "MCU '%s' I2C request to addr %i reports error %s" % ( + self.mcu.get_name(), self.i2c_address, status) + self.mcu.get_printer().invoke_shutdown(err_msg) def MCU_I2C_from_config(config, default_addr=None, default_speed=100000): # Load bus parameters diff --git a/src/i2ccmds.c b/src/i2ccmds.c index c51772f1d..621cfd378 100644 --- a/src/i2ccmds.c +++ b/src/i2ccmds.c @@ -17,6 +17,12 @@ enum { IF_SOFTWARE = 1, IF_HARDWARE = 2 }; +DECL_ENUMERATION("i2c_bus_status", "SUCCESS", I2C_BUS_SUCCESS); +DECL_ENUMERATION("i2c_bus_status", "NACK", I2C_BUS_NACK); +DECL_ENUMERATION("i2c_bus_status", "START_NACK", I2C_BUS_START_NACK); +DECL_ENUMERATION("i2c_bus_status", "START_READ_NACK", I2C_BUS_START_READ_NACK); +DECL_ENUMERATION("i2c_bus_status", "BUS_TIMEOUT", I2C_BUS_TIMEOUT); + void command_config_i2c(uint32_t *args) { @@ -78,8 +84,7 @@ void command_i2c_write(uint32_t *args) struct i2cdev_s *i2c = oid_lookup(oid, command_config_i2c); uint8_t data_len = args[1]; uint8_t *data = command_decode_ptr(args[2]); - int ret = i2c_dev_write(i2c, data_len, data); - i2c_shutdown_on_err(ret); + i2c_dev_write(i2c, data_len, data); } DECL_COMMAND(command_i2c_write, "i2c_write oid=%c data=%*s"); @@ -93,16 +98,20 @@ int i2c_dev_read(struct i2cdev_s *i2c, uint8_t reg_len, uint8_t *reg return i2c_read(i2c->i2c_hw, reg_len, reg, read_len, read); } -void command_i2c_read(uint32_t *args) +void command_i2c_transfer(uint32_t *args) { uint8_t oid = args[0]; struct i2cdev_s *i2c = oid_lookup(oid, command_config_i2c); - uint8_t reg_len = args[1]; - uint8_t *reg = command_decode_ptr(args[2]); - uint8_t data_len = args[3]; - uint8_t data[data_len]; - int ret = i2c_dev_read(i2c, reg_len, reg, data_len, data); - i2c_shutdown_on_err(ret); - sendf("i2c_read_response oid=%c response=%*s", oid, data_len, data); + uint8_t wlen = args[1]; + uint8_t *wdata = command_decode_ptr(args[2]); + uint8_t rlen = args[3]; + uint8_t rdata[rlen]; + uint8_t nack; + if (rlen > 0) + nack = i2c_dev_read(i2c, wlen, wdata, rlen, rdata); + else + nack = i2c_dev_write(i2c, wlen, wdata); + sendf("i2c_response oid=%c i2c_bus_status=%c response=%*s", + oid, nack, rlen, rdata); } -DECL_COMMAND(command_i2c_read, "i2c_read oid=%c reg=%*s read_len=%u"); +DECL_COMMAND(command_i2c_transfer, "i2c_transfer oid=%c write=%*s read_len=%u"); From 0c2c1dd13b590854075bc780219cdfad16e68ac0 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 11 Oct 2025 18:43:13 +0200 Subject: [PATCH 375/411] bus: allow mark i2c as write only If the bus is write only, with new i2c_transfer code it is possible to at least provide some feedback on error Signed-off-by: Timofey Titovets --- klippy/extras/bus.py | 42 ++++++++++++++++++++++++++++----- klippy/extras/display/uc1701.py | 3 ++- klippy/extras/sx1509.py | 3 ++- src/i2ccmds.c | 10 -------- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 922d2a1c5..f858e560e 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -4,6 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import mcu +import logging def resolve_bus_name(mcu, param, bus): # Find enumerations for the given bus @@ -155,12 +156,14 @@ def MCU_SPI_from_config(config, mode, pin_option="cs_pin", # Helper code for working with devices connected to an MCU via an I2C bus class MCU_I2C: - def __init__(self, mcu, bus, addr, speed, sw_pins=None): + def __init__(self, mcu, bus, addr, speed, sw_pins=None, + async_write_only=False): self.mcu = mcu self.bus = bus self.i2c_address = addr self.oid = self.mcu.create_oid() self.speed = speed + self.async_write_only = async_write_only self.config_fmt_ticks = None mcu.add_config_cmd("config_i2c oid=%d" % (self.oid,)) # Generate I2C bus config message @@ -180,7 +183,7 @@ 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 - self.i2c_transfer_cmd = None + self.i2c_transfer_cmd = self.i2c_write_only_cmd = None printer = self.mcu.get_printer() printer.register_event_handler("klippy:connect", self._handle_connect) # backward support i2c_write inside the init section @@ -198,6 +201,13 @@ class MCU_I2C: return self.i2c_address def get_command_queue(self): return self.cmd_queue + def _async_write_status(self, params): + status = params["i2c_bus_status"] + if status == "SUCCESS": + return + err_msg = "MCU '%s' I2C request to addr %i reports error %s" % ( + self.mcu.get_name(), self.i2c_address, status) + logging.error(err_msg) def build_config(self): if '%' in self.config_fmt: bus = resolve_bus_name(self.mcu, "i2c_bus", self.bus) @@ -209,9 +219,10 @@ class MCU_I2C: pulse_ticks = self.mcu.seconds_to_clock(1./self.speed/2) self.config_fmt = self.config_fmt_ticks % (pulse_ticks,) self.mcu.add_config_cmd(self.config_fmt) - self.i2c_write_cmd = self.mcu.lookup_command( - "i2c_write oid=%c data=%*s", cq=self.cmd_queue) if self.mcu.try_lookup_command("i2c_read oid=%c reg=%*s read_len=%u"): + self.i2c_write_cmd = self.mcu.lookup_command( + "i2c_write oid=%c data=%*s", cq=self.cmd_queue) + self.i2c_write_only_cmd = self.i2c_write_cmd self.i2c_read_cmd = self.mcu.lookup_query_command( "i2c_read oid=%c reg=%*s read_len=%u", "i2c_read_response oid=%c response=%*s", oid=self.oid, @@ -221,18 +232,32 @@ class MCU_I2C: "i2c_transfer oid=%c write=%*s read_len=%u", "i2c_response oid=%c i2c_bus_status=%c response=%*s", oid=self.oid, cq=self.cmd_queue) + self.i2c_write_only_cmd = self.mcu.lookup_command( + "i2c_transfer oid=%c write=%*s read_len=%u", + cq=self.cmd_queue) if self.mcu.is_fileoutput(): self.i2c_transfer_cmd = self.mcu.lookup_command( "i2c_transfer oid=%c write=%*s read_len=%u", cq=self.cmd_queue) + if self.async_write_only: + self.mcu.register_response(self._async_write_status, + "i2c_response", self.oid) self._configured = True def i2c_write_noack(self, data, minclock=0, reqclock=0): + if self.async_write_only: + self.i2c_write_only_cmd.send([self.oid, data, 0], + minclock=minclock, reqclock=reqclock) + return self.i2c_write_cmd.send([self.oid, data], minclock=minclock, reqclock=reqclock) def i2c_write(self, data, minclock=0, reqclock=0, retry=True): if not self._configured: self._to_write.append(data) return + if self.async_write_only: + self.i2c_write_only_cmd.send([self.oid, data, 0], + minclock=minclock, reqclock=reqclock) + return if self.i2c_transfer_cmd is not None: self.i2c_transfer(data, minclock=minclock, reqclock=reqclock, retry=retry) @@ -240,12 +265,16 @@ class MCU_I2C: self.i2c_write_cmd.send_wait_ack([self.oid, data], minclock=minclock, reqclock=reqclock) def i2c_read(self, write, read_len, retry=True): + if self.async_write_only: + raise mcu.error("I2C dev is write only") if self.i2c_transfer_cmd is not None: return self.i2c_transfer(write, read_len, retry=retry) return self.i2c_read_cmd.send([self.oid, write, read_len], retry=retry) def i2c_transfer(self, write, read_len=0, minclock=0, reqclock=0, retry=True): + if self.async_write_only: + raise mcu.error("I2C dev is write only") cmd = self.i2c_transfer_cmd if self.mcu.is_fileoutput(): cmd.send([self.oid, write, read_len], @@ -260,7 +289,8 @@ class MCU_I2C: self.mcu.get_name(), self.i2c_address, status) self.mcu.get_printer().invoke_shutdown(err_msg) -def MCU_I2C_from_config(config, default_addr=None, default_speed=100000): +def MCU_I2C_from_config(config, default_addr=None, default_speed=100000, + async_write_only=False): # Load bus parameters printer = config.get_printer() i2c_mcu = mcu.get_printer_mcu(printer, config.get('i2c_mcu', 'mcu')) @@ -286,7 +316,7 @@ def MCU_I2C_from_config(config, default_addr=None, default_speed=100000): bus = config.get('i2c_bus', None) sw_pins = None # Create MCU_I2C object - return MCU_I2C(i2c_mcu, bus, addr, speed, sw_pins) + return MCU_I2C(i2c_mcu, bus, addr, speed, sw_pins, async_write_only) ###################################################################### diff --git a/klippy/extras/display/uc1701.py b/klippy/extras/display/uc1701.py index 85b74decd..91b62baf1 100644 --- a/klippy/extras/display/uc1701.py +++ b/klippy/extras/display/uc1701.py @@ -130,7 +130,8 @@ class SPI4wire: class I2C: def __init__(self, config, default_addr): self.i2c = bus.MCU_I2C_from_config(config, default_addr=default_addr, - default_speed=400000) + default_speed=400000, + async_write_only=True) def send(self, cmds, is_data=False): if is_data: hdr = 0x40 diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index ce25bd027..0ad657f60 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -25,7 +25,8 @@ class SX1509(object): def __init__(self, config): self._printer = config.get_printer() self._name = config.get_name().split()[1] - self._i2c = bus.MCU_I2C_from_config(config, default_speed=400000) + self._i2c = bus.MCU_I2C_from_config(config, default_speed=400000, + async_write_only=True) self._ppins = self._printer.lookup_object("pins") self._ppins.register_chip("sx1509_" + self._name, self) self._mcu = self._i2c.get_mcu() diff --git a/src/i2ccmds.c b/src/i2ccmds.c index 621cfd378..d9996d93e 100644 --- a/src/i2ccmds.c +++ b/src/i2ccmds.c @@ -78,16 +78,6 @@ int i2c_dev_write(struct i2cdev_s *i2c, uint8_t write_len, uint8_t *data) return i2c_write(i2c->i2c_hw, write_len, data); } -void command_i2c_write(uint32_t *args) -{ - uint8_t oid = args[0]; - struct i2cdev_s *i2c = oid_lookup(oid, command_config_i2c); - uint8_t data_len = args[1]; - uint8_t *data = command_decode_ptr(args[2]); - i2c_dev_write(i2c, data_len, data); -} -DECL_COMMAND(command_i2c_write, "i2c_write oid=%c data=%*s"); - int i2c_dev_read(struct i2cdev_s *i2c, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *read) { From 13b4561beb237bedb41c65ae290b5a2ef9f18b16 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 7 Feb 2026 14:32:43 -0500 Subject: [PATCH 376/411] docs: Update Config_Changes.md to reflect i2c changes to sx1509 and uc1701 Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index cf2d53fd9..384c5b39f 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,11 @@ All dates in this document are approximate. ## Changes +20260207: The low-level i2c behavior of sx1509 and uc1701 devices has +changed. Previously an i2c error would result in a shutdown, and now +i2c errors when communicating with these devices will only generate +warnings in the log file. + 20260109: The status value `{printer.probe.last_z_result}` is deprecated; it will be removed in the near future. Use `{printer.probe.last_probe_position}` instead, and note that this new From 0dff097b72b78607e481954902d4b96ec7639a99 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 7 Feb 2026 20:18:23 +0100 Subject: [PATCH 377/411] stepcompress: wrap steps in struct qstep That allows us to make it self-explanatory what is contained here. Signed-off-by: Timofey Titovets --- klippy/chelper/stepcompress.c | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 141c8ac35..3176647ab 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -28,9 +28,13 @@ #define CHECK_LINES 1 #define QUEUE_START_SIZE 1024 +struct qstep { + uint32_t clock32; +}; + struct stepcompress { // Buffer management - uint32_t *queue, *queue_end, *queue_pos, *queue_next; + struct qstep *queue, *queue_end, *queue_pos, *queue_next; // Internal tracking uint32_t max_error; double mcu_time_offset, mcu_freq, last_step_print_time; @@ -85,10 +89,10 @@ struct points { // Given a requested step time, return the minimum and maximum // acceptable times static inline struct points -minmax_point(struct stepcompress *sc, uint32_t *pos) +minmax_point(struct stepcompress *sc, struct qstep *pos) { - uint32_t lsc = sc->last_step_clock, point = *pos - lsc; - uint32_t prevpoint = pos > sc->queue_pos ? *(pos-1) - lsc : 0; + uint32_t lsc = sc->last_step_clock, point = pos->clock32 - lsc; + uint32_t prevpoint = pos > sc->queue_pos ? (pos-1)->clock32 - lsc : 0; uint32_t max_error = (point - prevpoint) / 2; if (max_error > sc->max_error) max_error = sc->max_error; @@ -105,7 +109,7 @@ minmax_point(struct stepcompress *sc, uint32_t *pos) static struct step_move compress_bisect_add(struct stepcompress *sc) { - uint32_t *qlast = sc->queue_next; + struct qstep *qlast = sc->queue_next; if (qlast > sc->queue_pos + 65535) qlast = sc->queue_pos + 65535; struct points point = minmax_point(sc, sc->queue_pos); @@ -429,7 +433,8 @@ queue_append_far(struct stepcompress *sc) return ret; if (step_clock >= sc->last_step_clock + CLOCK_DIFF_MAX) return stepcompress_flush_far(sc, step_clock); - *sc->queue_next++ = step_clock; + sc->queue_next->clock32 = step_clock; + sc->queue_next++; return 0; } @@ -439,7 +444,7 @@ queue_append_extend(struct stepcompress *sc) { if (sc->queue_next - sc->queue_pos > 65535 + 2000) { // No point in keeping more than 64K steps in memory - uint32_t flush = (*(sc->queue_next-65535) + uint32_t flush = ((sc->queue_next-65535)->clock32 - (uint32_t)sc->last_step_clock); int ret = queue_flush(sc, sc->last_step_clock + flush); if (ret) @@ -466,7 +471,8 @@ queue_append_extend(struct stepcompress *sc) sc->queue_next = sc->queue + in_use; } - *sc->queue_next++ = sc->next_step_clock; + sc->queue_next->clock32 = sc->next_step_clock; + sc->queue_next++; sc->next_step_clock = 0; return 0; } @@ -484,7 +490,8 @@ queue_append(struct stepcompress *sc) return queue_append_far(sc); if (unlikely(sc->queue_next >= sc->queue_end)) return queue_append_extend(sc); - *sc->queue_next++ = sc->next_step_clock; + sc->queue_next->clock32 = sc->next_step_clock; + sc->queue_next++; sc->next_step_clock = 0; return 0; } From c2d0cc1967927448585937a25b42b37d20cefa43 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 22 Nov 2025 20:24:38 +0100 Subject: [PATCH 378/411] gcode: enrich param errors Give suggestion or avaliable options to the user. Signed-off-by: Timofey Titovets --- klippy/gcode.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/klippy/gcode.py b/klippy/gcode.py index 11b57cfab..e311d1ff9 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -321,8 +321,18 @@ class GCodeDispatch: else: key_param = gcmd.get(key) if key_param not in values: - raise gcmd.error("The value '%s' is not valid for %s" - % (key_param, key)) + keys = [] + guess = "" + for value in values: + if value is None: + continue + keys.append("\'%s\'" % value) + if len(key_param) and key_param in value: + guess = ". Did you mean \'%s\'?" % value + if guess == "": + guess = ". Options: %s" % (", ".join(k for k in keys)) + raise gcmd.error("The value '%s' is not valid for %s%s" + % (key_param, key, guess)) values[key_param](gcmd) # Low-level G-Code commands that are needed before the config file is loaded def cmd_M110(self, gcmd): From cfd9dc7242db59c9e4ede87a39e302aa802ec046 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 22 Nov 2025 20:04:31 +0100 Subject: [PATCH 379/411] force_move: refactor to mux commands Gcode mux supports arg verification and error output. Just reuse it. Signed-off-by: Timofey Titovets --- klippy/extras/force_move.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py index 277c68e3c..fb6984ec7 100644 --- a/klippy/extras/force_move.py +++ b/klippy/extras/force_move.py @@ -39,17 +39,24 @@ class ForceMove: self.stepper_kinematics = ffi_main.gc( ffi_lib.cartesian_stepper_alloc(b'x'), ffi_lib.free) # Register commands - gcode = self.printer.lookup_object('gcode') - gcode.register_command('STEPPER_BUZZ', self.cmd_STEPPER_BUZZ, - desc=self.cmd_STEPPER_BUZZ_help) - if config.getboolean("enable_force_move", False): - gcode.register_command('FORCE_MOVE', self.cmd_FORCE_MOVE, - desc=self.cmd_FORCE_MOVE_help) + self._enable_force_move = config.getboolean("enable_force_move", False) + if self._enable_force_move: + gcode = self.printer.lookup_object('gcode') gcode.register_command('SET_KINEMATIC_POSITION', self.cmd_SET_KINEMATIC_POSITION, desc=self.cmd_SET_KINEMATIC_POSITION_help) def register_stepper(self, config, mcu_stepper): - self.steppers[mcu_stepper.get_name()] = mcu_stepper + name = mcu_stepper.get_name() + self.steppers[name] = mcu_stepper + # Reuse mux helper args checks + gcode = self.printer.lookup_object('gcode') + gcode.register_mux_command('STEPPER_BUZZ', "STEPPER", name, + self.cmd_STEPPER_BUZZ, + desc=self.cmd_STEPPER_BUZZ_help) + if self._enable_force_move: + gcode.register_mux_command('FORCE_MOVE', "STEPPER", name, + self.cmd_FORCE_MOVE, + desc=self.cmd_FORCE_MOVE_help) def lookup_stepper(self, name): if name not in self.steppers: raise self.printer.config_error("Unknown stepper %s" % (name,)) @@ -82,14 +89,9 @@ class ForceMove: stepper.set_trapq(prev_trapq) stepper.set_stepper_kinematics(prev_sk) self.motion_queuing.wipe_trapq(self.trapq) - def _lookup_stepper(self, gcmd): - name = gcmd.get('STEPPER') - if name not in self.steppers: - raise gcmd.error("Unknown stepper %s" % (name,)) - return self.steppers[name] cmd_STEPPER_BUZZ_help = "Oscillate a given stepper to help id it" def cmd_STEPPER_BUZZ(self, gcmd): - stepper = self._lookup_stepper(gcmd) + stepper = self.lookup_stepper(gcmd.get('STEPPER')) logging.info("Stepper buzz %s", stepper.get_name()) did_enable = self._force_enable(stepper) toolhead = self.printer.lookup_object('toolhead') @@ -104,7 +106,7 @@ class ForceMove: 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) + stepper = self.lookup_stepper(gcmd.get('STEPPER')) distance = gcmd.get_float('DISTANCE') speed = gcmd.get_float('VELOCITY', above=0.) accel = gcmd.get_float('ACCEL', 0., minval=0.) From c85a1a676132aa8579ce21316c0b02fddb7f5edb Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 2 Dec 2025 22:00:36 +0100 Subject: [PATCH 380/411] stepper_enable: refactor to mux commands Signed-off-by: Timofey Titovets --- klippy/extras/stepper_enable.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/klippy/extras/stepper_enable.py b/klippy/extras/stepper_enable.py index 926e95922..ba2968bcb 100644 --- a/klippy/extras/stepper_enable.py +++ b/klippy/extras/stepper_enable.py @@ -81,11 +81,12 @@ class PrinterStepperEnable: gcode = self.printer.lookup_object('gcode') gcode.register_command("M18", self.cmd_M18) gcode.register_command("M84", self.cmd_M18) - gcode.register_command("SET_STEPPER_ENABLE", - self.cmd_SET_STEPPER_ENABLE, - desc=self.cmd_SET_STEPPER_ENABLE_help) def register_stepper(self, config, mcu_stepper): name = mcu_stepper.get_name() + gcode = self.printer.lookup_object('gcode') + gcode.register_mux_command('SET_STEPPER_ENABLE', "STEPPER", name, + self.cmd_SET_STEPPER_ENABLE, + desc=self.cmd_SET_STEPPER_ENABLE_help) 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): @@ -128,11 +129,7 @@ class PrinterStepperEnable: self.motor_off() cmd_SET_STEPPER_ENABLE_help = "Enable/disable individual stepper by name" def cmd_SET_STEPPER_ENABLE(self, gcmd): - stepper_name = gcmd.get('STEPPER', None) - if stepper_name not in self.enable_lines: - gcmd.respond_info('SET_STEPPER_ENABLE: Invalid stepper "%s"' - % (stepper_name,)) - return + stepper_name = gcmd.get('STEPPER') stepper_enable = gcmd.get_int('ENABLE', 1) self.set_motors_enable([stepper_name], stepper_enable) if stepper_enable: From 0a43774432c149eb07df8437626736345b057521 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 27 Jan 2026 03:22:16 +0100 Subject: [PATCH 381/411] heaters: refactor to mux commands Signed-off-by: Timofey Titovets --- klippy/extras/heaters.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/klippy/extras/heaters.py b/klippy/extras/heaters.py index fcec44fcd..ec4792dea 100644 --- a/klippy/extras/heaters.py +++ b/klippy/extras/heaters.py @@ -255,8 +255,6 @@ class PrinterHeaters: gcode.register_command("TURN_OFF_HEATERS", self.cmd_TURN_OFF_HEATERS, desc=self.cmd_TURN_OFF_HEATERS_help) gcode.register_command("M105", self.cmd_M105, when_not_ready=True) - gcode.register_command("TEMPERATURE_WAIT", self.cmd_TEMPERATURE_WAIT, - desc=self.cmd_TEMPERATURE_WAIT_help) def load_config(self, config): self.have_load_sensors = True # Load default temperature sensors @@ -299,7 +297,12 @@ class PrinterHeaters: "Unknown temperature sensor '%s'" % (sensor_type,)) return self.sensor_factories[sensor_type](config) def register_sensor(self, config, psensor, gcode_id=None): - self.available_sensors.append(config.get_name()) + sensor_name = config.get_name() + self.available_sensors.append(sensor_name) + gcode = self.printer.lookup_object('gcode') + gcode.register_mux_command('TEMPERATURE_WAIT', "SENSOR", sensor_name, + self.cmd_TEMPERATURE_WAIT, + desc=self.cmd_TEMPERATURE_WAIT_help) if gcode_id is None: gcode_id = config.get('gcode_id', None) if gcode_id is None: @@ -361,8 +364,6 @@ class PrinterHeaters: cmd_TEMPERATURE_WAIT_help = "Wait for a temperature on a sensor" def cmd_TEMPERATURE_WAIT(self, gcmd): sensor_name = gcmd.get('SENSOR') - if sensor_name not in self.available_sensors: - raise gcmd.error("Unknown sensor '%s'" % (sensor_name,)) min_temp = gcmd.get_float('MINIMUM', float('-inf')) max_temp = gcmd.get_float('MAXIMUM', float('inf'), above=min_temp) if min_temp == float('-inf') and max_temp == float('inf'): From 6d7d3403e49c2197c2d9a281dd152518c1ee28c8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 7 Feb 2026 14:49:00 -0500 Subject: [PATCH 382/411] stepcompress: Add some comments on internal structs Signed-off-by: Kevin O'Connor --- klippy/chelper/stepcompress.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index 3176647ab..0fe515ee5 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -28,10 +28,13 @@ #define CHECK_LINES 1 #define QUEUE_START_SIZE 1024 +// Storage for queuing steps (only lower 32 bits of step clock are stored as +// optimization to reduce memory, improve cache usage, and reduce 64 bit ops) struct qstep { uint32_t clock32; }; +// Main stepcompress object storage struct stepcompress { // Buffer management struct qstep *queue, *queue_end, *queue_pos, *queue_next; @@ -52,12 +55,14 @@ struct stepcompress { struct list_head history_list; }; +// Parameters of a single queue_step command struct step_move { uint32_t interval; uint16_t count; int16_t add; }; +// Storage for internal history of recently sent queue_step commands struct history_steps { struct list_node node; uint64_t first_clock, last_clock; From ce97c514ba249ae1a4a85ad652605578e21a20eb Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 10 Feb 2026 16:52:32 +0100 Subject: [PATCH 383/411] linux: fixed array out of bounds access for GPIO setup in linux process (#7196) Signed-off-by: Tom Mittendorf --- src/linux/gpio.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/linux/gpio.c b/src/linux/gpio.c index c7f4c5bf6..7d4d0b169 100644 --- a/src/linux/gpio.c +++ b/src/linux/gpio.c @@ -68,6 +68,7 @@ get_chip_fd(uint8_t chipId) struct gpio_out gpio_out_setup(uint32_t pin, uint8_t val) { + if (pin >= ARRAY_SIZE(gpio_lines)) shutdown("Not an output pin"); struct gpio_line *line = &gpio_lines[pin]; line->offset = GPIO2PIN(pin); line->chipid = GPIO2PORT(pin); @@ -132,6 +133,7 @@ gpio_out_toggle(struct gpio_out g) struct gpio_in gpio_in_setup(uint32_t pin, int8_t pull_up) { + if (pin >= ARRAY_SIZE(gpio_lines)) shutdown("Not an input pin"); struct gpio_line *line = &gpio_lines[pin]; line->offset = GPIO2PIN(pin); line->chipid = GPIO2PORT(pin); From 636a6141f181f6481280c627c600e63d7b2dc7d7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 11 Feb 2026 09:31:25 -0500 Subject: [PATCH 384/411] probe: Fix application of axis_twist_compensation Commit 2a1027ce changed the "probe:update_results" event to take a list, but failed to update the callers to utilize that modified list in its results. Change axis_twist_compensation to update all members in the list and change the callers to use the resulting list. Reported by @ritzi26. Signed-off-by: Kevin O'Connor --- klippy/extras/axis_twist_compensation.py | 35 ++++++++++++------------ klippy/extras/probe.py | 4 ++- klippy/extras/probe_eddy_current.py | 3 +- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index 081af9269..b6a6e2573 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -52,25 +52,26 @@ class AxisTwistCompensation: self._update_z_compensation_value) def _update_z_compensation_value(self, poslist): - pos = poslist[0] - zo = 0. - if self.z_compensations: - zo += self._get_interpolated_z_compensation( - pos.test_x, self.z_compensations, - self.compensation_start_x, - self.compensation_end_x - ) + for i in range(len(poslist)): + pos = poslist[i] + zo = 0. + if self.z_compensations: + zo += self._get_interpolated_z_compensation( + pos.test_x, self.z_compensations, + self.compensation_start_x, + self.compensation_end_x + ) - if self.zy_compensations: - zo += self._get_interpolated_z_compensation( - pos.test_y, self.zy_compensations, - self.compensation_start_y, - self.compensation_end_y - ) + if self.zy_compensations: + zo += self._get_interpolated_z_compensation( + pos.test_y, self.zy_compensations, + self.compensation_start_y, + self.compensation_end_y + ) - pos = manual_probe.ProbeResult(pos.bed_x, pos.bed_y, pos.bed_z + zo, - pos.test_x, pos.test_y, pos.test_z) - poslist[0] = pos + pos = manual_probe.ProbeResult(pos.bed_x, pos.bed_y, pos.bed_z + zo, + pos.test_x, pos.test_y, pos.test_z) + poslist[i] = pos def _get_interpolated_z_compensation( self, coord, z_compensations, diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index c57f9ef1d..044875318 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -379,7 +379,9 @@ class ProbeSessionHelper: reason += HINT_TIMEOUT raise self.printer.command_error(reason) # Allow axis_twist_compensation to update results - self.printer.send_event("probe:update_results", [epos]) + results = [epos] + self.printer.send_event("probe:update_results", results) + epos = results[0] # Report results gcode = self.printer.lookup_object('gcode') gcode.respond_info("probe: at %.3f,%.3f bed will contact at z=%.6f" diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 6952480a3..9863b7d58 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -650,8 +650,7 @@ class EddyScanningProbe: toolhead.get_last_move_time() results = self._gather.pull_probed() # Allow axis_twist_compensation to update results - for epos in results: - self._printer.send_event("probe:update_results", [epos]) + self._printer.send_event("probe:update_results", results) return results def end_probe_session(self): self._gather.finish() From 8a210d23fe5047ca420af13894ee46f94ea80a88 Mon Sep 17 00:00:00 2001 From: FranciscoStephens Date: Wed, 21 Jan 2026 14:50:45 -0300 Subject: [PATCH 385/411] bmi160: Add support for BMI160 accelerometer This adds host and firmware support for the Bosch BMI160 IMU. It includes support for both SPI and I2C communication protocols. The firmware implementation includes a specific SPI wake-up sequence (dummy read) required to switch the sensor interface mode reliably. Validated on Linux MCU (SPI) and RP2040 (I2C) with stable 1600Hz ODR. Signed-off-by: FranciscoStephens --- docs/Config_Reference.md | 33 +++++ klippy/extras/bmi160.py | 188 +++++++++++++++++++++++++++++ src/Kconfig | 9 +- src/Makefile | 1 + src/sensor_bmi160.c | 252 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 klippy/extras/bmi160.py create mode 100644 src/sensor_bmi160.c diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 8b17d9502..7bc54e42d 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1923,6 +1923,39 @@ Support for LIS3DH accelerometers. # See the "adxl345" section for information on this parameter. ``` +### [bmi160] + +BMI160 accelerometer. This sensor can be queried via I2C or SPI bus. +``` +[bmi160] +#i2c_address: +# Default is 105 (0x69). If SA0 is tied to GND, use 104 (0x68). +# Only used for I2C. +#i2c_mcu: +#i2c_bus: +#i2c_speed: +# See the "common I2C settings" section for a description of the +# above parameters. Only used for I2C. +#cs_pin: +#spi_speed: +#spi_bus: +#spi_software_sclk_pin: +#spi_software_mosi_pin: +#spi_software_miso_pin: +# See the "common SPI settings" section for a description of the +# above parameters. Only used for SPI. +#axes_map: x, y, z +# See the "adxl345" section for information on this parameter. +``` + +**Important:** Many BMI160 modules use ambiguous pin labels. For SPI: +- Use **SCL** for clock (not SCX) +- Use **SDA** for MOSI (not SDX) +- Use **SA0** for MISO +- Use **CS** for chip select + +The pins labeled SCX/SDX are for the auxiliary magnetometer bus. + ### [mpu9250] Support for MPU-9250, MPU-9255, MPU-6515, MPU-6050, and MPU-6500 diff --git a/klippy/extras/bmi160.py b/klippy/extras/bmi160.py new file mode 100644 index 000000000..041ae8a95 --- /dev/null +++ b/klippy/extras/bmi160.py @@ -0,0 +1,188 @@ +# Support for reading acceleration data from a BMI160 chip +# +# Copyright (C) 2025 Francisco Stephens +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from . import bus, adxl345, bulk_sensor + +# BMI160 registers +REG_CHIPID = 0x00 +REG_ACC_DATA_START = 0x12 +REG_ACC_CONF = 0x40 +REG_ACC_RANGE = 0x41 +REG_FIFO_DOWNS = 0x45 +REG_FIFO_CONFIG_0 = 0x46 +REG_FIFO_CONFIG_1 = 0x47 +REG_FIFO_DATA = 0x24 +REG_FIFO_LENGTH_0 = 0x22 +REG_CMD = 0x7E + +REG_MOD_READ = 0x80 + +# BMI160 commands for CMD register +CMD_ACC_PM_SUSPEND = 0x10 +CMD_ACC_PM_NORMAL = 0x11 +CMD_FIFO_FLUSH = 0xB0 + +# BMI160 constants +BMI160_DEV_ID = 0xD1 +# Target 1600Hz ODR, normal bandwidth, no undersampling +SET_ACC_CONF_1600HZ = 0x2C +# Set accelerometer range to +/-16g +SET_ACC_RANGE_16G = 0x0C +# Enable accelerometer FIFO, headerless mode +SET_FIFO_CONFIG_1 = 0x40 +# No FIFO downsampling +SET_FIFO_DOWNS = 0x00 + +# Scale factor for +/-16g range (Datasheet: 2048 LSB/g) +FREEFALL_ACCEL = 9.80665 * 1000. +SCALE = FREEFALL_ACCEL / 2048. + +BATCH_UPDATES = 0.100 + +BMI_I2C_ADDR = 0x69 + +# Printer class that controls BMI160 chip +class BMI160: + def __init__(self, config): + self.printer = config.get_printer() + self.reactor = self.printer.get_reactor() + adxl345.AccelCommandHelper(config, self) + self.axes_map = adxl345.read_axes_map(config, SCALE, SCALE, SCALE) + self.data_rate = 1600 + # Setup mcu sensor_bmi160 bulk query code + # Check for SPI or I2C + if config.get('cs_pin', None) is not None: + # Using 1MHz to match working Arduino test + self.bus = bus.MCU_SPI_from_config(config, 0, default_speed=1000000) + self.bus_type = 'spi' + else: + self.bus = bus.MCU_I2C_from_config(config, + default_addr=BMI_I2C_ADDR, default_speed=400000) + self.bus_type = 'i2c' + self.mcu = mcu = self.bus.get_mcu() + self.oid = oid = mcu.create_oid() + self.query_bmi160_cmd = None + mcu.add_config_cmd("config_bmi160 oid=%d bus_oid=%d bus_oid_type=%s" + % (oid, self.bus.get_oid(), self.bus_type)) + mcu.add_config_cmd("query_bmi160 oid=%d rest_ticks=0" + % (oid,), on_restart=True) + mcu.register_config_callback(self._build_config) + # Bulk sample message reading + chip_smooth = self.data_rate * BATCH_UPDATES * 2 + self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, "= 0x40 and reg != REG_CMD: + stored_val = self.read_reg(reg) + if stored_val != val: + raise self.printer.command_error( + "Failed to set BMI160 register [0x%x] to 0x%x: " + "got 0x%x. This is generally indicative of connection " + "problems (e.g. faulty wiring) or a faulty bmi160 " + "chip." % (reg, val, stored_val)) + def start_internal_client(self): + aqh = adxl345.AccelQueryHelper(self.printer) + self.batch_bulk.add_client(aqh.handle_batch) + return aqh + def _convert_samples(self, samples): + (x_pos, x_scale), (y_pos, y_scale), (z_pos, z_scale) = self.axes_map + count = 0 + for ptime, rx, ry, rz in samples: + raw_xyz = (rx, ry, rz) + x = round(raw_xyz[x_pos] * x_scale, 6) + y = round(raw_xyz[y_pos] * y_scale, 6) + z = round(raw_xyz[z_pos] * z_scale, 6) + samples[count] = (round(ptime, 6), x, y, z) + count += 1 + del samples[count:] + def _start_measurements(self): + # 1. Force SPI Mode (Dummy Read) + if self.bus_type == 'spi': + self.read_reg(0x7F) + self.reactor.pause(0.010) # 10ms for mode switch + + # 2. Verify ID + dev_id = self.read_reg(REG_CHIPID) + if dev_id != BMI160_DEV_ID: + raise self.printer.command_error( + "Invalid bmi160 id (got %x vs %x).\n" + "This is generally indicative of connection problems\n" + "(e.g. faulty wiring) or a faulty bmi160 chip." + % (dev_id, BMI160_DEV_ID)) + + # 3. Wake Up FIRST + # Send Normal Mode command + self.set_reg(REG_CMD, CMD_ACC_PM_NORMAL) + # CRITICAL: Wait 50ms for startup/PLL locking + self.reactor.pause(0.050) + + # 4. Configure Registers (While Awake) + self.set_reg(REG_ACC_CONF, SET_ACC_CONF_1600HZ) + self.set_reg(REG_ACC_RANGE, SET_ACC_RANGE_16G) + self.set_reg(REG_FIFO_DOWNS, SET_FIFO_DOWNS) + self.set_reg(REG_FIFO_CONFIG_1, SET_FIFO_CONFIG_1) + + # 5. Flush FIFO + self.set_reg(REG_CMD, CMD_FIFO_FLUSH) + self.reactor.pause(0.010) + + # 6. Start Bulk Reading + # Start timer roughly immediately + rest_ticks = self.mcu.seconds_to_clock(4. / self.data_rate) + self.query_bmi160_cmd.send([self.oid, rest_ticks]) + logging.info("BMI160 starting '%s' measurements", self.name) + self.ffreader.note_start() + self.last_error_count = 0 + + def _finish_measurements(self): + self.set_reg(REG_CMD, CMD_ACC_PM_SUSPEND) + self.query_bmi160_cmd.send_wait_ack([self.oid, 0]) + self.ffreader.note_end() + logging.info("BMI160 finished '%s' measurements", self.name) + def _process_batch(self, eventtime): + samples = self.ffreader.pull_samples() + self._convert_samples(samples) + if not samples: + return {} + return {'data': samples, 'errors': self.last_error_count, + 'overflows': self.ffreader.get_last_overflows()} + +def load_config(config): + return BMI160(config) + +def load_config_prefix(config): + return BMI160(config) diff --git a/src/Kconfig b/src/Kconfig index bf5149380..9c9790ff7 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -144,6 +144,10 @@ config WANT_LIS2DW bool depends on WANT_SPI || WANT_I2C default y +config WANT_BMI160 + bool + depends on WANT_SPI || WANT_I2C + default y config WANT_MPU9250 bool depends on WANT_I2C @@ -174,7 +178,7 @@ config WANT_SENSOR_ANGLE default y config NEED_SENSOR_BULK bool - depends on WANT_ADXL345 || WANT_LIS2DW || WANT_MPU9250 || WANT_ICM20948 \ + depends on WANT_ADXL345 || WANT_LIS2DW || WANT_BMI160 || WANT_MPU9250 || WANT_ICM20948 \ || WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 || WANT_SENSOR_ANGLE default y config WANT_TRIGGER_ANALOG @@ -232,6 +236,9 @@ config WANT_ADXL345 config WANT_LIS2DW bool "Support lis2dw and lis3dh 3-axis accelerometers" depends on WANT_SPI || WANT_I2C +config WANT_BMI160 + bool "Support BMI160 accelerometer" + depends on WANT_SPI || WANT_I2C config WANT_MPU9250 bool "Support MPU accelerometers" depends on WANT_I2C diff --git a/src/Makefile b/src/Makefile index dc6d4db52..4432656a9 100644 --- a/src/Makefile +++ b/src/Makefile @@ -20,6 +20,7 @@ src-$(CONFIG_WANT_SOFTWARE_I2C) += i2c_software.c src-$(CONFIG_WANT_THERMOCOUPLE) += thermocouple.c src-$(CONFIG_WANT_ADXL345) += sensor_adxl345.c src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c +src-$(CONFIG_WANT_BMI160) += sensor_bmi160.c src-$(CONFIG_WANT_MPU9250) += sensor_mpu9250.c src-$(CONFIG_WANT_ICM20948) += sensor_icm20948.c src-$(CONFIG_WANT_HX71X) += sensor_hx71x.c diff --git a/src/sensor_bmi160.c b/src/sensor_bmi160.c new file mode 100644 index 000000000..1381f9fcb --- /dev/null +++ b/src/sensor_bmi160.c @@ -0,0 +1,252 @@ +// Support for gathering acceleration data from BMI160 chip +// +// Copyright (C) 2025 Francisco Stephens +// +// This file may be distributed under the terms of the GNU GPLv3 license. +#include // memcpy +#include "autoconf.h" // CONFIG_WANT_SPI +#include "board/gpio.h" // irq_disable +#include "board/irq.h" // irq_disable +#include "board/misc.h" // timer_read_time +#include "basecmd.h" // oid_alloc +#include "command.h" // DECL_COMMAND +#include "sched.h" // DECL_TASK +#include "sensor_bulk.h" // sensor_bulk_report +#include "spicmds.h" // spidev_transfer +#include "i2ccmds.h" // i2cdev_s + +#define BMI_AR_DATAX0 0x12 +#define BMI_AM_READ 0x80 +#define BMI_FIFO_STATUS 0x22 +#define BMI_FIFO_DATA 0x24 + +#define BYTES_PER_SAMPLE 6 +#define BYTES_PER_BLOCK 48 + +struct bmi160 { + struct timer timer; + uint32_t rest_ticks; + union { + struct spidev_s *spi; + struct i2cdev_s *i2c; + }; + uint8_t bus_type; + uint8_t flags; + uint16_t fifo_bytes_pending; + struct sensor_bulk sb; +}; + +enum { + BMI_PENDING = 1<<0, +}; + +enum { + SPI_SERIAL, I2C_SERIAL, +}; + +DECL_ENUMERATION("bus_oid_type", "spi", SPI_SERIAL); +DECL_ENUMERATION("bus_oid_type", "i2c", I2C_SERIAL); + +static struct task_wake bmi160_wake; + +// Event handler that wakes bmi160_task() periodically +static uint_fast8_t +bmi160_event(struct timer *timer) +{ + struct bmi160 *ax = container_of(timer, struct bmi160, timer); + ax->flags |= BMI_PENDING; + sched_wake_task(&bmi160_wake); + return SF_DONE; +} + +void +command_config_bmi160(uint32_t *args) +{ + struct bmi160 *ax = oid_alloc(args[0], command_config_bmi160 + , sizeof(*ax)); + ax->timer.func = bmi160_event; + + switch (args[2]) { + case SPI_SERIAL: + if (CONFIG_WANT_SPI) { + ax->spi = spidev_oid_lookup(args[1]); + ax->bus_type = SPI_SERIAL; + break; + } else { + shutdown("bus_type spi unsupported"); + } + case I2C_SERIAL: + if (CONFIG_WANT_I2C) { + ax->i2c = i2cdev_oid_lookup(args[1]); + ax->bus_type = I2C_SERIAL; + break; + } else { + shutdown("bus_type i2c unsupported"); + } + default: + shutdown("bus_type invalid"); + } +} +DECL_COMMAND(command_config_bmi160, "config_bmi160 oid=%c" + " bus_oid=%c bus_oid_type=%c"); + +// Helper code to reschedule the bmi160_event() timer +static void +bmi160_reschedule_timer(struct bmi160 *ax) +{ + irq_disable(); + ax->timer.waketime = timer_read_time() + ax->rest_ticks; + sched_add_timer(&ax->timer); + irq_enable(); +} + +// Update local status tracking from newly read fifo status register +static void +update_fifo_status(struct bmi160 *ax, uint16_t fifo_bytes) +{ + // BMI160 FIFO can hold up to 1024 bytes + if (fifo_bytes > 1024) + ax->sb.possible_overflows++; + ax->fifo_bytes_pending = fifo_bytes; +} + +// Query fifo status register +static void +query_fifo_status(struct bmi160 *ax) +{ + uint16_t fifo_bytes = 0; + if (CONFIG_WANT_SPI && ax->bus_type == SPI_SERIAL) { + uint8_t fifo[3] = { BMI_FIFO_STATUS | BMI_AM_READ, 0x00, 0x00 }; + spidev_transfer(ax->spi, 1, sizeof(fifo), fifo); + fifo_bytes = (fifo[2] << 8) | fifo[1]; + } else if (CONFIG_WANT_I2C && ax->bus_type == I2C_SERIAL) { + uint8_t fifo_reg[1] = {BMI_FIFO_STATUS}; + uint8_t fifo_val[2]; + int ret = i2c_dev_read(ax->i2c, sizeof(fifo_reg), fifo_reg + , sizeof(fifo_val), fifo_val); + i2c_shutdown_on_err(ret); + fifo_bytes = (fifo_val[1] << 8) | fifo_val[0]; + } + update_fifo_status(ax, fifo_bytes); +} + +// Read 8 samples from FIFO via SPI +static void +read_fifo_block_spi(struct bmi160 *ax) +{ + uint8_t msg[BYTES_PER_BLOCK + 1] = {0}; + msg[0] = BMI_FIFO_DATA | BMI_AM_READ; + + spidev_transfer(ax->spi, 1, sizeof(msg), msg); + memcpy(ax->sb.data, &msg[1], BYTES_PER_BLOCK); +} + +// Read 8 samples from FIFO via i2c +static void +read_fifo_block_i2c(struct bmi160 *ax) +{ + uint8_t msg_reg[] = {BMI_FIFO_DATA}; + + int ret = i2c_dev_read(ax->i2c, sizeof(msg_reg), msg_reg + , BYTES_PER_BLOCK, ax->sb.data); + i2c_shutdown_on_err(ret); +} + +// Read from fifo and transmit data to host +static void +read_fifo_block(struct bmi160 *ax, uint8_t oid) +{ + if (CONFIG_WANT_SPI && ax->bus_type == SPI_SERIAL) + read_fifo_block_spi(ax); + else if (CONFIG_WANT_I2C && ax->bus_type == I2C_SERIAL) + read_fifo_block_i2c(ax); + ax->sb.data_count = BYTES_PER_BLOCK; + sensor_bulk_report(&ax->sb, oid); + ax->fifo_bytes_pending -= BYTES_PER_BLOCK; +} + +// Query accelerometer data +static void +bmi160_query(struct bmi160 *ax, uint8_t oid) +{ + if (ax->fifo_bytes_pending < BYTES_PER_BLOCK) + query_fifo_status(ax); + + if (ax->fifo_bytes_pending >= BYTES_PER_BLOCK) + read_fifo_block(ax, oid); + + // check if we need to run the task again (more packets in fifo?) + if (ax->fifo_bytes_pending >= BYTES_PER_BLOCK) { + // More data in fifo - wake this task again + sched_wake_task(&bmi160_wake); + } else { + // Sleep until next check time + ax->flags &= ~BMI_PENDING; + bmi160_reschedule_timer(ax); + } +} + +void +command_query_bmi160(uint32_t *args) +{ + struct bmi160 *ax = oid_lookup(args[0], command_config_bmi160); + + sched_del_timer(&ax->timer); + ax->flags = 0; + if (!args[1]) + // End measurements + return; + + // Start new measurements query + ax->rest_ticks = args[1]; + ax->fifo_bytes_pending = 0; + sensor_bulk_reset(&ax->sb); + bmi160_reschedule_timer(ax); +} +DECL_COMMAND(command_query_bmi160, "query_bmi160 oid=%c rest_ticks=%u"); + +void +command_query_bmi160_status(uint32_t *args) +{ + struct bmi160 *ax = oid_lookup(args[0], command_config_bmi160); + uint32_t time1 = 0; + uint32_t time2 = 0; + uint16_t fifo_bytes = 0; + + if (CONFIG_WANT_SPI && ax->bus_type == SPI_SERIAL) { + uint8_t fifo[3] = { BMI_FIFO_STATUS | BMI_AM_READ, 0x00, 0x00 }; + time1 = timer_read_time(); + spidev_transfer(ax->spi, 1, sizeof(fifo), fifo); + time2 = timer_read_time(); + fifo_bytes = (fifo[2] << 8) | fifo[1]; + } else if (CONFIG_WANT_I2C && ax->bus_type == I2C_SERIAL) { + uint8_t fifo_reg[1] = {BMI_FIFO_STATUS}; + uint8_t fifo_val[2]; + time1 = timer_read_time(); + int ret = i2c_dev_read(ax->i2c, sizeof(fifo_reg), fifo_reg + , sizeof(fifo_val), fifo_val); + time2 = timer_read_time(); + i2c_shutdown_on_err(ret); + fifo_bytes = (fifo_val[1] << 8) | fifo_val[0]; + } + update_fifo_status(ax, fifo_bytes); + + sensor_bulk_status(&ax->sb, args[0], time1, time2-time1 + , ax->fifo_bytes_pending); +} +DECL_COMMAND(command_query_bmi160_status, "query_bmi160_status oid=%c"); + +void +bmi160_task(void) +{ + if (!sched_check_wake(&bmi160_wake)) + return; + uint8_t oid; + struct bmi160 *ax; + foreach_oid(oid, ax, command_config_bmi160) { + uint_fast8_t flags = ax->flags; + if (flags & BMI_PENDING) + bmi160_query(ax, oid); + } +} +DECL_TASK(bmi160_task); From 1473b79ac00759b2b2d07b9d6c19029006f47fc3 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 10 Feb 2026 17:57:24 -0500 Subject: [PATCH 386/411] configfile: Only warn once for each message sent to runtime_warning() Also simplify the maintenance of warnings. Signed-off-by: Kevin O'Connor --- klippy/configfile.py | 52 +++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/klippy/configfile.py b/klippy/configfile.py index 8210de2ba..fe7e7bdf8 100644 --- a/klippy/configfile.py +++ b/klippy/configfile.py @@ -130,14 +130,8 @@ class ConfigWrapper: def deprecate(self, option, value=None): if not self.fileconfig.has_option(self.section, option): return - if value is None: - msg = ("Option '%s' in section '%s' is deprecated." - % (option, self.section)) - else: - msg = ("Value '%s' in option '%s' in section '%s' is deprecated." - % (value, option, self.section)) pconfig = self.printer.lookup_object("configfile") - pconfig.deprecate(self.section, option, value, msg) + pconfig.deprecate(self.section, option, value) ###################################################################### @@ -468,8 +462,6 @@ class PrinterConfig: self.autosave = ConfigAutoSave(printer) self.validate = ConfigValidate(printer) self.deprecated = {} - self.runtime_warnings = [] - self.deprecate_warnings = [] self.status_raw_config = {} self.status_warnings = [] def get_printer(self): @@ -496,27 +488,33 @@ class PrinterConfig: def check_unused_options(self, config): self.validate.check_unused(config.fileconfig) # Deprecation warnings + def _add_deprecated(self, data): + key = tuple(list(data.items())) + if key in self.deprecated: + return False + self.deprecated[key] = True + self.status_warnings = self.status_warnings + [data] + return True def runtime_warning(self, msg): - logging.warning(msg) res = {'type': 'runtime_warning', 'message': msg} - self.runtime_warnings.append(res) - self.status_warnings = self.runtime_warnings + self.deprecate_warnings + did_add = self._add_deprecated(res) + if did_add: + logging.warning(msg) def deprecate(self, section, option, value=None, msg=None): - key = (section, option, value) - if key in self.deprecated and self.deprecated[key] == msg: - return - self.deprecated[key] = msg - self.deprecate_warnings = [] - for (section, option, value), msg in self.deprecated.items(): - if value is None: - res = {'type': 'deprecated_option'} - else: - res = {'type': 'deprecated_value', 'value': value} - res['message'] = msg - res['section'] = section - res['option'] = option - self.deprecate_warnings.append(res) - self.status_warnings = self.runtime_warnings + self.deprecate_warnings + if value is None: + res = {'type': 'deprecated_option'} + defmsg = ("Option '%s' in section '%s' is deprecated." + % (option, self.section)) + else: + res = {'type': 'deprecated_value', 'value': value} + defmsg = ("Value '%s' in option '%s' in section '%s' is deprecated." + % (value, option, self.section)) + if msg is None: + msg = defmsg + res['message'] = msg + res['section'] = section + res['option'] = option + self._add_deprecated(res) # Status reporting def _build_status_config(self, config): self.status_raw_config = {} From 21604d8bc7d35637710a5b9fad8407f55a1dcb78 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 10 Feb 2026 18:12:29 -0500 Subject: [PATCH 387/411] configfile: Support warning for deprecated gcode commands Signed-off-by: Kevin O'Connor --- klippy/configfile.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/klippy/configfile.py b/klippy/configfile.py index fe7e7bdf8..754fc974f 100644 --- a/klippy/configfile.py +++ b/klippy/configfile.py @@ -515,6 +515,20 @@ class PrinterConfig: res['section'] = section res['option'] = option self._add_deprecated(res) + def deprecate_gcode(self, cmd, param=None, value=None, msg=None): + if param is None: + defmsg = "Command '%s' is deprecated." % (cmd,) + elif value is None: + defmsg = ("Parameter '%s' in command '%s' is deprecated." + % (param, cmd)) + else: + defmsg = ("Value '%s=%s' in command '%s' is deprecated." + % (param, value, cmd)) + if msg is None: + msg = defmsg + res = {'type': 'deprecated_gcode', 'message': msg, + 'command': cmd, 'parameter': param, 'value': str(value)} + self._add_deprecated(res) # Status reporting def _build_status_config(self, config): self.status_raw_config = {} From 5a877d72203262fc87e5e111d3317c0dc7e0ad4a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 1 Feb 2026 17:07:55 -0500 Subject: [PATCH 388/411] manual_stepper: Rework STOP_ON_ENDSTOP parameter Replace the integer values of STOP_ON_ENDSTOP with string values and deprecate the older format. The newer string values should make the commands easier to understand and allow for more homing options in the future. Signed-off-by: Kevin O'Connor --- config/sample-mmu2s-diy.cfg | 6 ++--- docs/Config_Changes.md | 6 +++++ docs/G-Codes.md | 43 +++++++++++++++++++++------------ klippy/extras/manual_stepper.py | 19 ++++++++++++--- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/config/sample-mmu2s-diy.cfg b/config/sample-mmu2s-diy.cfg index 41fe669f9..c4634761c 100644 --- a/config/sample-mmu2s-diy.cfg +++ b/config/sample-mmu2s-diy.cfg @@ -506,7 +506,7 @@ gcode: {% if printer["gcode_macro SELECT_TOOL"].tool_selected|int != -1 %} M118 Loading filament to PINDA ... MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=gear_stepper MOVE={printer["gcode_macro VAR_MMU2S"].pinda_load_length} STOP_ON_ENDSTOP=2 + MANUAL_STEPPER STEPPER=gear_stepper MOVE={printer["gcode_macro VAR_MMU2S"].pinda_load_length} STOP_ON_ENDSTOP=try_home MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 MANUAL_STEPPER STEPPER=gear_stepper MOVE=10 IS_FILAMENT_IN_PINDA @@ -579,7 +579,7 @@ gcode: M118 Unloading filament from extruder to PINDA ... MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0 {% if printer["gcode_macro VAR_MMU2S"].enable_5in1 == 0 %} - MANUAL_STEPPER STEPPER=gear_stepper MOVE=-{printer["gcode_macro VAR_MMU2S"].bowden_unload_length} SPEED=120 ACCEL=80 STOP_ON_ENDSTOP=-2 + MANUAL_STEPPER STEPPER=gear_stepper MOVE=-{printer["gcode_macro VAR_MMU2S"].bowden_unload_length} SPEED=120 ACCEL=80 STOP_ON_ENDSTOP=try_inverted_home IS_FILAMENT_STUCK_IN_PINDA {% else %} MANUAL_STEPPER STEPPER=gear_stepper MOVE=-{printer["gcode_macro VAR_MMU2S"].bowden_unload_length} SPEED=120 ACCEL=80 @@ -792,7 +792,7 @@ gcode: {% if printer["gcode_macro VAR_MMU2S"].enable_5in1 == 0 %} M118 Homing selector MANUAL_STEPPER STEPPER=selector_stepper SET_POSITION=0 - MANUAL_STEPPER STEPPER=selector_stepper MOVE=-76 STOP_ON_ENDSTOP=1 + MANUAL_STEPPER STEPPER=selector_stepper MOVE=-76 STOP_ON_ENDSTOP=home MANUAL_STEPPER STEPPER=selector_stepper SET_POSITION=0 {% endif %} MANUAL_STEPPER STEPPER=idler_stepper MOVE=0 diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 384c5b39f..d50a0f8e6 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,12 @@ All dates in this document are approximate. ## Changes +20260214: The `MANUAL_STEPPER` G-Code command `STOP_ON_ENDSTOP` +parameter has changed. See the +[MANUAL_STEPPER](G-Codes.md#manual_stepper) documentation for +details. Using the previous integer values (-2, -1, 1, 2) is +deprecated and support will be removed in the near future. + 20260207: The low-level i2c behavior of sx1509 and uc1701 devices has changed. Previously an i2c error would result in a shutdown, and now i2c errors when communicating with these devices will only generate diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 915537411..857ce3f61 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -990,22 +990,33 @@ enabled. #### MANUAL_STEPPER `MANUAL_STEPPER STEPPER=config_name [ENABLE=[0|1]] -[SET_POSITION=] [SPEED=] [ACCEL=] [MOVE= -[STOP_ON_ENDSTOP=[1|2|-1|-2]] [SYNC=0]]`: This command will alter the -state of the stepper. Use the ENABLE parameter to enable/disable the -stepper. Use the SET_POSITION parameter to force the stepper to think -it is at the given position. Use the MOVE parameter to request a -movement to the given position. If SPEED and/or ACCEL is specified -then the given values will be used instead of the defaults specified -in the config file. If an ACCEL of zero is specified then no -acceleration will be performed. If STOP_ON_ENDSTOP=1 is specified then -the move will end early should the endstop report as triggered (use -STOP_ON_ENDSTOP=2 to complete the move without error even if the -endstop does not trigger, use -1 or -2 to stop when the endstop -reports not triggered). Normally future G-Code commands will be -scheduled to run after the stepper move completes, however if a manual -stepper move uses SYNC=0 then future G-Code movement commands may run -in parallel with the stepper movement. +[SET_POSITION=] [SPEED=] [ACCEL=] [MOVE=] +[SYNC=0]]`: This command will alter the state of the stepper. Use the +ENABLE parameter to enable/disable the stepper. Use the SET_POSITION +parameter to force the stepper to think it is at the given +position. Use the MOVE parameter to request a movement to the given +position. If SPEED and/or ACCEL is specified then the given values +will be used instead of the defaults specified in the config file. If +an ACCEL of zero is specified then no acceleration will be +performed. Normally future G-Code commands will be scheduled to run +after the stepper move completes, however if a manual stepper move +uses SYNC=0 then future G-Code movement commands may run in parallel +with the stepper movement. + +`MANUAL_STEPPER STEPPER=config_name [SPEED=] [ACCEL=] +MOVE= STOP_ON_ENDSTOP=`: If STOP_ON_ENDSTOP is +specified then the move will end early if an endstop event occurs. The +`STOP_ON_ENDSTOP` parameter may be set to one of the following values: + +* `home`: The movement will stop when the endstop reports triggered + and the final position of the manual_stepper will be set such that + the trigger position matches the position specified in the `MOVE` + parameter. +* `inverted_home`: As above, however, the movement will stop when the + endstop reports it is in a non-triggered state. +* `try_home`, `try_inverted_home`: As above, but no error will be + reported if the movement fully completes without an endstop event + stopping the move early. `MANUAL_STEPPER STEPPER=config_name GCODE_AXIS=[A-Z] [LIMIT_VELOCITY=] [LIMIT_ACCEL=] diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 175f20915..dc5ef03e7 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -102,14 +102,27 @@ class ManualStepper: self.do_set_position(setpos) speed = gcmd.get_float('SPEED', self.velocity, above=0.) accel = gcmd.get_float('ACCEL', self.accel, minval=0.) - homing_move = gcmd.get_int('STOP_ON_ENDSTOP', 0) - if homing_move: + homing_move = gcmd.get('STOP_ON_ENDSTOP', None) + if homing_move is not None: + old_map = {'-2': 'try_inverted_home', '-1': 'inverted_home', + '1': 'home', '2': 'try_home'}.get(homing_move) + if old_map is not None: + pconfig = self.printer.lookup_object('configfile') + pconfig.deprecate_gcode("MANUAL_STEPPER", "STOP_ON_ENDSTOP", + homing_move) + homing_move = old_map + is_try = homing_move.startswith('try_') + homing_move = homing_move[is_try*4:] + is_inverted = homing_move.startswith('inverted_') + homing_move = homing_move[is_inverted*9:] + if homing_move != "home": + raise gcmd.error("Unknown STOP_ON_ENDSTOP request") movepos = gcmd.get_float('MOVE') if ((self.pos_min is not None and movepos < self.pos_min) or (self.pos_max is not None and movepos > self.pos_max)): raise gcmd.error("Move out of range") self.do_homing_move(movepos, speed, accel, - homing_move > 0, abs(homing_move) == 1) + not is_inverted, not is_try) elif gcmd.get_float('MOVE', None) is not None: movepos = gcmd.get_float('MOVE') if ((self.pos_min is not None and movepos < self.pos_min) From 57c2e0c960f8e25f56a66ba3a1e90e124f207001 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 1 Feb 2026 17:47:55 -0500 Subject: [PATCH 389/411] manual_stepper: Support STOP_ON_ENDSTOP=probe Support moving a manual_stepper until a trigger event, without resetting the final position. Signed-off-by: Kevin O'Connor --- docs/G-Codes.md | 11 ++++++----- klippy/extras/homing.py | 8 +++++--- klippy/extras/manual_stepper.py | 13 +++++++------ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 857ce3f61..ca7681ac0 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -1008,15 +1008,16 @@ MOVE= STOP_ON_ENDSTOP=`: If STOP_ON_ENDSTOP is specified then the move will end early if an endstop event occurs. The `STOP_ON_ENDSTOP` parameter may be set to one of the following values: +* `probe`: The movement will stop when the endstop reports triggered. * `home`: The movement will stop when the endstop reports triggered and the final position of the manual_stepper will be set such that the trigger position matches the position specified in the `MOVE` parameter. -* `inverted_home`: As above, however, the movement will stop when the - endstop reports it is in a non-triggered state. -* `try_home`, `try_inverted_home`: As above, but no error will be - reported if the movement fully completes without an endstop event - stopping the move early. +* `inverted_probe`, `inverted_home`: As above, however, the movement + will stop when the endstop reports it is in a non-triggered state. +* `try_probe`, `try_inverted_probe`, `try_home`, `try_inverted_home`: + As above, but no error will be reported if the movement fully + completes without an endstop event stopping the move early. `MANUAL_STEPPER STEPPER=config_name GCODE_AXIS=[A-Z] [LIMIT_VELOCITY=] [LIMIT_ACCEL=] diff --git a/klippy/extras/homing.py b/klippy/extras/homing.py index 723648c11..918f37e15 100644 --- a/klippy/extras/homing.py +++ b/klippy/extras/homing.py @@ -249,16 +249,18 @@ class PrinterHoming: gcode = self.printer.lookup_object('gcode') gcode.register_command('G28', self.cmd_G28) def manual_home(self, toolhead, endstops, pos, speed, - triggered, check_triggered): + probe_pos, triggered, check_triggered): hmove = HomingMove(self.printer, endstops, toolhead) try: - hmove.homing_move(pos, speed, triggered=triggered, - check_triggered=check_triggered) + epos = hmove.homing_move(pos, speed, probe_pos=probe_pos, + triggered=triggered, + check_triggered=check_triggered) except self.printer.command_error: if self.printer.is_shutdown(): raise self.printer.command_error( "Homing failed due to printer shutdown") raise + return epos def probing_move(self, mcu_probe, pos, speed): endstops = [(mcu_probe, "probe")] hmove = HomingMove(self.printer, endstops) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index dc5ef03e7..f0e7d1377 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -78,7 +78,8 @@ class ManualStepper: 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): + def do_homing_move(self, movepos, speed, accel, + probe_pos, triggered, check_trigger): if not self.can_home: raise self.printer.command_error( "No endstop for this manual stepper") @@ -87,7 +88,8 @@ class ManualStepper: endstops = self.rail.get_endstops() phoming = self.printer.lookup_object('homing') phoming.manual_home(self, endstops, pos, speed, - triggered, check_trigger) + probe_pos, triggered, check_trigger) + self.sync_print_time() cmd_MANUAL_STEPPER_help = "Command a manually configured stepper" def cmd_MANUAL_STEPPER(self, gcmd): if gcmd.get('GCODE_AXIS', None) is not None: @@ -115,14 +117,15 @@ class ManualStepper: homing_move = homing_move[is_try*4:] is_inverted = homing_move.startswith('inverted_') homing_move = homing_move[is_inverted*9:] - if homing_move != "home": + if homing_move not in ["probe", "home"]: raise gcmd.error("Unknown STOP_ON_ENDSTOP request") + is_probe = (homing_move == "probe") movepos = gcmd.get_float('MOVE') if ((self.pos_min is not None and movepos < self.pos_min) or (self.pos_max is not None and movepos > self.pos_max)): raise gcmd.error("Move out of range") self.do_homing_move(movepos, speed, accel, - not is_inverted, not is_try) + is_probe, not is_inverted, not is_try) elif gcmd.get_float('MOVE', None) is not None: movepos = gcmd.get_float('MOVE') if ((self.pos_min is not None and movepos < self.pos_min) @@ -220,8 +223,6 @@ class ManualStepper: drip_completion) # Clear trapq of any remaining parts of movement self.motion_queuing.wipe_trapq(self.trapq) - self.rail.set_position([self.commanded_pos, 0., 0.]) - self.sync_print_time() def get_kinematics(self): return self def get_steppers(self): From 187481e2514f30fbaa19241869f4485ab4289cea Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Mon, 2 Feb 2026 22:28:36 +0100 Subject: [PATCH 390/411] dual_carriage: Allow dual carriage inactivation Signed-off-by: Dmitry Butyugin --- config/sample-corexyuv.cfg | 22 ++++++++ docs/G-Codes.md | 42 ++++++++------ klippy/kinematics/cartesian.py | 9 +-- klippy/kinematics/generic_cartesian.py | 26 +++------ klippy/kinematics/hybrid_corexy.py | 3 +- klippy/kinematics/hybrid_corexz.py | 3 +- klippy/kinematics/idex_modes.py | 74 ++++++++++++++----------- test/klippy/generic_cartesian_iqex.cfg | 27 +++++++++ test/klippy/generic_cartesian_iqex.test | 7 +++ 9 files changed, 140 insertions(+), 73 deletions(-) diff --git a/config/sample-corexyuv.cfg b/config/sample-corexyuv.cfg index e33d8681a..156c3f183 100644 --- a/config/sample-corexyuv.cfg +++ b/config/sample-corexyuv.cfg @@ -157,6 +157,28 @@ gcode: SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=COPY SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder +[gcode_macro _DISABLE_AND_PARK_EXTRUDER] +gcode: + SAVE_GCODE_STATE NAME=disable_extruder + SAVE_DUAL_CARRIAGE_STATE NAME=disable_extruder + SET_HEATER_TEMPERATURE HEATER={params.EXTRUDER} TARGET=0 + SYNC_EXTRUDER_MOTION EXTRUDER={params.EXTRUDER} MOTION_QUEUE= + PARK_{params.EXTRUDER} + RESTORE_DUAL_CARRIAGE_STATE NAME=disable_extruder MOVE=0 + RESTORE_GCODE_STATE NAME=disable_extruder MOVE=0 + +[gcode_macro DISABLE_T0] +gcode: + SET_DUAL_CARRIAGE CARRIAGE=carriage_x MODE=INACTIVE + SET_DUAL_CARRIAGE CARRIAGE=carriage_y MODE=INACTIVE + _DISABLE_AND_PARK_EXTRUDER EXTRUDER=extruder + +[gcode_macro DISABLE_T1] +gcode: + SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=INACTIVE + SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=INACTIVE + _DISABLE_AND_PARK_EXTRUDER EXTRUDER=extruder + [printer] kinematics: generic_cartesian max_velocity: 300 diff --git a/docs/G-Codes.md b/docs/G-Codes.md index ca7681ac0..a4dd89fe4 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -343,19 +343,25 @@ The following command is available when the enabled. #### SET_DUAL_CARRIAGE -`SET_DUAL_CARRIAGE CARRIAGE= [MODE=[PRIMARY|COPY|MIRROR]]`: +`SET_DUAL_CARRIAGE CARRIAGE= [MODE=[PRIMARY|COPY|MIRROR|INACTIVE]]`: This command will change the mode of the specified carriage. If no `MODE` is provided it defaults to `PRIMARY`. `` must reference a defined primary or dual carriage for `generic_cartesian` kinematics or be 0 (for primary carriage) or 1 (for dual carriage) for all other kinematics supporting IDEX. Setting the mode to `PRIMARY` -deactivates the other carriage and makes the specified carriage execute -subsequent G-Code commands as-is. Before activating `COPY` or `MIRROR` -mode for a carriage, a different one must be activated as `PRIMARY` on -the same axis. When set to either of these two modes, the carriage -will then track the subsequent moves of its primary carriage and either -copy relative movements of it (in `COPY` mode) or execute them in the -opposite (mirror) direction (in `MIRROR` mode). +deactivates all other carriages on the same axis and makes the specified +carriage execute subsequent G-Code movement commands as-is. Before activating +`COPY` or `MIRROR` mode for a carriage, a different one must be activated as +`PRIMARY` on the same axis. When set to either of these two modes, the carriage +will track the subsequent G-Code moves and either copy relative movements +(in `COPY` mode) or execute them in the opposite (mirror) direction (in +`MIRROR` mode). Setting the mode to `INACTIVE` deactivates the carriage and +makes it ignore further G-Code moves. Note that deactivating the primary +carriage on the axis does not disable other carriages working in `COPY` or +`MIRROR` mode, which can be used to disable printing a failed part by any of +the tools and park that tool to prevent collisions with an unfinished part, see +this [sample configuration](../config/sample-corexyuv.cfg) for macros examples. + #### SAVE_DUAL_CARRIAGE_STATE `SAVE_DUAL_CARRIAGE_STATE [NAME=]`: Save the current positions @@ -366,14 +372,18 @@ to the given string. If NAME is not provided it defaults to "default". #### RESTORE_DUAL_CARRIAGE_STATE `RESTORE_DUAL_CARRIAGE_STATE [NAME=] [MOVE=[0|1] [MOVE_SPEED=]]`: -Restore the previously saved positions of the dual carriages and their modes, -unless "MOVE=0" is specified, in which case only the saved modes will be -restored, but not the positions of the carriages. If positions are being -restored and "MOVE_SPEED" is specified, then the toolhead moves will be -performed with the given speed (in mm/s); otherwise the toolhead move will -use the rail homing speed. Note that the carriages restore their positions -only over their own axis, which may be necessary to correctly restore COPY -and MIRROR mode of the dual carriage. +Restore the previously saved states of all dual and their primary carriages. +This command restores the modes of the carriages and moves them to their +previously saved positions, unless "MOVE=0" is specified. If positions are being +restored and "MOVE_SPEED" is specified, then the carriages will move with at +most the provided speed (in mm/s); otherwise the homing speeds of the +corresponding carriages will be used as a reference. Note that the carriages +restore their positions only over their own axes, which may be necessary to +correctly restore COPY and MIRROR mode of the dual carriage. In addition, this +command updates the Klipper toolhead position for each axis that has some dual +carriages: it is set to match the actual position of the activated primary +carriage of an axis or, if an axis does not have a saved primary carriage, +to the axis position when `SAVE_DUAL_CARRIAGE_STATE` command was called. ### [endstop_phase] diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index 67858d0bb..83bc426ac 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -48,10 +48,10 @@ class CartKinematics: def calc_position(self, stepper_positions): rails = self.rails if self.dc_module: - primary_rail = self.dc_module.get_primary_rail( - self.dual_carriage_axis) + dc_rail = self.dc_module.get_primary_rail(self.dual_carriage_axis) \ + or self.rails[self.dual_carriage_axis] rails = (rails[:self.dual_carriage_axis] + - [primary_rail] + rails[self.dual_carriage_axis+1:]) + [dc_rail] + rails[self.dual_carriage_axis+1:]) return [stepper_positions[rail.get_name()] for rail in rails] def update_limits(self, i, range): l, h = self.limits[i] @@ -64,9 +64,10 @@ class CartKinematics: rail.set_position(newpos) for axis_name in homing_axes: axis = "xyz".index(axis_name) + rail = None if self.dc_module and axis == self.dual_carriage_axis: rail = self.dc_module.get_primary_rail(self.dual_carriage_axis) - else: + if rail is None: rail = self.rails[axis] self.limits[axis] = rail.get_range() def clear_homing_state(self, clear_axes): diff --git a/klippy/kinematics/generic_cartesian.py b/klippy/kinematics/generic_cartesian.py index afb5d47ac..398f82689 100644 --- a/klippy/kinematics/generic_cartesian.py +++ b/klippy/kinematics/generic_cartesian.py @@ -254,21 +254,6 @@ class GenericCartesianKinematics: for c in config.get_prefix_sections('stepper ')] def get_steppers(self): return [s.get_stepper() for s in self.kin_steppers] - def get_primary_carriages(self): - carriages = [] - for a in range(3): - c = None - if self.dc_module is not None and a in self.dc_module.get_axes(): - primary_rail = self.dc_module.get_primary_rail(a) - for c in self.carriages.values(): - if c.get_rail() == primary_rail: - break - else: - for c in self.primary_carriages: - if c.get_axis() == a: - break - carriages.append(c) - return carriages def _get_kinematics_coeffs(self): matr = {s.get_name() : list(s.get_kin_coeffs()) for s in self.kin_steppers} @@ -340,7 +325,7 @@ class GenericCartesianKinematics: homing_state.home_rails([rail], forcepos, homepos) def home(self, homing_state): self._check_kinematics(self.printer.command_error) - primary_carriages = self.get_primary_carriages() + primary_carriages = {c.get_axis(): c for c in self.primary_carriages} # Each axis is homed independently and in order for axis in homing_state.get_axes(): if self.dc_module is not None and axis in self.dc_module.get_axes(): @@ -373,8 +358,13 @@ class GenericCartesianKinematics: self.max_z_velocity * z_ratio, self.max_z_accel * z_ratio) def get_status(self, eventtime): axes = [a for a, (l, h) in zip("xyz", self.limits) if l <= h] - ranges = [c.get_rail().get_range() - for c in self.get_primary_carriages()] + ranges = [(min(c.get_rail().get_range()[0] + for c in self.carriages.values() + if c.get_axis() == axis), + max(c.get_rail().get_range()[1] + for c in self.carriages.values() + if c.get_axis() == axis)) + for axis in range(3)] axes_min = gcode.Coord([r[0] for r in ranges]) axes_max = gcode.Coord([r[1] for r in ranges]) return { diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index 1cd2aa8bc..6a9d2137f 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -66,9 +66,10 @@ class HybridCoreXYKinematics: rail.set_position(newpos) for axis_name in homing_axes: axis = "xyz".index(axis_name) + rail = None if self.dc_module and axis == 0: rail = self.dc_module.get_primary_rail(axis) - else: + if rail is None: rail = self.rails[axis] self.limits[axis] = rail.get_range() def clear_homing_state(self, clear_axes): diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index 03e889376..0bbc10373 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -66,9 +66,10 @@ class HybridCoreXZKinematics: rail.set_position(newpos) for axis_name in homing_axes: axis = "xyz".index(axis_name) + rail = None if self.dc_module and axis == 0: rail = self.dc_module.get_primary_rail(axis) - else: + if rail is None: rail = self.rails[axis] self.limits[axis] = rail.get_range() def clear_homing_state(self, clear_axes): diff --git a/klippy/kinematics/idex_modes.py b/klippy/kinematics/idex_modes.py index 4fe6df830..d700e7ec8 100644 --- a/klippy/kinematics/idex_modes.py +++ b/klippy/kinematics/idex_modes.py @@ -13,7 +13,7 @@ COPY = 'COPY' MIRROR = 'MIRROR' class DualCarriages: - VALID_MODES = [PRIMARY, COPY, MIRROR] + VALID_MODES = [INACTIVE, PRIMARY, COPY, MIRROR] def __init__(self, printer, primary_rails, dual_rails, axes, safe_dist): self.printer = printer self._init_steppers(primary_rails + dual_rails) @@ -24,15 +24,13 @@ class DualCarriages: pc = primary_rails[i] safe_dist[i] = min(abs(pc.position_min - dc.position_min), abs(pc.position_max - dc.position_max)) - self.primary_mode_dcs = [None] * 3 + activated = [False] * 3 self.primary_rails = [] for i, c in enumerate(primary_rails): - activate = self.primary_mode_dcs[axes[i]] is None + activate, activated[axes[i]] = not activated[axes[i]], True dc_rail = DualCarriagesRail( printer, c, dual_rails[i], axes[i], safe_dist[i], active=activate) - if activate: - self.primary_mode_dcs[axes[i]] = dc_rail self.primary_rails.append(dc_rail) self.dual_rails = [ DualCarriagesRail(printer, c, primary_rails[i], @@ -83,9 +81,10 @@ class DualCarriages: def get_axes(self): return self.axes def get_primary_rail(self, axis): - if self.primary_mode_dcs[axis] is None: - return None - return self.primary_mode_dcs[axis].rail + for dc_rail in self.dc_rails.values(): + if dc_rail.mode == PRIMARY and dc_rail.axis == axis: + return dc_rail.rail + return None def get_dc_rail_wrapper(self, rail): for dc_rail in self.dc_rails.values(): if dc_rail.rail == rail: @@ -111,7 +110,6 @@ class DualCarriages: if target_dc.mode != PRIMARY: newpos = pos[:axis] + [target_dc.get_axis_position(pos)] \ + pos[axis+1:] - self.primary_mode_dcs[axis] = target_dc target_dc.activate(PRIMARY, newpos, old_position=pos) toolhead.set_position(newpos) kin.update_limits(axis, target_dc.rail.get_range()) @@ -141,30 +139,29 @@ class DualCarriages: return status def get_kin_range(self, toolhead, axis): pos = toolhead.get_position() - primary_carriage = self.primary_mode_dcs[axis] - if primary_carriage is None: - return (1.0, -1.0) - primary_pos = primary_carriage.get_axis_position(pos) - range_min = primary_carriage.rail.position_min - range_max = primary_carriage.rail.position_max + axis_pos = pos[axis] + range_min, range_max = -1e10, 1e10 for carriage in self.dc_rails.values(): if carriage.axis != axis: continue dcs = [carriage] + [dc for dc in self.dc_rails.values() if carriage.rail is dc.dual_rail] axes_pos = [dc.get_axis_position(pos) for dc in dcs] - # Check how dcs[0] affects the motion range of primary_carriage + # Check how dcs[0] affects the motion range if not dcs[0].is_active(): continue + elif dcs[0].mode == PRIMARY: + range_min = max(range_min, dcs[0].rail.position_min) + range_max = min(range_max, dcs[0].rail.position_max) elif dcs[0].mode == COPY: - range_min = max(range_min, primary_pos + range_min = max(range_min, axis_pos + dcs[0].rail.position_min - axes_pos[0]) - range_max = min(range_max, primary_pos + range_max = min(range_max, axis_pos + dcs[0].rail.position_max - axes_pos[0]) elif dcs[0].mode == MIRROR: - range_min = max(range_min, primary_pos + range_min = max(range_min, axis_pos + axes_pos[0] - dcs[0].rail.position_max) - range_max = min(range_max, primary_pos + range_max = min(range_max, axis_pos + axes_pos[0] - dcs[0].rail.position_min) safe_dist = dcs[0].safe_dist if not safe_dist or len(dcs) == 1: @@ -186,14 +183,14 @@ class DualCarriages: if dcs[0].mode in (PRIMARY, COPY): if self.get_dc_order(dcs[0], dcs[1]) > 0: - range_min = max(range_min, primary_pos + safe_move_dist) + range_min = max(range_min, axis_pos + safe_move_dist) else: - range_max = min(range_max, primary_pos + safe_move_dist) + range_max = min(range_max, axis_pos + safe_move_dist) else: # dcs[0].mode == MIRROR if self.get_dc_order(dcs[0], dcs[1]) > 0: - range_max = min(range_max, primary_pos - safe_move_dist) + range_max = min(range_max, axis_pos - safe_move_dist) else: - range_min = max(range_min, primary_pos - safe_move_dist) + range_min = max(range_min, axis_pos - safe_move_dist) if range_min > range_max: # During multi-MCU homing it is possible that the carriage @@ -231,8 +228,6 @@ class DualCarriages: axis = dc.axis if mode == INACTIVE: dc.inactivate(toolhead.get_position()) - if self.primary_mode_dcs[axis] is dc: - self.primary_mode_dcs[axis] = None elif mode == PRIMARY: self.toggle_active_dc_rail(dc) else: @@ -265,7 +260,7 @@ class DualCarriages: if mode not in self.VALID_MODES: raise gcmd.error("Invalid mode=%s specified" % (mode,)) if mode in [COPY, MIRROR]: - if self.primary_mode_dcs[dc_rail.axis] in [None, dc_rail]: + if self.get_primary_rail(dc_rail.axis) in [None, dc_rail.rail]: raise gcmd.error( "Must activate another carriage as PRIMARY first") curtime = self.printer.get_reactor().monotonic() @@ -275,6 +270,13 @@ class DualCarriages: raise gcmd.error( "Axis %s must be homed prior to enabling mode=%s" % (axis.upper(), mode)) + if mode == INACTIVE: + active_dcs = [dc for dc in self.dc_rails.values() + if dc.is_active() and dc.axis == dc_rail.axis] + if active_dcs == [dc_rail]: + raise gcmd.error( + "Cannot deactivate the only active carriage for axis %s" + % 'XYZ'[dc_rail.axis]) self.activate_dc_mode(dc_rail, mode) cmd_SAVE_DUAL_CARRIAGE_STATE_help = \ "Save dual carriages modes and positions" @@ -286,7 +288,8 @@ class DualCarriages: return {'carriage_modes': {dc.get_name() : dc.mode for dc in self.dc_rails.values()}, 'carriage_positions': {dc.get_name() : dc.get_axis_position(pos) - for dc in self.dc_rails.values()}} + for dc in self.dc_rails.values()}, + 'toolhead_position': tuple(pos)} cmd_RESTORE_DUAL_CARRIAGE_STATE_help = \ "Restore dual carriages modes and positions" def cmd_RESTORE_DUAL_CARRIAGE_STATE(self, gcmd): @@ -300,12 +303,12 @@ class DualCarriages: def restore_dual_carriage_state(self, saved_state, move, move_speed=0.): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() + move_pos = list(toolhead.get_position()) + dcs = list(self.dc_rails.values()) if move: homing_speed = 99999999. - move_pos = list(toolhead.get_position()) cur_pos = [] carriage_positions = saved_state['carriage_positions'] - dcs = list(self.dc_rails.values()) for dc in dcs: self.toggle_active_dc_rail(dc) homing_speed = min(homing_speed, dc.rail.homing_speed) @@ -335,9 +338,14 @@ class DualCarriages: cur_pos[primary_ind]) toolhead.manual_move(move_pos, move_speed or homing_speed) toolhead.flush_step_generation() - # Make sure the scaling coefficients are restored with the mode - for dc in dcs: - dc.inactivate(move_pos) + # Inactivate all carriages in order to restore scaling coefficients + # (if a move was requested) and correctly compute kinematics limits + for dc in dcs: + dc.inactivate(move_pos) + # Restore toolhead position in case some axes have no primary carriages + for axis in self.axes: + move_pos[axis] = saved_state['toolhead_position'][axis] + toolhead.set_position(move_pos) saved_modes = saved_state['carriage_modes'] saved_primary_dcs = [dc for dc in self.dc_rails.values() if saved_modes[dc.get_name()] == PRIMARY] diff --git a/test/klippy/generic_cartesian_iqex.cfg b/test/klippy/generic_cartesian_iqex.cfg index aa4c2f5cf..2351817ca 100644 --- a/test/klippy/generic_cartesian_iqex.cfg +++ b/test/klippy/generic_cartesian_iqex.cfg @@ -365,6 +365,33 @@ gcode: SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder SYNC_EXTRUDER_MOTION EXTRUDER=extruder3 MOTION_QUEUE=extruder +[gcode_macro _DISABLE_TOOL] +gcode: + SAVE_GCODE_STATE + SET_DUAL_CARRIAGE CARRIAGE={params.CARRIAGE} MODE=INACTIVE + SAVE_DUAL_CARRIAGE_STATE + SET_DUAL_CARRIAGE CARRIAGE={params.CARRIAGE} + G1 X{params.PARKING_POSITION} F12000 + SYNC_EXTRUDER_MOTION EXTRUDER={params.EXTRUDER} MOTION_QUEUE= + RESTORE_DUAL_CARRIAGE_STATE MOVE=0 + RESTORE_GCODE_STATE MOVE=0 + +[gcode_macro DISABLE_T0] +gcode: + _DISABLE_TOOL CARRIAGE=carriage_t0 EXTRUDER=extruder PARKING_POSITION={printer.configfile.settings["carriage carriage_t0"].position_min} + +[gcode_macro DISABLE_T1] +gcode: + _DISABLE_TOOL CARRIAGE=carriage_t1 EXTRUDER=extruder1 PARKING_POSITION={printer.configfile.settings["dual_carriage carriage_t1"].position_max} + +[gcode_macro DISABLE_T2] +gcode: + _DISABLE_TOOL CARRIAGE=carriage_t2 EXTRUDER=extruder2 PARKING_POSITION={printer.configfile.settings["dual_carriage carriage_t2"].position_min} + +[gcode_macro DISABLE_T3] +gcode: + _DISABLE_TOOL CARRIAGE=carriage_t3 EXTRUDER=extruder3 PARKING_POSITION={printer.configfile.settings["dual_carriage carriage_t3"].position_max} + [heater_bed] heater_pin: PA1 sensor_pin: PF3 # TB diff --git a/test/klippy/generic_cartesian_iqex.test b/test/klippy/generic_cartesian_iqex.test index b317dbf21..8e5bb5669 100644 --- a/test/klippy/generic_cartesian_iqex.test +++ b/test/klippy/generic_cartesian_iqex.test @@ -69,3 +69,10 @@ G1 X11 E0.1 F3000 SET_MIRROR_MODE2 G1 X10 Y10 F6000 G1 X11 E0.1 F3000 + +DISABLE_T0 +G1 X12 E0.1 +DISABLE_T2 +G1 X13 E0.1 +DISABLE_T3 +G1 X14 E0.1 From 41bc240b1630ef50826611cc2d10db865cc09abf Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 11 Feb 2026 12:27:24 -0500 Subject: [PATCH 391/411] adccmds: Support batching multiple reports into a single message Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 37 ++++++++++++++++++++++++++++++++----- src/adccmds.c | 30 +++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 909b8ddb5..02c17a067 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -1,9 +1,9 @@ # Interface to Klipper micro-controller code # -# Copyright (C) 2016-2025 Kevin O'Connor +# Copyright (C) 2016-2026 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import sys, os, zlib, logging, math +import sys, os, zlib, logging, math, struct import serialhdl, msgproto, pins, chelper, clocksync class error(Exception): @@ -541,6 +541,7 @@ class MCU_adc: self._oid = self._callback = None self._mcu.register_config_callback(self._build_config) self._inv_max_adc = 0. + self._unpack_from = struct.Struct(' +// Copyright (C) 2016-2026 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -17,6 +17,8 @@ struct analog_in { struct gpio_adc pin; uint8_t invalid_count, range_check_count; uint8_t state, sample_count; + uint8_t bytes_per_report, data_count; + uint8_t data[48]; }; static struct task_wake analog_wake; @@ -82,16 +84,23 @@ command_query_analog_in(uint32_t *args) a->sample_count = args[3]; a->state = a->sample_count + 1; a->rest_time = args[4]; - a->min_value = args[5]; - a->max_value = args[6]; - a->range_check_count = args[7]; + a->bytes_per_report = args[5]; + a->data_count = 0; + a->min_value = args[6]; + a->max_value = args[7]; + a->range_check_count = args[8]; if (! a->sample_count) return; + if (a->bytes_per_report > ARRAY_SIZE(a->data)) + shutdown("Invalid analog_in bytes_per_report"); sched_add_timer(&a->timer); } DECL_COMMAND(command_query_analog_in, "query_analog_in oid=%c clock=%u sample_ticks=%u sample_count=%c" - " rest_ticks=%u min_value=%hu max_value=%hu range_check_count=%c"); + " rest_ticks=%u bytes_per_report=%c" + " min_value=%hu max_value=%hu range_check_count=%c"); + +#define BYTES_PER_SAMPLE 2 void analog_in_task(void) @@ -112,8 +121,15 @@ analog_in_task(void) uint32_t next_begin_time = a->next_begin_time; a->state++; irq_enable(); - sendf("analog_in_state oid=%c next_clock=%u value=%hu" - , oid, next_begin_time, value); + uint8_t *d = &a->data[a->data_count]; + d[0] = value; + d[1] = value >> 8; + a->data_count += BYTES_PER_SAMPLE; + if (a->data_count + BYTES_PER_SAMPLE > a->bytes_per_report) { + sendf("analog_in_state oid=%c next_clock=%u values=%*s" + , oid, next_begin_time, a->data_count, a->data); + a->data_count = 0; + } } } DECL_TASK(analog_in_task); From a44e905ffe8db0a777296b6f48a1bda002b796e9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 14 Feb 2026 13:25:34 -0500 Subject: [PATCH 392/411] trigger_analog: Support not compiling trigger_analog on small flash builds Allow the user to deselect CONFIG_WANT_TRIGGER_ANALOG even if one of the sensors could utilize the support. Signed-off-by: Kevin O'Connor --- src/Kconfig | 5 +++++ src/sensor_ads1220.c | 2 ++ src/sensor_hx71x.c | 2 ++ src/sensor_ldc1612.c | 2 ++ src/trigger_analog.h | 16 ++++++++++++++++ test/configs/stm32f042.config | 1 + 6 files changed, 28 insertions(+) diff --git a/src/Kconfig b/src/Kconfig index 9c9790ff7..ae2d65d51 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -263,6 +263,11 @@ config WANT_LDC1612 config WANT_SENSOR_ANGLE bool "Support angle sensors" depends on WANT_SPI +comment "Other features" + depends on WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 +config WANT_TRIGGER_ANALOG + bool "Support for homing/probing events using analog sensors" + depends on WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 endmenu # Generic configuration options for CANbus diff --git a/src/sensor_ads1220.c b/src/sensor_ads1220.c index fdf770cf6..b9c798415 100644 --- a/src/sensor_ads1220.c +++ b/src/sensor_ads1220.c @@ -122,8 +122,10 @@ ads1220_attach_trigger_analog(uint32_t *args) { struct ads1220_adc *ads1220 = oid_lookup(oid, command_config_ads1220); ads1220->ta = trigger_analog_oid_lookup(args[1]); } +#if CONFIG_WANT_TRIGGER_ANALOG DECL_COMMAND(ads1220_attach_trigger_analog, "ads1220_attach_trigger_analog oid=%c trigger_analog_oid=%c"); +#endif // start/stop capturing ADC data void diff --git a/src/sensor_hx71x.c b/src/sensor_hx71x.c index 7d869d9d5..58729ab11 100644 --- a/src/sensor_hx71x.c +++ b/src/sensor_hx71x.c @@ -211,8 +211,10 @@ hx71x_attach_trigger_analog(uint32_t *args) { struct hx71x_adc *hx71x = oid_lookup(oid, command_config_hx71x); hx71x->ta = trigger_analog_oid_lookup(args[1]); } +#if CONFIG_WANT_TRIGGER_ANALOG DECL_COMMAND(hx71x_attach_trigger_analog, "hx71x_attach_trigger_analog oid=%c" " trigger_analog_oid=%c"); +#endif // start/stop capturing ADC data void diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 2c794fafe..056d29e28 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -89,8 +89,10 @@ ldc1612_attach_trigger_analog(uint32_t *args) { struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); ld->ta = trigger_analog_oid_lookup(args[1]); } +#if CONFIG_WANT_TRIGGER_ANALOG DECL_COMMAND(ldc1612_attach_trigger_analog, "ldc1612_attach_trigger_analog oid=%c trigger_analog_oid=%c"); +#endif #define DATA_ERROR_AMPLITUDE (1L << 28) diff --git a/src/trigger_analog.h b/src/trigger_analog.h index 53b36ce63..0dab2fbe8 100644 --- a/src/trigger_analog.h +++ b/src/trigger_analog.h @@ -2,9 +2,25 @@ #define __TRIGGER_ANALOG_H #include // uint8_t +#include "autoconf.h" // CONFIG_WANT_TRIGGER_ANALOG + +#if CONFIG_WANT_TRIGGER_ANALOG struct trigger_analog *trigger_analog_oid_lookup(uint8_t oid); void trigger_analog_note_error(struct trigger_analog *ta, uint8_t sensor_code); void trigger_analog_update(struct trigger_analog *ta, int32_t sample); +#else +// Dummy versions of code to avoid compile errors when not using trigger_analog + +static inline struct trigger_analog *trigger_analog_oid_lookup(uint8_t oid) { + return NULL; +} +static inline void +trigger_analog_note_error(struct trigger_analog *ta, uint8_t sensor_code) { } +static inline void +trigger_analog_update(struct trigger_analog *ta, int32_t sample) { } + +#endif + #endif // trigger_analog.h diff --git a/test/configs/stm32f042.config b/test/configs/stm32f042.config index 53cf1281e..872eb683b 100644 --- a/test/configs/stm32f042.config +++ b/test/configs/stm32f042.config @@ -6,3 +6,4 @@ CONFIG_WANT_LIS2DW=n CONFIG_WANT_LDC1612=n CONFIG_WANT_HX71X=n CONFIG_WANT_ADS1220=n +CONFIG_WANT_TRIGGER_ANALOG=n From 9e962ee3f4529ee2e51484b242483b4d9c86b39e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 14 Feb 2026 13:35:13 -0500 Subject: [PATCH 393/411] adccmds: Add support for homing/probing triggering with regular mcu adc pins Add support for trigger_analog to adccmds.c . Signed-off-by: Kevin O'Connor --- src/Kconfig | 6 +++--- src/adccmds.c | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Kconfig b/src/Kconfig index ae2d65d51..a30b1789c 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -183,7 +183,7 @@ config NEED_SENSOR_BULK default y config WANT_TRIGGER_ANALOG bool - depends on WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 + depends on WANT_ADC || WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 default y config NEED_SOS_FILTER bool @@ -264,10 +264,10 @@ config WANT_SENSOR_ANGLE bool "Support angle sensors" depends on WANT_SPI comment "Other features" - depends on WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 + depends on WANT_ADC || WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 config WANT_TRIGGER_ANALOG bool "Support for homing/probing events using analog sensors" - depends on WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 + depends on WANT_ADC || WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 endmenu # Generic configuration options for CANbus diff --git a/src/adccmds.c b/src/adccmds.c index 4283de9f3..97af92681 100644 --- a/src/adccmds.c +++ b/src/adccmds.c @@ -9,6 +9,7 @@ #include "board/irq.h" // irq_disable #include "command.h" // DECL_COMMAND #include "sched.h" // DECL_TASK +#include "trigger_analog.h" // trigger_analog_update struct analog_in { struct timer timer; @@ -19,6 +20,7 @@ struct analog_in { uint8_t state, sample_count; uint8_t bytes_per_report, data_count; uint8_t data[48]; + struct trigger_analog *ta; }; static struct task_wake analog_wake; @@ -100,6 +102,16 @@ DECL_COMMAND(command_query_analog_in, " rest_ticks=%u bytes_per_report=%c" " min_value=%hu max_value=%hu range_check_count=%c"); +void +command_analog_in_attach_trigger_analog(uint32_t *args) { + struct analog_in *a = oid_lookup(args[0], command_config_analog_in); + a->ta = trigger_analog_oid_lookup(args[1]); +} +#if CONFIG_WANT_TRIGGER_ANALOG +DECL_COMMAND(command_analog_in_attach_trigger_analog, + "analog_in_attach_trigger_analog oid=%c trigger_analog_oid=%c"); +#endif + #define BYTES_PER_SAMPLE 2 void @@ -121,6 +133,7 @@ analog_in_task(void) uint32_t next_begin_time = a->next_begin_time; a->state++; irq_enable(); + trigger_analog_update(a->ta, value); uint8_t *d = &a->data[a->data_count]; d[0] = value; d[1] = value >> 8; @@ -141,6 +154,7 @@ analog_in_shutdown(void) struct analog_in *a; foreach_oid(i, a, command_config_analog_in) { gpio_adc_cancel_sample(a->pin); + a->ta = NULL; if (a->sample_count) { a->state = a->sample_count + 1; a->next_begin_time += a->rest_time; From 5a2fd1009d327b8d7b3a1a00bb8bf9eec3208911 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 14 Feb 2026 12:43:28 -0500 Subject: [PATCH 394/411] mcu: Change MCU_adc.get_last_value() to return (time, value) Change the get_last_value() method to return time first then value (instead of value, then time). This makes get_last_value() match the order of parameters that is used in the adc update callback. This also fixes ads1x1x to return the "print_time" instead of a system time. Signed-off-by: Kevin O'Connor --- klippy/extras/adc_scaled.py | 2 +- klippy/extras/adc_temperature.py | 2 +- klippy/extras/ads1x1x.py | 8 ++++---- klippy/extras/query_adc.py | 2 +- klippy/mcu.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/klippy/extras/adc_scaled.py b/klippy/extras/adc_scaled.py index 80ea452f3..4fe83b984 100644 --- a/klippy/extras/adc_scaled.py +++ b/klippy/extras/adc_scaled.py @@ -23,7 +23,7 @@ class MCU_scaled_adc: max_adc = self._main.last_vref[1] min_adc = self._main.last_vssa[1] scaled_val = (read_value - min_adc) / (max_adc - min_adc) - self._last_state = (scaled_val, read_time) + self._last_state = (read_time, scaled_val) self._callback(read_time, scaled_val) def setup_adc_callback(self, report_time, callback): self._callback = callback diff --git a/klippy/extras/adc_temperature.py b/klippy/extras/adc_temperature.py index c53ae7056..7915502b8 100644 --- a/klippy/extras/adc_temperature.py +++ b/klippy/extras/adc_temperature.py @@ -57,7 +57,7 @@ class HelperTemperatureDiagnostics: def _clarify_adc_range(self, msg, details): if self.min_temp is None: return None - last_value, last_read_time = self.mcu_adc.get_last_value() + last_read_time, last_value = self.mcu_adc.get_last_value() if not last_read_time: return None if last_value >= self.min_adc and last_value <= self.max_adc: diff --git a/klippy/extras/ads1x1x.py b/klippy/extras/ads1x1x.py index bcdbaf698..4c9caa377 100644 --- a/klippy/extras/ads1x1x.py +++ b/klippy/extras/ads1x1x.py @@ -361,10 +361,10 @@ class ADS1X1X_pin: self.invalid_count = 0 # Publish result - measured_time = self._reactor.monotonic() - self._last_state = (target_value, measured_time) - self.callback(self.chip.mcu.estimated_print_time(measured_time), - target_value) + systime = self._reactor.monotonic() + measured_time = self.chip.mcu.estimated_print_time(systime) + self._last_state = (measured_time, target_value) + self.callback(measured_time, target_value) else: self.invalid_count = self.invalid_count + 1 self.check_invalid() diff --git a/klippy/extras/query_adc.py b/klippy/extras/query_adc.py index 2102aaf8f..55e509a0f 100644 --- a/klippy/extras/query_adc.py +++ b/klippy/extras/query_adc.py @@ -21,7 +21,7 @@ class QueryADC: msg = "Available ADC objects: %s" % (', '.join(objs),) gcmd.respond_info(msg) return - value, timestamp = self.adc[name].get_last_value() + timestamp, value = self.adc[name].get_last_value() msg = 'ADC object "%s" has value %.6f (timestamp %.3f)' % ( name, value, timestamp) pullup = gcmd.get_float('PULLUP', None, above=0.) diff --git a/klippy/mcu.py b/klippy/mcu.py index 02c17a067..879817116 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -601,7 +601,7 @@ class MCU_adc: next_clock = self._mcu.clock32_to_clock64(params['next_clock']) last_read_clock = next_clock - self._report_clock last_read_time = self._mcu.clock_to_print_time(last_read_clock) - self._last_state = (last_value, last_read_time) + self._last_state = (last_read_time, last_value) if self._callback is not None: self._callback(last_read_time, last_value) def _handle_analog_in_state(self, params): @@ -610,7 +610,7 @@ class MCU_adc: next_clock = self._mcu.clock32_to_clock64(params['next_clock']) last_read_clock = next_clock - self._report_clock last_read_time = self._mcu.clock_to_print_time(last_read_clock) - self._last_state = (last_value, last_read_time) + self._last_state = (last_read_time, last_value) if self._callback is not None: self._callback(last_read_time, last_value) From 8ed426b54c8a25bc2c380da7e381dc3a0b310548 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 14 Feb 2026 12:38:14 -0500 Subject: [PATCH 395/411] mcu: Convert ADC callback to pass a list of samples Support batching multiple samples together and pass those samples to the registered adc callback. Signed-off-by: Kevin O'Connor --- klippy/extras/adc_scaled.py | 23 ++++++----- klippy/extras/adc_temperature.py | 7 ++-- klippy/extras/ads1x1x.py | 13 ++++--- klippy/extras/buttons.py | 8 ++-- klippy/extras/hall_filament_width_sensor.py | 16 +++++--- klippy/extras/temperature_mcu.py | 10 +++-- .../extras/tsl1401cl_filament_width_sensor.py | 8 ++-- klippy/mcu.py | 38 +++++++++++-------- 8 files changed, 73 insertions(+), 50 deletions(-) diff --git a/klippy/extras/adc_scaled.py b/klippy/extras/adc_scaled.py index 4fe83b984..82c4aedf3 100644 --- a/klippy/extras/adc_scaled.py +++ b/klippy/extras/adc_scaled.py @@ -19,15 +19,16 @@ class MCU_scaled_adc: self._callback = None self.setup_adc_sample = self._mcu_adc.setup_adc_sample self.get_mcu = self._mcu_adc.get_mcu - def _handle_callback(self, read_time, read_value): + def _handle_callback(self, samples): max_adc = self._main.last_vref[1] min_adc = self._main.last_vssa[1] - scaled_val = (read_value - min_adc) / (max_adc - min_adc) - self._last_state = (read_time, scaled_val) - self._callback(read_time, scaled_val) - def setup_adc_callback(self, report_time, callback): + adjsamples = [(t, (read_value - min_adc) / (max_adc - min_adc)) + for t, read_value in samples] + self._last_state = adjsamples[-1] + self._callback(adjsamples) + def setup_adc_callback(self, callback): self._callback = callback - self._mcu_adc.setup_adc_callback(report_time, self._handle_callback) + self._mcu_adc.setup_adc_callback(self._handle_callback) def get_last_value(self): return self._last_state @@ -52,8 +53,8 @@ class PrinterADCScaled: pin_name = config.get(name + '_pin') ppins = self.printer.lookup_object('pins') mcu_adc = ppins.setup_pin('adc', pin_name) - mcu_adc.setup_adc_callback(REPORT_TIME, callback) - mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT) + mcu_adc.setup_adc_callback(callback) + mcu_adc.setup_adc_sample(REPORT_TIME, SAMPLE_TIME, SAMPLE_COUNT) query_adc = config.get_printer().load_object(config, 'query_adc') query_adc.register_adc(self.name + ":" + name, mcu_adc) return mcu_adc @@ -68,9 +69,11 @@ class PrinterADCScaled: adj_time = min(time_diff * self.inv_smooth_time, 1.) smoothed_value = last_value + value_diff * adj_time return (read_time, smoothed_value) - def vref_callback(self, read_time, read_value): + def vref_callback(self, samples): + read_time, read_value = samples[-1] self.last_vref = self.calc_smooth(read_time, read_value, self.last_vref) - def vssa_callback(self, read_time, read_value): + def vssa_callback(self, samples): + read_time, read_value = samples[-1] self.last_vssa = self.calc_smooth(read_time, read_value, self.last_vssa) def load_config_prefix(config): diff --git a/klippy/extras/adc_temperature.py b/klippy/extras/adc_temperature.py index 7915502b8..9661e84ef 100644 --- a/klippy/extras/adc_temperature.py +++ b/klippy/extras/adc_temperature.py @@ -21,20 +21,21 @@ class PrinterADCtoTemperature: self.adc_convert = adc_convert ppins = config.get_printer().lookup_object('pins') self.mcu_adc = ppins.setup_pin('adc', config.get('sensor_pin')) - self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback) + self.mcu_adc.setup_adc_callback(self.adc_callback) self.diag_helper = HelperTemperatureDiagnostics( config, self.mcu_adc, adc_convert.calc_temp) def setup_callback(self, temperature_callback): self.temperature_callback = temperature_callback def get_report_time_delta(self): return REPORT_TIME - def adc_callback(self, read_time, read_value): + def adc_callback(self, samples): + read_time, read_value = samples[-1] temp = self.adc_convert.calc_temp(read_value) self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) def setup_minmax(self, min_temp, max_temp): arange = [self.adc_convert.calc_adc(t) for t in [min_temp, max_temp]] min_adc, max_adc = sorted(arange) - self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT, + self.mcu_adc.setup_adc_sample(REPORT_TIME, SAMPLE_TIME, SAMPLE_COUNT, minval=min_adc, maxval=max_adc, range_check_count=RANGE_CHECK_COUNT) self.diag_helper.setup_diag_minmax(min_temp, max_temp, min_adc, max_adc) diff --git a/klippy/extras/ads1x1x.py b/klippy/extras/ads1x1x.py index 4c9caa377..e548970a2 100644 --- a/klippy/extras/ads1x1x.py +++ b/klippy/extras/ads1x1x.py @@ -364,7 +364,7 @@ class ADS1X1X_pin: systime = self._reactor.monotonic() measured_time = self.chip.mcu.estimated_print_time(systime) self._last_state = (measured_time, target_value) - self.callback(measured_time, target_value) + self.callback([(measured_time, target_value)]) else: self.invalid_count = self.invalid_count + 1 self.check_invalid() @@ -379,16 +379,17 @@ class ADS1X1X_pin: def get_mcu(self): return self.mcu - def setup_adc_callback(self, report_time, callback): - self.report_time = report_time + def setup_adc_callback(self, callback): self.callback = callback - self.chip.handle_report_time_update() - def setup_adc_sample(self, sample_time, sample_count, - minval=0., maxval=1., range_check_count=0): + def setup_adc_sample(self, report_time, sample_time=0., sample_count=1, + batch_num=1, minval=0., maxval=1., + range_check_count=0): + self.report_time = report_time self.minval = minval self.maxval = maxval self.range_check_count = range_check_count + self.chip.handle_report_time_update() def get_last_value(self): return self._last_state diff --git a/klippy/extras/buttons.py b/klippy/extras/buttons.py index f83dd5eda..b3432f853 100644 --- a/klippy/extras/buttons.py +++ b/klippy/extras/buttons.py @@ -104,8 +104,9 @@ class MCU_ADC_buttons: self.max_value = 0. ppins = printer.lookup_object('pins') self.mcu_adc = ppins.setup_pin('adc', self.pin) - self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) - self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback) + self.mcu_adc.setup_adc_sample(ADC_REPORT_TIME, + ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc.setup_adc_callback(self.adc_callback) query_adc = printer.lookup_object('query_adc') query_adc.register_adc('adc_button:' + pin.strip(), self.mcu_adc) @@ -114,7 +115,8 @@ class MCU_ADC_buttons: self.max_value = max(self.max_value, max_value) self.buttons.append((min_value, max_value, callback)) - def adc_callback(self, read_time, read_value): + def adc_callback(self, samples): + read_time, read_value = samples[-1] adc = max(.00001, min(.99999, read_value)) value = self.pullup * adc / (1.0 - adc) diff --git a/klippy/extras/hall_filament_width_sensor.py b/klippy/extras/hall_filament_width_sensor.py index 865e49afe..6c981303b 100644 --- a/klippy/extras/hall_filament_width_sensor.py +++ b/klippy/extras/hall_filament_width_sensor.py @@ -49,11 +49,13 @@ class HallFilamentWidthSensor: # Start adc self.ppins = self.printer.lookup_object('pins') self.mcu_adc = self.ppins.setup_pin('adc', self.pin1) - self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) - self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback) + self.mcu_adc.setup_adc_sample(ADC_REPORT_TIME, + ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc.setup_adc_callback(self.adc_callback) self.mcu_adc2 = self.ppins.setup_pin('adc', self.pin2) - self.mcu_adc2.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) - self.mcu_adc2.setup_adc_callback(ADC_REPORT_TIME, self.adc2_callback) + self.mcu_adc2.setup_adc_sample(ADC_REPORT_TIME, + ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc2.setup_adc_callback(self.adc2_callback) # extrude factor updating self.extrude_factor_update_timer = self.reactor.register_timer( self.extrude_factor_update_event) @@ -83,12 +85,14 @@ class HallFilamentWidthSensor: self.reactor.update_timer(self.extrude_factor_update_timer, self.reactor.NOW) - def adc_callback(self, read_time, read_value): + def adc_callback(self, samples): # read sensor value + read_time, read_value = samples[-1] self.lastFilamentWidthReading = round(read_value * 10000) - def adc2_callback(self, read_time, read_value): + def adc2_callback(self, samples): # read sensor value + read_time, read_value = samples[-1] self.lastFilamentWidthReading2 = round(read_value * 10000) # calculate diameter diameter_new = round((self.dia2 - self.dia1)/ diff --git a/klippy/extras/temperature_mcu.py b/klippy/extras/temperature_mcu.py index 6f3386a4b..686deea24 100644 --- a/klippy/extras/temperature_mcu.py +++ b/klippy/extras/temperature_mcu.py @@ -31,12 +31,13 @@ class PrinterTemperatureMCU: ppins = config.get_printer().lookup_object('pins') self.mcu_adc = ppins.setup_pin('adc', '%s:ADC_TEMPERATURE' % (mcu_name,)) - self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback) + self.mcu_adc.setup_adc_callback(self.adc_callback) self.diag_helper = adc_temperature.HelperTemperatureDiagnostics( config, self.mcu_adc, self.calc_temp) # Register callbacks if self.printer.get_start_args().get('debugoutput') is not None: - self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT) + self.mcu_adc.setup_adc_sample(REPORT_TIME, + SAMPLE_TIME, SAMPLE_COUNT) return self.printer.register_event_handler("klippy:mcu_identify", self.handle_mcu_identify) @@ -49,7 +50,8 @@ class PrinterTemperatureMCU: self.min_temp = min_temp self.max_temp = max_temp # Internal code - def adc_callback(self, read_time, read_value): + def adc_callback(self, samples): + read_time, read_value = samples[-1] temp = self.base_temperature + read_value * self.slope self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) def calc_temp(self, adc): @@ -95,7 +97,7 @@ class PrinterTemperatureMCU: # Setup min/max checks arange = [self.calc_adc(t) for t in [self.min_temp, self.max_temp]] min_adc, max_adc = sorted(arange) - self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT, + self.mcu_adc.setup_adc_sample(REPORT_TIME, SAMPLE_TIME, SAMPLE_COUNT, minval=min_adc, maxval=max_adc, range_check_count=RANGE_CHECK_COUNT) self.diag_helper.setup_diag_minmax(self.min_temp, self.max_temp, diff --git a/klippy/extras/tsl1401cl_filament_width_sensor.py b/klippy/extras/tsl1401cl_filament_width_sensor.py index 83480f467..b5b89e9a6 100644 --- a/klippy/extras/tsl1401cl_filament_width_sensor.py +++ b/klippy/extras/tsl1401cl_filament_width_sensor.py @@ -33,8 +33,9 @@ class FilamentWidthSensor: # Start adc self.ppins = self.printer.lookup_object('pins') self.mcu_adc = self.ppins.setup_pin('adc', self.pin) - self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) - self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback) + self.mcu_adc.setup_adc_sample(ADC_REPORT_TIME, + ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc.setup_adc_callback(self.adc_callback) # extrude factor updating self.extrude_factor_update_timer = self.reactor.register_timer( self.extrude_factor_update_event) @@ -57,8 +58,9 @@ class FilamentWidthSensor: self.reactor.update_timer(self.extrude_factor_update_timer, self.reactor.NOW) - def adc_callback(self, read_time, read_value): + def adc_callback(self, samples): # read sensor value + read_time, read_value = samples[-1] self.lastFilamentWidthReading = round(read_value * 5, 2) def update_filament_array(self, last_epos): diff --git a/klippy/mcu.py b/klippy/mcu.py index 879817116..75d608f82 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -535,7 +535,7 @@ class MCU_adc: self._pin = pin_params['pin'] self._min_sample = self._max_sample = 0. self._sample_time = self._report_time = 0. - self._sample_count = self._range_check_count = 0 + self._sample_count = self._batch_num = self._range_check_count = 0 self._report_clock = 0 self._last_state = (0., 0.) self._oid = self._callback = None @@ -544,15 +544,17 @@ class MCU_adc: self._unpack_from = struct.Struct('= (1<<16): + raise self._mcu.get_printer().config_error( + "ADC sample_count=%d too large for MCU" % (self._sample_count,)) self._inv_max_adc = 1.0 / max_adc self._report_clock = self._mcu.seconds_to_clock(self._report_time) min_sample = max(0, min(0xffff, int(self._min_sample * max_adc))) @@ -575,7 +580,8 @@ class MCU_adc: oldcmd = ( "query_analog_in oid=%c clock=%u sample_ticks=%u sample_count=%c" " rest_ticks=%u min_value=%hu max_value=%hu range_check_count=%c") - if self._mcu.try_lookup_command(oldcmd) is not None: + if (self._batch_num == 1 + and self._mcu.try_lookup_command(oldcmd) is not None): self._mcu.add_config_cmd( "query_analog_in oid=%d clock=%d sample_ticks=%d" " sample_count=%d rest_ticks=%d" @@ -587,12 +593,13 @@ class MCU_adc: "analog_in_state", self._oid) return BYTES_PER_SAMPLE = 2 + bytes_per_report = self._batch_num * BYTES_PER_SAMPLE self._mcu.add_config_cmd( "query_analog_in oid=%d clock=%d sample_ticks=%d sample_count=%d" " rest_ticks=%d bytes_per_report=%d" " min_value=%d max_value=%d range_check_count=%d" % ( self._oid, clock, sample_ticks, self._sample_count, - self._report_clock, BYTES_PER_SAMPLE, min_sample, max_sample, + self._report_clock, bytes_per_report, min_sample, max_sample, self._range_check_count), is_init=True) self._mcu.register_response(self._handle_analog_in_state, "analog_in_state", self._oid) @@ -603,16 +610,17 @@ class MCU_adc: last_read_time = self._mcu.clock_to_print_time(last_read_clock) self._last_state = (last_read_time, last_value) if self._callback is not None: - self._callback(last_read_time, last_value) + self._callback([(last_read_time, last_value)]) def _handle_analog_in_state(self, params): values = self._unpack_from(params['values']) - last_value = values[0] * self._inv_max_adc next_clock = self._mcu.clock32_to_clock64(params['next_clock']) - last_read_clock = next_clock - self._report_clock - last_read_time = self._mcu.clock_to_print_time(last_read_clock) - self._last_state = (last_read_time, last_value) + ctpt = self._mcu.clock_to_print_time + num = len(values) + samples = [(ctpt(next_clock - (num - i)*self._report_clock), + values[i] * self._inv_max_adc) for i in range(num)] + self._last_state = samples[-1] if self._callback is not None: - self._callback(last_read_time, last_value) + self._callback(samples) ###################################################################### From 2f2219b4b7f07cdb710000f97c5307f680bdec66 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 17 Feb 2026 13:10:16 -0500 Subject: [PATCH 396/411] buttons: Use ADC batching for ADC based buttons Increase ADC report time from every 15ms to every 10ms and batch every 5 reports into a single message. This should improve the responsiveness and reduce the communication overhead when using ADC buttons. Signed-off-by: Kevin O'Connor --- klippy/extras/buttons.py | 56 +++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/klippy/extras/buttons.py b/klippy/extras/buttons.py index b3432f853..4ffb3c38b 100644 --- a/klippy/extras/buttons.py +++ b/klippy/extras/buttons.py @@ -86,10 +86,11 @@ class MCU_buttons: # ADC button tracking ###################################################################### -ADC_REPORT_TIME = 0.015 +ADC_REPORT_TIME = 0.010 ADC_DEBOUNCE_TIME = 0.025 ADC_SAMPLE_TIME = 0.001 ADC_SAMPLE_COUNT = 6 +ADC_BATCH_COUNT = 5 class MCU_ADC_buttons: def __init__(self, printer, pin, pullup): @@ -105,7 +106,8 @@ class MCU_ADC_buttons: ppins = printer.lookup_object('pins') self.mcu_adc = ppins.setup_pin('adc', self.pin) self.mcu_adc.setup_adc_sample(ADC_REPORT_TIME, - ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT, + batch_num=ADC_BATCH_COUNT) self.mcu_adc.setup_adc_callback(self.adc_callback) query_adc = printer.lookup_object('query_adc') query_adc.register_adc('adc_button:' + pin.strip(), self.mcu_adc) @@ -116,35 +118,35 @@ class MCU_ADC_buttons: self.buttons.append((min_value, max_value, callback)) def adc_callback(self, samples): - read_time, read_value = samples[-1] - adc = max(.00001, min(.99999, read_value)) - value = self.pullup * adc / (1.0 - adc) + for read_time, read_value in samples: + adc = max(.00001, min(.99999, read_value)) + value = self.pullup * adc / (1.0 - adc) - # Determine button pressed - btn = None - if self.min_value <= value <= self.max_value: - for i, (min_value, max_value, cb) in enumerate(self.buttons): - if min_value < value < max_value: - btn = i - break + # Determine button pressed + btn = None + if self.min_value <= value <= self.max_value: + for i, (min_value, max_value, cb) in enumerate(self.buttons): + if min_value < value < max_value: + btn = i + break - # If the button changed, due to noise or pressing: - if btn != self.last_button: - # reset the debouncing timer - self.last_debouncetime = read_time + # If the button changed, due to noise or pressing: + if btn != self.last_button: + # reset the debouncing timer + self.last_debouncetime = read_time - # button debounce check & new button pressed - if ((read_time - self.last_debouncetime) >= ADC_DEBOUNCE_TIME - and self.last_button == btn and self.last_pressed != btn): - # release last_pressed - if self.last_pressed is not None: - self.call_button(self.last_pressed, False) - self.last_pressed = None - if btn is not None: - self.call_button(btn, True) - self.last_pressed = btn + # button debounce check & new button pressed + if ((read_time - self.last_debouncetime) >= ADC_DEBOUNCE_TIME + and self.last_button == btn and self.last_pressed != btn): + # release last_pressed + if self.last_pressed is not None: + self.call_button(self.last_pressed, False) + self.last_pressed = None + if btn is not None: + self.call_button(btn, True) + self.last_pressed = btn - self.last_button = btn + self.last_button = btn def call_button(self, button, state): minval, maxval, callback = self.buttons[button] From a445013f7a337e3c3c73544d12e36c1f1c9d0a2a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 11 Feb 2026 09:15:34 -0500 Subject: [PATCH 397/411] configfile: Add support for warning on deprecated mcu code Signed-off-by: Kevin O'Connor --- klippy/configfile.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/klippy/configfile.py b/klippy/configfile.py index 754fc974f..d37404c25 100644 --- a/klippy/configfile.py +++ b/klippy/configfile.py @@ -529,6 +529,17 @@ class PrinterConfig: res = {'type': 'deprecated_gcode', 'message': msg, 'command': cmd, 'parameter': param, 'value': str(value)} self._add_deprecated(res) + def deprecate_mcu_code(self, mcu, feature, msg=None): + mcu_name = mcu.get_name() + if msg is None: + vhost = self.printer.start_args['software_version'] + vmcu = mcu.get_status()['mcu_version'] + msg = ("MCU '%s' has deprecated code (it is missing feature '%s')." + " Recompiling and flashing is recommended (MCU version '%s'," + " host version '%s')." % (mcu_name, feature, vmcu, vhost)) + res = {'type': 'deprecated_mcu_code', 'message': msg, + 'mcu': mcu_name, 'feature': feature} + self._add_deprecated(res) # Status reporting def _build_status_config(self, config): self.status_raw_config = {} From 016474b68d74deb831424845726ab24dd1b70483 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 11 Feb 2026 09:58:16 -0500 Subject: [PATCH 398/411] bus: Note mcu code deprecation if missing spi_set_sw_bus Signed-off-by: Kevin O'Connor --- klippy/extras/bus.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index f858e560e..64ac39f6f 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -92,6 +92,9 @@ class MCU_SPI: "mode=%u pulse_ticks=%u"): pulse_ticks = self.mcu.seconds_to_clock(1./self.speed) self.config_fmt = self.config_fmt_ticks % (pulse_ticks,) + else: + configfile = self.mcu.get_printer().lookup_object('configfile') + configfile.deprecate_mcu_code(self.mcu, 'spi_set_sw_bus') self.mcu.add_config_cmd(self.config_fmt) self.spi_send_cmd = self.mcu.lookup_command( "spi_send oid=%c data=%*s", cq=self.cmd_queue) From ee28dec93d08e2d46567e78eba9adad15a1c7cba Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 11 Feb 2026 10:00:31 -0500 Subject: [PATCH 399/411] bus: Note mcu code deprecation if missing i2c_set_sw_bus Signed-off-by: Kevin O'Connor --- klippy/extras/bus.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 64ac39f6f..e90140196 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -221,6 +221,9 @@ class MCU_I2C: " pulse_ticks=%u address=%u"): pulse_ticks = self.mcu.seconds_to_clock(1./self.speed/2) self.config_fmt = self.config_fmt_ticks % (pulse_ticks,) + else: + configfile = self.mcu.get_printer().lookup_object('configfile') + configfile.deprecate_mcu_code(self.mcu, 'i2c_set_sw_bus') self.mcu.add_config_cmd(self.config_fmt) if self.mcu.try_lookup_command("i2c_read oid=%c reg=%*s read_len=%u"): self.i2c_write_cmd = self.mcu.lookup_command( From 6bbc90697a795598c922be59fcd5467f6d0bd87f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 11 Feb 2026 10:02:43 -0500 Subject: [PATCH 400/411] bus: Note mcu code deprecation if missing i2c_transfer Signed-off-by: Kevin O'Connor --- klippy/extras/bus.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index e90140196..3a6ef5842 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -233,6 +233,8 @@ class MCU_I2C: "i2c_read oid=%c reg=%*s read_len=%u", "i2c_read_response oid=%c response=%*s", oid=self.oid, cq=self.cmd_queue) + configfile = self.mcu.get_printer().lookup_object('configfile') + configfile.deprecate_mcu_code(self.mcu, 'i2c_transfer') else: self.i2c_transfer_cmd = self.mcu.lookup_query_command( "i2c_transfer oid=%c write=%*s read_len=%u", From 90c72b798644cebb772a21619792a6c9deb098ec Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 11 Feb 2026 10:12:44 -0500 Subject: [PATCH 401/411] stepper: Note mcu code deprecation if missing STEPPER_STEP_BOTH_EDGE Signed-off-by: Kevin O'Connor --- klippy/stepper.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klippy/stepper.py b/klippy/stepper.py index 8d93d6c16..200f637a5 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -100,6 +100,9 @@ class MCU_stepper: elif sou: # MCU has optimized step/unstep - better to use that want_both_edges = False + if not ssbe: + configfile = self._mcu.get_printer().lookup_object("configfile") + configfile.deprecate_mcu_code(self._mcu, 'STEPPER_STEP_BOTH_EDGE') if want_both_edges: self._step_both_edge = True invert_step = -1 From 0213ad7536294d8d00370e4c10c551efd3bdd059 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 11 Feb 2026 10:19:28 -0500 Subject: [PATCH 402/411] canbus_stats: Note mcu code deprecation if missing get_canbus_status Signed-off-by: Kevin O'Connor --- klippy/extras/canbus_stats.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/klippy/extras/canbus_stats.py b/klippy/extras/canbus_stats.py index 0ddafaf36..fb8c88c91 100644 --- a/klippy/extras/canbus_stats.py +++ b/klippy/extras/canbus_stats.py @@ -32,6 +32,8 @@ class PrinterCANBusStats: self.mcu = self.printer.lookup_object(mcu_name) # Lookup status query command if self.mcu.try_lookup_command("get_canbus_status") is None: + configfile = self.printer.lookup_object('configfile') + configfile.deprecate_mcu_code(self.mcu, 'get_canbus_status') return self.get_canbus_status_cmd = self.mcu.lookup_query_command( "get_canbus_status", From 60b6271c98a0d3d7353dd810fddfb33aadfd6af6 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 1 Feb 2026 02:43:56 +0100 Subject: [PATCH 403/411] motan: print avaliable keys on error Signed-off-by: Timofey Titovets --- scripts/motan/readlog.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/motan/readlog.py b/scripts/motan/readlog.py index 43c01619b..d5ae0fe66 100644 --- a/scripts/motan/readlog.py +++ b/scripts/motan/readlog.py @@ -737,6 +737,19 @@ class LogManager: self.status_tracker = TrackStatus(self, "status", self.start_status) self.jdispatch.add_handler("status", "status") return self.status_tracker + def get_tip_msg(self, subscription_id): + available = [] + dataset_name = subscription_id.split(":")[0] + for k in self.log_subscriptions: + if dataset_name in k: + # Extract available keys + name = k.split(":")[1:] + if len(name) == 1: + suggest = "%s(%s,...)" % (dataset_name, name[0]) + available.append(suggest) + else: + available.append(name) + return "Available options: %s" % (", ".join(available)) def setup_dataset(self, name): if name in self.datasets: return self.datasets[name] @@ -750,7 +763,9 @@ class LogManager: if cls.SubscriptionIdParts: subscription_id = ":".join(name_parts[:cls.SubscriptionIdParts]) if subscription_id not in self.log_subscriptions: - raise error("Dataset '%s' not in capture" % (subscription_id,)) + tip_msg = self.get_tip_msg(subscription_id) + raise error("Dataset '%s' not in capture\n%s" % ( + subscription_id, tip_msg)) self.jdispatch.add_handler(name, subscription_id) self.datasets[name] = hdl = cls(self, name, name_parts) return hdl From 43b2d55d9b91f59b50a9b2ec868f5ab998fe493c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 23 Feb 2026 14:59:21 -0500 Subject: [PATCH 404/411] motan: Don't default to subscribing to all sensors in data_logger Subscribing to all sensors can cause a burden on the MCU communication channels. Add a new '-s' command line option to data_logger.py that allows a user to request the subscriptions that they wish to capture. Signed-off-by: Kevin O'Connor --- docs/Debugging.md | 2 +- scripts/motan/data_logger.py | 110 ++++++++++++++++++++++++++--------- 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/docs/Debugging.md b/docs/Debugging.md index be3699c28..b400d6e95 100644 --- a/docs/Debugging.md +++ b/docs/Debugging.md @@ -90,7 +90,7 @@ later analyzed. To use this feature, Klipper must be started with the Data logging is enabled with the `data_logger.py` tool. For example: ``` -~/klipper/scripts/motan/data_logger.py /tmp/klippy_uds mylog +~/klipper/scripts/motan/data_logger.py /tmp/klippy_uds mylog -s '*' ``` This command will connect to the Klipper API Server, subscribe to diff --git a/scripts/motan/data_logger.py b/scripts/motan/data_logger.py index fd4de7a5d..844b1f7c0 100755 --- a/scripts/motan/data_logger.py +++ b/scripts/motan/data_logger.py @@ -1,14 +1,30 @@ #!/usr/bin/env python # Tool to subscribe to motion data and log it to a disk file # -# Copyright (C) 2020-2021 Kevin O'Connor +# Copyright (C) 2020-2026 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import sys, os, optparse, socket, select, json, errno, time, zlib +import sys, os, optparse, socket, select, json, errno, time, zlib, fnmatch INDEX_UPDATE_TIME = 5.0 ClientInfo = {'program': 'motan_data_logger', 'version': 'v0.1'} +# Available subscriptions when a given config type is found +ConfigSubscriptions = [ + # (cfgtype, capture_name, api_request, request_params) + ('adxl345', '{ct}:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), + ('lis2dw', '{ct}:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), + ('mpu9250', '{ct}:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), + ('angle', '{ct}:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), + ('probe_eddy_current', 'ldc1612:{csn}', 'ldc1612/dump_ldc1612', + {'sensor': '{csn}'}), + ('tmc2130', 'stallguard:{csn}', 'tmc/stallguard_dump', {'name': '{csn}'}), + ('tmc2209', 'stallguard:{csn}', 'tmc/stallguard_dump', {'name': '{csn}'}), + ('tmc2260', 'stallguard:{csn}', 'tmc/stallguard_dump', {'name': '{csn}'}), + ('tmc2240', 'stallguard:{csn}', 'tmc/stallguard_dump', {'name': '{csn}'}), + ('tmc5160', 'stallguard:{csn}', 'tmc/stallguard_dump', {'name': '{csn}'}), +] + def webhook_socket_create(uds_filename): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.setblocking(0) @@ -53,7 +69,7 @@ class LogWriter: self.comp = None class DataLogger: - def __init__(self, uds_filename, log_prefix): + def __init__(self, uds_filename, log_prefix, want_subscriptions): # IO self.webhook_socket = webhook_socket_create(uds_filename) self.poll = select.poll() @@ -68,6 +84,9 @@ class DataLogger: # get_status databasing self.db = {} self.next_index_time = 0. + # Subscriptions + self.want_subscriptions = want_subscriptions + self.need_subscriptions = [] # Start login process self.send_query("info", "info", {"client_info": ClientInfo}, self.handle_info) @@ -122,6 +141,48 @@ class DataLogger: self.process_socket() except KeyboardInterrupt as e: self.finish("Keyboard Interrupt") + # Subscription handling + def build_subscriptions(self, status): + avail = [] + # trapq and stepq subscriptions from motion_report + motion_report = status.get("motion_report", {}) + for trapq in motion_report.get("trapq", []): + avail.append(("trapq:" + trapq, "motion_report/dump_trapq", + {"name": trapq})) + for stepper in motion_report.get("steppers", []): + avail.append(("stepq:" + stepper, "motion_report/dump_stepper", + {"name": stepper})) + # config based subsciriptions + config = status["configfile"]["settings"] + cfgtypes = {p[0]: p for p in ConfigSubscriptions} + for cfgname in config.keys(): + ct = cfgname.split()[0] + if ct not in cfgtypes: + continue + cfgtype, capture_name, request, params = cfgtypes[ct] + cfgparts = cfgname.split() + csn = cfgparts[-1] + if len(cfgparts) > 1: + csn = ' '.join(cfgparts[1:]) + fill = {'ct':ct, 'csn': csn} + capture_name = capture_name.format(**fill) + request = request.format(**fill) + params = {k: v.format(**fill) for k, v in params.items()} + avail.append((capture_name, request, params)) + # Report available subscriptions + capture_names = [a[0] for a in avail] + sys.stdout.write("Available subscriptions:\n %s\n" + % ('\n '.join(sorted(capture_names)).strip(),)) + # Limit subscriptions to those desired + want_captures = {} + for pattern in self.want_subscriptions: + m = fnmatch.filter(capture_names, pattern) + want_captures.update({cn: 1 for cn in m}) + want = sorted(list(want_captures.keys())) + avail_by_name = {a[0]:a for a in avail} + self.need_subscriptions = [avail_by_name[n] for n in want] + sys.stdout.write("Subscribing to:\n %s\n" + % ('\n '.join(want).strip(),)) # Query response handlers def send_subscribe(self, msg_id, method, params, cb=None, async_cb=None): if cb is None: @@ -142,30 +203,12 @@ class DataLogger: result = msg["result"] self.next_index_time = result["eventtime"] + INDEX_UPDATE_TIME self.db["status"] = status = result["status"] - # Subscribe to trapq and stepper queue updates - motion_report = status.get("motion_report", {}) - for trapq in motion_report.get("trapq", []): - self.send_subscribe("trapq:" + trapq, "motion_report/dump_trapq", - {"name": trapq}) - for stepper in motion_report.get("steppers", []): - self.send_subscribe("stepq:" + stepper, - "motion_report/dump_stepper", {"name": stepper}) - # Subscribe to additional sensor data - stypes = ["adxl345", "lis2dw", "mpu9250", "angle"] - stypes = {st:st for st in stypes} - stypes['probe_eddy_current'] = 'ldc1612' - config = status["configfile"]["settings"] - for cfgname in config.keys(): - for capprefix, st in sorted(stypes.items()): - if cfgname == capprefix or cfgname.startswith(capprefix + " "): - aname = cfgname.split()[-1] - lname = "%s:%s" % (st, aname) - qcmd = "%s/dump_%s" % (st, st) - self.send_subscribe(lname, qcmd, {"sensor": aname}) - if cfgname.startswith("tmc"): - driver = ' '.join(cfgname.split()[1:]) - self.send_subscribe("stallguard:" + driver, - "tmc/stallguard_dump", {"name": driver}) + # Determine available subscriptions + self.build_subscriptions(status) + # Subscribe + for capture_name, request, params in self.need_subscriptions: + self.send_subscribe(capture_name, request, params) + sys.stdout.write("Starting capture...\n") def handle_dump(self, msg, raw_msg): msg_id = msg["id"] if "result" not in msg: @@ -197,12 +240,23 @@ def nice(): def main(): usage = "%prog [options] " opts = optparse.OptionParser(usage) + opts.add_option("-s", "--subscribe", dest="subscribe", default="", + help="comma separated list of subscription patterns") + opts.add_option("--no-default", action="store_true", + help="disable default subscriptions") options, args = opts.parse_args() if len(args) != 2: opts.error("Incorrect number of arguments") + # Determine subscriptions + want_subs = ['trapq:*', 'stepq:*'] + if options.no_default: + want_subs = [] + want_subs.extend([s.strip() for s in options.subscribe.split(',')]) + + # Connect and start capture nice() - dl = DataLogger(args[0], args[1]) + dl = DataLogger(args[0], args[1], want_subs) dl.run() if __name__ == '__main__': From 3aa56f3bd038c2f51f5cc81157039fd23838fd64 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 23 Feb 2026 15:24:16 -0500 Subject: [PATCH 405/411] motan: Rename "adxl345" dataset to "accelerometer" Make it possible to use other types of accelerometers in the motan_graph.py tool. Signed-off-by: Kevin O'Connor --- scripts/motan/data_logger.py | 6 +++--- scripts/motan/readlog.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/motan/data_logger.py b/scripts/motan/data_logger.py index 844b1f7c0..ff1f17e42 100755 --- a/scripts/motan/data_logger.py +++ b/scripts/motan/data_logger.py @@ -12,9 +12,9 @@ ClientInfo = {'program': 'motan_data_logger', 'version': 'v0.1'} # Available subscriptions when a given config type is found ConfigSubscriptions = [ # (cfgtype, capture_name, api_request, request_params) - ('adxl345', '{ct}:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), - ('lis2dw', '{ct}:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), - ('mpu9250', '{ct}:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), + ('adxl345', 'accelerometer:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), + ('lis2dw', 'accelerometer:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), + ('mpu9250', 'accelerometer:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), ('angle', '{ct}:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), ('probe_eddy_current', 'ldc1612:{csn}', 'ldc1612/dump_ldc1612', {'sensor': '{csn}'}), diff --git a/scripts/motan/readlog.py b/scripts/motan/readlog.py index d5ae0fe66..11b1a7e0d 100644 --- a/scripts/motan/readlog.py +++ b/scripts/motan/readlog.py @@ -393,25 +393,26 @@ class HandleStepPhase: LogHandlers["step_phase"] = HandleStepPhase # Extract accelerometer data -class HandleADXL345: +class HandleAccelerometer: SubscriptionIdParts = 2 ParametersMin = ParametersMax = 2 DataSets = [ - ('adxl345(,)', 'Accelerometer for given axis (x, y, or z)'), + ('accelerometer(,)', + 'Accelerometer for given axis (x, y, or z)'), ] def __init__(self, lmanager, name, name_parts): self.name = name - self.adxl_name = name_parts[1] + self.accel_name = name_parts[1] self.jdispatch = lmanager.get_jdispatch() self.next_accel_time = self.last_accel_time = 0. self.next_accel = self.last_accel = (0., 0., 0.) self.cur_data = [] self.data_pos = 0 if name_parts[2] not in 'xyz': - raise error("Unknown adxl345 data selection '%s'" % (name,)) + raise error("Unknown accelerometer data selection '%s'" % (name,)) self.axis = 'xyz'.index(name_parts[2]) def get_label(self): - label = '%s %s acceleration' % (self.adxl_name, 'xyz'[self.axis]) + label = '%s %s acceleration' % (self.accel_name, 'xyz'[self.axis]) return {'label': label, 'units': 'Acceleration\n(mm/s^2)'} def pull_data(self, req_time): axis = self.axis @@ -434,7 +435,8 @@ class HandleADXL345: self.next_accel_time, x, y, z = self.cur_data[self.data_pos] self.next_accel = (x, y, z) self.data_pos += 1 -LogHandlers["adxl345"] = HandleADXL345 +LogHandlers["accelerometer"] = HandleAccelerometer +LogHandlers["adxl345"] = HandleAccelerometer # XXX - old capture name # Extract positions from magnetic angle sensor class HandleAngle: From 54c7b65d5d5685c4f6e9649a3d3be9356a6125dc Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 23 Feb 2026 15:26:33 -0500 Subject: [PATCH 406/411] motan: Support capturing bmi160 and icm20948 accelerometer data Signed-off-by: Kevin O'Connor --- scripts/motan/data_logger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/motan/data_logger.py b/scripts/motan/data_logger.py index ff1f17e42..051634312 100755 --- a/scripts/motan/data_logger.py +++ b/scripts/motan/data_logger.py @@ -15,6 +15,8 @@ ConfigSubscriptions = [ ('adxl345', 'accelerometer:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), ('lis2dw', 'accelerometer:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), ('mpu9250', 'accelerometer:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), + ('bmi160', 'accelerometer:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), + ('icm20948', 'accelerometer:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), ('angle', '{ct}:{csn}', '{ct}/dump_{ct}', {'sensor': '{csn}'}), ('probe_eddy_current', 'ldc1612:{csn}', 'ldc1612/dump_ldc1612', {'sensor': '{csn}'}), From 016f6e62b812ab0adda3d9cdc9e737ef117ba07f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 22 Feb 2026 12:12:55 -0500 Subject: [PATCH 407/411] probe_eddy_current: Stop measurements on an error in EddyScanningProbe Register an event handler to make sure measurements are stopped if a gcode error occurs while using EddyScanningProbe. EddyTap and EddyDescend use ProbeSessionHelper which already provides this handling, but EddyScanningProbe doesn't use that wrapper so it must be implemented manually. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 9863b7d58..447252011 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -609,6 +609,11 @@ class EddyScanningProbe: self._sample_time_delay = 0.050 self._sample_time = 0. self._is_rapid = False + self._printer.register_event_handler("gcode:command_error", + self._handle_command_error) + def _handle_command_error(self): + if self._gather is not None: + self.end_probe_session() def _lookup_toolhead_pos(self, pos_time): toolhead = self._printer.lookup_object('toolhead') kin = toolhead.get_kinematics() From 7e0dcc3c49d88130b84e8e6c27715f51d7d2cac6 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 23 Feb 2026 20:42:34 -0500 Subject: [PATCH 408/411] probe_eddy_current: Improve gap detection in EddyTap Verify no gaps at start and end of requested capture range. Raise an error if a gap is detected. Raise an error if not enough data collected during "tap". Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 47 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 447252011..6b7635de1 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -514,7 +514,25 @@ class EddyTap: raw_threshold = convert_frequency(self._tap_threshold) self._trigger_analog.set_trigger('diff_peak_gt', raw_threshold) # Measurement analysis to determine "tap" position - def central_diff(self, times, values): + def _validate_samples_time(self, measures, start_time, end_time): + cmderr = self._printer.command_error + if end_time - start_time < 0.100: + raise cmderr("Tap detected too close to start of move") + timestamps = [m[0] for m in measures] + if len(timestamps) < 2: + raise cmderr("Unable to obtain probe_eddy_current sensor readings") + ts = [start_time] + timestamps + [end_time] + tdiffs = [ts[i] - ts[i-1] for i in range(1, len(ts))] + tmax = max(tdiffs) + tmin = min(tdiffs[1:-1]) + cycle_time = 1.0 / self._sensor_helper.get_samples_per_second() + if tmax > cycle_time * 1.25: + raise cmderr("Eddy: Gaps in the data: %.3f > %.3f" + % (tmax, cycle_time * 1.25)) + if tmin < cycle_time * 0.75: + raise cmderr("Eddy: CLKIN frequency too low: %.3f < %.3f" + % (tmin, cycle_time * 0.75)) + def _central_diff(self, times, values): velocity = [0.0] * len(values) for i in range(1, len(values) - 1): delta_v = (values[i+1] - values[i-1]) @@ -523,38 +541,19 @@ class EddyTap: velocity[0] = (values[1] - values[0]) / (times[1] - times[0]) velocity[-1] = (values[-1] - values[-2]) / (times[-1] - times[-2]) return velocity - def validate_samples_time(self, timestamps): - sps = self._sensor_helper.get_samples_per_second() - cycle_time = 1.0 / sps - SYNC_SLACK = 0.001 - for i in range(1, len(timestamps)): - tdiff = timestamps[i] - timestamps[i-1] - if cycle_time + SYNC_SLACK < tdiff: - logging.error("Eddy: Gaps in the data: %.3f < %.3f" % ( - (cycle_time + SYNC_SLACK, tdiff) - )) - break - if cycle_time - SYNC_SLACK > tdiff: - logging.error( - "Eddy: CLKIN frequency too low: %.3f > %.3f" % ( - (cycle_time - SYNC_SLACK, tdiff) - )) - break def _pull_tap_time(self, measures): tap_time = [] tap_value = [] for time, freq, z in measures: tap_time.append(time) tap_value.append(freq) - # If samples have gaps this will not produce adequate data - self.validate_samples_time(tap_time) # Do the same filtering as on the MCU but without induced lag main_design = self._filter_design.get_main_filter() try: fvals = main_design.filtfilt(tap_value) except ValueError as e: raise self._printer.command_error(str(e)) - velocity = self.central_diff(tap_time, fvals) + velocity = self._central_diff(tap_time, fvals) peak_velocity = max(velocity) i = velocity.index(peak_velocity) return tap_time[i] @@ -565,7 +564,8 @@ class EddyTap: s.get_past_mcu_position(pos_time)) for s in kin.get_steppers()} return kin.calc_position(kin_spos) - def _analyze_tap(self, measures): + def _analyze_tap(self, measures, start_time, end_time): + self._validate_samples_time(measures, start_time, end_time) pos_time = self._pull_tap_time(measures) trig_pos = self._lookup_toolhead_pos(pos_time) return manual_probe.ProbeResult(trig_pos[0], trig_pos[1], trig_pos[2], @@ -591,7 +591,8 @@ class EddyTap: # Filter short move start_time = move_start_time end_time = trigger_time - self._gather.add_probe_request(self._analyze_tap, start_time, end_time) + self._gather.add_probe_request(self._analyze_tap, start_time, end_time, + start_time, end_time) def pull_probed_results(self): return self._gather.pull_probed() def end_probe_session(self): From 391834ba12858bfa614f75c993f33cb4a51a2906 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 27 Feb 2026 19:24:29 -0500 Subject: [PATCH 409/411] docs: Rework how METHOD= parameter works in G-Codes.md Describe the METHOD= parameter in the probe_eddy_current chapter. Use similar layout for both the load_cell_probe and probe_eddy_current sections. Avoid duplicating the METHOD= parmeter for other commands. Signed-off-by: Kevin O'Connor --- docs/G-Codes.md | 78 +++++++++++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/docs/G-Codes.md b/docs/G-Codes.md index a4dd89fe4..f399138d1 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -930,10 +930,27 @@ is calibrated a force in grams is also reported. ### [load_cell_probe] -The following commands are enabled if a +The commands below are enabled if a [load_cell config section](Config_Reference.md#load_cell_probe) has been enabled. +In addition, commands that perform probes, such as [`PROBE`](#probe), +[`PROBE_ACCURACY`](#probe_accuracy), +[`BED_MESH_CALIBRATE`](#bed_mesh_calibrate) etc. will accept +additional parameters if a `[load_cell_probe]` is defined. The +parameters override the corresponding settings from the +[`[load_cell_probe]`](./Config_Reference.md#load_cell_probe) +configuration: +- `FORCE_SAFETY_LIMIT=` +- `TRIGGER_FORCE=` +- `DRIFT_FILTER_CUTOFF_FREQUENCY=` +- `DRIFT_FILTER_DELAY=<1|2>` +- `BUZZ_FILTER_CUTOFF_FREQUENCY=` +- `BUZZ_FILTER_DELAY=<1|2>` +- `NOTCH_FILTER_FREQUENCIES=` +- `NOTCH_FILTER_QUALITY=` +- `TARE_TIME=` + ### LOAD_CELL_TEST_TAP `LOAD_CELL_TEST_TAP [TAPS=] [TIMEOUT=]`: Run a testing routine that reports taps on the load cell. The toolhead will not move but the load cell @@ -944,23 +961,6 @@ QUERY_ENDSTOPS and QUERY_PROBE for load cell probes. - `TIMEOOUT`: the time, in seconds, that the tool waits for each tab before aborting. -### Load Cell Command Extensions -Commands that perform probes, such as [`PROBE`](#probe), -[`PROBE_ACCURACY`](#probe_accuracy), -[`BED_MESH_CALIBRATE`](#bed_mesh_calibrate) etc. will accept additional -parameters if a `[load_cell_probe]` is defined. The parameters override the -corresponding settings from the -[`[load_cell_probe]`](./Config_Reference.md#load_cell_probe) configuration: -- `FORCE_SAFETY_LIMIT=` -- `TRIGGER_FORCE=` -- `DRIFT_FILTER_CUTOFF_FREQUENCY=` -- `DRIFT_FILTER_DELAY=<1|2>` -- `BUZZ_FILTER_CUTOFF_FREQUENCY=` -- `BUZZ_FILTER_DELAY=<1|2>` -- `NOTCH_FILTER_FREQUENCIES=` -- `NOTCH_FILTER_QUALITY=` -- `TARE_TIME=` - ### [manual_probe] The manual_probe module is automatically loaded. @@ -1183,25 +1183,23 @@ The following commands are available when a see the [probe calibrate guide](Probe_Calibrate.md)). #### PROBE -`PROBE [METHOD=] [PROBE_SPEED=] [LIFT_SPEED=] -[SAMPLES=] [SAMPLE_RETRACT_DIST=] [SAMPLES_TOLERANCE=] +`PROBE [PROBE_SPEED=] [LIFT_SPEED=] [SAMPLES=] +[SAMPLE_RETRACT_DIST=] [SAMPLES_TOLERANCE=] [SAMPLES_TOLERANCE_RETRIES=] [SAMPLES_RESULT=median|average]`: Move the nozzle downwards until the probe triggers. If any of the optional parameters are provided they override their equivalent setting in the [probe config section](Config_Reference.md#probe). -The optional parameter `METHOD` is probe-specific. #### QUERY_PROBE `QUERY_PROBE`: Report the current status of the probe ("triggered" or "open"). #### PROBE_ACCURACY -`PROBE_ACCURACY [METHOD=] [PROBE_SPEED=] [SAMPLES=] +`PROBE_ACCURACY [PROBE_SPEED=] [SAMPLES=] [SAMPLE_RETRACT_DIST=]`: Calculate the maximum, minimum, average, median, and standard deviation of multiple probe samples. By default, 10 SAMPLES are taken. Otherwise the optional parameters default to their equivalent setting in the probe config section. -The optional parameter `METHOD` is probe-specific. #### PROBE_CALIBRATE `PROBE_CALIBRATE [SPEED=] [=]`: Run a @@ -1220,10 +1218,36 @@ Requires a `SAVE_CONFIG` to take effect. ### [probe_eddy_current] -The following commands are available when a +The commands below are available when a [probe_eddy_current config section](Config_Reference.md#probe_eddy_current) is enabled. +In addition, commands that perform probes, such as [`PROBE`](#probe), +[`PROBE_ACCURACY`](#probe_accuracy), +[`BED_MESH_CALIBRATE`](#bed_mesh_calibrate) etc. will accept +additional parameters if a `[probe_eddy_current]` section is defined: +- `METHOD=`: This alters the probing mechanism: + - `METHOD=scan`: The toolhead does not descend. Instead the toolhead + will pause briefly above each target location and return the + measured height at that position. + - `METHOD=rapid_scan`: The toolhead does not descend and does not + pause at each target location. The value returned is the measured + height around the time that the toolhead was near each target + position. + - `METHOD=tap`: The toolhead will descend until the nozzle makes + contact with the bed. This method is only available if + `tap_threshold` is specified in the `[probe_eddy_current]` config + section. + - default: If no `METHOD` parameter is specified then the default + behavior is for the toolhead to descend until the sensor detects + that the distance to the bed is at or below the `z_offset` + parameter specified in the `[probe_eddy_current]` config section. +- `SAMPLE_TIME=