From cdb0c0cb92de283d92a6b87d03e9bd2891e83ecc Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:13:22 +0100 Subject: [PATCH 01/13] Support for SHT4x Sensors --- klippy/extras/sht4x.py | 263 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 klippy/extras/sht4x.py diff --git a/klippy/extras/sht4x.py b/klippy/extras/sht4x.py new file mode 100644 index 000000000..9880a089f --- /dev/null +++ b/klippy/extras/sht4x.py @@ -0,0 +1,263 @@ +# Support for sht4x temperature sensors +# +# Copyright (C) 2025 Milzo +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import logging +from . import bus +import threading + +# SHT4x I2C Commands +DEFAULT_ADDR = 0x44 +RESET = 0x94 +REQUEST_CHIPID = 0x89 + +# Measurement commands +HIGH_PRECISION_MODE = 0xFD # ~8.3ms measurement time +MEDIUM_PRECISION_MODE = 0xF6 # ~4.5ms measurement time +LOW_PRECISION_MODE = 0xE0 # ~1.6ms measurement time + +# Timing constants from datasheet +MEASUREMENT_TIMES = { + HIGH_PRECISION_MODE: 0.0083, # 8.3ms + MEDIUM_PRECISION_MODE: 0.0045, # 4.5ms + LOW_PRECISION_MODE: 0.0016 # 1.6ms +} + +SOFT_RESET_TIME = 0.001 # 1ms max +REPORT_TIME = 0.8 # How often to report readings + + +class SHT4X: + def __init__(self, config): + self.printer = config.get_printer() + self.name = config.get_name().split()[-1] + self.reactor = self.printer.get_reactor() + + # I2C setup + valid_addresses = [0x44, 0x45, 0x46] + addr = config.getint('i2c_address', DEFAULT_ADDR) + if addr not in valid_addresses: + addr_list = ', '.join([f'{a:#x}' for a in valid_addresses]) + raise config.error(f"Invalid I2C address {addr:#x}. " + f"Valid addresses are: {addr_list}") + + speed = config.getint('i2c_speed', 100000) + self.i2c = bus.MCU_I2C_from_config( + config, default_addr=addr, default_speed=speed) + self.mcu = self.i2c.get_mcu() + + # Precision setup + precision = config.get('precision', 'high').lower() + precision_map = { + 'high': HIGH_PRECISION_MODE, + 'medium': MEDIUM_PRECISION_MODE, + 'low': LOW_PRECISION_MODE + } + if precision not in precision_map: + raise config.error(f"Invalid precision value '{precision}'. " + "Valid options are: high, medium, low") + self.precision_mode = precision_map[precision] + + # Core sensor state + self.temp = 0 + self.humidity = 0 + self._error_count = 0 + self.sensor_ready = False + self.measurement_lock = threading.Lock() + self._callback = None + self.serial_number = None + + # Register sensor + self.printer.add_object("sht4x " + self.name, self) + self.printer.register_event_handler("klippy:connect", + self.handle_connect) + + def handle_connect(self): + """Initialize sensor""" + try: + self.reset() + # Add a small delay after reset + self.reactor.pause(self.reactor.monotonic() + 0.01) # 10ms + + # Try to read serial number + self.serial_number = self.get_serial_number() + if self.serial_number: + logging.info("SHT4X: Sensor serial number: 0x%08X", + self.serial_number) + + # Test measurement + if self._test_measurement(): + self.sensor_ready = True + logging.info("SHT4X: Sensor ready") + self.sample_timer = self.reactor.register_timer( + self.sample_sensor) + self.reactor.update_timer(self.sample_timer, + self.reactor.NOW) + else: + logging.error("SHT4X: Sensor test failed") + except Exception as e: + logging.error("SHT4X: Init failed: %s" % str(e)) + + def _test_measurement(self): + """Quick measurement test""" + try: + recv = self.get_measurements() + if len(recv) != 6: + logging.error("SHT4X: Invalid data length: %d", len(recv)) + return False + if not self._validate_crc(recv): + logging.error("SHT4X: CRC validation failed") + return False + return True + except Exception as e: + logging.error("SHT4X: Test measurement failed: %s", str(e)) + return False + + def _validate_crc(self, data): + """Validate CRC for both temp and humidity""" + temp_crc = data[2] + humidity_crc = data[5] + return (temp_crc == self._crc8(data[0:2]) and + humidity_crc == self._crc8(data[3:5])) + + def setup_minmax(self, min_temp, max_temp): + pass + + def setup_callback(self, cb): + self._callback = cb + + def sample_sensor(self, eventtime): + """Main sampling loop""" + if not self.sensor_ready: + return eventtime + REPORT_TIME + + try: + with self.measurement_lock: + recv = self.get_measurements() + + if len(recv) != 6 or not self._validate_crc(recv): + raise Exception("Invalid data or CRC failed") + + # Convert temperature + raw_temp = (recv[0] << 8) | recv[1] + temp = -45.0 + 175.0 * raw_temp / 65535.0 + + # Check if temperature is in reasonable range + if -50 <= temp <= 150: + self.temp = temp + else: + raise ValueError(f"Temperature out of reasonable range: " + f"{temp}°C") + + # Convert humidity + raw_humidity = (recv[3] << 8) | recv[4] + humidity_percent = -6.0 + 125.0 * raw_humidity / 65535.0 + self.humidity = max(min(humidity_percent, 100.0), 0.0) + + self._error_count = 0 + + # Report to Klipper + if self._callback is not None: + measured_time = self.reactor.monotonic() + self._callback( + self.mcu.estimated_print_time(measured_time), + self.temp) + + except Exception as e: + self._error_count += 1 + logging.warning("SHT4X: Error %d: %s" % + (self._error_count, str(e))) + + # Add retry logic + if self._error_count <= 3: + # Retry immediately for first few errors + return eventtime + 0.5 # Short delay before retry + elif self._error_count <= 10: + return eventtime + 2.0 # Longer delay for persistent errors + else: + logging.error("SHT4X: Too many errors, sensor failed") + self.sensor_ready = False + + return eventtime + REPORT_TIME + + def get_measurements(self): + """Get sensor data with retry logic for NACK handling""" + data = [self.precision_mode] + try: + self.i2c.i2c_write(data) + + measurement_time = MEASUREMENT_TIMES[self.precision_mode] + self.reactor.pause(self.reactor.monotonic() + measurement_time) + + # Try up to 3 times to read the data + for retry in range(3): + try: + recv = self.i2c.i2c_read([], 6) + return bytearray(recv['response']) + except Exception as e: + if "NACK" in str(e) and retry < 2: + # Sensor might still be busy, wait a bit more + self.reactor.pause(self.reactor.monotonic() + + measurement_time) + else: + raise + except Exception as e: + raise Exception(f"Failed to get measurements: {e}") + + def get_serial_number(self): + """Read the sensor's unique serial number""" + try: + self.i2c.i2c_write([REQUEST_CHIPID]) + self.reactor.pause(self.reactor.monotonic() + 0.001) # Small delay + recv = self.i2c.i2c_read([], 6) + data = bytearray(recv['response']) + if len(data) != 6 or not self._validate_crc(data): + raise Exception("Invalid serial number data or CRC failed") + + # Extract the serial number + serial_msb = (data[0] << 8) | data[1] + serial_lsb = (data[3] << 8) | data[4] + return (serial_msb << 16) | serial_lsb + except Exception as e: + logging.warning("SHT4X: Failed to read serial number: %s", str(e)) + return None + + def reset(self): + """Reset sensor""" + with self.measurement_lock: + data = [RESET] + self.i2c.i2c_write(data) + self.reactor.pause(self.reactor.monotonic() + SOFT_RESET_TIME) + + def get_status(self, eventtime): + """Return sensor status for Mainsail""" + return { + 'temperature': round(self.temp, 2), + 'humidity': round(self.humidity, 1), + 'sensor_ready': self.sensor_ready, + 'serial_number': (f"0x{self.serial_number:08X}" + if self.serial_number else "unknown") + } + + def _crc8(self, buffer): + """CRC8 checksum implementation per datasheet""" + crc = 0xFF + for byte in buffer: + crc ^= byte + for _ in range(8): + if crc & 0x80: + crc = (crc << 1) ^ 0x31 + else: + crc = crc << 1 + return crc & 0xFF + + +def load_config(config): + pheaters = config.get_printer().load_object(config, "heaters") + pheaters.add_sensor_factory("SHT4X", SHT4X) + + +def load_config_prefix(config): + return SHT4X(config) \ No newline at end of file From e4e2569575cc7e4ebd4c89c2523b3c12af41d0ca Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:17:36 +0100 Subject: [PATCH 02/13] Update temperature_sensors.cfg added sht4x module --- klippy/extras/temperature_sensors.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klippy/extras/temperature_sensors.cfg b/klippy/extras/temperature_sensors.cfg index 4fbe5492c..74daf761d 100644 --- a/klippy/extras/temperature_sensors.cfg +++ b/klippy/extras/temperature_sensors.cfg @@ -20,6 +20,9 @@ [sht3x] +# Load "SHT4X" sensor +[sht4x] + # Load "AHT10" [aht10] From 395e9c078063133030a30dadc97a948f724b886b Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:26:52 +0100 Subject: [PATCH 03/13] Update Config_Reference.md added references relating to sht4x sensors --- docs/Config_Reference.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 83de96096..ac22f73ab 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3004,6 +3004,27 @@ sensor_type: SHT3X # See the "common I2C settings" section for a description of the # above parameters. ``` +### SHT4x Sensor + +SHT4X family two wire interface (I2C) environmental sensor. These sensors +have a range of -40~125°C, making them suitable for various temperature and +humidity monitoring applications including chambers. +They offer improved accuracy and faster response times compared to previous generations, +while maintaining compatibility for fan/heater control implementations. + +``` +#sensor_type: SHT4X +#i2c_mcu: +#i2c_bus: +#i2c_software_scl_pin: +#i2c_software_sda_pin: +#i2c_address: +#i2c_speed: + # Defaults to 400kHz on RPi, 100kHz elsewhere + # See the "common I2C settings" section for a description of the above parameters. +#precision: + # Default: "high" | Options: high, medium, low +``` ### LM75 temperature sensor From 2781c30d934b3ed1be5613dc2568c88443b0b32a Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:32:17 +0100 Subject: [PATCH 04/13] Update Status_Reference.md added linking to sht4x and fixed typo on sht31, should be sht3x --- docs/Status_Reference.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index 77313682f..d8988106c 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -497,7 +497,8 @@ The following information is available in [bme280 config_section_name](Config_Reference.md#bmp280bme280bme680-temperature-sensor), [htu21d config_section_name](Config_Reference.md#htu21d-sensor), -[sht3x config_section_name](Config_Reference.md#sht31-sensor), +[sht3x config_section_name](Config_Reference.md#sht3x-sensor), +[sht4x config_section_name](Config_Reference.md#sht4x-sensor), [lm75 config_section_name](Config_Reference.md#lm75-temperature-sensor), [temperature_host config_section_name](Config_Reference.md#host-temperature-sensor) and From dcceb1d7ab4fb41b4c4a9b30be858cac93bcbc4d Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:45:06 +0100 Subject: [PATCH 05/13] Update sht4x.py added whitespace at EOF --- klippy/extras/sht4x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/sht4x.py b/klippy/extras/sht4x.py index 9880a089f..f427a22bf 100644 --- a/klippy/extras/sht4x.py +++ b/klippy/extras/sht4x.py @@ -260,4 +260,4 @@ def load_config(config): def load_config_prefix(config): - return SHT4X(config) \ No newline at end of file + return SHT4X(config) From 467117ac6b80aca1b9b4a50253147f3734858151 Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:55:22 +0100 Subject: [PATCH 06/13] Update sht4x.py --- klippy/extras/sht4x.py | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/klippy/extras/sht4x.py b/klippy/extras/sht4x.py index f427a22bf..c0a057276 100644 --- a/klippy/extras/sht4x.py +++ b/klippy/extras/sht4x.py @@ -1,9 +1,3 @@ -# Support for sht4x temperature sensors -# -# Copyright (C) 2025 Milzo -# -# This file may be distributed under the terms of the GNU GPLv3 license. - import logging from . import bus import threading @@ -20,12 +14,12 @@ LOW_PRECISION_MODE = 0xE0 # ~1.6ms measurement time # Timing constants from datasheet MEASUREMENT_TIMES = { - HIGH_PRECISION_MODE: 0.0083, # 8.3ms - MEDIUM_PRECISION_MODE: 0.0045, # 4.5ms - LOW_PRECISION_MODE: 0.0016 # 1.6ms + HIGH_PRECISION_MODE: 0.0083, # 8.3ms max according to datasheet + MEDIUM_PRECISION_MODE: 0.0045, # 4.5ms max according to datasheet + LOW_PRECISION_MODE: 0.0016 # 1.6ms max according to datasheet } -SOFT_RESET_TIME = 0.001 # 1ms max +SOFT_RESET_TIME = 0.001 # 1ms max according to datasheet REPORT_TIME = 0.8 # How often to report readings @@ -36,12 +30,12 @@ class SHT4X: self.reactor = self.printer.get_reactor() # I2C setup - valid_addresses = [0x44, 0x45, 0x46] + valid_addresses = [0x44, 0x45, 0x46] # From datasheet addr = config.getint('i2c_address', DEFAULT_ADDR) if addr not in valid_addresses: - addr_list = ', '.join([f'{a:#x}' for a in valid_addresses]) - raise config.error(f"Invalid I2C address {addr:#x}. " - f"Valid addresses are: {addr_list}") + addr_list = ', '.join(['0x%x' % a for a in valid_addresses]) + raise config.error("Invalid I2C address 0x%x. " + "Valid addresses are: %s" % (addr, addr_list)) speed = config.getint('i2c_speed', 100000) self.i2c = bus.MCU_I2C_from_config( @@ -56,8 +50,8 @@ class SHT4X: 'low': LOW_PRECISION_MODE } if precision not in precision_map: - raise config.error(f"Invalid precision value '{precision}'. " - "Valid options are: high, medium, low") + raise config.error("Invalid precision value '%s'. " + "Valid options are: high, medium, low" % precision) self.precision_mode = precision_map[precision] # Core sensor state @@ -148,8 +142,8 @@ class SHT4X: if -50 <= temp <= 150: self.temp = temp else: - raise ValueError(f"Temperature out of reasonable range: " - f"{temp}°C") + raise ValueError("Temperature out of reasonable range: " + "%f°C" % temp) # Convert humidity raw_humidity = (recv[3] << 8) | recv[4] @@ -204,7 +198,7 @@ class SHT4X: else: raise except Exception as e: - raise Exception(f"Failed to get measurements: {e}") + raise Exception("Failed to get measurements: %s" % e) def get_serial_number(self): """Read the sensor's unique serial number""" @@ -233,12 +227,12 @@ class SHT4X: def get_status(self, eventtime): """Return sensor status for Mainsail""" + serial_str = "0x%08X" % self.serial_number if self.serial_number else "unknown" return { 'temperature': round(self.temp, 2), 'humidity': round(self.humidity, 1), 'sensor_ready': self.sensor_ready, - 'serial_number': (f"0x{self.serial_number:08X}" - if self.serial_number else "unknown") + 'serial_number': serial_str } def _crc8(self, buffer): @@ -258,6 +252,6 @@ def load_config(config): pheaters = config.get_printer().load_object(config, "heaters") pheaters.add_sensor_factory("SHT4X", SHT4X) - def load_config_prefix(config): return SHT4X(config) + From 0a8c16a367a29b432e0780c3b4a33881e60ae502 Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:01:48 +0100 Subject: [PATCH 07/13] Update sht4x.py --- klippy/extras/sht4x.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/klippy/extras/sht4x.py b/klippy/extras/sht4x.py index c0a057276..547d2e877 100644 --- a/klippy/extras/sht4x.py +++ b/klippy/extras/sht4x.py @@ -51,7 +51,8 @@ class SHT4X: } if precision not in precision_map: raise config.error("Invalid precision value '%s'. " - "Valid options are: high, medium, low" % precision) + "Valid options are: high, medium, low" + % precision) self.precision_mode = precision_map[precision] # Core sensor state @@ -204,7 +205,8 @@ class SHT4X: """Read the sensor's unique serial number""" try: self.i2c.i2c_write([REQUEST_CHIPID]) - self.reactor.pause(self.reactor.monotonic() + 0.001) # Small delay + # Small delay + self.reactor.pause(self.reactor.monotonic() + 0.001) recv = self.i2c.i2c_read([], 6) data = bytearray(recv['response']) if len(data) != 6 or not self._validate_crc(data): @@ -215,7 +217,8 @@ class SHT4X: serial_lsb = (data[3] << 8) | data[4] return (serial_msb << 16) | serial_lsb except Exception as e: - logging.warning("SHT4X: Failed to read serial number: %s", str(e)) + logging.warning("SHT4X: Failed to read serial number: %s", + str(e)) return None def reset(self): @@ -227,7 +230,10 @@ class SHT4X: def get_status(self, eventtime): """Return sensor status for Mainsail""" - serial_str = "0x%08X" % self.serial_number if self.serial_number else "unknown" + if self.serial_number: + serial_str = "0x%08X" % self.serial_number + else: + serial_str = "unknown" return { 'temperature': round(self.temp, 2), 'humidity': round(self.humidity, 1), @@ -251,7 +257,6 @@ class SHT4X: def load_config(config): pheaters = config.get_printer().load_object(config, "heaters") pheaters.add_sensor_factory("SHT4X", SHT4X) - + def load_config_prefix(config): return SHT4X(config) - From cad5ebdd6e5966f5d483a0d9a5158dcc9460c1cf Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:04:05 +0100 Subject: [PATCH 08/13] Update sht4x.py --- klippy/extras/sht4x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/sht4x.py b/klippy/extras/sht4x.py index 547d2e877..ba02542c4 100644 --- a/klippy/extras/sht4x.py +++ b/klippy/extras/sht4x.py @@ -257,6 +257,6 @@ class SHT4X: def load_config(config): pheaters = config.get_printer().load_object(config, "heaters") pheaters.add_sensor_factory("SHT4X", SHT4X) - + def load_config_prefix(config): return SHT4X(config) From 3ec3fc55f04756cbc50c56dd79091028749a5edb Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:14:24 +0100 Subject: [PATCH 09/13] Update sht4x.py fix for more build errors --- klippy/extras/sht4x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/sht4x.py b/klippy/extras/sht4x.py index ba02542c4..e0e818b76 100644 --- a/klippy/extras/sht4x.py +++ b/klippy/extras/sht4x.py @@ -144,7 +144,7 @@ class SHT4X: self.temp = temp else: raise ValueError("Temperature out of reasonable range: " - "%f°C" % temp) + "%fC" % temp) # Convert humidity raw_humidity = (recv[3] << 8) | recv[4] From ad170ae128c1ba6b560ed5cb0d91a321afc914c4 Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:38:22 +0100 Subject: [PATCH 10/13] Update sht4x.py --- klippy/extras/sht4x.py | 243 ++++++++++++----------------------------- 1 file changed, 68 insertions(+), 175 deletions(-) diff --git a/klippy/extras/sht4x.py b/klippy/extras/sht4x.py index e0e818b76..e250b09be 100644 --- a/klippy/extras/sht4x.py +++ b/klippy/extras/sht4x.py @@ -1,95 +1,52 @@ +# SHT4x i2c temperature sensors +# +# Copyright (C) 2025 Milzo +# +# This file may be distributed under the terms of the GNU GPLv3 license. import logging from . import bus -import threading # SHT4x I2C Commands DEFAULT_ADDR = 0x44 RESET = 0x94 -REQUEST_CHIPID = 0x89 -# Measurement commands -HIGH_PRECISION_MODE = 0xFD # ~8.3ms measurement time -MEDIUM_PRECISION_MODE = 0xF6 # ~4.5ms measurement time -LOW_PRECISION_MODE = 0xE0 # ~1.6ms measurement time - -# Timing constants from datasheet -MEASUREMENT_TIMES = { - HIGH_PRECISION_MODE: 0.0083, # 8.3ms max according to datasheet - MEDIUM_PRECISION_MODE: 0.0045, # 4.5ms max according to datasheet - LOW_PRECISION_MODE: 0.0016 # 1.6ms max according to datasheet -} - -SOFT_RESET_TIME = 0.001 # 1ms max according to datasheet -REPORT_TIME = 0.8 # How often to report readings +# Measurement command (high precision) +MEASUREMENT_CMD = 0xFD +# Measurement timing constant +MEASUREMENT_TIME = 0.009 # 9ms +RESET_TIME = 0.001 # 1ms class SHT4X: def __init__(self, config): self.printer = config.get_printer() self.name = config.get_name().split()[-1] self.reactor = self.printer.get_reactor() - + # I2C setup - valid_addresses = [0x44, 0x45, 0x46] # From datasheet addr = config.getint('i2c_address', DEFAULT_ADDR) - if addr not in valid_addresses: - addr_list = ', '.join(['0x%x' % a for a in valid_addresses]) - raise config.error("Invalid I2C address 0x%x. " - "Valid addresses are: %s" % (addr, addr_list)) - - speed = config.getint('i2c_speed', 100000) - self.i2c = bus.MCU_I2C_from_config( - config, default_addr=addr, default_speed=speed) + self.i2c = bus.MCU_I2C_from_config(config, default_addr=addr) self.mcu = self.i2c.get_mcu() - - # Precision setup - precision = config.get('precision', 'high').lower() - precision_map = { - 'high': HIGH_PRECISION_MODE, - 'medium': MEDIUM_PRECISION_MODE, - 'low': LOW_PRECISION_MODE - } - if precision not in precision_map: - raise config.error("Invalid precision value '%s'. " - "Valid options are: high, medium, low" - % precision) - self.precision_mode = precision_map[precision] - - # Core sensor state + + # Sensor state self.temp = 0 self.humidity = 0 self._error_count = 0 self.sensor_ready = False - self.measurement_lock = threading.Lock() - self._callback = None - self.serial_number = None - - # Register sensor + + # Register with Klipper self.printer.add_object("sht4x " + self.name, self) - self.printer.register_event_handler("klippy:connect", - self.handle_connect) + self.printer.register_event_handler("klippy:connect", self.handle_connect) def handle_connect(self): """Initialize sensor""" try: self.reset() - # Add a small delay after reset - self.reactor.pause(self.reactor.monotonic() + 0.01) # 10ms - - # Try to read serial number - self.serial_number = self.get_serial_number() - if self.serial_number: - logging.info("SHT4X: Sensor serial number: 0x%08X", - self.serial_number) - - # Test measurement if self._test_measurement(): self.sensor_ready = True logging.info("SHT4X: Sensor ready") - self.sample_timer = self.reactor.register_timer( - self.sample_sensor) - self.reactor.update_timer(self.sample_timer, - self.reactor.NOW) + self.sample_timer = self.reactor.register_timer(self.sample_sensor) + self.reactor.update_timer(self.sample_timer, self.reactor.NOW) else: logging.error("SHT4X: Sensor test failed") except Exception as e: @@ -99,150 +56,87 @@ class SHT4X: """Quick measurement test""" try: recv = self.get_measurements() - if len(recv) != 6: - logging.error("SHT4X: Invalid data length: %d", len(recv)) - return False - if not self._validate_crc(recv): - logging.error("SHT4X: CRC validation failed") - return False - return True - except Exception as e: - logging.error("SHT4X: Test measurement failed: %s", str(e)) + return self._validate_crc(recv) + except Exception: return False def _validate_crc(self, data): """Validate CRC for both temp and humidity""" temp_crc = data[2] humidity_crc = data[5] - return (temp_crc == self._crc8(data[0:2]) and + return (temp_crc == self._crc8(data[0:2]) and humidity_crc == self._crc8(data[3:5])) def setup_minmax(self, min_temp, max_temp): - pass + pass # Required by Klipper interface def setup_callback(self, cb): self._callback = cb def sample_sensor(self, eventtime): - """Main sampling loop""" + """Main sensor sampling method""" if not self.sensor_ready: - return eventtime + REPORT_TIME - + return eventtime + 1.0 + try: - with self.measurement_lock: - recv = self.get_measurements() - - if len(recv) != 6 or not self._validate_crc(recv): - raise Exception("Invalid data or CRC failed") - - # Convert temperature - raw_temp = (recv[0] << 8) | recv[1] - temp = -45.0 + 175.0 * raw_temp / 65535.0 - - # Check if temperature is in reasonable range - if -50 <= temp <= 150: - self.temp = temp - else: - raise ValueError("Temperature out of reasonable range: " - "%fC" % temp) - - # Convert humidity - raw_humidity = (recv[3] << 8) | recv[4] - humidity_percent = -6.0 + 125.0 * raw_humidity / 65535.0 - self.humidity = max(min(humidity_percent, 100.0), 0.0) - - self._error_count = 0 - - # Report to Klipper - if self._callback is not None: - measured_time = self.reactor.monotonic() - self._callback( - self.mcu.estimated_print_time(measured_time), - self.temp) - + # Get measurements + recv = self.get_measurements() + + if not self._validate_crc(recv): + raise Exception("CRC validation failed") + + # Process temperature + raw_temp = (recv[0] << 8) | recv[1] + self.temp = -45.0 + 175.0 * raw_temp / 65535.0 + + # Process humidity + raw_humidity = (recv[3] << 8) | recv[4] + humidity_percent = -6.0 + 125.0 * raw_humidity / 65535.0 + self.humidity = max(min(humidity_percent, 100.0), 0.0) + + self._error_count = 0 + + # Report to Klipper + if hasattr(self, '_callback'): + self._callback(self.mcu.estimated_print_time(eventtime), self.temp) + except Exception as e: self._error_count += 1 - logging.warning("SHT4X: Error %d: %s" % - (self._error_count, str(e))) - - # Add retry logic - if self._error_count <= 3: - # Retry immediately for first few errors - return eventtime + 0.5 # Short delay before retry - elif self._error_count <= 10: - return eventtime + 2.0 # Longer delay for persistent errors - else: + logging.warning("SHT4X: Error %d: %s" % (self._error_count, str(e))) + + if self._error_count > 5: logging.error("SHT4X: Too many errors, sensor failed") self.sensor_ready = False - - return eventtime + REPORT_TIME + + return eventtime + 1.0 def get_measurements(self): - """Get sensor data with retry logic for NACK handling""" - data = [self.precision_mode] - try: - self.i2c.i2c_write(data) - - measurement_time = MEASUREMENT_TIMES[self.precision_mode] - self.reactor.pause(self.reactor.monotonic() + measurement_time) - - # Try up to 3 times to read the data - for retry in range(3): - try: - recv = self.i2c.i2c_read([], 6) - return bytearray(recv['response']) - except Exception as e: - if "NACK" in str(e) and retry < 2: - # Sensor might still be busy, wait a bit more - self.reactor.pause(self.reactor.monotonic() + - measurement_time) - else: - raise - except Exception as e: - raise Exception("Failed to get measurements: %s" % e) - - def get_serial_number(self): - """Read the sensor's unique serial number""" - try: - self.i2c.i2c_write([REQUEST_CHIPID]) - # Small delay - self.reactor.pause(self.reactor.monotonic() + 0.001) - recv = self.i2c.i2c_read([], 6) - data = bytearray(recv['response']) - if len(data) != 6 or not self._validate_crc(data): - raise Exception("Invalid serial number data or CRC failed") - - # Extract the serial number - serial_msb = (data[0] << 8) | data[1] - serial_lsb = (data[3] << 8) | data[4] - return (serial_msb << 16) | serial_lsb - except Exception as e: - logging.warning("SHT4X: Failed to read serial number: %s", - str(e)) - return None + """Get temperature and humidity measurements with proper timing""" + # Send measurement command and wait for acknowledgment + params = self.i2c.i2c_write_wait_ack([MEASUREMENT_CMD]) + completion_time = params['completion_time'] + + # Wait for measurement to complete + self.reactor.pause(completion_time + MEASUREMENT_TIME) + + # Read response after proper delay + params = self.i2c.i2c_read([], 6) + return bytearray(params['response']) def reset(self): - """Reset sensor""" - with self.measurement_lock: - data = [RESET] - self.i2c.i2c_write(data) - self.reactor.pause(self.reactor.monotonic() + SOFT_RESET_TIME) + """Reset sensor with proper timing""" + params = self.i2c.i2c_write_wait_ack([RESET]) + self.reactor.pause(params['completion_time'] + RESET_TIME) def get_status(self, eventtime): """Return sensor status for Mainsail""" - if self.serial_number: - serial_str = "0x%08X" % self.serial_number - else: - serial_str = "unknown" return { 'temperature': round(self.temp, 2), - 'humidity': round(self.humidity, 1), - 'sensor_ready': self.sensor_ready, - 'serial_number': serial_str + 'humidity': round(self.humidity, 1) } def _crc8(self, buffer): - """CRC8 checksum implementation per datasheet""" + """CRC8 checksum for SHT4x sensors""" crc = 0xFF for byte in buffer: crc ^= byte @@ -253,7 +147,6 @@ class SHT4X: crc = crc << 1 return crc & 0xFF - def load_config(config): pheaters = config.get_printer().load_object(config, "heaters") pheaters.add_sensor_factory("SHT4X", SHT4X) From 890bd39c659d137261ad908c0ff585f5540bca63 Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:58:52 +0100 Subject: [PATCH 11/13] Update sht4x.py --- klippy/extras/sht4x.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/klippy/extras/sht4x.py b/klippy/extras/sht4x.py index e250b09be..c0c15f5c7 100644 --- a/klippy/extras/sht4x.py +++ b/klippy/extras/sht4x.py @@ -22,21 +22,22 @@ class SHT4X: self.printer = config.get_printer() self.name = config.get_name().split()[-1] self.reactor = self.printer.get_reactor() - + # I2C setup addr = config.getint('i2c_address', DEFAULT_ADDR) self.i2c = bus.MCU_I2C_from_config(config, default_addr=addr) self.mcu = self.i2c.get_mcu() - + # Sensor state self.temp = 0 self.humidity = 0 self._error_count = 0 self.sensor_ready = False - + # Register with Klipper self.printer.add_object("sht4x " + self.name, self) - self.printer.register_event_handler("klippy:connect", self.handle_connect) + self.printer.register_event_handler( + "klippy:connect", self.handle_connect) def handle_connect(self): """Initialize sensor""" @@ -45,7 +46,8 @@ class SHT4X: if self._test_measurement(): self.sensor_ready = True logging.info("SHT4X: Sensor ready") - self.sample_timer = self.reactor.register_timer(self.sample_sensor) + self.sample_timer = self.reactor.register_timer( + self.sample_sensor) self.reactor.update_timer(self.sample_timer, self.reactor.NOW) else: logging.error("SHT4X: Sensor test failed") @@ -77,37 +79,38 @@ class SHT4X: """Main sensor sampling method""" if not self.sensor_ready: return eventtime + 1.0 - + try: # Get measurements recv = self.get_measurements() - + if not self._validate_crc(recv): raise Exception("CRC validation failed") - + # Process temperature raw_temp = (recv[0] << 8) | recv[1] self.temp = -45.0 + 175.0 * raw_temp / 65535.0 - + # Process humidity raw_humidity = (recv[3] << 8) | recv[4] humidity_percent = -6.0 + 125.0 * raw_humidity / 65535.0 self.humidity = max(min(humidity_percent, 100.0), 0.0) - + self._error_count = 0 - + # Report to Klipper if hasattr(self, '_callback'): - self._callback(self.mcu.estimated_print_time(eventtime), self.temp) - + self._callback(self.mcu.estimated_print_time( + eventtime), self.temp) + except Exception as e: self._error_count += 1 logging.warning("SHT4X: Error %d: %s" % (self._error_count, str(e))) - + if self._error_count > 5: logging.error("SHT4X: Too many errors, sensor failed") self.sensor_ready = False - + return eventtime + 1.0 def get_measurements(self): @@ -115,10 +118,10 @@ class SHT4X: # Send measurement command and wait for acknowledgment params = self.i2c.i2c_write_wait_ack([MEASUREMENT_CMD]) completion_time = params['completion_time'] - + # Wait for measurement to complete self.reactor.pause(completion_time + MEASUREMENT_TIME) - + # Read response after proper delay params = self.i2c.i2c_read([], 6) return bytearray(params['response']) From 4a4978a925ef0983f09b1983aeb0a76af6e5acc5 Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:02:33 +0100 Subject: [PATCH 12/13] Update sht4x.py --- klippy/extras/sht4x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/sht4x.py b/klippy/extras/sht4x.py index c0c15f5c7..225550bbe 100644 --- a/klippy/extras/sht4x.py +++ b/klippy/extras/sht4x.py @@ -66,7 +66,7 @@ class SHT4X: """Validate CRC for both temp and humidity""" temp_crc = data[2] humidity_crc = data[5] - return (temp_crc == self._crc8(data[0:2]) and + return (temp_crc == self._crc8(data[0:2]) and humidity_crc == self._crc8(data[3:5])) def setup_minmax(self, min_temp, max_temp): From ea708e1997810831d6950c01c7c728dfa801e6e9 Mon Sep 17 00:00:00 2001 From: Milz0 <70971571+Milz0@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:38:23 +0100 Subject: [PATCH 13/13] Update sht4x.py Refactoring of code --- klippy/extras/sht4x.py | 237 ++++++++++++++++++++++------------------- 1 file changed, 125 insertions(+), 112 deletions(-) diff --git a/klippy/extras/sht4x.py b/klippy/extras/sht4x.py index 225550bbe..6ca3422dc 100644 --- a/klippy/extras/sht4x.py +++ b/klippy/extras/sht4x.py @@ -1,158 +1,171 @@ -# SHT4x i2c temperature sensors +# SHT4X i2c based temperature sensors support # # Copyright (C) 2025 Milzo # # This file may be distributed under the terms of the GNU GPLv3 license. import logging from . import bus +###################################################################### +# Compatible Sensors: +# SHT40, SHT41, SHT45 - Sensirion SHT4X series +# +###################################################################### +SHT4X_I2C_ADDR = 0x44 -# SHT4x I2C Commands -DEFAULT_ADDR = 0x44 -RESET = 0x94 - -# Measurement command (high precision) -MEASUREMENT_CMD = 0xFD - -# Measurement timing constant -MEASUREMENT_TIME = 0.009 # 9ms -RESET_TIME = 0.001 # 1ms +SHT4X_CMD = { + 'MEASURE': { + 'STRETCH_ENABLED': { + 'HIGH_REP': [0xFD], # High precision, 8.2ms + 'MED_REP': [0xF6], # Medium precision, 4.5ms + 'LOW_REP': [0xE0] # Low precision, 1.7ms + }, + }, + 'OTHER': { + 'SOFTRESET': [0x94], # Soft reset + } +} class SHT4X: def __init__(self, config): self.printer = config.get_printer() self.name = config.get_name().split()[-1] self.reactor = self.printer.get_reactor() - - # I2C setup - addr = config.getint('i2c_address', DEFAULT_ADDR) - self.i2c = bus.MCU_I2C_from_config(config, default_addr=addr) - self.mcu = self.i2c.get_mcu() - - # Sensor state - self.temp = 0 - self.humidity = 0 - self._error_count = 0 - self.sensor_ready = False - - # Register with Klipper + self.i2c = bus.MCU_I2C_from_config( + config, default_addr=SHT4X_I2C_ADDR, default_speed=100000) + self._error = self.i2c.get_mcu().error + self.report_time = config.getint('sht4x_report_time', 1, minval=1) + self.deviceId = config.get('sensor_type') + self.temp = self.min_temp = self.max_temp = self.humidity = 0. + self.sample_timer = self.reactor.register_timer(self._sample_sht4x) self.printer.add_object("sht4x " + self.name, self) - self.printer.register_event_handler( - "klippy:connect", self.handle_connect) + self.printer.register_event_handler("klippy:connect", + self.handle_connect) def handle_connect(self): - """Initialize sensor""" - try: - self.reset() - if self._test_measurement(): - self.sensor_ready = True - logging.info("SHT4X: Sensor ready") - self.sample_timer = self.reactor.register_timer( - self.sample_sensor) - self.reactor.update_timer(self.sample_timer, self.reactor.NOW) - else: - logging.error("SHT4X: Sensor test failed") - except Exception as e: - logging.error("SHT4X: Init failed: %s" % str(e)) - - def _test_measurement(self): - """Quick measurement test""" - try: - recv = self.get_measurements() - return self._validate_crc(recv) - except Exception: - return False - - def _validate_crc(self, data): - """Validate CRC for both temp and humidity""" - temp_crc = data[2] - humidity_crc = data[5] - return (temp_crc == self._crc8(data[0:2]) and - humidity_crc == self._crc8(data[3:5])) + self._init_sht4x() + self.reactor.update_timer(self.sample_timer, self.reactor.NOW) def setup_minmax(self, min_temp, max_temp): - pass # Required by Klipper interface + self.min_temp = min_temp + self.max_temp = max_temp def setup_callback(self, cb): self._callback = cb - def sample_sensor(self, eventtime): - """Main sensor sampling method""" - if not self.sensor_ready: - return eventtime + 1.0 + def get_report_time_delta(self): + return self.report_time + def _init_sht4x(self): try: - # Get measurements - recv = self.get_measurements() + # Soft reset the device + if hasattr(self.i2c, 'i2c_write_wait_ack'): + self.i2c.i2c_write_wait_ack(SHT4X_CMD['OTHER']['SOFTRESET']) + else: + self.i2c.i2c_write(SHT4X_CMD['OTHER']['SOFTRESET']) + # Wait after reset + self.reactor.pause(self.reactor.monotonic() + 0.001) - if not self._validate_crc(recv): - raise Exception("CRC validation failed") + logging.info("sht4x: initialized for single-shot measurements") - # Process temperature - raw_temp = (recv[0] << 8) | recv[1] - self.temp = -45.0 + 175.0 * raw_temp / 65535.0 + except Exception: + logging.exception("sht4x: initialization failed") + raise - # Process humidity - raw_humidity = (recv[3] << 8) | recv[4] - humidity_percent = -6.0 + 125.0 * raw_humidity / 65535.0 - self.humidity = max(min(humidity_percent, 100.0), 0.0) + def _sample_sht4x(self, eventtime): + try: + # Single-shot measurement with retries + retries = 5 + params = None + error = None - self._error_count = 0 + while retries > 0 and params is None: + try: + # Send measurement command + if hasattr(self.i2c, 'i2c_write_wait_ack'): + self.i2c.i2c_write_wait_ack( + SHT4X_CMD['MEASURE']['STRETCH_ENABLED']['HIGH_REP']) + else: + self.i2c.i2c_write( + SHT4X_CMD['MEASURE']['STRETCH_ENABLED']['HIGH_REP']) - # Report to Klipper - if hasattr(self, '_callback'): - self._callback(self.mcu.estimated_print_time( - eventtime), self.temp) + # Wait for measurement to complete + self.reactor.pause(self.reactor.monotonic() + 0.009) - except Exception as e: - self._error_count += 1 - logging.warning("SHT4X: Error %d: %s" % (self._error_count, str(e))) + # Read 6 bytes + params = self.i2c.i2c_read([], 6, retry=False) - if self._error_count > 5: - logging.error("SHT4X: Too many errors, sensor failed") - self.sensor_ready = False + except Exception as e: + logging.exception( + "sht4x: measurement attempt failed: %s", e) + error = e + self.reactor.pause(self.reactor.monotonic() + .5) + retries -= 1 - return eventtime + 1.0 + if params is None: + raise error - def get_measurements(self): - """Get temperature and humidity measurements with proper timing""" - # Send measurement command and wait for acknowledgment - params = self.i2c.i2c_write_wait_ack([MEASUREMENT_CMD]) - completion_time = params['completion_time'] + response = bytearray(params['response']) + rtemp = response[0] << 8 + rtemp |= response[1] + if self._crc8(rtemp) != response[2]: + logging.warning( + "sht4x: Checksum error on Temperature reading!" + ) + else: + self.temp = -45 + (175 * rtemp / 65535) + logging.debug("sht4x: Temperature %.2f " % self.temp) - # Wait for measurement to complete - self.reactor.pause(completion_time + MEASUREMENT_TIME) + rhumid = response[3] << 8 + rhumid |= response[4] + if self._crc8(rhumid) != response[5]: + logging.warning("sht4x: Checksum error on Humidity reading!") + else: + self.humidity = 100 * rhumid / 65535 + logging.debug("sht4x: Humidity %.2f " % self.humidity) - # Read response after proper delay - params = self.i2c.i2c_read([], 6) - return bytearray(params['response']) + except Exception: + logging.exception("sht4x: Error reading data") + self.temp = self.humidity = .0 + return self.reactor.NEVER - def reset(self): - """Reset sensor with proper timing""" - params = self.i2c.i2c_write_wait_ack([RESET]) - self.reactor.pause(params['completion_time'] + RESET_TIME) + if self.temp < self.min_temp or self.temp > self.max_temp: + self.printer.invoke_shutdown( + "sht4x: temperature %0.1f outside range of %0.1f:%.01f" + % (self.temp, self.min_temp, self.max_temp)) - def get_status(self, eventtime): - """Return sensor status for Mainsail""" - return { - 'temperature': round(self.temp, 2), - 'humidity': round(self.humidity, 1) - } + 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 _crc8(self, buffer): - """CRC8 checksum for SHT4x sensors""" + def _split_bytes(self, data): + bytes = [] + for i in range((data.bit_length() + 7) // 8): + bytes.append((data >> i*8) & 0xFF) + bytes.reverse() + return bytes + + def _crc8(self, data): + #crc8 polynomial for 16bit value, CRC8 -> x^8 + x^5 + x^4 + 1 + SHT4X_CRC8_POLYNOMINAL= 0x31 crc = 0xFF - for byte in buffer: + data_bytes = self._split_bytes(data) + for byte in data_bytes: crc ^= byte for _ in range(8): if crc & 0x80: - crc = (crc << 1) ^ 0x31 + crc = (crc << 1) ^ SHT4X_CRC8_POLYNOMINAL else: - crc = crc << 1 + crc <<= 1 return crc & 0xFF -def load_config(config): - pheaters = config.get_printer().load_object(config, "heaters") - pheaters.add_sensor_factory("SHT4X", SHT4X) + def get_status(self, eventtime): + return { + 'temperature': round(self.temp, 2), + 'humidity': round(self.humidity, 1), + } -def load_config_prefix(config): - return SHT4X(config) +def load_config(config): + # Register sensor + pheater = config.get_printer().lookup_object("heaters") + pheater.add_sensor_factory("SHT4X", SHT4X)