From 2fb3d54e2f8086fb310936c9136dbb05dc753ed5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 1 Jun 2026 14:41:38 -0400 Subject: [PATCH 01/12] stale-issue-bot: Update lock-threads to v6 (from v4) Update the github workflow lock-threads action in hope that it will fix run failure reports. Signed-off-by: Kevin O'Connor --- .github/workflows/stale-issue-bot.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-issue-bot.yaml b/.github/workflows/stale-issue-bot.yaml index b23511615555..9b03510928ae 100644 --- a/.github/workflows/stale-issue-bot.yaml +++ b/.github/workflows/stale-issue-bot.yaml @@ -334,7 +334,7 @@ jobs: if: github.repository == 'Klipper3d/klipper' runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v6 with: issue-inactive-days: '180' issue-lock-reason: '' From 666781a6ecd4d72a1c6568f0b5b0fa19b18f9c62 Mon Sep 17 00:00:00 2001 From: wildBill83 Date: Tue, 3 Mar 2026 10:34:30 -0600 Subject: [PATCH 02/12] stm32: Add STM32H750 with 32kb bootloader offset STM32H750 only has 128kb of storage so the STM32H7 typical offset of 128kb would be too large, I added it to the 32kb bootloader selections. Signed-off-by: Bill Horan Signed-off-by: Kevin O'Connor --- src/stm32/Kconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index 9c7db5b5ee34..5574c9c125af 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -295,7 +295,7 @@ choice config STM32_FLASH_START_7000 bool "28KiB bootloader" if MACH_STM32F1 config STM32_FLASH_START_8000 - bool "32KiB bootloader" if MACH_STM32F1 || MACH_STM32F2 || MACH_STM32F4 || MACH_STM32F7 + bool "32KiB bootloader" if MACH_STM32F1 || MACH_STM32F2 || MACH_STM32F4 || MACH_STM32F7 || MACH_STM32H750 config STM32_FLASH_START_8800 bool "34KiB bootloader" if MACH_STM32F103 config STM32_FLASH_START_20200 @@ -314,7 +314,7 @@ choice config STM32_FLASH_START_4000 bool "16KiB bootloader" if MACH_STM32F207 || MACH_STM32F401 || MACH_STM32F4x5 || MACH_STM32F103 || MACH_STM32F072 config STM32_FLASH_START_20000 - bool "128KiB bootloader" if MACH_STM32H743 || MACH_STM32H723 || MACH_STM32F7 + bool "128KiB bootloader" if MACH_STM32H743 || MACH_STM32H723 || MACH_STM32F7 || MACH_STM32H750 config STM32_FLASH_START_0000 bool "No bootloader" From 43f9219cfda5fe65c982f06bc50d9b386dc148bb Mon Sep 17 00:00:00 2001 From: wildBill83 Date: Tue, 3 Mar 2026 10:34:30 -0600 Subject: [PATCH 03/12] stm32: Increase STM32H750 speed to 480Mhz (from 400Mhz) Signed-off-by: Bill Horan Signed-off-by: Kevin O'Connor --- src/stm32/Kconfig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index 5574c9c125af..99059614e2e1 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -212,7 +212,8 @@ config CLOCK_FREQ default 170000000 if MACH_STM32G431 default 170000000 if MACH_STM32G474 default 520000000 if MACH_STM32H723 - default 400000000 if MACH_STM32H743 || MACH_STM32H750 + default 400000000 if MACH_STM32H743 + default 480000000 if MACH_STM32H750 default 80000000 if MACH_STM32L412 default 64000000 if MACH_N32G45x && STM32_CLOCK_REF_INTERNAL default 128000000 if MACH_N32G45x From 5a7ff1caf3be9dd2675904f046560cf7ad1f907f Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sun, 24 May 2026 14:16:22 +0200 Subject: [PATCH 04/12] load_cell: Fixed reporting of errors on sensor timeout Signed-off-by: Dmitry Butyugin --- klippy/extras/load_cell.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py index 112d9640bdc9..49f340cdb8f9 100644 --- a/klippy/extras/load_cell.py +++ b/klippy/extras/load_cell.py @@ -323,10 +323,12 @@ def _collect_until(self, timeout): while self.is_started: now = self._reactor.monotonic() if self._mcu.estimated_print_time(now) > timeout: - _, (errors, overflows) = self._finish_collecting() + samples, err = self._finish_collecting() + errors, overflows = err if err else (0, 0) raise self._printer.command_error( - "LoadCellSampleCollector timed out! Errors: %i," - " Overflows: %i" % (errors, overflows)) + "LoadCellSampleCollector timed out! Collected %i samples," + " Errors: %i, Overflows: %i" % (len(samples), errors, + overflows)) if self._mcu.is_fileoutput(): break self._reactor.pause(now + RETRY_DELAY) From 64753499b86de0c216c66e53957af0e6cb1ee2ad Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sun, 24 May 2026 15:03:27 +0200 Subject: [PATCH 05/12] load_cell: Fixed double-counting errors and added ADCs statuses Signed-off-by: Dmitry Butyugin --- klippy/extras/ads1220.py | 7 +++++++ klippy/extras/ads131m0x.py | 7 +++++++ klippy/extras/hx71x.py | 7 +++++++ klippy/extras/load_cell.py | 15 +++++++++++---- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index f4ddaf9f8f63..af83da604c7c 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -120,6 +120,13 @@ def get_mcu(self): def get_samples_per_second(self): return self.sps + def get_status(self, eventtime): + return { + 'errors': self.last_error_count, + 'overflows': self.ffreader.get_last_overflows(), + 'sample_rate': self.get_samples_per_second(), + } + def lookup_sensor_error(self, error_code): return "Unknown ads1220 error" % (error_code,) diff --git a/klippy/extras/ads131m0x.py b/klippy/extras/ads131m0x.py index 9298edf70101..dccf6094c411 100644 --- a/klippy/extras/ads131m0x.py +++ b/klippy/extras/ads131m0x.py @@ -209,6 +209,13 @@ def get_mcu(self): def get_samples_per_second(self): return self.sps + def get_status(self, eventtime): + return { + 'errors': self.last_error_count, + 'overflows': self.ffreader.get_last_overflows(), + 'sample_rate': self.get_samples_per_second(), + } + def lookup_sensor_error(self, error_code): return self._sensor_errors.get(error_code, "Unknown %s error" % (self.sensor_type,)) diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index 76563d484494..90d5091a7b3e 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -78,6 +78,13 @@ def get_mcu(self): def get_samples_per_second(self): return self.sps + def get_status(self, eventtime): + return { + 'errors': self.last_error_count, + 'overflows': self.ffreader.get_last_overflows(), + 'sample_rate': self.get_samples_per_second(), + } + def lookup_sensor_error(self, error_code): return "Unknown hx71x error %d" % (error_code,) diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py index 49f340cdb8f9..5ba7467cc39e 100644 --- a/klippy/extras/load_cell.py +++ b/klippy/extras/load_cell.py @@ -286,12 +286,14 @@ def __init__(self, printer, load_cell): self._samples = [] self._errors = 0 self._overflows = 0 + self._start_errors = 0 + self._start_overflows = 0 def _on_samples(self, msg): if not self.is_started: return False # already stopped, ignore - self._errors += msg['errors'] - self._overflows += msg['overflows'] + self._errors = msg['errors'] + self._overflows = msg['overflows'] samples = msg['data'] for sample in samples: time = sample[0] @@ -310,9 +312,9 @@ def _finish_collecting(self): self.min_count = float("inf") # In Python 3.5 math.inf is better samples = self._samples self._samples = [] - errors = self._errors + errors = max(0, self._errors - self._start_errors) self._errors = 0 - overflows = self._overflows + overflows = max(0, self._overflows - self._start_overflows) self._overflows = 0 if self._mcu.is_fileoutput(): samples = [(0., 0., 0.)] @@ -340,6 +342,10 @@ def start_collecting(self, min_time=None): return self.min_time = min_time if min_time is not None else self.min_time self.is_started = True + sensor_status = self._load_cell.sensor.get_status( + self._reactor.monotonic()) + self._start_errors = sensor_status['errors'] + self._start_overflows = sensor_status['overflows'] self._load_cell.add_client(self._on_samples) # stop collecting immediately and return results @@ -519,6 +525,7 @@ def get_status(self, eventtime): 'counts_per_gram': self.counts_per_gram, 'reference_tare_counts': self.reference_tare_counts, 'tare_counts': self.tare_counts}) + status.update(self.sensor.get_status(eventtime)) return status From 4c137ed3e58b2ede5afd90427f889169890c74f6 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sun, 24 May 2026 15:14:19 +0200 Subject: [PATCH 06/12] docs: Added missing load_cell[_probe] fields, fixed formatting Signed-off-by: Dmitry Butyugin --- docs/Status_Reference.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index 19e36ca42c47..32b20b8e0426 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -316,22 +316,29 @@ The following information is available for each `[led led_name]`, ## load_cell The following information is available for each `[load_cell name]`: -- 'is_calibrated': True/False is the load cell calibrated -- 'counts_per_gram': The number of raw sensor counts that equals 1 gram of force -- 'reference_tare_counts': The reference number of raw sensor counts for 0 force -- 'tare_counts': The current number of raw sensor counts for 0 force -- 'force_g': The force in grams, averaged over the last polling period. -- 'min_force_g': The minimum force in grams, over the last polling period. -- 'max_force_g': The maximum force in grams, over the last polling period. +- `is_calibrated`: True/False whether the load cell is calibrated. +- `counts_per_gram`: The number of raw sensor counts that equals 1 gram of force. +- `reference_tare_counts`: The reference number of raw sensor counts for 0 force. +- `tare_counts`: The current number of raw sensor counts for 0 force. +- `force_g`: The force in grams, averaged over the last polling period. +- `min_force_g`: The minimum force in grams, over the last polling period. +- `max_force_g`: The maximum force in grams, over the last polling period. +- `errors`: The number of sensor errors detected since the last start + of measurements. +- `overflows`: The number of data buffer overflows detected since the last + start of measurements. +- `sample_rate`: The sensor's sample rate in samples per second. ## load_cell_probe The following information is available for `[load_cell_probe]`: - all items from [load_cell](Status_Reference.md#load_cell) - all items from [probe](Status_Reference.md#probe) -- 'endstop_tare_counts': the load cell probe keeps a tare value independent of -the load cell. This re-set at the start of each probe. -- 'last_trigger_time': timestamp of the last homing trigger +- `endstop_tare_counts`: The load cell probe keeps a tare value independent of + the load cell. This is re-set at the start of each probe. +- `last_trigger_time`: Timestamp of the last homing trigger. +- `last_z_result`: The Z position result of the last tap. +- `is_last_tap_valid`: True if the last tap result is valid. ## manual_probe From 8a2da852f365df0f69c5a80d96944dccc85396e3 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Sun, 31 Dec 2023 15:07:32 -0500 Subject: [PATCH 07/12] spi_flash: add support for fast SPI speeds Some SD cards will fail to initialize at the 400KHz clock documented in the official specification. Add a "fast speed" option that runs at 4MHz. Signed-off-by: Eric Callahan --- scripts/flash-sdcard.sh | 9 ++++++--- scripts/spi_flash/spi_flash.py | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/scripts/flash-sdcard.sh b/scripts/flash-sdcard.sh index 6a08eab14eae..852540c27961 100755 --- a/scripts/flash-sdcard.sh +++ b/scripts/flash-sdcard.sh @@ -11,6 +11,7 @@ KLIPPER_DICT_DEFAULT="${SRCDIR}/out/klipper.dict" SPI_FLASH="${SRCDIR}/scripts/spi_flash/spi_flash.py" BAUD_ARG="" CHECK_ARG="" +SPI_SPEED_ARG="" # Force script to exit if an error occurs set -e @@ -18,7 +19,7 @@ print_help_message() { echo "SD Card upload utility for Klipper" echo - echo "usage: flash_sdcard.sh [-h] [-l] [-c] [-b ] [-f ] [-d ]" + echo "usage: flash_sdcard.sh [-h] [-l] [-c] [-s] [-b ] [-f ] [-d ]" echo " " echo echo "positional arguments:" @@ -29,13 +30,14 @@ print_help_message() echo " -h show this message" echo " -l list available boards" echo " -c run flash check/verify only (skip upload)" + echo " -s use fast SPI speed (4MHz)" echo " -b serial baud rate (default is 250000)" echo " -f path to klipper.bin" echo " -d path to klipper.dict for firmware validation" } # Parse command line "optional args" -while getopts "hlcb:f:d:" arg; do +while getopts "hlcsb:f:d:" arg; do case $arg in h) print_help_message @@ -46,6 +48,7 @@ while getopts "hlcb:f:d:" arg; do exit 0 ;; c) CHECK_ARG="-c";; + s) SPI_SPEED_ARG="-s";; b) BAUD_ARG="-b ${OPTARG}";; f) KLIPPER_BIN=$OPTARG;; d) KLIPPER_DICT=$OPTARG;; @@ -85,4 +88,4 @@ fi # Run Script echo "Flashing ${KLIPPER_BIN} to ${DEVICE}" -${KLIPPY_ENV} ${SPI_FLASH} ${CHECK_ARG} ${BAUD_ARG} ${KLIPPER_DICT} ${DEVICE} ${BOARD} ${KLIPPER_BIN} +${KLIPPY_ENV} ${SPI_FLASH} ${CHECK_ARG} ${SPI_SPEED_ARG} ${BAUD_ARG} ${KLIPPER_DICT} ${DEVICE} ${BOARD} ${KLIPPER_BIN} diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index af93b53e9637..3a778cefed23 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -98,6 +98,7 @@ def check_need_convert(board_name, config): SDIO_OID = 0 SPI_MODE = 0 SD_SPI_SPEED = 400000 +FAST_SD_SPI_SPEED = 4000000 # MCU Command Constants RESET_CMD = "reset" GET_CFG_CMD = "get_config" @@ -1303,6 +1304,9 @@ def check_need_restart(self): def _configure_mcu_spibus(self, printfunc=logging.info): # TODO: add commands for buttons? Or perhaps an endstop? We # just need to be able to query the status of the detect pin + fast_spi = self.board_config['fast_spi'] + spi_speed = FAST_SD_SPI_SPEED if fast_spi else SD_SPI_SPEED + output_line("Requested SPI Clock Frequency: %d" % (spi_speed,)) cs_pin = self.board_config['cs_pin'].upper() bus = self.board_config['spi_bus'] bus_enums = self.enumerations.get( @@ -1310,7 +1314,7 @@ def _configure_mcu_spibus(self, printfunc=logging.info): pin_enums = self.enumerations.get('pin') if bus == "swspi": mcu_freq = self.clocksync.print_time_to_clock(1) - pulse_ticks = mcu_freq//SD_SPI_SPEED + pulse_ticks = mcu_freq//spi_speed cfgpins = self.board_config['spi_pins'] pins = [p.strip().upper() for p in cfgpins.split(',') if p.strip()] pin_err_msg = "Invalid Software SPI Pins: %s" % (cfgpins,) @@ -1323,13 +1327,13 @@ def _configure_mcu_spibus(self, printfunc=logging.info): SW_SPI_BUS_CMDS[0] % (SPI_OID, pins[0], pins[1], pins[2], SPI_MODE, pulse_ticks), SW_SPI_BUS_CMDS[1] % (SPI_OID, pins[0], pins[1], pins[2], - SPI_MODE, SD_SPI_SPEED) + SPI_MODE, spi_speed) ] else: if bus not in bus_enums: raise SPIFlashError("Invalid SPI Bus: %s" % (bus,)) bus_cmds = [ - SPI_BUS_CMD % (SPI_OID, bus, SPI_MODE, SD_SPI_SPEED), + SPI_BUS_CMD % (SPI_OID, bus, SPI_MODE, spi_speed), ] if cs_pin not in pin_enums: raise SPIFlashError("Invalid CS Pin: %s" % (cs_pin,)) @@ -1682,6 +1686,10 @@ def main(): parser.add_argument( "-c","--check", action="store_true", help="Perform flash check/verify only") + parser.add_argument( + "-s", "--fast-spi", action="store_true", + help="Use fast SPI speed (4MHz)" + ) parser.add_argument( "device", metavar="", help="Device Serial Port") parser.add_argument( @@ -1701,6 +1709,7 @@ def main(): flash_args['klipper_bin_path'] = args.klipper_bin_path flash_args['klipper_dict_path'] = args.dict_path flash_args['verify_only'] = args.check + flash_args['fast_spi'] = args.fast_spi if args.check: # override board_defs setting when doing verify-only: flash_args['skip_verify'] = False From b4424a6582618be94e26ab5871a76d2ecbc64fc6 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Sun, 31 Dec 2023 16:23:15 -0500 Subject: [PATCH 08/12] docs: note fast spi option in sdcard_updates.md Signed-off-by: Eric Callahan --- docs/SDCard_Updates.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/SDCard_Updates.md b/docs/SDCard_Updates.md index 432446de78cf..a1717ed26c73 100644 --- a/docs/SDCard_Updates.md +++ b/docs/SDCard_Updates.md @@ -48,7 +48,7 @@ All options can be viewed by the help screen: ./scripts/flash-sdcard.sh -h SD Card upload utility for Klipper -usage: flash_sdcard.sh [-h] [-l] [-c] [-b ] [-f ] +usage: flash_sdcard.sh [-h] [-l] [-c] [-s] [-b ] [-f ] positional arguments: @@ -59,6 +59,7 @@ optional arguments: -h show this message -l list available boards -c run flash check/verify only (skip upload) + -s use fast SPI speed (4MHz) -b serial baud rate (default is 250000) -f path to klipper.bin ``` @@ -87,6 +88,25 @@ use SDIO mode instead of SPI to access their SD Cards. (See Caveats below) But, it can also be used anytime to verify if the code flashed into the board matches the version in your build folder on any supported board. +## Failure to Initialize + +Some SD cards may fail to initialize at the default SPI speed of 400KHz. In +this situation it is possible to use `-s` to drive the SPI peripheral at 4MHz. +For example: + +``` +./scripts/flash-sdcard.sh -s /dev/ttyACM0 btt-skr-v1.3 +``` + +If the device still fails to initialize then cause of the failure is +unrelated to the speed and likely a result of one of the following +conditions: + +- The SD card is improperly formatted. Must be `fat` or `fat32`. +- Attempt to initialize a card using the SPI interface that has already + been initialized over SDIO. +- The SD card has failed or is corrupt. + ## Caveats - As mentioned in the introduction, this method only works for upgrading From 430a2a8a0054c5d5ac3f8cdaf5c968a3b1df49d3 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Mon, 1 Jan 2024 07:42:39 -0500 Subject: [PATCH 09/12] spi_flash: remove invalid reference to "long" Also remove unused "utils" import. Signed-off-by: Eric Callahan --- scripts/spi_flash/spi_flash.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index 3a778cefed23..16d5077b193b 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -17,7 +17,6 @@ import json import board_defs import fatfs_lib -import util import reactor import serialhdl import clocksync @@ -1010,7 +1009,7 @@ def _check_command(self, check_func, cmd, args, is_app_cmd=False, tries=15, def _send_command(self, cmd, args, wait=0): cmd_code = SD_COMMANDS[cmd] argument = 0 - if isinstance(args, int) or isinstance(args, long): + if isinstance(args, int): argument = args & 0xFFFFFFFF elif isinstance(args, list) and len(args) == 4: argument = ((args[0] << 24) & 0xFF000000) | \ From ccdb7717644e5815e39340c0be4f57af0df86015 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 13 Jun 2026 22:45:54 -0400 Subject: [PATCH 10/12] probe_eddy_current: Separate least squares math from EddyTap class Move the low-level least squares math to a new TapBestFit class and add additional code comments. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 293 +++++++++++++++------------- 1 file changed, 156 insertions(+), 137 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 8e877899c076..b5131f306bac 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -548,6 +548,160 @@ def probe_results_from_avg(measures, toolhead_pos, calibration, offsets): (offsets[0], offsets[1], sensor_z)) +###################################################################### +# Data fitting for "tap" +###################################################################### + +# Given a list of (frequency, z) pairs, find the coefficients +# z_contact, freq_contact, depress_slope, slope, and slope2 that best +# fit the data to the formulas `frequency = freq_contact + +# depress_slope*(z-z_contact)` when z<=z_contact and `frequency = +# freq_contact + slope*(z-z_contact) + slope2*(z-z_contact)*(z-z_contact)` +# when z>=z_contact. This implements a form of non-linear least +# squares. +class TapBestFit: + def __init__(self): + self._least_squares_cache = {} + def _build_ls_matrix(self, samples, est_z_contact): + # The function here is only a reference for the optimized version below + len_samples = len(samples) + eqs = [[0.] * 4 for i in range(len_samples)] + ans = [[0.] for i in range(len_samples)] + for i, (step_z, sensor_freq) in enumerate(samples): + ans[i][0] = sensor_freq + eq = eqs[i] + eq[0] = 1. + if step_z <= est_z_contact: + # 1*c0 + (z-ezc)*c1 + ezc*c2 + ezc*ezc*c3 = freq + eq[1] = step_z - est_z_contact + eq[2] = est_z_contact + eq[3] = est_z_contact * est_z_contact + else: + # 1*c0 + 0*c1 + z*c2 + z*z*c3 = freq + eq[1] = 0. + eq[2] = step_z + eq[3] = step_z * step_z + eqst = mathutil.mat_transp(eqs) + eqst_eqs = mathutil.mat_mat_mul(eqst, eqs) + eqst_ans = mathutil.mat_mat_mul(eqst, ans) + return eqst_eqs, eqst_ans + def _build_sums(self, samples, num_le): + sum_le_z = sum_le_z2 = sum_le_freq = sum_le_freq_z = 0. + for z, freq in samples[:num_le]: + sum_le_z += z + sum_le_z2 += z**2 + sum_le_freq += freq + sum_le_freq_z += freq*z + sum_gt_z = sum_gt_z2 = sum_gt_z3 = sum_gt_z4 = 0. + sum_gt_freq = sum_gt_freq_z = sum_gt_freq_z2 = 0. + for z, freq in samples[num_le:]: + sum_gt_z += z + sum_gt_z2 += z**2 + sum_gt_z3 += z**3 + sum_gt_z4 += z**4 + sum_gt_freq += freq + sum_gt_freq_z += freq*z + sum_gt_freq_z2 += freq * z**2 + return (sum_le_z, sum_le_z2, sum_le_freq, sum_le_freq_z, + sum_gt_z, sum_gt_z2, sum_gt_z3, sum_gt_z4, + sum_gt_freq, sum_gt_freq_z, sum_gt_freq_z2) + def _build_ls_matrix_opt(self, samples, est_z_contact): + # This function is an optimized version of _build_ls_matrix() + num_le = bisect.bisect(samples, (est_z_contact, sys.float_info.max)) + # Check for previously calculated raw freq/z counters + sums = self._least_squares_cache.get(num_le) + if sums is None: + sums = self._build_sums(samples, num_le) + self._least_squares_cache[num_le] = sums + (sum_le_z, sum_le_z2, sum_le_freq, sum_le_freq_z, + sum_gt_z, sum_gt_z2, sum_gt_z3, sum_gt_z4, + sum_gt_freq, sum_gt_freq_z, sum_gt_freq_z2) = sums + num_samples = len(samples) + ezc = est_z_contact + ezc2 = ezc**2 + ezc3 = ezc**3 + ezc4 = ezc**4 + # Build matrices for least squares evaluation + eqst_eqs = [[0.] * 4 for i in range(4)] + eqst_eqs[0][0] = num_samples + eqst_eqs[1][1] = sum_le_z2 - 2*ezc*sum_le_z + num_le*ezc2 + eqst_eqs[2][2] = sum_gt_z2 + num_le*ezc2 + eqst_eqs[3][3] = sum_gt_z4 + num_le*ezc4 + eqst_eqs[0][1] = eqst_eqs[1][0] = sum_le_z - num_le*ezc + eqst_eqs[0][2] = eqst_eqs[2][0] = sum_gt_z + num_le*ezc + eqst_eqs[0][3] = eqst_eqs[3][0] = sum_gt_z2 + num_le*ezc2 + eqst_eqs[2][3] = eqst_eqs[3][2] = sum_gt_z3 + num_le*ezc3 + eqst_eqs[2][1] = eqst_eqs[1][2] = ezc * eqst_eqs[0][1] + eqst_eqs[3][1] = eqst_eqs[1][3] = ezc2 * eqst_eqs[0][1] + eqst_ans = [[0.] for i in range(4)] + eqst_ans[0][0] = sum_le_freq + sum_gt_freq + eqst_ans[1][0] = sum_le_freq_z - ezc*sum_le_freq + eqst_ans[2][0] = sum_gt_freq_z + ezc*sum_le_freq + eqst_ans[3][0] = sum_gt_freq_z2 + ezc2 * sum_le_freq + return eqst_eqs, eqst_ans + def _calc_least_squares(self, samples, est_z_contact): + eqst_eqs, eqst_ans = self._build_ls_matrix_opt(samples, est_z_contact) + coeffs = mathutil.gaussian_solve(eqst_eqs, eqst_ans) + if coeffs is not None and coeffs[3][0] < 0.: + # z**2 factor can't be negative - retry using only linear + alt_eqst_eqs = [ee[:3] for ee in eqst_eqs[:3]] + alt_eqst_ans = eqst_ans[:3] + coeffs = mathutil.gaussian_solve(alt_eqst_eqs, alt_eqst_ans) + if coeffs is not None: + coeffs = coeffs + [[0.]] + if coeffs is None: + return sys.float_info.max, [[0.]] * 4 + rel_err = -sum([c[0]*a[0] for c, a in zip(coeffs, eqst_ans)]) + return rel_err, coeffs + def find_best_fit(self, data): + #for d in data: + # logging.info("sample: freq=%.3f z=%.6f", d[0], d[1][2]) + self._least_squares_cache.clear() + # Change base of freq/z measurements to improve numerical stability + base_z = .5 * (data[0][1][2] + data[-1][1][2]) + base_freq = .5 * (data[0][0] + data[-1][0]) + samples = [(d[1][2] - base_z, d[0] - base_freq) for d in data] + # Run least squares with various z values to reduce residual error + min_z = best_z = samples[0][0] + max_z = samples[-1][0] + best_err = sys.float_info.max + best_coeffs = [0., 0., 0., 0.] + while max_z - min_z > 0.000050: + # Select z value to check + mid_z = (min_z + max_z) * .5 + if best_z < mid_z: + guess_z = (best_z + max_z) * .5 + else: + guess_z = (min_z + best_z) * .5 + # Calculate least squares error for given z + guess_err, coeffs = self._calc_least_squares(samples, guess_z) + # Update search bounds + if guess_err < best_err: + if guess_z > best_z: + min_z = best_z + else: + max_z = best_z + best_z = guess_z + best_err = guess_err + best_coeffs = coeffs + else: + if guess_z > best_z: + max_z = guess_z + else: + min_z = guess_z + self._least_squares_cache.clear() + # Return to original freq/z measurement base + bc = [v[0] for v in best_coeffs] + z_contact = base_z + best_z + freq_contact = base_freq + bc[0] + best_z*bc[2] + best_z*best_z*bc[3] + depress_slope = bc[1] + slope = bc[2] + 2.*best_z*bc[3] + slope2 = bc[3] + #logging.info("probe_analysis: coeffs=%s", + # (z_contact, freq_contact, depress_slope, slope, slope2)) + return z_contact, freq_contact, depress_slope, slope, slope2 + + ###################################################################### # Probe sessions ###################################################################### @@ -623,7 +777,6 @@ def __init__(self, config, sensor_helper, param_helper, trigger_analog): self._filter_design = None self._tap_z_offset = config.getfloat('tap_z_offset', 0.) self._tap_threshold = config.getfloat('tap_threshold', 0., above=0.) - self._least_squares_cache = {} self._current_tap_threshold = 0. self._setup_tap() self._last_tap = None @@ -687,141 +840,6 @@ def _lookup_toolhead_pos(self, pos_time): s.get_past_mcu_position(pos_time)) for s in kin.get_steppers()} return kin.calc_position(kin_spos) - def _build_ls_matrix(self, samples, est_z_contact): - # The function here is only a reference for the optimized version below - len_samples = len(samples) - eqs = [[0.] * 4 for i in range(len_samples)] - ans = [[0.] for i in range(len_samples)] - for i, (step_z, sensor_freq) in enumerate(samples): - ans[i][0] = sensor_freq - eq = eqs[i] - eq[0] = 1. - if step_z <= est_z_contact: - # 1*c0 + (z-ezc)*c1 + ezc*c2 + ezc*ezc*c3 = freq - eq[1] = step_z - est_z_contact - eq[2] = est_z_contact - eq[3] = est_z_contact * est_z_contact - else: - # 1*c0 + 0*c1 + z*c2 + z*z*c3 = freq - eq[1] = 0. - eq[2] = step_z - eq[3] = step_z * step_z - eqst = mathutil.mat_transp(eqs) - eqst_eqs = mathutil.mat_mat_mul(eqst, eqs) - eqst_ans = mathutil.mat_mat_mul(eqst, ans) - return eqst_eqs, eqst_ans - def _build_sums(self, samples, num_le): - sum_le_z = sum_le_z2 = sum_le_freq = sum_le_freq_z = 0. - for z, freq in samples[:num_le]: - sum_le_z += z - sum_le_z2 += z**2 - sum_le_freq += freq - sum_le_freq_z += freq*z - sum_gt_z = sum_gt_z2 = sum_gt_z3 = sum_gt_z4 = 0. - sum_gt_freq = sum_gt_freq_z = sum_gt_freq_z2 = 0. - for z, freq in samples[num_le:]: - sum_gt_z += z - sum_gt_z2 += z**2 - sum_gt_z3 += z**3 - sum_gt_z4 += z**4 - sum_gt_freq += freq - sum_gt_freq_z += freq*z - sum_gt_freq_z2 += freq * z**2 - return (sum_le_z, sum_le_z2, sum_le_freq, sum_le_freq_z, - sum_gt_z, sum_gt_z2, sum_gt_z3, sum_gt_z4, - sum_gt_freq, sum_gt_freq_z, sum_gt_freq_z2) - def _build_ls_matrix_opt(self, samples, est_z_contact): - # This function is an optimized version of _build_ls_matrix() - num_le = bisect.bisect(samples, (est_z_contact, sys.float_info.max)) - # Check for previously calculated raw freq/z counters - sums = self._least_squares_cache.get(num_le) - if sums is None: - sums = self._build_sums(samples, num_le) - self._least_squares_cache[num_le] = sums - (sum_le_z, sum_le_z2, sum_le_freq, sum_le_freq_z, - sum_gt_z, sum_gt_z2, sum_gt_z3, sum_gt_z4, - sum_gt_freq, sum_gt_freq_z, sum_gt_freq_z2) = sums - num_samples = len(samples) - ezc = est_z_contact - ezc2 = ezc**2 - ezc3 = ezc**3 - ezc4 = ezc**4 - # Build matrices for least squares evaluation - eqst_eqs = [[0.] * 4 for i in range(4)] - eqst_eqs[0][0] = num_samples - eqst_eqs[1][1] = sum_le_z2 - 2*ezc*sum_le_z + num_le*ezc2 - eqst_eqs[2][2] = sum_gt_z2 + num_le*ezc2 - eqst_eqs[3][3] = sum_gt_z4 + num_le*ezc4 - eqst_eqs[0][1] = eqst_eqs[1][0] = sum_le_z - num_le*ezc - eqst_eqs[0][2] = eqst_eqs[2][0] = sum_gt_z + num_le*ezc - eqst_eqs[0][3] = eqst_eqs[3][0] = sum_gt_z2 + num_le*ezc2 - eqst_eqs[2][3] = eqst_eqs[3][2] = sum_gt_z3 + num_le*ezc3 - eqst_eqs[2][1] = eqst_eqs[1][2] = ezc * eqst_eqs[0][1] - eqst_eqs[3][1] = eqst_eqs[1][3] = ezc2 * eqst_eqs[0][1] - eqst_ans = [[0.] for i in range(4)] - eqst_ans[0][0] = sum_le_freq + sum_gt_freq - eqst_ans[1][0] = sum_le_freq_z - ezc*sum_le_freq - eqst_ans[2][0] = sum_gt_freq_z + ezc*sum_le_freq - eqst_ans[3][0] = sum_gt_freq_z2 + ezc2 * sum_le_freq - return eqst_eqs, eqst_ans - def _calc_least_squares(self, samples, est_z_contact): - eqst_eqs, eqst_ans = self._build_ls_matrix_opt(samples, est_z_contact) - coeffs = mathutil.gaussian_solve(eqst_eqs, eqst_ans) - if coeffs is not None and coeffs[3][0] < 0.: - # z**2 factor can't be negative - retry using only linear - alt_eqst_eqs = [ee[:3] for ee in eqst_eqs[:3]] - alt_eqst_ans = eqst_ans[:3] - coeffs = mathutil.gaussian_solve(alt_eqst_eqs, alt_eqst_ans) - if coeffs is not None: - coeffs = coeffs + [[0.]] - if coeffs is None: - return sys.float_info.max, [[0.]] * 4 - rel_err = -sum([c[0]*a[0] for c, a in zip(coeffs, eqst_ans)]) - return rel_err, coeffs - def _find_least_squares(self, data): - #for d in data: - # logging.info("sample: freq=%.3f z=%.6f", d[0], d[1][2]) - self._least_squares_cache.clear() - # Change base of freq/z measurements to improve numerical stability - base_z = .5 * (data[0][1][2] + data[-1][1][2]) - base_freq = .5 * (data[0][0] + data[-1][0]) - samples = [(d[1][2] - base_z, d[0] - base_freq) for d in data] - # Run least squares with various z values to reduce residual error - min_z = best_z = samples[0][0] - max_z = samples[-1][0] - best_err = sys.float_info.max - best_coeffs = [0., 0., 0., 0.] - while max_z - min_z > 0.000050: - # Select z value to check - mid_z = (min_z + max_z) * .5 - if best_z < mid_z: - guess_z = (best_z + max_z) * .5 - else: - guess_z = (min_z + best_z) * .5 - # Calculate least squares error for given z - guess_err, coeffs = self._calc_least_squares(samples, guess_z) - # Update search bounds - if guess_err < best_err: - if guess_z > best_z: - min_z = best_z - else: - max_z = best_z - best_z = guess_z - best_err = guess_err - best_coeffs = coeffs - else: - if guess_z > best_z: - max_z = guess_z - else: - min_z = guess_z - self._least_squares_cache.clear() - # Return to original freq/z measurement base - bc = [v[0] for v in best_coeffs] - final_coeffs = (base_z + best_z, - base_freq + bc[0] + best_z*bc[2] + best_z*best_z*bc[3], - bc[1], bc[2] + 2.*best_z*bc[3], bc[3]) - #logging.info("probe_analysis: coeffs=%s", final_coeffs) - return final_coeffs def _error_detect(self, msg): raise self._printer.command_error("Unable to detect tap: %s" % (msg,)) def _analyze_pullback(self, measures, start_time, end_time): @@ -838,7 +856,8 @@ def _analyze_pullback(self, measures, start_time, end_time): self._error_detect("insufficient lift (%.6f vs %.6f)" % (max_z - min_z, 0.350)) # Find best fit for extracted measurements - coeffs = self._find_least_squares(data) + tap_fit = TapBestFit() + coeffs = tap_fit.find_best_fit(data) z_contact, freq_contact, depress_slope, slope, slope2 = coeffs self._last_tap = ("fail", z_contact - min_z, coeffs) reactor.pause(0.) From c707dd19214709dc23684b254a68e3bf69e4cfb3 Mon Sep 17 00:00:00 2001 From: Kevin Blackburn-Matzen Date: Wed, 3 Jun 2026 22:43:32 -0700 Subject: [PATCH 11/12] sht3x: Fix CRC computation when high byte is zero The CRC helper took a 16-bit integer and split it into bytes with a _split_bytes() helper based on bit_length(). When the high byte was zero (a common case for in-range temperature and humidity readings) bit_length() reported 8 bits or fewer, so _split_bytes() returned a single byte and the CRC was computed over one byte instead of the two the sensor checksums. The computed CRC then mismatched the sensor's and the reading was logged as a checksum error. Replace the variable-length split with a fixed two-byte split so the checksum is always computed over the full two-byte value, matching the SHT3x datasheet. This also drops the now-unused _split_bytes() helper. Signed-off-by: Kevin Blackburn-Matzen --- klippy/extras/sht3x.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/klippy/extras/sht3x.py b/klippy/extras/sht3x.py index 8151bd46242a..b5e1fd9fb62e 100644 --- a/klippy/extras/sht3x.py +++ b/klippy/extras/sht3x.py @@ -155,18 +155,11 @@ def _sample_sht3x(self, eventtime): self._callback(print_time, self.temp) return measured_time + self.report_time - 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 SHT3X_CRC8_POLYNOMINAL= 0x31 crc = 0xFF - data_bytes = self._split_bytes(data) + data_bytes = [data >> 8 & 0xFF, data & 0xFF] for byte in data_bytes: crc ^= byte for _ in range(8): From d6ea62542d3f14a1faf55305c85ed0cbe361a233 Mon Sep 17 00:00:00 2001 From: freakyDude Date: Wed, 11 Feb 2026 17:51:02 +0100 Subject: [PATCH 12/12] docs: Corrected the z_thermal_adjust example The 'z_thermal_adjust' config section example was named and had a maximum deviation property missing. Signed-off-by: Frank Roth --- docs/Load_Cell.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Load_Cell.md b/docs/Load_Cell.md index f3ff11e9221e..21987ec666c2 100644 --- a/docs/Load_Cell.md +++ b/docs/Load_Cell.md @@ -309,11 +309,12 @@ Set up z_thermal_adjust to reference the `extruder` as the source of temperature data. E.g.: ``` -[z_thermal_adjust nozzle] +[z_thermal_adjust] temp_coeff=-0.00045455 sensor_type: temperature_combined sensor_list: extruder combination_method: max +maximum_deviation: 999 min_temp: 0 max_temp: 400 max_z_adjustment: 0.1