From 92813090831ea6f61f1958e077961f068530abfd Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 25 Feb 2026 19:58:57 -0500 Subject: [PATCH 1/6] ads1220: Remove dead self.bulk_queue code Signed-off-by: Kevin O'Connor --- klippy/extras/ads1220.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index 891783922..f4ddaf9f8 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -85,8 +85,6 @@ class ADS1220: if drdy_pin_mcu != self.mcu: raise config.error("ADS1220 config error: SPI communication and" " data_ready_pin must be on the same MCU") - # Bulk Sensor Setup - self.bulk_queue = bulk_sensor.BulkDataQueue(self.mcu, oid=self.oid) # Clock tracking chip_smooth = self.sps * UPDATE_INTERVAL * 2 # Measurement conversion From d819a899114c6575bb62614b710c4ee016d52e10 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 25 Feb 2026 19:59:35 -0500 Subject: [PATCH 2/6] hx71x: Remove dead self.bulk_queue code Signed-off-by: Kevin O'Connor --- klippy/extras/hx71x.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index a1bc20529..76563d484 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -42,8 +42,6 @@ class HX71xBase: # gain/channel choices self.gain_channel = int(config.getchoice('gain', gain_options, default=default_gain)) - ## Bulk Sensor Setup - self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, oid=self.oid) # Clock tracking chip_smooth = self.sps * UPDATE_INTERVAL * 2 self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, " Date: Fri, 27 Feb 2026 13:20:54 -0500 Subject: [PATCH 3/6] mcu: Minor code reorg in MCUConfigHelper Split _send_config() into _finalize_config() and _send_cfg_init_commands(). Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 75d608f82..05aa3f574 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -966,6 +966,7 @@ class MCUConfigHelper: self._config_cmds = [] self._restart_cmds = [] self._init_cmds = [] + self._config_crc = 0 self._mcu_freq = 0. self._reserved_move_slots = 0 # Register handlers @@ -973,7 +974,7 @@ class MCUConfigHelper: printer.register_event_handler("klippy:mcu_identify", self._mcu_identify) printer.register_event_handler("klippy:connect", self._connect) - def _send_config(self, prev_crc): + def _finalize_config(self): # Build config commands for cb in self._config_callbacks: cb() @@ -987,24 +988,11 @@ class MCUConfigHelper: cmdlist[i] = pin_resolver.update_command(cmd) # Calculate config CRC encoded_config = '\n'.join(self._config_cmds).encode() - 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: - 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) + self._config_crc = zlib.crc32(encoded_config) & 0xffffffff + self._config_cmds.append("finalize_config crc=%d" % (self._config_crc,)) + def _send_cfg_init_commands(self, cmds): try: - if prev_crc is None: - logging.info("Sending MCU '%s' printer configuration...", - self._name) - for c in self._config_cmds: - self._serial.send(c) - else: - for c in self._restart_cmds: - self._serial.send(c) - # Transmit init messages - for c in self._init_cmds: + for c in cmds: self._serial.send(c) except msgproto.enumeration_error as e: enum_name, enum_value = e.get_enum_params() @@ -1029,12 +1017,17 @@ class MCUConfigHelper: self._name,)) return config_params def _connect(self): + # Finalize the config and check if a restart is needed + restart_helper = self._conn_helper.get_restart_helper() config_params = self._send_get_config() if not config_params['is_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) + self._finalize_config() + cfg_init_cmds = self._config_cmds + self._init_cmds + logging.info("Sending MCU '%s' printer configuration...", + self._name) + self._send_cfg_init_commands(cfg_init_cmds) config_params = self._send_get_config() if not config_params['is_config'] and not self._mcu.is_fileoutput(): raise error("Unable to configure MCU '%s'" % (self._name,)) @@ -1044,7 +1037,13 @@ class MCUConfigHelper: raise error("Failed automated reset of MCU '%s'" % (self._name,)) # Already configured - send init commands - self._send_config(config_params['crc']) + self._finalize_config() + if self._config_crc != config_params['crc']: + restart_helper.check_restart_on_crc_mismatch() + raise error("MCU '%s' CRC does not match config" + % (self._name,)) + cfg_init_cmds = self._restart_cmds + self._init_cmds + self._send_cfg_init_commands(cfg_init_cmds) # Setup steppersync with the move_count returned by get_config move_count = config_params['move_count'] if move_count < self._reserved_move_slots: From 82e322210e7e41cd4d5ec0cd46847b2cd2c57faa Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 26 Feb 2026 14:45:52 -0500 Subject: [PATCH 4/6] mcu: Track if the mcu config phase has completed Raise an error if mcu configuration commands are issued after the config has been completed. Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/klippy/mcu.py b/klippy/mcu.py index 05aa3f574..1131a7c58 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -961,6 +961,7 @@ class MCUConfigHelper: self._reactor = printer.get_reactor() self._name = mcu.get_name() # Configuration tracking + self._config_finalized = False self._oid_count = 0 self._config_callbacks = [] self._config_cmds = [] @@ -978,6 +979,7 @@ class MCUConfigHelper: # Build config commands for cb in self._config_callbacks: cb() + self._config_finalized = True self._config_cmds.insert(0, "allocate_oids count=%d" % (self._oid_count,)) # Resolve pin names @@ -1071,19 +1073,28 @@ class MCUConfigHelper: " to be able to resolve a maximum nominal duration" " of %ds. Max possible duration: %ds" % (self._name, MAX_NOMINAL_DURATION, max_possible)) + def _verify_not_finalized(self): + if self._config_finalized: + raise error("Internal error! MCU already configured") # Config creation helpers + def is_config_finalized(self): + return self._config_finalized def setup_pin(self, pin_type, pin_params): + self._verify_not_finalized() 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._mcu, pin_params) def create_oid(self): + self._verify_not_finalized() self._oid_count += 1 return self._oid_count - 1 def register_config_callback(self, cb): + self._verify_not_finalized() self._config_callbacks.append(cb) def add_config_cmd(self, cmd, is_init=False, on_restart=False): + self._verify_not_finalized() if is_init: self._init_cmds.append(cmd) elif on_restart: From 9f8000b7075e339cb081fa2faea466963225b1de Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 26 Feb 2026 14:36:36 -0500 Subject: [PATCH 5/6] mcu: Rework mcu.register_response() to mcu.register_serial_response() Rename mcu.register_response() to mcu.register_serial_response and validate the message response format. Only register the callbacks after the mcu has been initialized. This avoids invoking callbacks with data from a previous sessions, which could lead to internal errors. Signed-off-by: Kevin O'Connor --- klippy/extras/bulk_sensor.py | 6 ++- klippy/extras/bus.py | 6 ++- klippy/extras/buttons.py | 5 +- klippy/extras/canbus_stats.py | 5 +- klippy/extras/ds18b20.py | 5 +- klippy/extras/pulse_counter.py | 6 ++- klippy/extras/spi_temperature.py | 5 +- klippy/mcu.py | 79 +++++++++++++++++++++++--------- 8 files changed, 82 insertions(+), 35 deletions(-) diff --git a/klippy/extras/bulk_sensor.py b/klippy/extras/bulk_sensor.py index b0aa320d0..651e14aa7 100644 --- a/klippy/extras/bulk_sensor.py +++ b/klippy/extras/bulk_sensor.py @@ -112,14 +112,16 @@ class BatchWebhooksClient: self.cconn.send(tmp) return True +SENSOR_BULK_FMT = "sensor_bulk_data oid=%c sequence=%hu data=%*s" + # Helper class to store incoming messages in a queue class BulkDataQueue: - def __init__(self, mcu, msg_name="sensor_bulk_data", oid=None): + def __init__(self, mcu, msg_fmt=SENSOR_BULK_FMT, oid=None): # Measurement storage (accessed from background thread) self.lock = threading.Lock() self.raw_samples = [] # Register callback with mcu - mcu.register_response(self._handle_data, msg_name, oid) + mcu.register_serial_response(self._handle_data, msg_fmt, oid) def _handle_data(self, params): with self.lock: self.raw_samples.append(params) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 3a6ef5842..6bd9bc750 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -248,8 +248,10 @@ class MCU_I2C: "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.mcu.register_serial_response( + self._async_write_status, + "i2c_response oid=%c i2c_bus_status=%c response=%*s", + self.oid) self._configured = True def i2c_write_noack(self, data, minclock=0, reqclock=0): if self.async_write_only: diff --git a/klippy/extras/buttons.py b/klippy/extras/buttons.py index 4ffb3c38b..50f2a5a9b 100644 --- a/klippy/extras/buttons.py +++ b/klippy/extras/buttons.py @@ -52,8 +52,9 @@ class MCU_buttons: " rest_ticks=%d retransmit_count=%d invert=%d" % ( self.oid, clock, rest_ticks, RETRANSMIT_COUNT, self.invert), is_init=True) - self.mcu.register_response(self.handle_buttons_state, - "buttons_state", self.oid) + self.mcu.register_serial_response( + self.handle_buttons_state, + "buttons_state oid=%c ack_count=%c state=%*s", self.oid) def handle_buttons_state(self, params): # Expand the message ack_count from 8-bit ack_count = self.ack_count diff --git a/klippy/extras/canbus_stats.py b/klippy/extras/canbus_stats.py index fb8c88c91..5bc609d06 100644 --- a/klippy/extras/canbus_stats.py +++ b/klippy/extras/canbus_stats.py @@ -40,8 +40,9 @@ class PrinterCANBusStats: "canbus_status rx_error=%u tx_error=%u tx_retries=%u" " canbus_bus_state=%u") # Register usb_canbus_state message handling (for usb to canbus bridge) - self.mcu.register_response(self.handle_usb_canbus_state, - "usb_canbus_state") + if self.mcu.check_valid_response("usb_canbus_state discard=%u"): + self.mcu.register_serial_response(self.handle_usb_canbus_state, + "usb_canbus_state discard=%u") # Register periodic query timer self.reactor.register_timer(self.query_event, self.reactor.NOW) def handle_usb_canbus_state(self, params): diff --git a/klippy/extras/ds18b20.py b/klippy/extras/ds18b20.py index 37b301046..8ad60ba25 100644 --- a/klippy/extras/ds18b20.py +++ b/klippy/extras/ds18b20.py @@ -26,8 +26,9 @@ class DS18B20: ) self._mcu = mcu.get_printer_mcu(self.printer, config.get('sensor_mcu')) self.oid = self._mcu.create_oid() - self._mcu.register_response(self._handle_ds18b20_response, - "ds18b20_result", self.oid) + self._mcu.register_serial_response( + self._handle_ds18b20_response, + "ds18b20_result oid=%c next_clock=%u value=%i fault=%u", self.oid) self._mcu.register_config_callback(self._build_config) def _build_config(self): diff --git a/klippy/extras/pulse_counter.py b/klippy/extras/pulse_counter.py index a75871873..2f771fe06 100644 --- a/klippy/extras/pulse_counter.py +++ b/klippy/extras/pulse_counter.py @@ -28,8 +28,10 @@ class MCU_counter: self._mcu.add_config_cmd( "query_counter oid=%d clock=%d poll_ticks=%d sample_ticks=%d" % (self._oid, clock, self._poll_ticks, sample_ticks), is_init=True) - self._mcu.register_response(self._handle_counter_state, - "counter_state", self._oid) + self._mcu.register_serial_response( + self._handle_counter_state, + "counter_state oid=%c next_clock=%u count=%u count_clock=%u", + self._oid) # Callback is called periodically every sample_time def setup_callback(self, cb): diff --git a/klippy/extras/spi_temperature.py b/klippy/extras/spi_temperature.py index 92d1b385f..908bb4773 100644 --- a/klippy/extras/spi_temperature.py +++ b/klippy/extras/spi_temperature.py @@ -29,8 +29,9 @@ class SensorBase: self.mcu = mcu = self.spi.get_mcu() # Reader chip configuration self.oid = oid = mcu.create_oid() - mcu.register_response(self._handle_spi_response, - "thermocouple_result", oid) + mcu.register_serial_response( + self._handle_spi_response, + "thermocouple_result oid=%c next_clock=%u value=%u fault=%c", oid) mcu.register_config_callback(self._build_config) def setup_minmax(self, min_temp, max_temp): adc_range = [self.calc_adc(min_temp), self.calc_adc(max_temp)] diff --git a/klippy/mcu.py b/klippy/mcu.py index 1131a7c58..67025b218 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -128,6 +128,24 @@ class CommandWrapper: def get_command_tag(self): return self._msgtag +# Wrapper for long-lived serial subscriptions (callbacks via background thread) +class AsyncResponseWrapper: + def __init__(self, conn_helper, cfg_helper, callback, msgformat, oid=None): + self._serial = conn_helper.get_serial() + self._callback = callback + self._msgformat = msgformat + self._name = msgformat.split()[0] + self._oid = oid + if cfg_helper.is_config_finalized(): + self._register() + else: + cfg_helper.register_post_init_callback(self._register) + def _register(self): + self._serial.get_msgparser().lookup_command(self._msgformat) + self._serial.register_response(self._callback, self._name, self._oid) + def unregister(self): + self._serial.register_response(None, self._name, self._oid) + ###################################################################### # Wrapper classes for MCU pins @@ -146,6 +164,7 @@ class MCU_trsync: self._trdispatch_mcu = None self._oid = mcu.create_oid() self._cmd_queue = mcu.alloc_command_queue() + self._response_trsync = None self._trsync_start_cmd = self._trsync_set_timeout_cmd = None self._trsync_trigger_cmd = self._trsync_query_cmd = None self._stepper_stop_cmd = None @@ -232,8 +251,10 @@ class MCU_trsync: ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.trdispatch_mcu_setup(self._trdispatch_mcu, clock, expire_clock, expire_ticks, min_extend_ticks) - self._mcu.register_response(self._handle_trsync_state, - "trsync_state", self._oid) + self._response_trsync = self._mcu.register_serial_response( + self._handle_trsync_state, + "trsync_state oid=%c can_trigger=%c trigger_reason=%c clock=%u", + self._oid) self._trsync_start_cmd.send([self._oid, report_clock, report_ticks, self.REASON_COMMS_TIMEOUT], reqclock=clock) for s in self._steppers: @@ -243,7 +264,8 @@ class MCU_trsync: 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): - self._mcu.register_response(None, "trsync_state", self._oid) + self._response_trsync.unregister() + self._response_trsync = None self._trigger_completion = None if self._mcu.is_fileoutput(): return self.REASON_ENDSTOP_HIT @@ -589,8 +611,9 @@ class MCU_adc: self._oid, clock, sample_ticks, self._sample_count, self._report_clock, min_sample, max_sample, self._range_check_count), is_init=True) - self._mcu.register_response(self._old_handle_analog_in_state, - "analog_in_state", self._oid) + self._mcu.register_serial_response( + self._old_handle_analog_in_state, + "analog_in_state oid=%c next_clock=%u value=%hu", self._oid) return BYTES_PER_SAMPLE = 2 bytes_per_report = self._batch_num * BYTES_PER_SAMPLE @@ -601,8 +624,9 @@ class MCU_adc: self._oid, clock, sample_ticks, self._sample_count, 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) + self._mcu.register_serial_response( + self._handle_analog_in_state, + "analog_in_state oid=%c next_clock=%u values=%*s", self._oid) def _old_handle_analog_in_state(self, params): last_value = params['value'] * self._inv_max_adc next_clock = self._mcu.clock32_to_clock64(params['next_clock']) @@ -852,9 +876,9 @@ class MCUConnectHelper: logging.info(self.log_info()) # 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') + self._serial.register_response(self._handle_shutdown, 'shutdown') + self._serial.register_response(self._handle_shutdown, 'is_shutdown') + self._serial.register_response(self._handle_starting, 'starting') def _analyze_shutdown(self, msg, details): if self._mcu.is_fileoutput(): return @@ -921,7 +945,7 @@ class MCUStatsHelper: 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') + self._serial.register_response(self._handle_mcu_stats, 'stats') def _ready(self): if self._mcu.is_fileoutput(): return @@ -964,6 +988,7 @@ class MCUConfigHelper: self._config_finalized = False self._oid_count = 0 self._config_callbacks = [] + self._post_init_callbacks = [] self._config_cmds = [] self._restart_cmds = [] self._init_cmds = [] @@ -1023,29 +1048,32 @@ class MCUConfigHelper: restart_helper = self._conn_helper.get_restart_helper() config_params = self._send_get_config() if not config_params['is_config']: + # Not configured - sending full config will be required restart_helper.check_restart_on_send_config() - # Not configured - send config and issue get_config again self._finalize_config() cfg_init_cmds = self._config_cmds + self._init_cmds logging.info("Sending MCU '%s' printer configuration...", self._name) - self._send_cfg_init_commands(cfg_init_cmds) - config_params = self._send_get_config() - if not config_params['is_config'] and not self._mcu.is_fileoutput(): - raise error("Unable to configure MCU '%s'" % (self._name,)) else: + # Already configured - may need to only send init commands start_reason = self._printer.get_start_args().get("start_reason") if start_reason == 'firmware_restart': raise error("Failed automated reset of MCU '%s'" % (self._name,)) - # Already configured - send init commands self._finalize_config() if self._config_crc != config_params['crc']: restart_helper.check_restart_on_crc_mismatch() raise error("MCU '%s' CRC does not match config" % (self._name,)) cfg_init_cmds = self._restart_cmds + self._init_cmds - self._send_cfg_init_commands(cfg_init_cmds) + # Send config and init messages + self._send_cfg_init_commands(cfg_init_cmds) + config_params = self._send_get_config() + if not config_params['is_config'] and not self._mcu.is_fileoutput(): + raise error("Unable to configure MCU '%s'" % (self._name,)) + # Run post_init callbacks + for cb in self._post_init_callbacks: + cb() # Setup steppersync with the move_count returned by get_config move_count = config_params['move_count'] if move_count < self._reserved_move_slots: @@ -1101,6 +1129,9 @@ class MCUConfigHelper: self._restart_cmds.append(cmd) else: self._config_cmds.append(cmd) + def register_post_init_callback(self, cb): + self._verify_not_finalized() + self._post_init_callbacks.append(cb) def get_query_slot(self, oid): slot = self.seconds_to_clock(oid * .01) t = int(self._mcu.estimated_print_time(self._reactor.monotonic()) + 1.5) @@ -1167,11 +1198,17 @@ 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() + def register_serial_response(self, cb, msg, oid=None): + return AsyncResponseWrapper(self._conn_helper, self._config_helper, + cb, msg, oid) + def check_valid_response(self, msgformat): + try: + self._serial.get_msgparser().lookup_command(msgformat) + except self._serial.get_msgparser().error as e: + return False + return True # MsgParser wrappers def get_enumerations(self): return self._serial.get_msgparser().get_enumerations() From 8e4e42af8f6c2f60f4ac16916a1452be2a432add Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 27 Feb 2026 14:05:07 -0500 Subject: [PATCH 6/6] mcu: Improve unknown message response warnings in AsyncResponseWrapper When delaying response registration, don't warn about any of these messages that arrive prior to the aactual response registration. Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/klippy/mcu.py b/klippy/mcu.py index 67025b218..5f980d5eb 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -139,6 +139,7 @@ class AsyncResponseWrapper: if cfg_helper.is_config_finalized(): self._register() else: + self._serial.register_response((lambda p: None), self._name, oid) cfg_helper.register_post_init_callback(self._register) def _register(self): self._serial.get_msgparser().lookup_command(self._msgformat)