From 79930ed99a1fc284f41af5755908aa1fab948ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lepin?= Date: Sat, 11 May 2024 02:18:38 +0200 Subject: [PATCH 001/111] config: Add safe_z_home section for Creality CR-6 SE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Creality CR-6 SE has a strain gauge on its hotend used for z-probing and homing. Currently, running G28 to home all axes puts the hotend just outside of the print bed and thus assumes a wrong homing point for the Z axis. This change aims to address this issue by setting a safe Z-homing point (in the middle of the print bed) into the Creality CR-6 SE 2020 and 2021-revision config files. Signed-off-by: Stéphane Lepin --- config/printer-creality-cr6se-2020.cfg | 4 ++++ config/printer-creality-cr6se-2021.cfg | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/config/printer-creality-cr6se-2020.cfg b/config/printer-creality-cr6se-2020.cfg index 11d827eaf..2929a9ac4 100644 --- a/config/printer-creality-cr6se-2020.cfg +++ b/config/printer-creality-cr6se-2020.cfg @@ -98,6 +98,10 @@ z_offset: 0.0 speed: 2.0 samples: 5 +[safe_z_home] +home_xy_position: 117, 117 +z_hop: 10 + [filament_switch_sensor filament_sensor] pause_on_runout: true switch_pin: ^!PA7 diff --git a/config/printer-creality-cr6se-2021.cfg b/config/printer-creality-cr6se-2021.cfg index 12c17120f..932a9263d 100644 --- a/config/printer-creality-cr6se-2021.cfg +++ b/config/printer-creality-cr6se-2021.cfg @@ -98,6 +98,10 @@ z_offset: 0.0 speed: 2.0 samples: 5 +[safe_z_home] +home_xy_position: 117, 117 +z_hop: 10 + [filament_switch_sensor filament_sensor] pause_on_runout: true switch_pin: ^!PA7 From 8f510da12bf51a58205ebd81873ec8efbaa32c43 Mon Sep 17 00:00:00 2001 From: Dropeffect GmbH Date: Thu, 2 May 2024 10:39:38 +0100 Subject: [PATCH 002/111] stm32g4: Fix ADC3 common interface registers name to ADC345_COMMON Use ADC345_COMMON instead of ADC3_COMMON for stm32g4 ADC3 channel. Signed-off-by: Amr Elsayed from Dropeffect GmbH --- src/stm32/stm32h7_adc.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stm32/stm32h7_adc.c b/src/stm32/stm32h7_adc.c index e9dc8f845..3c217ca27 100644 --- a/src/stm32/stm32h7_adc.c +++ b/src/stm32/stm32h7_adc.c @@ -189,7 +189,11 @@ gpio_adc_setup(uint32_t pin) if (chan >= 2 * ADCIN_BANK_SIZE) { chan -= 2 * ADCIN_BANK_SIZE; adc = ADC3; +#if CONFIG_MACH_STM32G4 + adc_common = ADC345_COMMON; +#else adc_common = ADC3_COMMON; +#endif } else #endif #ifdef ADC2 From 80b55d352811c628bd0204fdda837acb9fce31d1 Mon Sep 17 00:00:00 2001 From: Dropeffect GmbH Date: Thu, 2 May 2024 11:25:08 +0100 Subject: [PATCH 003/111] stm32: Add FDCAN2 channel needed for stm32g4 alternate pins Some of the alternate pins defined are routed to FDCAN2 instead of FDCAN1, this commit uses the correct IRQ register and peripheral clock enable bit to enable FDCAN on those pins. Signed-off-by: Amr Elsayed from Dropeffect GmbH --- src/stm32/fdcan.c | 11 +++++++++-- src/stm32/stm32g4.c | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/stm32/fdcan.c b/src/stm32/fdcan.c index b0e8c01d1..5344d26b1 100644 --- a/src/stm32/fdcan.c +++ b/src/stm32/fdcan.c @@ -60,16 +60,23 @@ || CONFIG_STM32_CANBUS_PB5_PB6 ||CONFIG_STM32_CANBUS_PB12_PB13) #define SOC_CAN FDCAN1 #define MSG_RAM (((struct fdcan_ram_layout*)SRAMCAN_BASE)->fdcan1) + #if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4 + #define CAN_IT0_IRQn FDCAN1_IT0_IRQn + #endif #else #define SOC_CAN FDCAN2 #define MSG_RAM (((struct fdcan_ram_layout*)SRAMCAN_BASE)->fdcan2) + #if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4 + #define CAN_IT0_IRQn FDCAN2_IT0_IRQn + #endif #endif #if CONFIG_MACH_STM32G0 #define CAN_IT0_IRQn TIM16_FDCAN_IT0_IRQn #define CAN_FUNCTION GPIO_FUNCTION(3) // Alternative function mapping number -#elif CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4 - #define CAN_IT0_IRQn FDCAN1_IT0_IRQn +#endif + +#if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4 #define CAN_FUNCTION GPIO_FUNCTION(9) // Alternative function mapping number #endif diff --git a/src/stm32/stm32g4.c b/src/stm32/stm32g4.c index 139ea8eaa..1eed3ec18 100644 --- a/src/stm32/stm32g4.c +++ b/src/stm32/stm32g4.c @@ -22,6 +22,12 @@ lookup_clock_line(uint32_t periph_base) if (periph_base < APB2PERIPH_BASE) { uint32_t pos = (periph_base - APB1PERIPH_BASE) / 0x400; if (pos < 32) { +#if defined(FDCAN2_BASE) + if (periph_base == FDCAN2_BASE) + return (struct cline){.en = &RCC->APB1ENR1, + .rst = &RCC->APB1RSTR1, + .bit = 1 << 25}; +#endif return (struct cline){.en = &RCC->APB1ENR1, .rst = &RCC->APB1RSTR1, .bit = 1 << pos}; From 472d8e5b669de075d6d94b461b5f08d5901d3bda Mon Sep 17 00:00:00 2001 From: Dropeffect GmbH Date: Thu, 2 May 2024 11:42:30 +0100 Subject: [PATCH 004/111] stm32: Add STM32G474 chip to Kconfig Signed-off-by: Amr Elsayed from Dropeffect GmbH --- src/stm32/Kconfig | 7 +++++++ test/configs/stm32g474.config | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 test/configs/stm32g474.config diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index d14622a25..dbd6ff959 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -88,6 +88,9 @@ choice config MACH_STM32G431 bool "STM32G431" select MACH_STM32G4 + config MACH_STM32G474 + bool "STM32G474" + select MACH_STM32G4 config MACH_STM32H723 bool "STM32H723" select MACH_STM32H7 @@ -181,6 +184,7 @@ config MCU default "stm32g0b0xx" if MACH_STM32G0B0 default "stm32g0b1xx" if MACH_STM32G0B1 default "stm32g431xx" if MACH_STM32G431 + default "stm32g474xx" if MACH_STM32G474 default "stm32h723xx" if MACH_STM32H723 default "stm32h743xx" if MACH_STM32H743 default "stm32h750xx" if MACH_STM32H750 @@ -199,6 +203,7 @@ config CLOCK_FREQ default 216000000 if MACH_STM32F765 default 64000000 if MACH_STM32G0 default 150000000 if MACH_STM32G431 + default 170000000 if MACH_STM32G474 default 400000000 if MACH_STM32H7 # 400Mhz is max Klipper currently supports default 80000000 if MACH_STM32L412 default 64000000 if MACH_N32G45x && STM32_CLOCK_REF_INTERNAL @@ -213,6 +218,7 @@ config FLASH_SIZE default 0x40000 if MACH_STM32F2 || MACH_STM32F401 || MACH_STM32H723 default 0x80000 if MACH_STM32F4x5 || MACH_STM32F446 default 0x20000 if MACH_STM32G0 || MACH_STM32G431 + default 0x40000 if MACH_STM32G474 default 0x20000 if MACH_STM32H750 default 0x200000 if MACH_STM32H743 || MACH_STM32F765 default 0x20000 if MACH_N32G45x @@ -233,6 +239,7 @@ config RAM_SIZE default 0x2800 if MACH_STM32F103x6 default 0x5000 if MACH_STM32F103 && !MACH_STM32F103x6 # Ram size of stm32f103x8 default 0x8000 if MACH_STM32G431 + default 0x20000 if MACH_STM32G474 default 0xa000 if MACH_STM32L412 default 0x20000 if MACH_STM32F207 default 0x10000 if MACH_STM32F401 diff --git a/test/configs/stm32g474.config b/test/configs/stm32g474.config new file mode 100644 index 000000000..da38a9179 --- /dev/null +++ b/test/configs/stm32g474.config @@ -0,0 +1,3 @@ +# Base config file for STM32G474 ARM processor +CONFIG_MACH_STM32=y +CONFIG_MACH_STM32G474=y From f01c8853ca5c4f7d89ea1ae9a33779fe9c0446b1 Mon Sep 17 00:00:00 2001 From: Alex Voinea Date: Mon, 13 May 2024 22:31:39 +0200 Subject: [PATCH 005/111] tmc: Do not pass the frequency directly to the helpers Use the new get_tmc_frequency() instead. Signed-off-by: Alex Voinea --- klippy/extras/tmc.py | 5 +++-- klippy/extras/tmc2130.py | 2 +- klippy/extras/tmc2208.py | 2 +- klippy/extras/tmc2209.py | 2 +- klippy/extras/tmc2240.py | 2 +- klippy/extras/tmc5160.py | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 8143882ab..4ce9466a4 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -592,7 +592,7 @@ def TMCtstepHelper(step_dist, mres, tmc_freq, velocity): return 0xfffff # Helper to configure stealthChop-spreadCycle transition velocity -def TMCStealthchopHelper(config, mcu_tmc, tmc_freq): +def TMCStealthchopHelper(config, mcu_tmc): fields = mcu_tmc.get_fields() en_pwm_mode = False velocity = config.getfloat('stealthchop_threshold', None, minval=0.) @@ -606,7 +606,8 @@ def TMCStealthchopHelper(config, mcu_tmc, tmc_freq): rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig) step_dist = rotation_dist / steps_per_rotation mres = fields.get_field("mres") - tpwmthrs = TMCtstepHelper(step_dist, mres, tmc_freq, velocity) + tpwmthrs = TMCtstepHelper(step_dist, mres, mcu_tmc.get_tmc_frequency(), + velocity) fields.set_field("tpwmthrs", tpwmthrs) reg = fields.lookup_register("en_pwm_mode", None) diff --git a/klippy/extras/tmc2130.py b/klippy/extras/tmc2130.py index 62a9abbfe..3da346b4e 100644 --- a/klippy/extras/tmc2130.py +++ b/klippy/extras/tmc2130.py @@ -296,7 +296,7 @@ class TMC2130: self.get_status = cmdhelper.get_status # Setup basic register values tmc.TMCWaveTableHelper(config, self.mcu_tmc) - tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY) + tmc.TMCStealthchopHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # CHOPCONF diff --git a/klippy/extras/tmc2208.py b/klippy/extras/tmc2208.py index 421c53781..86476acc5 100644 --- a/klippy/extras/tmc2208.py +++ b/klippy/extras/tmc2208.py @@ -197,7 +197,7 @@ class TMC2208: self.get_status = cmdhelper.get_status # Setup basic register values self.fields.set_field("mstep_reg_select", True) - tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY) + tmc.TMCStealthchopHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF diff --git a/klippy/extras/tmc2209.py b/klippy/extras/tmc2209.py index c248c2d68..1149cdc21 100644 --- a/klippy/extras/tmc2209.py +++ b/klippy/extras/tmc2209.py @@ -73,7 +73,7 @@ class TMC2209: self.get_status = cmdhelper.get_status # Setup basic register values self.fields.set_field("mstep_reg_select", True) - tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY) + tmc.TMCStealthchopHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF diff --git a/klippy/extras/tmc2240.py b/klippy/extras/tmc2240.py index 14d5dd918..f4c75aa99 100644 --- a/klippy/extras/tmc2240.py +++ b/klippy/extras/tmc2240.py @@ -364,7 +364,7 @@ class TMC2240: # Setup basic register values tmc.TMCWaveTableHelper(config, self.mcu_tmc) self.fields.set_config_field(config, "offset_sin90", 0) - tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY) + tmc.TMCStealthchopHelper(config, self.mcu_tmc) set_config_field = self.fields.set_config_field # GCONF set_config_field(config, "multistep_filt", True) diff --git a/klippy/extras/tmc5160.py b/klippy/extras/tmc5160.py index 7ff47abf9..e37435e6b 100644 --- a/klippy/extras/tmc5160.py +++ b/klippy/extras/tmc5160.py @@ -335,7 +335,7 @@ class TMC5160: self.get_status = cmdhelper.get_status # Setup basic register values tmc.TMCWaveTableHelper(config, self.mcu_tmc) - tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY) + tmc.TMCStealthchopHelper(config, self.mcu_tmc) set_config_field = self.fields.set_config_field # GCONF set_config_field(config, "multistep_filt", True) From 1ca1054957bef8acc76c0e23955dcca54bf17bc8 Mon Sep 17 00:00:00 2001 From: Alex Voinea Date: Wed, 22 Mar 2023 19:36:35 +0100 Subject: [PATCH 006/111] tmc2130: implement missing HighVelocity fields in the config Signed-off-by: Alex Voinea --- docs/Config_Reference.md | 2 ++ klippy/extras/tmc2130.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index b66140552..7516122fa 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3347,6 +3347,8 @@ run_current: #driver_TOFF: 4 #driver_HEND: 7 #driver_HSTRT: 0 +#driver_VHIGHFS: 0 +#driver_VHIGHCHM: 0 #driver_PWM_AUTOSCALE: True #driver_PWM_FREQ: 1 #driver_PWM_GRAD: 4 diff --git a/klippy/extras/tmc2130.py b/klippy/extras/tmc2130.py index 3da346b4e..98cfea53e 100644 --- a/klippy/extras/tmc2130.py +++ b/klippy/extras/tmc2130.py @@ -304,6 +304,8 @@ class TMC2130: set_config_field(config, "hstrt", 0) set_config_field(config, "hend", 7) set_config_field(config, "tbl", 1) + set_config_field(config, "vhighfs", 0) + set_config_field(config, "vhighchm", 0) # COOLCONF set_config_field(config, "sgt", 0) # IHOLDIRUN From 0f3f29101ca49e913354811430d43776f13bcdbb Mon Sep 17 00:00:00 2001 From: Alex Voinea Date: Thu, 23 Mar 2023 21:09:53 +0100 Subject: [PATCH 007/111] tmc: Implement CoolStep fields for all drivers Signed-off-by: Alex Voinea --- docs/Config_Reference.md | 11 +++++++++++ klippy/extras/tmc2130.py | 6 ++++++ klippy/extras/tmc2209.py | 6 ++++++ 3 files changed, 23 insertions(+) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 7516122fa..ee09bf2c0 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3354,6 +3354,12 @@ run_current: #driver_PWM_GRAD: 4 #driver_PWM_AMPL: 128 #driver_SGT: 0 +#driver_SEMIN: 0 +#driver_SEUP: 0 +#driver_SEMAX: 0 +#driver_SEDN: 0 +#driver_SEIMIN: 0 +#driver_SFILT: 0 # Set the given register during the configuration of the TMC2130 # chip. This may be used to set custom motor parameters. The # defaults for each parameter are next to the parameter name in the @@ -3469,6 +3475,11 @@ run_current: #driver_PWM_GRAD: 14 #driver_PWM_OFS: 36 #driver_SGTHRS: 0 +#driver_SEMIN: 0 +#driver_SEUP: 0 +#driver_SEMAX: 0 +#driver_SEDN: 0 +#driver_SEIMIN: 0 # Set the given register during the configuration of the TMC2209 # chip. This may be used to set custom motor parameters. The # defaults for each parameter are next to the parameter name in the diff --git a/klippy/extras/tmc2130.py b/klippy/extras/tmc2130.py index 98cfea53e..f995fb7ab 100644 --- a/klippy/extras/tmc2130.py +++ b/klippy/extras/tmc2130.py @@ -307,7 +307,13 @@ class TMC2130: set_config_field(config, "vhighfs", 0) set_config_field(config, "vhighchm", 0) # COOLCONF + set_config_field(config, "semin", 0) + set_config_field(config, "seup", 0) + set_config_field(config, "semax", 0) + set_config_field(config, "sedn", 0) + set_config_field(config, "seimin", 0) set_config_field(config, "sgt", 0) + set_config_field(config, "sfilt", 0) # IHOLDIRUN set_config_field(config, "iholddelay", 8) # PWMCONF diff --git a/klippy/extras/tmc2209.py b/klippy/extras/tmc2209.py index 1149cdc21..ac997a81c 100644 --- a/klippy/extras/tmc2209.py +++ b/klippy/extras/tmc2209.py @@ -83,6 +83,12 @@ class TMC2209: set_config_field(config, "hstrt", 5) set_config_field(config, "hend", 0) set_config_field(config, "tbl", 2) + # COOLCONF + set_config_field(config, "semin", 0) + set_config_field(config, "seup", 0) + set_config_field(config, "semax", 0) + set_config_field(config, "sedn", 0) + set_config_field(config, "seimin", 0) # IHOLDIRUN set_config_field(config, "iholddelay", 8) # PWMCONF From 5249d955bb0a4ee131cc3def5d9d53d61e2d4334 Mon Sep 17 00:00:00 2001 From: Alex Voinea Date: Tue, 14 May 2024 22:21:58 +0200 Subject: [PATCH 008/111] tmc: Implement coolstep_threshold for drivers that support it Signed-off-by: Alex Voinea --- docs/Config_Reference.md | 28 ++++++++++++++++++++++++++++ klippy/extras/tmc.py | 16 ++++++++++++++++ klippy/extras/tmc2130.py | 1 + klippy/extras/tmc2209.py | 1 + klippy/extras/tmc2240.py | 2 ++ klippy/extras/tmc5160.py | 2 ++ 6 files changed, 50 insertions(+) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index ee09bf2c0..d48715d87 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3317,6 +3317,13 @@ run_current: # set, "stealthChop" mode will be enabled if the stepper motor # velocity is below this value. The default is 0, which disables # "stealthChop" mode. +#coolstep_threshold: +# The velocity (in mm/s) to set the TMC driver internal "CoolStep" +# threshold to. When set, the coolstep feature will be enabled if +# the stepper motor velocity is near or above this value. Important +# - if coolstep_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is above the coolstep +# threshold! The default is to not enable the coolstep feature. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 @@ -3456,6 +3463,13 @@ run_current: #sense_resistor: 0.110 #stealthchop_threshold: 0 # See the "tmc2208" section for the definition of these parameters. +#coolstep_threshold: +# The velocity (in mm/s) to set the TMC driver internal "CoolStep" +# threshold to. When set, the coolstep feature will be enabled if +# the stepper motor velocity is near or above this value. Important +# - if coolstep_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is above the coolstep +# threshold! The default is to not enable the coolstep feature. #uart_address: # The address of the TMC2209 chip for UART messages (an integer # between 0 and 3). This is typically used when multiple TMC2209 @@ -3614,6 +3628,13 @@ run_current: # set, "stealthChop" mode will be enabled if the stepper motor # velocity is below this value. The default is 0, which disables # "stealthChop" mode. +#coolstep_threshold: +# The velocity (in mm/s) to set the TMC driver internal "CoolStep" +# threshold to. When set, the coolstep feature will be enabled if +# the stepper motor velocity is near or above this value. Important +# - if coolstep_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is above the coolstep +# threshold! The default is to not enable the coolstep feature. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 @@ -3735,6 +3756,13 @@ run_current: # set, "stealthChop" mode will be enabled if the stepper motor # velocity is below this value. The default is 0, which disables # "stealthChop" mode. +#coolstep_threshold: +# The velocity (in mm/s) to set the TMC driver internal "CoolStep" +# threshold to. When set, the coolstep feature will be enabled if +# the stepper motor velocity is near or above this value. Important +# - if coolstep_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is above the coolstep +# threshold! The default is to not enable the coolstep feature. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 4ce9466a4..e7c0239f0 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -616,3 +616,19 @@ def TMCStealthchopHelper(config, mcu_tmc): else: # TMC2208 uses en_spreadCycle fields.set_field("en_spreadcycle", not en_pwm_mode) + +# Helper to configure StallGuard and CoolStep minimum velocity +def TMCVcoolthrsHelper(config, mcu_tmc): + fields = mcu_tmc.get_fields() + velocity = config.getfloat('coolstep_threshold', None, minval=0.) + tcoolthrs = 0 + + if velocity is not None: + stepper_name = " ".join(config.get_name().split()[1:]) + sconfig = config.getsection(stepper_name) + rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig) + step_dist = rotation_dist / steps_per_rotation + mres = fields.get_field("mres") + tcoolthrs = TMCtstepHelper(step_dist, mres, + mcu_tmc.get_tmc_frequency(), velocity) + fields.set_field("tcoolthrs", tcoolthrs) diff --git a/klippy/extras/tmc2130.py b/klippy/extras/tmc2130.py index f995fb7ab..44d4342ba 100644 --- a/klippy/extras/tmc2130.py +++ b/klippy/extras/tmc2130.py @@ -297,6 +297,7 @@ class TMC2130: # Setup basic register values tmc.TMCWaveTableHelper(config, self.mcu_tmc) tmc.TMCStealthchopHelper(config, self.mcu_tmc) + tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # CHOPCONF diff --git a/klippy/extras/tmc2209.py b/klippy/extras/tmc2209.py index ac997a81c..fbd8d1c10 100644 --- a/klippy/extras/tmc2209.py +++ b/klippy/extras/tmc2209.py @@ -74,6 +74,7 @@ class TMC2209: # Setup basic register values self.fields.set_field("mstep_reg_select", True) tmc.TMCStealthchopHelper(config, self.mcu_tmc) + tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF diff --git a/klippy/extras/tmc2240.py b/klippy/extras/tmc2240.py index f4c75aa99..3989b9021 100644 --- a/klippy/extras/tmc2240.py +++ b/klippy/extras/tmc2240.py @@ -365,6 +365,8 @@ class TMC2240: tmc.TMCWaveTableHelper(config, self.mcu_tmc) self.fields.set_config_field(config, "offset_sin90", 0) tmc.TMCStealthchopHelper(config, self.mcu_tmc) + tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) + # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF set_config_field(config, "multistep_filt", True) diff --git a/klippy/extras/tmc5160.py b/klippy/extras/tmc5160.py index e37435e6b..97d2d6f94 100644 --- a/klippy/extras/tmc5160.py +++ b/klippy/extras/tmc5160.py @@ -336,6 +336,8 @@ class TMC5160: # Setup basic register values tmc.TMCWaveTableHelper(config, self.mcu_tmc) tmc.TMCStealthchopHelper(config, self.mcu_tmc) + tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) + # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF set_config_field(config, "multistep_filt", True) From ed8dca8df08924e7df9291d5e4ff88c9fee4ca4e Mon Sep 17 00:00:00 2001 From: Alex Voinea Date: Tue, 14 May 2024 22:23:06 +0200 Subject: [PATCH 009/111] tmc: Implement high_velocity_threshold for drivers that support it Signed-off-by: Alex Voinea --- docs/Config_Reference.md | 24 ++++++++++++++++++++++++ klippy/extras/tmc.py | 17 +++++++++++++++++ klippy/extras/tmc2130.py | 1 + klippy/extras/tmc2240.py | 1 + klippy/extras/tmc5160.py | 4 ++++ 5 files changed, 47 insertions(+) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index d48715d87..9a26556f0 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3324,6 +3324,14 @@ run_current: # - if coolstep_threshold is set and "sensorless homing" is used, # then one must ensure that the homing speed is above the coolstep # threshold! The default is to not enable the coolstep feature. +#high_velocity_threshold: +# The velocity (in mm/s) to set the TMC driver internal "high +# velocity" threshold (THIGH) to. This is typically used to disable +# the "CoolStep" feature at high speeds. Important - if +# high_velocity_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is below the high +# velocity threshold! The default is to not set a TMC "high +# velocity" threshold. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 @@ -3635,6 +3643,14 @@ run_current: # - if coolstep_threshold is set and "sensorless homing" is used, # then one must ensure that the homing speed is above the coolstep # threshold! The default is to not enable the coolstep feature. +#high_velocity_threshold: +# The velocity (in mm/s) to set the TMC driver internal "high +# velocity" threshold (THIGH) to. This is typically used to disable +# the "CoolStep" feature at high speeds. Important - if +# high_velocity_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is below the high +# velocity threshold! The default is to not set a TMC "high +# velocity" threshold. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 @@ -3763,6 +3779,14 @@ run_current: # - if coolstep_threshold is set and "sensorless homing" is used, # then one must ensure that the homing speed is above the coolstep # threshold! The default is to not enable the coolstep feature. +#high_velocity_threshold: +# The velocity (in mm/s) to set the TMC driver internal "high +# velocity" threshold (THIGH) to. This is typically used to disable +# the "CoolStep" feature at high speeds. Important - if +# high_velocity_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is below the high +# velocity threshold! The default is to not set a TMC "high +# velocity" threshold. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index e7c0239f0..366e00b15 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -632,3 +632,20 @@ def TMCVcoolthrsHelper(config, mcu_tmc): tcoolthrs = TMCtstepHelper(step_dist, mres, mcu_tmc.get_tmc_frequency(), velocity) fields.set_field("tcoolthrs", tcoolthrs) + +# Helper to configure StallGuard and CoolStep maximum velocity and +# SpreadCycle-FullStepping (High velocity) mode threshold. +def TMCVhighHelper(config, mcu_tmc): + fields = mcu_tmc.get_fields() + velocity = config.getfloat('high_velocity_threshold', None, minval=0.) + thigh = 0 + + if velocity is not None: + stepper_name = " ".join(config.get_name().split()[1:]) + sconfig = config.getsection(stepper_name) + rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig) + step_dist = rotation_dist / steps_per_rotation + mres = fields.get_field("mres") + thigh = TMCtstepHelper(step_dist, mres, + mcu_tmc.get_tmc_frequency(), velocity) + fields.set_field("thigh", thigh) diff --git a/klippy/extras/tmc2130.py b/klippy/extras/tmc2130.py index 44d4342ba..20a25c66c 100644 --- a/klippy/extras/tmc2130.py +++ b/klippy/extras/tmc2130.py @@ -298,6 +298,7 @@ class TMC2130: tmc.TMCWaveTableHelper(config, self.mcu_tmc) tmc.TMCStealthchopHelper(config, self.mcu_tmc) tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) + tmc.TMCVhighHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # CHOPCONF diff --git a/klippy/extras/tmc2240.py b/klippy/extras/tmc2240.py index 3989b9021..1e2a49157 100644 --- a/klippy/extras/tmc2240.py +++ b/klippy/extras/tmc2240.py @@ -366,6 +366,7 @@ class TMC2240: self.fields.set_config_field(config, "offset_sin90", 0) tmc.TMCStealthchopHelper(config, self.mcu_tmc) tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) + tmc.TMCVhighHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF diff --git a/klippy/extras/tmc5160.py b/klippy/extras/tmc5160.py index 97d2d6f94..02e16cd4d 100644 --- a/klippy/extras/tmc5160.py +++ b/klippy/extras/tmc5160.py @@ -242,6 +242,9 @@ Fields["TCOOLTHRS"] = { Fields["TSTEP"] = { "tstep": 0xfffff << 0 } +Fields["THIGH"] = { + "thigh": 0xfffff << 0 +} SignedFields = ["cur_a", "cur_b", "sgt", "xactual", "vactual", "pwm_scale_auto"] @@ -337,6 +340,7 @@ class TMC5160: tmc.TMCWaveTableHelper(config, self.mcu_tmc) tmc.TMCStealthchopHelper(config, self.mcu_tmc) tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) + tmc.TMCVhighHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF From e0cbd7b5fc1ce6d1dfbc8daf8e59f57bf3c5e5b9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 14 May 2024 18:22:58 -0400 Subject: [PATCH 010/111] docs: Minor wording change to coolstep_threshold in Config_Reference.md Signed-off-by: Kevin O'Connor --- docs/Config_Reference.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 9a26556f0..9d5cb763c 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3319,7 +3319,7 @@ run_current: # "stealthChop" mode. #coolstep_threshold: # The velocity (in mm/s) to set the TMC driver internal "CoolStep" -# threshold to. When set, the coolstep feature will be enabled if +# threshold to. If set, the coolstep feature will be enabled when # the stepper motor velocity is near or above this value. Important # - if coolstep_threshold is set and "sensorless homing" is used, # then one must ensure that the homing speed is above the coolstep @@ -3473,7 +3473,7 @@ run_current: # See the "tmc2208" section for the definition of these parameters. #coolstep_threshold: # The velocity (in mm/s) to set the TMC driver internal "CoolStep" -# threshold to. When set, the coolstep feature will be enabled if +# threshold to. If set, the coolstep feature will be enabled when # the stepper motor velocity is near or above this value. Important # - if coolstep_threshold is set and "sensorless homing" is used, # then one must ensure that the homing speed is above the coolstep @@ -3638,7 +3638,7 @@ run_current: # "stealthChop" mode. #coolstep_threshold: # The velocity (in mm/s) to set the TMC driver internal "CoolStep" -# threshold to. When set, the coolstep feature will be enabled if +# threshold to. If set, the coolstep feature will be enabled when # the stepper motor velocity is near or above this value. Important # - if coolstep_threshold is set and "sensorless homing" is used, # then one must ensure that the homing speed is above the coolstep @@ -3774,7 +3774,7 @@ run_current: # "stealthChop" mode. #coolstep_threshold: # The velocity (in mm/s) to set the TMC driver internal "CoolStep" -# threshold to. When set, the coolstep feature will be enabled if +# threshold to. If set, the coolstep feature will be enabled when # the stepper motor velocity is near or above this value. Important # - if coolstep_threshold is set and "sensorless homing" is used, # then one must ensure that the homing speed is above the coolstep From dae8b8cacff583b180c650d9a6d1601907a12f1e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 15 May 2024 11:22:22 -0400 Subject: [PATCH 011/111] docs: Update jinja2 requirement in mkdocs-requirements.txt Update the jinja2 version to suppress security warnings. Klipper is not impacted by the vulnerability, but it is harmless to update the version. Signed-off-by: Kevin O'Connor --- docs/_klipper3d/mkdocs-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_klipper3d/mkdocs-requirements.txt b/docs/_klipper3d/mkdocs-requirements.txt index 739288959..96bf60051 100644 --- a/docs/_klipper3d/mkdocs-requirements.txt +++ b/docs/_klipper3d/mkdocs-requirements.txt @@ -1,5 +1,5 @@ # Python virtualenv module requirements for mkdocs -jinja2==3.1.3 +jinja2==3.1.4 mkdocs==1.2.4 mkdocs-material==8.1.3 mkdocs-simple-hooks==0.1.3 From 694d38c79105f2d33242487faa0532d1291ee7b6 Mon Sep 17 00:00:00 2001 From: voidtrance <30448940+voidtrance@users.noreply.github.com> Date: Wed, 15 May 2024 18:38:42 -0700 Subject: [PATCH 012/111] bed_mesh: Fix adaptive probe count on delta printers (#6600) Round beds require an odd number of probe points in order to prevent erroneously truncating the mesh. The adaptive mesh algorithm did not consider that and as a result, it was possible to generate adaptive meshes with even number of probe points. This change fixes this by increasing the probe point count by 1 in cases where the adaptive probe points are even. Signed-off-by: Mitko Haralanov --- klippy/extras/bed_mesh.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 87f2324a4..095ccf1fd 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -652,8 +652,11 @@ class BedMeshCalibrate: self.origin = adapted_origin self.mesh_min = (-self.radius, -self.radius) self.mesh_max = (self.radius, self.radius) + new_probe_count = max(new_x_probe_count, new_y_probe_count) + # Adaptive meshes require odd number of points + new_probe_count += 1 - (new_probe_count % 2) self.mesh_config["x_count"] = self.mesh_config["y_count"] = \ - max(new_x_probe_count, new_y_probe_count) + new_probe_count else: self.mesh_min = adjusted_mesh_min self.mesh_max = adjusted_mesh_max From b7f7b8a346388cc32d80b6e6f60e5fdb4cbd3ce6 Mon Sep 17 00:00:00 2001 From: Frans-willem Hardijzer Date: Wed, 7 Feb 2024 20:25:36 +0100 Subject: [PATCH 013/111] idex_modes: Bugfix for kinematic position calculation. idex_mode would swap the X and dual-carriage rail in some cases (homing), but not in others. As such, the position calculation was correct while homing, but incorrect for the second carriage during normal moves. This commit fixes homing to work without swapped rails, removes the swapping of rails while homing, and removes the ability to swap rails (as it is now no longer used). Fix has been tested in a Hybrid_CoreXY IDEX printer (Voron Double Dragon). Hybrid_CoreXZ has identical changes and is similar enough that I am confident it will work as intended. Changes to cartesion seem simple enough, but would benefit from someone running a couple of tests. Signed-off-by: Frans-Willem Hardijzer --- klippy/kinematics/cartesian.py | 17 ++++++++++++----- klippy/kinematics/hybrid_corexy.py | 12 +++++++----- klippy/kinematics/hybrid_corexz.py | 12 +++++++----- klippy/kinematics/idex_modes.py | 15 ++++++++------- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index 6d576b5b5..9774672e2 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -52,20 +52,27 @@ class CartKinematics: def get_steppers(self): return [s for rail in self.rails for s in rail.get_steppers()] def calc_position(self, stepper_positions): - return [stepper_positions[rail.get_name()] for rail in self.rails] + rails = self.rails + if self.dc_module: + primary_rail = self.dc_module.get_primary_rail().get_rail() + rails = (rails[:self.dc_module.axis] + + [primary_rail] + rails[self.dc_module.axis+1:]) + return [stepper_positions[rail.get_name()] for rail in rails] def update_limits(self, i, range): l, h = self.limits[i] # Only update limits if this axis was already homed, # otherwise leave in un-homed state. if l <= h: self.limits[i] = range - def override_rail(self, i, rail): - self.rails[i] = rail def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - if i in homing_axes: - self.limits[i] = rail.get_range() + for axis in homing_axes: + if self.dc_module and axis == self.dc_module.axis: + rail = self.dc_module.get_primary_rail().get_rail() + else: + rail = self.rails[axis] + self.limits[axis] = rail.get_range() def note_z_not_homed(self): # Helper for Safe Z Home self.limits[2] = (1.0, -1.0) diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index 1c2164eb7..e852826af 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -57,7 +57,7 @@ class HybridCoreXYKinematics: pos = [stepper_positions[rail.get_name()] for rail in self.rails] if (self.dc_module is not None and 'PRIMARY' == \ self.dc_module.get_status()['carriage_1']): - return [pos[0] - pos[1], pos[1], pos[2]] + return [pos[3] - pos[1], pos[1], pos[2]] else: return [pos[0] + pos[1], pos[1], pos[2]] def update_limits(self, i, range): @@ -66,13 +66,15 @@ class HybridCoreXYKinematics: # otherwise leave in un-homed state. if l <= h: self.limits[i] = range - def override_rail(self, i, rail): - self.rails[i] = rail def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - if i in homing_axes: - self.limits[i] = rail.get_range() + for axis in homing_axes: + if self.dc_module and axis == self.dc_module.axis: + rail = self.dc_module.get_primary_rail().get_rail() + else: + rail = self.rails[axis] + self.limits[axis] = rail.get_range() def note_z_not_homed(self): # Helper for Safe Z Home self.limits[2] = (1.0, -1.0) diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index 0eaea117e..58e6b0d39 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -57,7 +57,7 @@ class HybridCoreXZKinematics: pos = [stepper_positions[rail.get_name()] for rail in self.rails] if (self.dc_module is not None and 'PRIMARY' == \ self.dc_module.get_status()['carriage_1']): - return [pos[0] - pos[2], pos[1], pos[2]] + return [pos[3] - pos[2], pos[1], pos[2]] else: return [pos[0] + pos[2], pos[1], pos[2]] def update_limits(self, i, range): @@ -66,13 +66,15 @@ class HybridCoreXZKinematics: # otherwise leave in un-homed state. if l <= h: self.limits[i] = range - def override_rail(self, i, rail): - self.rails[i] = rail def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - if i in homing_axes: - self.limits[i] = rail.get_range() + for axis in homing_axes: + if self.dc_module and axis == self.dc_module.axis: + rail = self.dc_module.get_primary_rail().get_rail() + else: + rail = self.rails[axis] + self.limits[axis] = rail.get_range() def note_z_not_homed(self): # Helper for Safe Z Home self.limits[2] = (1.0, -1.0) diff --git a/klippy/kinematics/idex_modes.py b/klippy/kinematics/idex_modes.py index 2ce91afe8..f2618d080 100644 --- a/klippy/kinematics/idex_modes.py +++ b/klippy/kinematics/idex_modes.py @@ -42,7 +42,12 @@ class DualCarriages: desc=self.cmd_RESTORE_DUAL_CARRIAGE_STATE_help) def get_rails(self): return self.dc - def toggle_active_dc_rail(self, index, override_rail=False): + def get_primary_rail(self): + for rail in self.dc: + if rail.mode == PRIMARY: + return rail + return None + def toggle_active_dc_rail(self, index): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() pos = toolhead.get_position() @@ -52,15 +57,11 @@ class DualCarriages: if i != index: if dc.is_active(): dc.inactivate(pos) - if override_rail: - kin.override_rail(3, dc_rail) target_dc = self.dc[index] if target_dc.mode != PRIMARY: newpos = pos[:self.axis] + [target_dc.get_axis_position(pos)] \ + pos[self.axis+1:] target_dc.activate(PRIMARY, newpos, old_position=pos) - if override_rail: - kin.override_rail(self.axis, target_dc.get_rail()) toolhead.set_position(newpos) kin.update_limits(self.axis, target_dc.get_rail().get_range()) def home(self, homing_state): @@ -72,10 +73,10 @@ class DualCarriages: # the same direction and the first carriage homes on the second one enumerated_dcs.reverse() for i, dc_rail in enumerated_dcs: - self.toggle_active_dc_rail(i, override_rail=True) + self.toggle_active_dc_rail(i) kin.home_axis(homing_state, self.axis, dc_rail.get_rail()) # Restore the original rails ordering - self.toggle_active_dc_rail(0, override_rail=True) + self.toggle_active_dc_rail(0) def get_status(self, eventtime=None): return {('carriage_%d' % (i,)) : dc.mode for (i, dc) in enumerate(self.dc)} From faee2c0e52506f157d38b747613df213d659226a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 14 May 2024 18:37:38 -0400 Subject: [PATCH 014/111] tmc: Refactor TMCtstepHelper() Update TMCtstepHelper() to obtain the step_distance, tmc_frequency, and mres fields directly. Signed-off-by: Kevin O'Connor --- klippy/extras/tmc.py | 59 +++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 366e00b15..1a5059fa8 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -278,16 +278,14 @@ class TMCCommandHelper: raise gcmd.error("Unknown field name '%s'" % (field_name,)) value = gcmd.get_int('VALUE', None) velocity = gcmd.get_float('VELOCITY', None, minval=0.) - tmc_frequency = self.mcu_tmc.get_tmc_frequency() - if tmc_frequency is None and velocity is not None: - raise gcmd.error("VELOCITY parameter not supported by this driver") if (value is None) == (velocity is None): raise gcmd.error("Specify either VALUE or VELOCITY") if velocity is not None: - step_dist = self.stepper.get_step_dist() - mres = self.fields.get_field("mres") - value = TMCtstepHelper(step_dist, mres, tmc_frequency, - velocity) + if self.mcu_tmc.get_tmc_frequency() is None: + raise gcmd.error( + "VELOCITY parameter not supported by this driver") + value = TMCtstepHelper(self.mcu_tmc, velocity, + pstepper=self.stepper) reg_val = self.fields.set_field(field_name, value) print_time = self.printer.lookup_object('toolhead').get_last_move_time() self.mcu_tmc.set_register(reg_name, reg_val, print_time) @@ -583,13 +581,21 @@ def TMCMicrostepHelper(config, mcu_tmc): fields.set_field("intpol", config.getboolean("interpolate", True)) # Helper for calculating TSTEP based values from velocity -def TMCtstepHelper(step_dist, mres, tmc_freq, velocity): - if velocity > 0.: - step_dist_256 = step_dist / (1 << mres) - threshold = int(tmc_freq * step_dist_256 / velocity + .5) - return max(0, min(0xfffff, threshold)) - else: +def TMCtstepHelper(mcu_tmc, velocity, pstepper=None, config=None): + if velocity <= 0.: return 0xfffff + if pstepper is not None: + step_dist = pstepper.get_step_dist() + else: + stepper_name = " ".join(config.get_name().split()[1:]) + sconfig = config.getsection(stepper_name) + rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig) + step_dist = rotation_dist / steps_per_rotation + mres = mcu_tmc.get_fields().get_field("mres") + step_dist_256 = step_dist / (1 << mres) + tmc_freq = mcu_tmc.get_tmc_frequency() + threshold = int(tmc_freq * step_dist_256 / velocity + .5) + return max(0, min(0xfffff, threshold)) # Helper to configure stealthChop-spreadCycle transition velocity def TMCStealthchopHelper(config, mcu_tmc): @@ -600,14 +606,7 @@ def TMCStealthchopHelper(config, mcu_tmc): if velocity is not None: en_pwm_mode = True - - stepper_name = " ".join(config.get_name().split()[1:]) - sconfig = config.getsection(stepper_name) - rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig) - step_dist = rotation_dist / steps_per_rotation - mres = fields.get_field("mres") - tpwmthrs = TMCtstepHelper(step_dist, mres, mcu_tmc.get_tmc_frequency(), - velocity) + tpwmthrs = TMCtstepHelper(mcu_tmc, velocity, config=config) fields.set_field("tpwmthrs", tpwmthrs) reg = fields.lookup_register("en_pwm_mode", None) @@ -622,15 +621,8 @@ def TMCVcoolthrsHelper(config, mcu_tmc): fields = mcu_tmc.get_fields() velocity = config.getfloat('coolstep_threshold', None, minval=0.) tcoolthrs = 0 - if velocity is not None: - stepper_name = " ".join(config.get_name().split()[1:]) - sconfig = config.getsection(stepper_name) - rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig) - step_dist = rotation_dist / steps_per_rotation - mres = fields.get_field("mres") - tcoolthrs = TMCtstepHelper(step_dist, mres, - mcu_tmc.get_tmc_frequency(), velocity) + tcoolthrs = TMCtstepHelper(mcu_tmc, velocity, config=config) fields.set_field("tcoolthrs", tcoolthrs) # Helper to configure StallGuard and CoolStep maximum velocity and @@ -639,13 +631,6 @@ def TMCVhighHelper(config, mcu_tmc): fields = mcu_tmc.get_fields() velocity = config.getfloat('high_velocity_threshold', None, minval=0.) thigh = 0 - if velocity is not None: - stepper_name = " ".join(config.get_name().split()[1:]) - sconfig = config.getsection(stepper_name) - rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig) - step_dist = rotation_dist / steps_per_rotation - mres = fields.get_field("mres") - thigh = TMCtstepHelper(step_dist, mres, - mcu_tmc.get_tmc_frequency(), velocity) + thigh = TMCtstepHelper(mcu_tmc, velocity, config=config) fields.set_field("thigh", thigh) From 2efde0111e949c87f72409fbe43a6a96d5aa07f5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 14 May 2024 21:27:01 -0400 Subject: [PATCH 015/111] tmc: Save and restore thigh during sensorless homing Make sure thigh is set to zero during sensorless homing, as it would not make sense for it to be enabled. Signed-off-by: Kevin O'Connor --- docs/Config_Reference.md | 21 ++++++--------------- klippy/extras/tmc.py | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 9d5cb763c..1338d46f9 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3327,11 +3327,8 @@ run_current: #high_velocity_threshold: # The velocity (in mm/s) to set the TMC driver internal "high # velocity" threshold (THIGH) to. This is typically used to disable -# the "CoolStep" feature at high speeds. Important - if -# high_velocity_threshold is set and "sensorless homing" is used, -# then one must ensure that the homing speed is below the high -# velocity threshold! The default is to not set a TMC "high -# velocity" threshold. +# the "CoolStep" feature at high speeds. The default is to not set a +# TMC "high velocity" threshold. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 @@ -3646,11 +3643,8 @@ run_current: #high_velocity_threshold: # The velocity (in mm/s) to set the TMC driver internal "high # velocity" threshold (THIGH) to. This is typically used to disable -# the "CoolStep" feature at high speeds. Important - if -# high_velocity_threshold is set and "sensorless homing" is used, -# then one must ensure that the homing speed is below the high -# velocity threshold! The default is to not set a TMC "high -# velocity" threshold. +# the "CoolStep" feature at high speeds. The default is to not set a +# TMC "high velocity" threshold. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 @@ -3782,11 +3776,8 @@ run_current: #high_velocity_threshold: # The velocity (in mm/s) to set the TMC driver internal "high # velocity" threshold (THIGH) to. This is typically used to disable -# the "CoolStep" feature at high speeds. Important - if -# high_velocity_threshold is set and "sensorless homing" is used, -# then one must ensure that the homing speed is below the high -# velocity threshold! The default is to not set a TMC "high -# velocity" threshold. +# the "CoolStep" feature at high speeds. The default is to not set a +# TMC "high velocity" threshold. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 1a5059fa8..a797b866e 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -479,7 +479,7 @@ class TMCVirtualPinHelper: self.diag_pin_field = None self.mcu_endstop = None self.en_pwm = False - self.pwmthrs = self.coolthrs = 0 + self.pwmthrs = self.coolthrs = self.thigh = 0 # Register virtual_endstop pin name_parts = config.get_name().split() ppins = self.printer.lookup_object("pins") @@ -503,8 +503,8 @@ class TMCVirtualPinHelper: def handle_homing_move_begin(self, hmove): if self.mcu_endstop not in hmove.get_mcu_endstops(): return + # Enable/disable stealthchop self.pwmthrs = self.fields.get_field("tpwmthrs") - self.coolthrs = self.fields.get_field("tcoolthrs") reg = self.fields.lookup_register("en_pwm_mode", None) if reg is None: # On "stallguard4" drivers, "stealthchop" must be enabled @@ -518,12 +518,21 @@ class TMCVirtualPinHelper: self.fields.set_field("en_pwm_mode", 0) val = self.fields.set_field(self.diag_pin_field, 1) self.mcu_tmc.set_register("GCONF", val) + # Enable tcoolthrs (if not already) + self.coolthrs = self.fields.get_field("tcoolthrs") if self.coolthrs == 0: tc_val = self.fields.set_field("tcoolthrs", 0xfffff) self.mcu_tmc.set_register("TCOOLTHRS", tc_val) + # Disable thigh + reg = self.fields.lookup_register("thigh", None) + if reg is not None: + self.thigh = self.fields.get_field("thigh") + th_val = self.fields.set_field("thigh", 0) + self.mcu_tmc.set_register(reg, th_val) def handle_homing_move_end(self, hmove): if self.mcu_endstop not in hmove.get_mcu_endstops(): return + # Restore stealthchop/spreadcycle reg = self.fields.lookup_register("en_pwm_mode", None) if reg is None: tp_val = self.fields.set_field("tpwmthrs", self.pwmthrs) @@ -533,8 +542,14 @@ class TMCVirtualPinHelper: self.fields.set_field("en_pwm_mode", self.en_pwm) val = self.fields.set_field(self.diag_pin_field, 0) self.mcu_tmc.set_register("GCONF", val) + # Restore tcoolthrs tc_val = self.fields.set_field("tcoolthrs", self.coolthrs) self.mcu_tmc.set_register("TCOOLTHRS", tc_val) + # Restore thigh + reg = self.fields.lookup_register("thigh", None) + if reg is not None: + th_val = self.fields.set_field("thigh", self.thigh) + self.mcu_tmc.set_register(reg, th_val) ###################################################################### From 5d52b32e645d71f8e6b8b5426f89fe61eaf92e56 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 16 May 2024 12:23:48 -0400 Subject: [PATCH 016/111] tmc: Remove code that could read microsteps in tmc config sections Setting of microsteps in the stepper config section has been required since commit eea0137b. Remove the no longer needed compatibility code. Signed-off-by: Kevin O'Connor --- klippy/extras/tmc.py | 10 +++------- klippy/stepper.py | 3 ++- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index a797b866e..1d8599e2e 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -577,7 +577,7 @@ def TMCWaveTableHelper(config, mcu_tmc): set_config_field(config, "start_sin", 0) set_config_field(config, "start_sin90", 247) -# Helper to configure and query the microstep settings +# Helper to configure the microstep settings def TMCMicrostepHelper(config, mcu_tmc): fields = mcu_tmc.get_fields() stepper_name = " ".join(config.get_name().split()[1:]) @@ -585,13 +585,9 @@ def TMCMicrostepHelper(config, mcu_tmc): raise config.error( "Could not find config section '[%s]' required by tmc driver" % (stepper_name,)) - stepper_config = ms_config = config.getsection(stepper_name) - if (stepper_config.get('microsteps', None, note_valid=False) is None - and config.get('microsteps', None, note_valid=False) is not None): - # Older config format with microsteps in tmc config section - ms_config = config + sconfig = config.getsection(stepper_name) steps = {256: 0, 128: 1, 64: 2, 32: 3, 16: 4, 8: 5, 4: 6, 2: 7, 1: 8} - mres = ms_config.getchoice('microsteps', steps) + mres = sconfig.getchoice('microsteps', steps) fields.set_field("mres", mres) fields.set_field("intpol", config.getboolean("interpolate", True)) diff --git a/klippy/stepper.py b/klippy/stepper.py index 56c8ec758..9b692904d 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -265,6 +265,7 @@ def parse_gear_ratio(config, note_valid): # Obtain "step distance" information from a config section def parse_step_distance(config, units_in_radians=None, note_valid=False): + # Check rotation_distance and gear_ratio if units_in_radians is None: # Caller doesn't know if units are in radians - infer it rd = config.get('rotation_distance', None, note_valid=False) @@ -276,7 +277,7 @@ def parse_step_distance(config, units_in_radians=None, note_valid=False): else: rotation_dist = config.getfloat('rotation_distance', above=0., note_valid=note_valid) - # Newer config format with rotation_distance + # Check microsteps and full_steps_per_rotation microsteps = config.getint('microsteps', minval=1, note_valid=note_valid) full_steps = config.getint('full_steps_per_rotation', 200, minval=1, note_valid=note_valid) From 236d780a0a3f00017e1a1193e036f0e0eb641437 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 16 May 2024 23:08:23 -0400 Subject: [PATCH 017/111] probe_eddy_current: Fix wait for samples in probing_move() Make sure to wait until all samples are available before performing analysis on the data. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 858fb6e09..f670765c2 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -260,20 +260,22 @@ class EddyEndstopWrapper: trig_pos = phoming.probing_move(self, pos, speed) if not self._trigger_time: return trig_pos - # Wait for 200ms to elapse since trigger time + # Wait for samples to arrive + start_time = self._trigger_time + 0.050 + end_time = start_time + 0.100 reactor = self._printer.get_reactor() while 1: + if self._samples and self._samples[-1]['data'][-1][0] >= end_time: + break systime = reactor.monotonic() est_print_time = self._mcu.estimated_print_time(systime) - need_delay = self._trigger_time + 0.200 - est_print_time - if need_delay <= 0.: - break - reactor.pause(systime + need_delay) + if est_print_time > self._trigger_time + 1.0: + raise self._printer.command_error( + "probe_eddy_current sensor outage") + reactor.pause(systime + 0.010) # Find position since trigger samples = self._samples self._samples = [] - start_time = self._trigger_time + 0.050 - end_time = start_time + 0.100 samp_sum = 0. samp_count = 0 for msg in samples: From 29bfbd02f924c6cfc0f339129bc8ba302cabc4d4 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Sun, 21 Apr 2024 06:04:24 -0400 Subject: [PATCH 018/111] probe_eddy_current: fix attribute name Signed-off-by: Eric Callahan --- klippy/extras/probe_eddy_current.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index f670765c2..73616af04 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -208,7 +208,7 @@ class EddyEndstopWrapper: if self._is_sampling: return self._is_sampling = True - self._is_from_home = is_home + self._start_from_home = is_home self._sensor_helper.add_client(self._add_measurement) def _stop_measurements(self, is_home=False): if not self._is_sampling or (is_home and not self._start_from_home): From 6fac654352f0fbfb24dbfacadef9baf1aa79c2e9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Apr 2024 12:37:01 -0400 Subject: [PATCH 019/111] probe_eddy_current: Calibrate every 40um instead of 50um A 40um distance is more likely to be a full step distance on common Z leadscrews (which often use a rotation distance of 8mm). Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 73616af04..8b9d705ba 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -80,19 +80,19 @@ class EddyCalibration: return True self.printer.lookup_object(self.name).add_client(handle_batch) toolhead.dwell(1.) - # Move to each 50um position - max_z = 4 - samp_dist = 0.050 - num_steps = int(max_z / samp_dist + .5) + 1 + # Move to each 40um position + max_z = 4.0 + samp_dist = 0.040 + req_zpos = [i*samp_dist for i in range(int(max_z / samp_dist) + 1)] start_pos = toolhead.get_position() times = [] - for i in range(num_steps): + for zpos in req_zpos: # Move to next position (always descending to reduce backlash) hop_pos = list(start_pos) - hop_pos[2] += i * samp_dist + 0.500 + hop_pos[2] += zpos + 0.500 move(hop_pos, move_speed) next_pos = list(start_pos) - next_pos[2] += i * samp_dist + next_pos[2] += zpos move(next_pos, move_speed) # Note sample timing start_query_time = toolhead.get_last_move_time() + 0.050 From 3dc7c9ab291ccbf9a90996705e3e460e680a1e8f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 May 2024 13:21:21 -0400 Subject: [PATCH 020/111] test: Disable ldc1612 on stm32f042 build to reduce size Signed-off-by: Kevin O'Connor --- test/configs/stm32f042.config | 1 + 1 file changed, 1 insertion(+) diff --git a/test/configs/stm32f042.config b/test/configs/stm32f042.config index 7f1e879fb..12cc0922e 100644 --- a/test/configs/stm32f042.config +++ b/test/configs/stm32f042.config @@ -3,3 +3,4 @@ CONFIG_MACH_STM32=y CONFIG_MACH_STM32F042=y CONFIG_WANT_SOFTWARE_I2C=n CONFIG_WANT_LIS2DW=n +CONFIG_WANT_LDC1612=n From cb6cce3934bb37cd17d845b16640413b093bbe45 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 May 2024 11:20:05 -0400 Subject: [PATCH 021/111] sensor_ldc1612: Don't require DRDY bit to be set on data read It is not clear if DRDY is cleared during a STATUS read (which could occur from command_query_ldc1612_status() ). So, just check the "unread conversion" bit when reading data. Signed-off-by: Kevin O'Connor --- src/sensor_ldc1612.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 9258ce6dc..2e3f5694e 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -124,7 +124,7 @@ ldc1612_query(struct ldc1612 *ld, uint8_t oid) // Check if data available uint16_t status = read_reg_status(ld); - if (status != 0x48) + if (!(status & 0x08)) return; // Read coil0 frequency @@ -185,7 +185,7 @@ command_query_ldc1612_status(uint32_t *args) uint16_t status = read_reg_status(ld); uint32_t time2 = timer_read_time(); - uint32_t fifo = status == 0x48 ? BYTES_PER_SAMPLE : 0; + uint32_t fifo = status & 0x08 ? BYTES_PER_SAMPLE : 0; sensor_bulk_status(&ld->sb, args[0], time1, time2-time1, fifo); } DECL_COMMAND(command_query_ldc1612_status, "query_ldc1612_status oid=%c"); From 04c562941c65c0f62f39179c0c2cf4f580c6960f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 3 May 2024 11:17:28 -0400 Subject: [PATCH 022/111] sensor_ldc1612: Add support for chips with INTB line routed to mcu If the INTB line is available it can reduce the MCU load. Signed-off-by: Kevin O'Connor --- docs/Config_Reference.md | 3 +++ klippy/extras/ldc1612.py | 13 ++++++++++-- src/sensor_ldc1612.c | 46 +++++++++++++++++++++++++++++++++------- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 1338d46f9..6b42fe48d 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2007,6 +2007,9 @@ Support for eddy current inductive probes. One may define this section sensor_type: ldc1612 # The sensor chip used to perform eddy current measurements. This # parameter must be provided and must be set to ldc1612. +#intb_pin: +# MCU gpio pin connected to the ldc1612 sensor's INTB pin (if +# available). The default is to not use the INTB pin. #z_offset: # The nominal distance (in mm) between the nozzle and bed that a # probing attempt should stop at. This parameter must be provided. diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index 2ae4dd7d7..08cab965e 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -87,8 +87,17 @@ class LDC1612: self.oid = oid = mcu.create_oid() self.query_ldc1612_cmd = None self.ldc1612_setup_home_cmd = self.query_ldc1612_home_state_cmd = None - mcu.add_config_cmd("config_ldc1612 oid=%d i2c_oid=%d" - % (oid, self.i2c.get_oid())) + if config.get('intb_pin', None) is not None: + ppins = config.get_printer().lookup_object("pins") + pin_params = ppins.lookup_pin(config.get('intb_pin')) + if pin_params['chip'] != mcu: + raise config.error("ldc1612 intb_pin must be on same mcu") + mcu.add_config_cmd( + "config_ldc1612_with_intb oid=%d i2c_oid=%d intb_pin=%s" + % (oid, self.i2c.get_oid(), pin_params['pin'])) + else: + mcu.add_config_cmd("config_ldc1612 oid=%d i2c_oid=%d" + % (oid, self.i2c.get_oid())) mcu.add_config_cmd("query_ldc1612 oid=%d rest_ticks=0" % (oid,), on_restart=True) mcu.register_config_callback(self._build_config) diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 2e3f5694e..3db4de4b2 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -17,7 +17,7 @@ #include "trsync.h" // trsync_do_trigger enum { - LDC_PENDING = 1<<0, + LDC_PENDING = 1<<0, LDC_HAVE_INTB = 1<<1, LH_AWAIT_HOMING = 1<<1, LH_CAN_TRIGGER = 1<<2 }; @@ -27,6 +27,7 @@ struct ldc1612 { struct i2cdev_s *i2c; uint8_t flags; struct sensor_bulk sb; + struct gpio_in intb_pin; // homing struct trsync *ts; uint8_t homing_flags; @@ -37,6 +38,13 @@ struct ldc1612 { static struct task_wake ldc1612_wake; +// Check if the intb line is "asserted" +static int +check_intb_asserted(struct ldc1612 *ld) +{ + return !gpio_in_read(ld->intb_pin); +} + // Query ldc1612 data static uint_fast8_t ldc1612_event(struct timer *timer) @@ -44,8 +52,10 @@ ldc1612_event(struct timer *timer) struct ldc1612 *ld = container_of(timer, struct ldc1612, timer); if (ld->flags & LDC_PENDING) ld->sb.possible_overflows++; - ld->flags |= LDC_PENDING; - sched_wake_task(&ldc1612_wake); + if (!(ld->flags & LDC_HAVE_INTB) || check_intb_asserted(ld)) { + ld->flags |= LDC_PENDING; + sched_wake_task(&ldc1612_wake); + } ld->timer.waketime += ld->rest_ticks; return SF_RESCHEDULE; } @@ -60,6 +70,17 @@ command_config_ldc1612(uint32_t *args) } DECL_COMMAND(command_config_ldc1612, "config_ldc1612 oid=%c i2c_oid=%c"); +void +command_config_ldc1612_with_intb(uint32_t *args) +{ + command_config_ldc1612(args); + struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); + ld->intb_pin = gpio_in_setup(args[2], 1); + ld->flags = LDC_HAVE_INTB; +} +DECL_COMMAND(command_config_ldc1612_with_intb, + "config_ldc1612_with_intb oid=%c i2c_oid=%c intb_pin=%c"); + void command_ldc1612_setup_home(uint32_t *args) { @@ -117,13 +138,11 @@ read_reg_status(struct ldc1612 *ld) static void ldc1612_query(struct ldc1612 *ld, uint8_t oid) { - // Clear pending flag + // Check if data available (and clear INTB line) + uint16_t status = read_reg_status(ld); irq_disable(); ld->flags &= ~LDC_PENDING; irq_enable(); - - // Check if data available - uint16_t status = read_reg_status(ld); if (!(status & 0x08)) return; @@ -161,7 +180,7 @@ command_query_ldc1612(uint32_t *args) struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); sched_del_timer(&ld->timer); - ld->flags = 0; + ld->flags &= ~LDC_PENDING; if (!args[1]) // End measurements return; @@ -181,6 +200,17 @@ command_query_ldc1612_status(uint32_t *args) { struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); + if (ld->flags & LDC_HAVE_INTB) { + // Check if a sample is pending in the chip via the intb line + irq_disable(); + uint32_t time = timer_read_time(); + int p = check_intb_asserted(ld); + irq_enable(); + sensor_bulk_status(&ld->sb, args[0], time, 0, p ? BYTES_PER_SAMPLE : 0); + return; + } + + // Query sensor to see if a sample is pending uint32_t time1 = timer_read_time(); uint16_t status = read_reg_status(ld); uint32_t time2 = timer_read_time(); From 4709f1fad50b642f744a8804608d55855d7d7b42 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 1 May 2024 20:46:49 -0400 Subject: [PATCH 023/111] sensor_ldc1612: Create new check_home() helper function Signed-off-by: Kevin O'Connor --- src/sensor_ldc1612.c | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 3db4de4b2..f53b4e2ff 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -111,6 +111,26 @@ command_query_ldc1612_home_state(uint32_t *args) DECL_COMMAND(command_query_ldc1612_home_state, "query_ldc1612_home_state oid=%c"); +// Check if a sample should trigger a homing event +static void +check_home(struct ldc1612 *ld, uint32_t data) +{ + uint8_t homing_flags = ld->homing_flags; + if (!(homing_flags & LH_CAN_TRIGGER)) + return; + uint32_t time = timer_read_time(); + if ((homing_flags & LH_AWAIT_HOMING) + && timer_is_before(time, ld->homing_clock)) + return; + homing_flags &= ~LH_AWAIT_HOMING; + if (data > ld->trigger_threshold) { + homing_flags = 0; + ld->homing_clock = time; + trsync_do_trigger(ld->ts, ld->trigger_reason); + } + ld->homing_flags = homing_flags; +} + // Chip registers #define REG_DATA0_MSB 0x00 #define REG_DATA0_LSB 0x01 @@ -153,21 +173,8 @@ ldc1612_query(struct ldc1612 *ld, uint8_t oid) ld->sb.data_count += BYTES_PER_SAMPLE; // Check for endstop trigger - uint8_t homing_flags = ld->homing_flags; - if (homing_flags & LH_CAN_TRIGGER) { - uint32_t time = timer_read_time(); - if (!(homing_flags & LH_AWAIT_HOMING) - || !timer_is_before(time, ld->homing_clock)) { - homing_flags &= ~LH_AWAIT_HOMING; - uint32_t data = (d[0] << 24L) | (d[1] << 16L) | (d[2] << 8) | d[3]; - if (data > ld->trigger_threshold) { - homing_flags = 0; - ld->homing_clock = time; - trsync_do_trigger(ld->ts, ld->trigger_reason); - } - ld->homing_flags = homing_flags; - } - } + uint32_t data = (d[0] << 24L) | (d[1] << 16L) | (d[2] << 8) | d[3]; + check_home(ld, data); // Flush local buffer if needed if (ld->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(ld->sb.data)) From 37482178b5482a013a4a5a62789705ccaf1e03c6 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 19 May 2024 13:15:30 -0400 Subject: [PATCH 024/111] mcu: Raise an error on a failed home_wait() call Raise a printer.command_error exception if a home_wait() call fails. This makes it easier to support future types of homing errors. Signed-off-by: Kevin O'Connor --- klippy/extras/bltouch.py | 6 +++++- klippy/extras/homing.py | 9 ++++++--- klippy/extras/probe_eddy_current.py | 5 +++-- klippy/mcu.py | 18 ++++++++++-------- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 49385428a..b01cdb9ea 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -116,7 +116,11 @@ class BLTouchEndstopWrapper: self.mcu_endstop.home_start(self.action_end_time, ENDSTOP_SAMPLE_TIME, ENDSTOP_SAMPLE_COUNT, ENDSTOP_REST_TIME, triggered=triggered) - trigger_time = self.mcu_endstop.home_wait(self.action_end_time + 0.100) + try: + trigger_time = self.mcu_endstop.home_wait( + self.action_end_time + 0.100) + except self.printer.command_error as e: + return False return trigger_time > 0. def raise_probe(self): self.sync_mcu_print_time() diff --git a/klippy/extras/homing.py b/klippy/extras/homing.py index 634ad81b7..06b52f1ec 100644 --- a/klippy/extras/homing.py +++ b/klippy/extras/homing.py @@ -98,11 +98,14 @@ class HomingMove: trigger_times = {} move_end_print_time = self.toolhead.get_last_move_time() for mcu_endstop, name in self.endstops: - trigger_time = mcu_endstop.home_wait(move_end_print_time) + try: + trigger_time = mcu_endstop.home_wait(move_end_print_time) + except self.printer.command_error as e: + if error is None: + error = "Error during homing %s: %s" % (name, str(e)) + continue if trigger_time > 0.: trigger_times[name] = trigger_time - elif trigger_time < 0. and error is None: - error = "Communication timeout during homing %s" % (name,) elif check_triggered and error is None: error = "No trigger on %s after full movement" % (name,) # Determine stepper halt positions diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 8b9d705ba..636c800de 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -243,8 +243,9 @@ class EddyEndstopWrapper: trigger_time = self._sensor_helper.clear_home() self._stop_measurements(is_home=True) res = self._dispatch.stop() - if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - return -1. + if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + raise self._printer.command_error( + "Communication timeout during homing") if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: return 0. if self._mcu.is_fileoutput(): diff --git a/klippy/mcu.py b/klippy/mcu.py index 2a01c5a7c..d7a679acb 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -104,9 +104,9 @@ class CommandWrapper: class MCU_trsync: REASON_ENDSTOP_HIT = 1 - REASON_COMMS_TIMEOUT = 2 - REASON_HOST_REQUEST = 3 - REASON_PAST_END_TIME = 4 + REASON_HOST_REQUEST = 2 + REASON_PAST_END_TIME = 3 + REASON_COMMS_TIMEOUT = 4 def __init__(self, mcu, trdispatch): self._mcu = mcu self._trdispatch = trdispatch @@ -180,7 +180,7 @@ class MCU_trsync: if tc is not None: self._trigger_completion = None reason = params['trigger_reason'] - is_failure = (reason == self.REASON_COMMS_TIMEOUT) + is_failure = (reason >= self.REASON_COMMS_TIMEOUT) self._reactor.async_complete(tc, is_failure) elif self._home_end_clock is not None: clock = self._mcu.clock32_to_clock64(params['clock']) @@ -279,8 +279,9 @@ class TriggerDispatch: ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.trdispatch_stop(self._trdispatch) res = [trsync.stop() for trsync in self._trsyncs] - if any([r == MCU_trsync.REASON_COMMS_TIMEOUT for r in res]): - return MCU_trsync.REASON_COMMS_TIMEOUT + err_res = [r for r in res if r >= MCU_trsync.REASON_COMMS_TIMEOUT] + if err_res: + return err_res[0] return res[0] class MCU_endstop: @@ -334,8 +335,9 @@ class MCU_endstop: self._dispatch.wait_end(home_end_time) self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) res = self._dispatch.stop() - if res == MCU_trsync.REASON_COMMS_TIMEOUT: - return -1. + if res >= MCU_trsync.REASON_COMMS_TIMEOUT: + cmderr = self._mcu.get_printer().command_error + raise cmderr("Communication timeout during homing") if res != MCU_trsync.REASON_ENDSTOP_HIT: return 0. if self._mcu.is_fileoutput(): From 4a92727eab61349f2e24fd4296c7fb06740dffe9 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 1 May 2024 21:10:17 -0400 Subject: [PATCH 025/111] sensor_ldc1612: Halt homing if sensor reports a warning Explicitly check for sensor warnings during homing and report an error code back to the host. Signed-off-by: Kevin O'Connor --- klippy/extras/ldc1612.py | 9 +++++---- klippy/extras/probe_eddy_current.py | 9 ++++++--- src/sensor_ldc1612.c | 11 +++++++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index 08cab965e..cdc01e045 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -121,7 +121,7 @@ class LDC1612: oid=self.oid, cq=cmdqueue) self.ldc1612_setup_home_cmd = self.mcu.lookup_command( "ldc1612_setup_home oid=%c clock=%u threshold=%u" - " trsync_oid=%c trigger_reason=%c", cq=cmdqueue) + " trsync_oid=%c trigger_reason=%c error_reason=%c", cq=cmdqueue) self.query_ldc1612_home_state_cmd = self.mcu.lookup_query_command( "query_ldc1612_home_state oid=%c", "ldc1612_home_state oid=%c homing=%c trigger_clock=%u", @@ -138,13 +138,14 @@ class LDC1612: def add_client(self, cb): self.batch_bulk.add_client(cb) # Homing - def setup_home(self, print_time, trigger_freq, trsync_oid, reason): + def setup_home(self, print_time, trigger_freq, + trsync_oid, hit_reason, err_reason): clock = self.mcu.print_time_to_clock(print_time) tfreq = int(trigger_freq * (1<<28) / float(LDC1612_FREQ) + 0.5) self.ldc1612_setup_home_cmd.send( - [self.oid, clock, tfreq, trsync_oid, reason]) + [self.oid, clock, tfreq, trsync_oid, hit_reason, err_reason]) def clear_home(self): - self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0]) + self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0, 0]) if self.mcu.is_fileoutput(): return 0. params = self.query_ldc1612_home_state_cmd.send([self.oid]) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 636c800de..3f4a5e206 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -185,6 +185,7 @@ class EddyCalibration: # Helper for implementing PROBE style commands class EddyEndstopWrapper: + REASON_SENSOR_ERROR = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 def __init__(self, config, sensor_helper, calibration): self._printer = config.get_printer() self._sensor_helper = sensor_helper @@ -236,7 +237,7 @@ class EddyEndstopWrapper: trigger_completion = self._dispatch.start(print_time) self._sensor_helper.setup_home( print_time, trigger_freq, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT) + mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.REASON_SENSOR_ERROR) return trigger_completion def home_wait(self, home_end_time): self._dispatch.wait_end(home_end_time) @@ -244,8 +245,10 @@ class EddyEndstopWrapper: self._stop_measurements(is_home=True) res = self._dispatch.stop() if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - raise self._printer.command_error( - "Communication timeout during homing") + if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + raise self._printer.command_error( + "Communication timeout during homing") + raise self._printer.command_error("Eddy current sensor error") if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: return 0. if self._mcu.is_fileoutput(): diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index f53b4e2ff..6e52c177c 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -31,7 +31,7 @@ struct ldc1612 { // homing struct trsync *ts; uint8_t homing_flags; - uint8_t trigger_reason; + uint8_t trigger_reason, error_reason; uint32_t trigger_threshold; uint32_t homing_clock; }; @@ -95,11 +95,12 @@ command_ldc1612_setup_home(uint32_t *args) ld->homing_clock = args[1]; ld->ts = trsync_oid_lookup(args[3]); ld->trigger_reason = args[4]; + ld->error_reason = args[5]; ld->homing_flags = LH_AWAIT_HOMING | LH_CAN_TRIGGER; } DECL_COMMAND(command_ldc1612_setup_home, "ldc1612_setup_home oid=%c clock=%u threshold=%u" - " trsync_oid=%c trigger_reason=%c"); + " trsync_oid=%c trigger_reason=%c error_reason=%c"); void command_query_ldc1612_home_state(uint32_t *args) @@ -118,6 +119,12 @@ check_home(struct ldc1612 *ld, uint32_t data) uint8_t homing_flags = ld->homing_flags; if (!(homing_flags & LH_CAN_TRIGGER)) return; + if (data > 0x0fffffff) { + // Sensor reports an issue - cancel homing + ld->homing_flags = 0; + trsync_do_trigger(ld->ts, ld->error_reason); + return; + } uint32_t time = timer_read_time(); if ((homing_flags & LH_AWAIT_HOMING) && timer_is_before(time, ld->homing_clock)) From b6a0063235dd58bc9c1bb5767e87e09c9927f39a Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 19 May 2024 01:52:36 +0200 Subject: [PATCH 026/111] tmc5160: csactual -> cs_actual Correct the name of "cs_actual" and correct the size on tmc5160. Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/tmc2240.py | 2 +- klippy/extras/tmc5160.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/klippy/extras/tmc2240.py b/klippy/extras/tmc2240.py index 1e2a49157..21c835e26 100644 --- a/klippy/extras/tmc2240.py +++ b/klippy/extras/tmc2240.py @@ -95,7 +95,7 @@ Fields["DRV_STATUS"] = { "s2vsb": 0x01 << 13, "stealth": 0x01 << 14, "fsactive": 0x01 << 15, - "csactual": 0x1F << 16, + "cs_actual": 0x1F << 16, "stallguard": 0x01 << 24, "ot": 0x01 << 25, "otpw": 0x01 << 26, diff --git a/klippy/extras/tmc5160.py b/klippy/extras/tmc5160.py index 02e16cd4d..b773135c2 100644 --- a/klippy/extras/tmc5160.py +++ b/klippy/extras/tmc5160.py @@ -118,7 +118,7 @@ Fields["DRV_STATUS"] = { "s2vsb": 0x01 << 13, "stealth": 0x01 << 14, "fsactive": 0x01 << 15, - "csactual": 0xFF << 16, + "cs_actual": 0x1F << 16, "stallguard": 0x01 << 24, "ot": 0x01 << 25, "otpw": 0x01 << 26, From 3078912f1d9e063906b9b40eda73d63065900212 Mon Sep 17 00:00:00 2001 From: Elias Bakken Date: Sat, 25 May 2024 21:47:48 +0200 Subject: [PATCH 027/111] stm32: STM32F031 updates (#6607) Add support for STM32F031x6 which is the 32 KB version of the STM32F031 MCU. Add new I2C bus variant. Signed-off by: Elias Bakken --- src/stm32/Kconfig | 3 +-- src/stm32/stm32f0_i2c.c | 3 +++ test/configs/stm32f031.config | 8 ++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index dbd6ff959..037e37bbe 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -211,8 +211,7 @@ config CLOCK_FREQ config FLASH_SIZE hex - default 0x4000 if MACH_STM32F031 - default 0x8000 if MACH_STM32F042 + default 0x8000 if MACH_STM32F031 || MACH_STM32F042 default 0x20000 if MACH_STM32F070 || MACH_STM32F072 default 0x10000 if MACH_STM32F103 || MACH_STM32L412 # Flash size of stm32f103x8 (64KiB) default 0x40000 if MACH_STM32F2 || MACH_STM32F401 || MACH_STM32H723 diff --git a/src/stm32/stm32f0_i2c.c b/src/stm32/stm32f0_i2c.c index 1441079f7..597b48460 100644 --- a/src/stm32/stm32f0_i2c.c +++ b/src/stm32/stm32f0_i2c.c @@ -22,6 +22,8 @@ struct i2c_info { DECL_CONSTANT_STR("BUS_PINS_i2c1_PF1_PF0", "PF1,PF0"); DECL_ENUMERATION("i2c_bus", "i2c1_PB8_PB9", 2); DECL_CONSTANT_STR("BUS_PINS_i2c1_PB8_PB9", "PB8,PB9"); + DECL_ENUMERATION("i2c_bus", "i2c1_PB8_PB7", 3); + DECL_CONSTANT_STR("BUS_PINS_i2c1_PB8_PB7", "PB8,PB7"); // Deprecated "i2c1a" style mappings DECL_ENUMERATION("i2c_bus", "i2c1", 0); DECL_CONSTANT_STR("BUS_PINS_i2c1", "PB6,PB7"); @@ -93,6 +95,7 @@ static const struct i2c_info i2c_bus[] = { { I2C1, GPIO('B', 6), GPIO('B', 7), GPIO_FUNCTION(1) }, { I2C1, GPIO('F', 1), GPIO('F', 0), GPIO_FUNCTION(1) }, { I2C1, GPIO('B', 8), GPIO('B', 9), GPIO_FUNCTION(1) }, + { I2C1, GPIO('B', 8), GPIO('B', 7), GPIO_FUNCTION(1) }, #elif CONFIG_MACH_STM32F7 { I2C1, GPIO('B', 6), GPIO('B', 7), GPIO_FUNCTION(1) }, #elif CONFIG_MACH_STM32G0 diff --git a/test/configs/stm32f031.config b/test/configs/stm32f031.config index aa9c282be..a8c95cfe7 100644 --- a/test/configs/stm32f031.config +++ b/test/configs/stm32f031.config @@ -1,5 +1,9 @@ -# Base config file for STM32F031 boards +# Base config file for STM32F031x4 (16KB) boards with serial on PA14/PA15 CONFIG_MACH_STM32=y CONFIG_MACH_STM32F031=y -CONFIG_WANT_GPIO_BITBANGING=n +CONFIG_LOW_LEVEL_OPTIONS=y +CONFIG_STM32_SERIAL_USART2_ALT_PA15_PA14=y +CONFIG_STM32_CLOCK_REF_INTERNAL=y +CONFIG_STM32_FLASH_START_0000=y CONFIG_WANT_DISPLAYS=n +CONFIG_WANT_GPIO_BITBANGING=n From 6cd174208bd9bbd51dc0d519a26661fb926d038a Mon Sep 17 00:00:00 2001 From: Jayofelony Date: Tue, 28 May 2024 02:57:42 +0200 Subject: [PATCH 028/111] config: Add Artillery Genius Pro config (#6604) Signed-off-by: Jeroen Oudshoorn --- config/printer-artillery-genius-pro-2022.cfg | 126 +++++++++++++++++++ test/klippy/printers.test | 1 + 2 files changed, 127 insertions(+) create mode 100644 config/printer-artillery-genius-pro-2022.cfg diff --git a/config/printer-artillery-genius-pro-2022.cfg b/config/printer-artillery-genius-pro-2022.cfg new file mode 100644 index 000000000..aec7841c5 --- /dev/null +++ b/config/printer-artillery-genius-pro-2022.cfg @@ -0,0 +1,126 @@ +# This file contains pin mappings for the Artillery Genius Pro (2022) +# with a Artillery_Ruby-v1.2 board. To use this config, during "make menuconfig" +# select the STM32F401 with "No bootloader" and USB (on PA11/PA12) +# communication. + +# To flash this firmware, set the physical bridge between +3.3V and Boot0 PIN +# on Artillery_Ruby mainboard. Then run the command: +# make flash FLASH_DEVICE=/dev/serial/by-id/usb-Klipper_stm32f401xc_*-if00 + +# See docs/Config_Reference.md for a description of parameters. + +[extruder] +max_extrude_only_distance: 700.0 +step_pin: PA7 +dir_pin: PA6 +enable_pin: !PC4 +microsteps: 16 +rotation_distance: 7.1910 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PC9 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC0 +min_temp: 0 +max_temp: 250 +control: pid +pid_Kp: 23.223 +pid_Ki: 1.518 +pid_Kd: 88.826 + +[stepper_x] +step_pin: !PB14 +dir_pin: PB13 +enable_pin: !PB15 +microsteps: 16 +rotation_distance: 40 +endstop_pin: !PA2 +position_endstop: 0 +position_max: 220 +homing_speed: 60 + +[stepper_y] +step_pin: PB10 +dir_pin: PB2 +enable_pin: !PB12 +microsteps: 16 +rotation_distance: 40 +endstop_pin: !PA1 +position_endstop: 0 +position_max: 220 +homing_speed: 60 + +[stepper_z] +step_pin: PB0 +dir_pin: !PC5 +enable_pin: !PB1 +microsteps: 16 +rotation_distance: 8 +endstop_pin: probe:z_virtual_endstop +position_max: 250 +position_min: -5 + +[heater_bed] +heater_pin: PA8 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC1 +min_temp: 0 +max_temp: 130 +control: pid +pid_Kp: 23.223 +pid_Ki: 1.518 +pid_Kd: 88.826 + +[bed_screws] +screw1: 38,45 +screw2: 180,45 +screw3: 180,180 +screw4: 38,180 + +[fan] +pin: PC8 +off_below: 0.1 + +[heater_fan hotend_fan] +pin: PC7 +heater: extruder +heater_temp: 50.0 + +[controller_fan stepper_fan] +pin: PC6 +idle_timeout: 300 + +[mcu] +serial: /dev/serial/by-id/usb-Klipper_stm32f401xc_ + +[printer] +kinematics: cartesian +max_velocity: 500 +max_accel: 4000 +max_z_velocity: 50 +square_corner_velocity: 5.0 +max_z_accel: 100 + +[bltouch] +sensor_pin: PC2 +control_pin: PC3 +x_offset:27.25 +y_offset:-12.8 +z_offset: 0.25 +speed:10 +samples:1 +samples_result:average + +[bed_mesh] +speed: 800 +mesh_min: 30, 20 +mesh_max: 210, 200 +probe_count: 5,5 +algorithm: bicubic +move_check_distance: 3.0 + +[safe_z_home] +home_xy_position: 110,110 +speed: 100 +z_hop: 10 +z_hop_speed: 5 diff --git a/test/klippy/printers.test b/test/klippy/printers.test index e404f6f15..ba7adb614 100644 --- a/test/klippy/printers.test +++ b/test/klippy/printers.test @@ -204,6 +204,7 @@ CONFIG ../../config/printer-voxelab-aquila-2021.cfg # Printers using the stm32f401 DICTIONARY stm32f401.dict CONFIG ../../config/generic-fysetc-cheetah-v2.0.cfg +CONFIG ../../config/printer-artillery-genius-pro-2022.cfg CONFIG ../../config/printer-artillery-sidewinder-x2-2022.cfg CONFIG ../../config/printer-artillery-sidewinder-x3-plus-2024.cfg CONFIG ../../config/printer-creality-ender5-s1-2023.cfg From 49c0ad6369670da574f550aa878ce9f6e1899e74 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 4 Jun 2024 15:49:09 -0400 Subject: [PATCH 029/111] motan: Fix logic error resulting in incorrect stepper phase graphing The mcu_phase_offset should be added not subtracted. Signed-off-by: Kevin O'Connor --- scripts/motan/readlog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/motan/readlog.py b/scripts/motan/readlog.py index 1b44c9375..48284ec2b 100644 --- a/scripts/motan/readlog.py +++ b/scripts/motan/readlog.py @@ -293,7 +293,7 @@ class HandleStepPhase: self._pull_block(req_time) continue step_pos = step_data[data_pos][1] - return (step_pos - self.mcu_phase_offset) % self.phases + return (step_pos + self.mcu_phase_offset) % self.phases def _pull_block(self, req_time): step_data = self.step_data del step_data[:-1] From 12f92c55f1c956bf415b379828c7b0bafae35026 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 27 May 2024 13:33:17 -0400 Subject: [PATCH 030/111] probe: Code movement in probe.py Move code around in probe.py and add some comments. No code changes. Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 155 ++++++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 70 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 073c875cc..1a04a61ef 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -13,6 +13,11 @@ consider reducing the Z axis minimum position so the probe can travel further (the Z minimum position can be negative). """ + +###################################################################### +# Probe device implementation helpers +###################################################################### + class PrinterProbe: def __init__(self, config, mcu_probe): self.printer = config.get_printer() @@ -290,76 +295,10 @@ class PrinterProbe: configfile.set(self.name, 'z_offset', "%.3f" % (new_calibrate,)) cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset" -# Endstop wrapper that enables probe specific features -class ProbeEndstopWrapper: - def __init__(self, config): - self.printer = config.get_printer() - self.position_endstop = config.getfloat('z_offset') - self.stow_on_each_sample = config.getboolean( - 'deactivate_on_each_sample', True) - gcode_macro = self.printer.load_object(config, 'gcode_macro') - self.activate_gcode = gcode_macro.load_template( - config, 'activate_gcode', '') - self.deactivate_gcode = gcode_macro.load_template( - config, 'deactivate_gcode', '') - # Create an "endstop" object to handle the probe pin - ppins = self.printer.lookup_object('pins') - pin = config.get('pin') - pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True) - mcu = pin_params['chip'] - self.mcu_endstop = mcu.setup_pin('endstop', pin_params) - self.printer.register_event_handler('klippy:mcu_identify', - self._handle_mcu_identify) - # Wrappers - self.get_mcu = self.mcu_endstop.get_mcu - self.add_stepper = self.mcu_endstop.add_stepper - self.get_steppers = self.mcu_endstop.get_steppers - self.home_start = self.mcu_endstop.home_start - self.home_wait = self.mcu_endstop.home_wait - self.query_endstop = self.mcu_endstop.query_endstop - # multi probes state - self.multi = 'OFF' - def _handle_mcu_identify(self): - kin = self.printer.lookup_object('toolhead').get_kinematics() - for stepper in kin.get_steppers(): - if stepper.is_active_axis('z'): - self.add_stepper(stepper) - def _raise_probe(self): - toolhead = self.printer.lookup_object('toolhead') - start_pos = toolhead.get_position() - self.deactivate_gcode.run_gcode_from_command() - if toolhead.get_position()[:3] != start_pos[:3]: - raise self.printer.command_error( - "Toolhead moved during probe deactivate_gcode script") - def _lower_probe(self): - toolhead = self.printer.lookup_object('toolhead') - start_pos = toolhead.get_position() - self.activate_gcode.run_gcode_from_command() - if toolhead.get_position()[:3] != start_pos[:3]: - raise self.printer.command_error( - "Toolhead moved during probe activate_gcode script") - def multi_probe_begin(self): - if self.stow_on_each_sample: - return - self.multi = 'FIRST' - def multi_probe_end(self): - if self.stow_on_each_sample: - return - self._raise_probe() - self.multi = 'OFF' - def probing_move(self, pos, speed): - phoming = self.printer.lookup_object('homing') - return phoming.probing_move(self, pos, speed) - def probe_prepare(self, hmove): - if self.multi == 'OFF' or self.multi == 'FIRST': - self._lower_probe() - if self.multi == 'FIRST': - self.multi = 'ON' - def probe_finish(self, hmove): - if self.multi == 'OFF': - self._raise_probe() - def get_position_endstop(self): - return self.position_endstop + +###################################################################### +# Tools for utilizing the probe +###################################################################### # Helper code that can probe a series of points and report the # position at each point. @@ -456,5 +395,81 @@ class ProbePointsHelper: self.results.append(kin_pos) self._manual_probe_start() + +###################################################################### +# Handle [probe] config +###################################################################### + +# Endstop wrapper that enables probe specific features +class ProbeEndstopWrapper: + def __init__(self, config): + self.printer = config.get_printer() + self.position_endstop = config.getfloat('z_offset') + self.stow_on_each_sample = config.getboolean( + 'deactivate_on_each_sample', True) + gcode_macro = self.printer.load_object(config, 'gcode_macro') + self.activate_gcode = gcode_macro.load_template( + config, 'activate_gcode', '') + self.deactivate_gcode = gcode_macro.load_template( + config, 'deactivate_gcode', '') + # Create an "endstop" object to handle the probe pin + ppins = self.printer.lookup_object('pins') + pin = config.get('pin') + pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True) + mcu = pin_params['chip'] + self.mcu_endstop = mcu.setup_pin('endstop', pin_params) + self.printer.register_event_handler('klippy:mcu_identify', + self._handle_mcu_identify) + # Wrappers + self.get_mcu = self.mcu_endstop.get_mcu + self.add_stepper = self.mcu_endstop.add_stepper + self.get_steppers = self.mcu_endstop.get_steppers + self.home_start = self.mcu_endstop.home_start + self.home_wait = self.mcu_endstop.home_wait + self.query_endstop = self.mcu_endstop.query_endstop + # multi probes state + self.multi = 'OFF' + def _handle_mcu_identify(self): + kin = self.printer.lookup_object('toolhead').get_kinematics() + for stepper in kin.get_steppers(): + if stepper.is_active_axis('z'): + self.add_stepper(stepper) + def _raise_probe(self): + toolhead = self.printer.lookup_object('toolhead') + start_pos = toolhead.get_position() + self.deactivate_gcode.run_gcode_from_command() + if toolhead.get_position()[:3] != start_pos[:3]: + raise self.printer.command_error( + "Toolhead moved during probe deactivate_gcode script") + def _lower_probe(self): + toolhead = self.printer.lookup_object('toolhead') + start_pos = toolhead.get_position() + self.activate_gcode.run_gcode_from_command() + if toolhead.get_position()[:3] != start_pos[:3]: + raise self.printer.command_error( + "Toolhead moved during probe activate_gcode script") + def multi_probe_begin(self): + if self.stow_on_each_sample: + return + self.multi = 'FIRST' + def multi_probe_end(self): + if self.stow_on_each_sample: + return + self._raise_probe() + self.multi = 'OFF' + def probing_move(self, pos, speed): + phoming = self.printer.lookup_object('homing') + return phoming.probing_move(self, pos, speed) + def probe_prepare(self, hmove): + if self.multi == 'OFF' or self.multi == 'FIRST': + self._lower_probe() + if self.multi == 'FIRST': + self.multi = 'ON' + def probe_finish(self, hmove): + if self.multi == 'OFF': + self._raise_probe() + def get_position_endstop(self): + return self.position_endstop + def load_config(config): return PrinterProbe(config, ProbeEndstopWrapper(config)) From bec47e049278e9745173ebdb8b184ff3cf4d98c2 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 23 May 2024 13:32:04 -0400 Subject: [PATCH 031/111] probe: Split out new ProbeSessionHelper() class from PrinterProbe() Separate out the PrinterProbe() class to make the external probe interfaces more clear. Signed-off-by: Kevin O'Connor --- klippy/extras/axis_twist_compensation.py | 3 ++- klippy/extras/probe.py | 30 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index ad08ad559..cd3b84177 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -186,7 +186,8 @@ class Calibrater: probe_points[self.current_point_index][1], None)) # probe the point - self.current_measured_z = self.probe.run_probe(self.gcmd)[2] + pos = probe.run_single_probe(self.probe, self.gcmd) + self.current_measured_z = pos[2] # horizontal_move_z (to prevent probe trigger or hitting bed) self._move_helper((None, None, self.horizontal_move_z)) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 1a04a61ef..80a6d4e75 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -18,7 +18,8 @@ can travel further (the Z minimum position can be negative). # Probe device implementation helpers ###################################################################### -class PrinterProbe: +# Helper to track multiple probe attempts in a single command +class ProbeSessionHelper: def __init__(self, config, mcu_probe): self.printer = config.get_printer() self.name = config.get_name() @@ -375,14 +376,15 @@ class ProbePointsHelper: if self.horizontal_move_z < self.probe_offsets[2]: raise gcmd.error("horizontal_move_z can't be less than" " probe's z_offset") - probe.multi_probe_begin() + probe_session = probe.start_probe_session(gcmd) + probe_session.multi_probe_begin() while 1: done = self._move_next() if done: break - pos = probe.run_probe(gcmd) + pos = probe_session.run_probe(gcmd) self.results.append(pos) - probe.multi_probe_end() + probe_session.multi_probe_end() def _manual_probe_start(self): done = self._move_next() if not done: @@ -395,6 +397,12 @@ class ProbePointsHelper: self.results.append(kin_pos) self._manual_probe_start() +# Helper to obtain a single probe measurement +def run_single_probe(probe, gcmd): + probe_session = probe.start_probe_session(gcmd) + pos = probe_session.run_probe(gcmd) + return pos + ###################################################################### # Handle [probe] config @@ -471,5 +479,19 @@ class ProbeEndstopWrapper: def get_position_endstop(self): return self.position_endstop +# Main external probe interface +class PrinterProbe: + def __init__(self, config, mcu_probe): + self.printer = config.get_printer() + self.probe_session = ProbeSessionHelper(config, mcu_probe) + def get_lift_speed(self, gcmd=None): + return self.probe_session.get_lift_speed(gcmd) + def get_offsets(self): + return self.probe_session.get_offsets() + def get_status(self, eventtime): + return self.probe_session.get_status(eventtime) + def start_probe_session(self, gcmd): + return self.probe_session + def load_config(config): return PrinterProbe(config, ProbeEndstopWrapper(config)) From 8fc11b4a2e0dd33cd99e4d3c8ce4107acf4772ff Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 22 May 2024 20:59:28 -0400 Subject: [PATCH 032/111] probe: Introduce new ProbeCommandHelper class Move the PROBE and QUERY_PROBE commands from ProbeSessionHelper class to new ProbeCommandHelper class. Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 61 ++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 80a6d4e75..c7e838aa8 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -18,6 +18,41 @@ can travel further (the Z minimum position can be negative). # Probe device implementation helpers ###################################################################### +# Helper to implement common probing commands +class ProbeCommandHelper: + def __init__(self, config, probe, query_endstop=None): + self.printer = config.get_printer() + self.probe = probe + self.query_endstop = query_endstop + self.name = config.get_name() + gcode = self.printer.lookup_object('gcode') + # QUERY_PROBE command + self.last_state = False + gcode.register_command('QUERY_PROBE', self.cmd_QUERY_PROBE, + desc=self.cmd_QUERY_PROBE_help) + # PROBE command + self.last_z_result = 0. + gcode.register_command('PROBE', self.cmd_PROBE, + desc=self.cmd_PROBE_help) + def get_status(self, eventtime): + return {'name': self.name, + 'last_query': self.last_state, + 'last_z_result': self.last_z_result} + cmd_QUERY_PROBE_help = "Return the status of the z-probe" + def cmd_QUERY_PROBE(self, gcmd): + if self.query_endstop is None: + raise gcmd.error("Probe does not support QUERY_PROBE") + toolhead = self.printer.lookup_object('toolhead') + print_time = toolhead.get_last_move_time() + res = self.query_endstop(print_time) + self.last_state = res + gcmd.respond_info("probe: %s" % (["open", "TRIGGERED"][not not res],)) + cmd_PROBE_help = "Probe Z-height at current XY position" + def cmd_PROBE(self, gcmd): + pos = run_single_probe(self.probe, gcmd) + gcmd.respond_info("Result is z=%.6f" % (pos[2],)) + self.last_z_result = pos[2] + # Helper to track multiple probe attempts in a single command class ProbeSessionHelper: def __init__(self, config, mcu_probe): @@ -31,8 +66,6 @@ class ProbeSessionHelper: self.z_offset = config.getfloat('z_offset') self.probe_calibrate_z = 0. self.multi_probe_pending = False - self.last_state = False - self.last_z_result = 0. self.gcode_move = self.printer.load_object(config, "gcode_move") # Infer Z position to move to during a probe if config.has_section('stepper_z'): @@ -69,10 +102,6 @@ class ProbeSessionHelper: self._handle_command_error) # Register PROBE/QUERY_PROBE commands self.gcode = self.printer.lookup_object('gcode') - self.gcode.register_command('PROBE', self.cmd_PROBE, - desc=self.cmd_PROBE_help) - self.gcode.register_command('QUERY_PROBE', self.cmd_QUERY_PROBE, - desc=self.cmd_QUERY_PROBE_help) self.gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, desc=self.cmd_PROBE_CALIBRATE_help) self.gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, @@ -196,22 +225,6 @@ class ProbeSessionHelper: if samples_result == 'median': return self._calc_median(positions) return self._calc_mean(positions) - cmd_PROBE_help = "Probe Z-height at current XY position" - def cmd_PROBE(self, gcmd): - pos = self.run_probe(gcmd) - gcmd.respond_info("Result is z=%.6f" % (pos[2],)) - self.last_z_result = pos[2] - cmd_QUERY_PROBE_help = "Return the status of the z-probe" - def cmd_QUERY_PROBE(self, gcmd): - toolhead = self.printer.lookup_object('toolhead') - print_time = toolhead.get_last_move_time() - res = self.mcu_probe.query_endstop(print_time) - self.last_state = res - gcmd.respond_info("probe: %s" % (["open", "TRIGGERED"][not not res],)) - def get_status(self, eventtime): - return {'name': self.name, - 'last_query': self.last_state, - 'last_z_result': self.last_z_result} cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position" def cmd_PROBE_ACCURACY(self, gcmd): speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.) @@ -483,13 +496,15 @@ class ProbeEndstopWrapper: class PrinterProbe: def __init__(self, config, mcu_probe): self.printer = config.get_printer() + self.cmd_helper = ProbeCommandHelper(config, self, + mcu_probe.query_endstop) self.probe_session = ProbeSessionHelper(config, mcu_probe) def get_lift_speed(self, gcmd=None): return self.probe_session.get_lift_speed(gcmd) def get_offsets(self): return self.probe_session.get_offsets() def get_status(self, eventtime): - return self.probe_session.get_status(eventtime) + return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): return self.probe_session From 6f6122a576c42852ca8835f9cef5993d2ac76a1d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 23 May 2024 13:51:04 -0400 Subject: [PATCH 033/111] probe: Move Z_OFFSET_APPLY_PROBE to ProbeCommandHelper class Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index c7e838aa8..103a35862 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -34,6 +34,10 @@ class ProbeCommandHelper: self.last_z_result = 0. gcode.register_command('PROBE', self.cmd_PROBE, desc=self.cmd_PROBE_help) + # Other commands + gcode.register_command('Z_OFFSET_APPLY_PROBE', + self.cmd_Z_OFFSET_APPLY_PROBE, + desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) def get_status(self, eventtime): return {'name': self.name, 'last_query': self.last_state, @@ -52,6 +56,22 @@ class ProbeCommandHelper: pos = run_single_probe(self.probe, gcmd) gcmd.respond_info("Result is z=%.6f" % (pos[2],)) self.last_z_result = pos[2] + cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset" + def cmd_Z_OFFSET_APPLY_PROBE(self, gcmd): + gcode_move = self.printer.lookup_object("gcode_move") + offset = gcode_move.get_status()['homing_origin'].z + if offset == 0: + gcmd.respond_info("Nothing to do: Z Offset is 0") + return + z_offset = self.probe.get_offsets()[2] + new_calibrate = z_offset - offset + gcmd.respond_info( + "%s: z_offset: %.3f\n" + "The SAVE_CONFIG command will update the printer config file\n" + "with the above and restart the printer." + % (self.name, new_calibrate)) + configfile = self.printer.lookup_object('configfile') + configfile.set(self.name, 'z_offset', "%.3f" % (new_calibrate,)) # Helper to track multiple probe attempts in a single command class ProbeSessionHelper: @@ -66,7 +86,6 @@ class ProbeSessionHelper: self.z_offset = config.getfloat('z_offset') self.probe_calibrate_z = 0. self.multi_probe_pending = False - self.gcode_move = self.printer.load_object(config, "gcode_move") # Infer Z position to move to during a probe if config.has_section('stepper_z'): zconfig = config.getsection('stepper_z') @@ -106,9 +125,6 @@ class ProbeSessionHelper: desc=self.cmd_PROBE_CALIBRATE_help) self.gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, desc=self.cmd_PROBE_ACCURACY_help) - self.gcode.register_command('Z_OFFSET_APPLY_PROBE', - self.cmd_Z_OFFSET_APPLY_PROBE, - desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) def _handle_homing_move_begin(self, hmove): if self.mcu_probe in hmove.get_mcu_endstops(): self.mcu_probe.probe_prepare(hmove) @@ -294,20 +310,6 @@ class ProbeSessionHelper: # Start manual probe manual_probe.ManualProbeHelper(self.printer, gcmd, self.probe_calibrate_finalize) - def cmd_Z_OFFSET_APPLY_PROBE(self,gcmd): - offset = self.gcode_move.get_status()['homing_origin'].z - configfile = self.printer.lookup_object('configfile') - if offset == 0: - self.gcode.respond_info("Nothing to do: Z Offset is 0") - else: - new_calibrate = self.z_offset - offset - self.gcode.respond_info( - "%s: z_offset: %.3f\n" - "The SAVE_CONFIG command will update the printer config file\n" - "with the above and restart the printer." - % (self.name, new_calibrate)) - configfile.set(self.name, 'z_offset', "%.3f" % (new_calibrate,)) - cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset" ###################################################################### From 6ea5b94d1e3add2bb15cbe9c2f5fd9c7da2e0970 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 23 May 2024 14:51:01 -0400 Subject: [PATCH 034/111] probe: Convert probe.get_lift_speed() to probe.get_print_params() Add a get_print_params() method that can extract all the common probing parameters. Replace get_lift_speed() with this more general function. Signed-off-by: Kevin O'Connor --- klippy/extras/axis_twist_compensation.py | 2 +- klippy/extras/probe.py | 73 +++++++++++++----------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index cd3b84177..e01951abb 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -95,7 +95,7 @@ class Calibrater: config = self.printer.lookup_object('configfile') raise config.error( "AXIS_TWIST_COMPENSATION requires [probe] to be defined") - self.lift_speed = self.probe.get_lift_speed() + self.lift_speed = self.probe.get_probe_params()['lift_speed'] self.probe_x_offset, self.probe_y_offset, _ = \ self.probe.get_offsets() diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 103a35862..c1ee40d99 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -125,6 +125,7 @@ class ProbeSessionHelper: desc=self.cmd_PROBE_CALIBRATE_help) self.gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, desc=self.cmd_PROBE_ACCURACY_help) + self.dummy_gcode_cmd = self.gcode.create_gcode_command("", "", {}) def _handle_homing_move_begin(self, hmove): if self.mcu_probe in hmove.get_mcu_endstops(): self.mcu_probe.probe_prepare(hmove) @@ -157,10 +158,26 @@ class ProbeSessionHelper: if pin_params['invert'] or pin_params['pullup']: raise pins.error("Can not pullup/invert probe virtual endstop") return self.mcu_probe - def get_lift_speed(self, gcmd=None): - if gcmd is not None: - return gcmd.get_float("LIFT_SPEED", self.lift_speed, above=0.) - return self.lift_speed + def get_probe_params(self, gcmd=None): + if gcmd is None: + gcmd = self.dummy_gcode_cmd + probe_speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.) + lift_speed = gcmd.get_float("LIFT_SPEED", self.lift_speed, above=0.) + samples = gcmd.get_int("SAMPLES", self.sample_count, minval=1) + sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST", + self.sample_retract_dist, above=0.) + samples_tolerance = gcmd.get_float("SAMPLES_TOLERANCE", + self.samples_tolerance, minval=0.) + samples_retries = gcmd.get_int("SAMPLES_TOLERANCE_RETRIES", + self.samples_retries, minval=0) + samples_result = gcmd.get("SAMPLES_RESULT", self.samples_result) + return {'probe_speed': probe_speed, + 'lift_speed': lift_speed, + 'samples': samples, + 'sample_retract_dist': sample_retract_dist, + 'samples_tolerance': samples_tolerance, + 'samples_tolerance_retries': samples_retries, + 'samples_result': samples_result} def get_offsets(self): return self.x_offset, self.y_offset, self.z_offset def _probe(self, speed): @@ -204,68 +221,58 @@ class ProbeSessionHelper: # even number of samples return self._calc_mean(z_sorted[middle-1:middle+1]) def run_probe(self, gcmd): - speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.) - lift_speed = self.get_lift_speed(gcmd) - sample_count = gcmd.get_int("SAMPLES", self.sample_count, minval=1) - sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST", - self.sample_retract_dist, above=0.) - samples_tolerance = gcmd.get_float("SAMPLES_TOLERANCE", - self.samples_tolerance, minval=0.) - samples_retries = gcmd.get_int("SAMPLES_TOLERANCE_RETRIES", - self.samples_retries, minval=0) - samples_result = gcmd.get("SAMPLES_RESULT", self.samples_result) + params = self.get_probe_params(gcmd) must_notify_multi_probe = not self.multi_probe_pending if must_notify_multi_probe: self.multi_probe_begin() probexy = self.printer.lookup_object('toolhead').get_position()[:2] retries = 0 positions = [] + sample_count = params['samples'] while len(positions) < sample_count: # Probe position - pos = self._probe(speed) + pos = self._probe(params['probe_speed']) positions.append(pos) # Check samples tolerance z_positions = [p[2] for p in positions] - if max(z_positions) - min(z_positions) > samples_tolerance: - if retries >= samples_retries: + if max(z_positions)-min(z_positions) > params['samples_tolerance']: + if retries >= params['samples_tolerance_retries']: raise gcmd.error("Probe samples exceed samples_tolerance") gcmd.respond_info("Probe samples exceed tolerance. Retrying...") retries += 1 positions = [] # Retract if len(positions) < sample_count: - self._move(probexy + [pos[2] + sample_retract_dist], lift_speed) + self._move(probexy + [pos[2] + params['sample_retract_dist']], + params['lift_speed']) if must_notify_multi_probe: self.multi_probe_end() # Calculate and return result - if samples_result == 'median': + if params['samples_result'] == 'median': return self._calc_median(positions) return self._calc_mean(positions) cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position" def cmd_PROBE_ACCURACY(self, gcmd): - speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.) - lift_speed = self.get_lift_speed(gcmd) + params = self.get_probe_params(gcmd) sample_count = gcmd.get_int("SAMPLES", 10, minval=1) - sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST", - self.sample_retract_dist, above=0.) toolhead = self.printer.lookup_object('toolhead') pos = toolhead.get_position() gcmd.respond_info("PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f" " (samples=%d retract=%.3f" " speed=%.1f lift_speed=%.1f)\n" % (pos[0], pos[1], pos[2], - sample_count, sample_retract_dist, - speed, lift_speed)) + sample_count, params['sample_retract_dist'], + params['probe_speed'], params['lift_speed'])) # Probe bed sample_count times self.multi_probe_begin() positions = [] while len(positions) < sample_count: # Probe position - pos = self._probe(speed) + pos = self._probe(params['probe_speed']) positions.append(pos) # Retract - liftpos = [None, None, pos[2] + sample_retract_dist] - self._move(liftpos, lift_speed) + liftpos = [None, None, pos[2] + params['sample_retract_dist']] + self._move(liftpos, params['lift_speed']) self.multi_probe_end() # Calculate maximum, minimum and average values max_value = max([p[2] for p in positions]) @@ -297,12 +304,12 @@ class ProbeSessionHelper: def cmd_PROBE_CALIBRATE(self, gcmd): manual_probe.verify_no_manual_probe(self.printer) # Perform initial probe - lift_speed = self.get_lift_speed(gcmd) + params = self.get_probe_params(gcmd) curpos = self.run_probe(gcmd) # Move away from the bed self.probe_calibrate_z = curpos[2] curpos[2] += 5. - self._move(curpos, lift_speed) + self._move(curpos, params['lift_speed']) # Move the nozzle over the probe point curpos[0] += self.x_offset curpos[1] += self.y_offset @@ -386,7 +393,7 @@ class ProbePointsHelper: self._manual_probe_start() return # Perform automatic probing - self.lift_speed = probe.get_lift_speed(gcmd) + self.lift_speed = probe.get_probe_params(gcmd)['lift_speed'] self.probe_offsets = probe.get_offsets() if self.horizontal_move_z < self.probe_offsets[2]: raise gcmd.error("horizontal_move_z can't be less than" @@ -501,8 +508,8 @@ class PrinterProbe: self.cmd_helper = ProbeCommandHelper(config, self, mcu_probe.query_endstop) self.probe_session = ProbeSessionHelper(config, mcu_probe) - def get_lift_speed(self, gcmd=None): - return self.probe_session.get_lift_speed(gcmd) + def get_probe_params(self, gcmd=None): + return self.probe_session.get_probe_params(gcmd) def get_offsets(self): return self.probe_session.get_offsets() def get_status(self, eventtime): From 292512f8136ffabbc500612f1c80bdbe2588b8e1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 23 May 2024 12:40:53 -0400 Subject: [PATCH 035/111] probe: Move PROBE_CALIBRATE to ProbeCommandHelper class Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 66 ++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index c1ee40d99..c3b8a0dbf 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -34,10 +34,16 @@ class ProbeCommandHelper: self.last_z_result = 0. gcode.register_command('PROBE', self.cmd_PROBE, desc=self.cmd_PROBE_help) + # PROBE_CALIBRATE command + self.probe_calibrate_z = 0. + gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, + desc=self.cmd_PROBE_CALIBRATE_help) # Other commands gcode.register_command('Z_OFFSET_APPLY_PROBE', self.cmd_Z_OFFSET_APPLY_PROBE, desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) + def _move(self, coord, speed): + self.printer.lookup_object('toolhead').manual_move(coord, speed) def get_status(self, eventtime): return {'name': self.name, 'last_query': self.last_state, @@ -56,6 +62,35 @@ class ProbeCommandHelper: pos = run_single_probe(self.probe, gcmd) gcmd.respond_info("Result is z=%.6f" % (pos[2],)) self.last_z_result = pos[2] + def probe_calibrate_finalize(self, kin_pos): + if kin_pos is None: + return + z_offset = self.probe_calibrate_z - kin_pos[2] + gcode = self.printer.lookup_object('gcode') + gcode.respond_info( + "%s: z_offset: %.3f\n" + "The SAVE_CONFIG command will update the printer config file\n" + "with the above and restart the printer." % (self.name, z_offset)) + configfile = self.printer.lookup_object('configfile') + configfile.set(self.name, 'z_offset', "%.3f" % (z_offset,)) + cmd_PROBE_CALIBRATE_help = "Calibrate the probe's z_offset" + def cmd_PROBE_CALIBRATE(self, gcmd): + manual_probe.verify_no_manual_probe(self.printer) + params = self.probe.get_probe_params(gcmd) + # Perform initial probe + curpos = run_single_probe(self.probe, gcmd) + # Move away from the bed + self.probe_calibrate_z = curpos[2] + curpos[2] += 5. + self._move(curpos, params['lift_speed']) + # Move the nozzle over the probe point + x_offset, y_offset, z_offset = self.probe.get_offsets() + curpos[0] += x_offset + curpos[1] += y_offset + self._move(curpos, params['probe_speed']) + # Start manual probe + manual_probe.ManualProbeHelper(self.printer, gcmd, + self.probe_calibrate_finalize) cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset" def cmd_Z_OFFSET_APPLY_PROBE(self, gcmd): gcode_move = self.printer.lookup_object("gcode_move") @@ -77,14 +112,12 @@ class ProbeCommandHelper: class ProbeSessionHelper: def __init__(self, config, mcu_probe): self.printer = config.get_printer() - self.name = config.get_name() self.mcu_probe = mcu_probe self.speed = config.getfloat('speed', 5.0, above=0.) self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.) self.x_offset = config.getfloat('x_offset', 0.) self.y_offset = config.getfloat('y_offset', 0.) self.z_offset = config.getfloat('z_offset') - self.probe_calibrate_z = 0. self.multi_probe_pending = False # Infer Z position to move to during a probe if config.has_section('stepper_z'): @@ -121,8 +154,6 @@ class ProbeSessionHelper: self._handle_command_error) # Register PROBE/QUERY_PROBE commands self.gcode = self.printer.lookup_object('gcode') - self.gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, - desc=self.cmd_PROBE_CALIBRATE_help) self.gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, desc=self.cmd_PROBE_ACCURACY_help) self.dummy_gcode_cmd = self.gcode.create_gcode_command("", "", {}) @@ -290,33 +321,6 @@ class ProbeSessionHelper: "probe accuracy results: maximum %.6f, minimum %.6f, range %.6f, " "average %.6f, median %.6f, standard deviation %.6f" % ( max_value, min_value, range_value, avg_value, median, sigma)) - def probe_calibrate_finalize(self, kin_pos): - if kin_pos is None: - return - z_offset = self.probe_calibrate_z - kin_pos[2] - self.gcode.respond_info( - "%s: z_offset: %.3f\n" - "The SAVE_CONFIG command will update the printer config file\n" - "with the above and restart the printer." % (self.name, z_offset)) - configfile = self.printer.lookup_object('configfile') - configfile.set(self.name, 'z_offset', "%.3f" % (z_offset,)) - cmd_PROBE_CALIBRATE_help = "Calibrate the probe's z_offset" - def cmd_PROBE_CALIBRATE(self, gcmd): - manual_probe.verify_no_manual_probe(self.printer) - # Perform initial probe - params = self.get_probe_params(gcmd) - curpos = self.run_probe(gcmd) - # Move away from the bed - self.probe_calibrate_z = curpos[2] - curpos[2] += 5. - self._move(curpos, params['lift_speed']) - # Move the nozzle over the probe point - curpos[0] += self.x_offset - curpos[1] += self.y_offset - self._move(curpos, self.speed) - # Start manual probe - manual_probe.ManualProbeHelper(self.printer, gcmd, - self.probe_calibrate_finalize) ###################################################################### From f9a2920cee3b9311788bb5c53f1a9b4a3a01c9ee Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 23 May 2024 15:22:47 -0400 Subject: [PATCH 036/111] probe: Move PROBE_ACCURACY command to ProbeCommandHelper class Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 140 ++++++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 66 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index c3b8a0dbf..fbafb0b7f 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -13,6 +13,22 @@ consider reducing the Z axis minimum position so the probe can travel further (the Z minimum position can be negative). """ +# Calculate the average Z from a set of positions +def calc_probe_z_average(positions, method='average'): + if method != 'median': + # Use mean average + count = float(len(positions)) + return [sum([pos[i] for pos in positions]) / count + for i in range(3)] + # Use median + z_sorted = sorted(positions, key=(lambda p: p[2])) + middle = len(positions) // 2 + if (len(positions) & 1) == 1: + # odd number of samples + return z_sorted[middle] + # even number of samples + return calc_probe_z_average(z_sorted[middle-1:middle+1], 'average') + ###################################################################### # Probe device implementation helpers @@ -39,6 +55,8 @@ class ProbeCommandHelper: gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, desc=self.cmd_PROBE_CALIBRATE_help) # Other commands + gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, + desc=self.cmd_PROBE_ACCURACY_help) gcode.register_command('Z_OFFSET_APPLY_PROBE', self.cmd_Z_OFFSET_APPLY_PROBE, desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) @@ -91,6 +109,51 @@ class ProbeCommandHelper: # Start manual probe manual_probe.ManualProbeHelper(self.printer, gcmd, self.probe_calibrate_finalize) + cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position" + def cmd_PROBE_ACCURACY(self, gcmd): + params = self.probe.get_probe_params(gcmd) + sample_count = gcmd.get_int("SAMPLES", 10, minval=1) + toolhead = self.printer.lookup_object('toolhead') + pos = toolhead.get_position() + gcmd.respond_info("PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f" + " (samples=%d retract=%.3f" + " speed=%.1f lift_speed=%.1f)\n" + % (pos[0], pos[1], pos[2], + sample_count, params['sample_retract_dist'], + params['probe_speed'], params['lift_speed'])) + # Create dummy gcmd with SAMPLES=1 + fo_params = dict(gcmd.get_command_parameters()) + fo_params['SAMPLES'] = '1' + gcode = self.printer.lookup_object('gcode') + fo_gcmd = gcode.create_gcode_command("", "", fo_params) + # Probe bed sample_count times + probe_session = self.probe.start_probe_session(fo_gcmd) + probe_session.multi_probe_begin() + positions = [] + while len(positions) < sample_count: + # Probe position + pos = probe_session.run_probe(fo_gcmd) + positions.append(pos) + # Retract + liftpos = [None, None, pos[2] + params['sample_retract_dist']] + self._move(liftpos, params['lift_speed']) + probe_session.multi_probe_end() + # Calculate maximum, minimum and average values + max_value = max([p[2] for p in positions]) + min_value = min([p[2] for p in positions]) + range_value = max_value - min_value + avg_value = calc_probe_z_average(positions, 'average')[2] + median = calc_probe_z_average(positions, 'median')[2] + # calculate the standard deviation + deviation_sum = 0 + for i in range(len(positions)): + deviation_sum += pow(positions[i][2] - avg_value, 2.) + sigma = (deviation_sum / len(positions)) ** 0.5 + # Show information + gcmd.respond_info( + "probe accuracy results: maximum %.6f, minimum %.6f, range %.6f, " + "average %.6f, median %.6f, standard deviation %.6f" % ( + max_value, min_value, range_value, avg_value, median, sigma)) cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset" def cmd_Z_OFFSET_APPLY_PROBE(self, gcmd): gcode_move = self.printer.lookup_object("gcode_move") @@ -119,6 +182,8 @@ class ProbeSessionHelper: self.y_offset = config.getfloat('y_offset', 0.) self.z_offset = config.getfloat('z_offset') self.multi_probe_pending = False + gcode = self.printer.lookup_object('gcode') + self.dummy_gcode_cmd = gcode.create_gcode_command("", "", {}) # Infer Z position to move to during a probe if config.has_section('stepper_z'): zconfig = config.getsection('stepper_z') @@ -152,11 +217,6 @@ class ProbeSessionHelper: self._handle_home_rails_end) self.printer.register_event_handler("gcode:command_error", self._handle_command_error) - # Register PROBE/QUERY_PROBE commands - self.gcode = self.printer.lookup_object('gcode') - self.gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, - desc=self.cmd_PROBE_ACCURACY_help) - self.dummy_gcode_cmd = self.gcode.create_gcode_command("", "", {}) def _handle_homing_move_begin(self, hmove): if self.mcu_probe in hmove.get_mcu_endstops(): self.mcu_probe.probe_prepare(hmove) @@ -234,29 +294,17 @@ class ProbeSessionHelper: axis_twist_compensation.get_z_compensation_value(pos)) # add z compensation to probe position epos[2] += z_compensation - self.gcode.respond_info("probe at %.3f,%.3f is z=%.6f" - % (epos[0], epos[1], epos[2])) + gcode = self.printer.lookup_object('gcode') + gcode.respond_info("probe at %.3f,%.3f is z=%.6f" + % (epos[0], epos[1], epos[2])) return epos[:3] - def _move(self, coord, speed): - self.printer.lookup_object('toolhead').manual_move(coord, speed) - def _calc_mean(self, positions): - count = float(len(positions)) - return [sum([pos[i] for pos in positions]) / count - for i in range(3)] - def _calc_median(self, positions): - z_sorted = sorted(positions, key=(lambda p: p[2])) - middle = len(positions) // 2 - if (len(positions) & 1) == 1: - # odd number of samples - return z_sorted[middle] - # even number of samples - return self._calc_mean(z_sorted[middle-1:middle+1]) def run_probe(self, gcmd): params = self.get_probe_params(gcmd) must_notify_multi_probe = not self.multi_probe_pending if must_notify_multi_probe: self.multi_probe_begin() - probexy = self.printer.lookup_object('toolhead').get_position()[:2] + toolhead = self.printer.lookup_object('toolhead') + probexy = toolhead.get_position()[:2] retries = 0 positions = [] sample_count = params['samples'] @@ -274,53 +322,13 @@ class ProbeSessionHelper: positions = [] # Retract if len(positions) < sample_count: - self._move(probexy + [pos[2] + params['sample_retract_dist']], - params['lift_speed']) + toolhead.manual_move( + probexy + [pos[2] + params['sample_retract_dist']], + params['lift_speed']) if must_notify_multi_probe: self.multi_probe_end() # Calculate and return result - if params['samples_result'] == 'median': - return self._calc_median(positions) - return self._calc_mean(positions) - cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position" - def cmd_PROBE_ACCURACY(self, gcmd): - params = self.get_probe_params(gcmd) - sample_count = gcmd.get_int("SAMPLES", 10, minval=1) - toolhead = self.printer.lookup_object('toolhead') - pos = toolhead.get_position() - gcmd.respond_info("PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f" - " (samples=%d retract=%.3f" - " speed=%.1f lift_speed=%.1f)\n" - % (pos[0], pos[1], pos[2], - sample_count, params['sample_retract_dist'], - params['probe_speed'], params['lift_speed'])) - # Probe bed sample_count times - self.multi_probe_begin() - positions = [] - while len(positions) < sample_count: - # Probe position - pos = self._probe(params['probe_speed']) - positions.append(pos) - # Retract - liftpos = [None, None, pos[2] + params['sample_retract_dist']] - self._move(liftpos, params['lift_speed']) - self.multi_probe_end() - # Calculate maximum, minimum and average values - max_value = max([p[2] for p in positions]) - min_value = min([p[2] for p in positions]) - range_value = max_value - min_value - avg_value = self._calc_mean(positions)[2] - median = self._calc_median(positions)[2] - # calculate the standard deviation - deviation_sum = 0 - for i in range(len(positions)): - deviation_sum += pow(positions[i][2] - avg_value, 2.) - sigma = (deviation_sum / len(positions)) ** 0.5 - # Show information - gcmd.respond_info( - "probe accuracy results: maximum %.6f, minimum %.6f, range %.6f, " - "average %.6f, median %.6f, standard deviation %.6f" % ( - max_value, min_value, range_value, avg_value, median, sigma)) + return calc_probe_z_average(positions, params['samples_result']) ###################################################################### From de9798fb5be252627a47d73b89607243e201cab6 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 23 May 2024 15:48:15 -0400 Subject: [PATCH 037/111] probe: Move offset handling to new ProbeOffsetsHelper class Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index fbafb0b7f..0bf87460f 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -176,11 +176,6 @@ class ProbeSessionHelper: def __init__(self, config, mcu_probe): self.printer = config.get_printer() self.mcu_probe = mcu_probe - self.speed = config.getfloat('speed', 5.0, above=0.) - self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.) - self.x_offset = config.getfloat('x_offset', 0.) - self.y_offset = config.getfloat('y_offset', 0.) - self.z_offset = config.getfloat('z_offset') self.multi_probe_pending = False gcode = self.printer.lookup_object('gcode') self.dummy_gcode_cmd = gcode.create_gcode_command("", "", {}) @@ -193,6 +188,9 @@ class ProbeSessionHelper: pconfig = config.getsection('printer') self.z_position = pconfig.getfloat('minimum_z_position', 0., note_valid=False) + # Configurable probing speeds + self.speed = config.getfloat('speed', 5.0, above=0.) + self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.) # Multi-sample support (for improved accuracy) self.sample_count = config.getint('samples', 1, minval=1) self.sample_retract_dist = config.getfloat('sample_retract_dist', 2., @@ -269,8 +267,6 @@ class ProbeSessionHelper: 'samples_tolerance': samples_tolerance, 'samples_tolerance_retries': samples_retries, 'samples_result': samples_result} - def get_offsets(self): - return self.x_offset, self.y_offset, self.z_offset def _probe(self, speed): toolhead = self.printer.lookup_object('toolhead') curtime = self.printer.get_reactor().monotonic() @@ -330,6 +326,15 @@ class ProbeSessionHelper: # Calculate and return result return calc_probe_z_average(positions, params['samples_result']) +# Helper to read the xyz probe offsets from the config +class ProbeOffsetsHelper: + def __init__(self, config): + self.x_offset = config.getfloat('x_offset', 0.) + self.y_offset = config.getfloat('y_offset', 0.) + self.z_offset = config.getfloat('z_offset') + def get_offsets(self): + return self.x_offset, self.y_offset, self.z_offset + ###################################################################### # Tools for utilizing the probe @@ -519,11 +524,12 @@ class PrinterProbe: self.printer = config.get_printer() self.cmd_helper = ProbeCommandHelper(config, self, mcu_probe.query_endstop) + self.probe_offsets = ProbeOffsetsHelper(config) self.probe_session = ProbeSessionHelper(config, mcu_probe) def get_probe_params(self, gcmd=None): return self.probe_session.get_probe_params(gcmd) def get_offsets(self): - return self.probe_session.get_offsets() + return self.probe_offsets.get_offsets() def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): From 982a50c70ae6aa17538806de41137158ef682d58 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 23 May 2024 15:58:39 -0400 Subject: [PATCH 038/111] probe: Split z_virtual_endstop handling to new HomingViaProbeHelper class Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 94 +++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 37 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 0bf87460f..12e6109f6 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -171,40 +171,15 @@ class ProbeCommandHelper: configfile = self.printer.lookup_object('configfile') configfile.set(self.name, 'z_offset', "%.3f" % (new_calibrate,)) -# Helper to track multiple probe attempts in a single command -class ProbeSessionHelper: +# Homing via probe:z_virtual_endstop +class HomingViaProbeHelper: def __init__(self, config, mcu_probe): self.printer = config.get_printer() self.mcu_probe = mcu_probe self.multi_probe_pending = False - gcode = self.printer.lookup_object('gcode') - self.dummy_gcode_cmd = gcode.create_gcode_command("", "", {}) - # Infer Z position to move to during a probe - if config.has_section('stepper_z'): - zconfig = config.getsection('stepper_z') - self.z_position = zconfig.getfloat('position_min', 0., - note_valid=False) - else: - pconfig = config.getsection('printer') - self.z_position = pconfig.getfloat('minimum_z_position', 0., - note_valid=False) - # Configurable probing speeds - self.speed = config.getfloat('speed', 5.0, above=0.) - self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.) - # Multi-sample support (for improved accuracy) - self.sample_count = config.getint('samples', 1, minval=1) - self.sample_retract_dist = config.getfloat('sample_retract_dist', 2., - above=0.) - atypes = {'median': 'median', 'average': 'average'} - self.samples_result = config.getchoice('samples_result', atypes, - 'average') - self.samples_tolerance = config.getfloat('samples_tolerance', 0.100, - minval=0.) - self.samples_retries = config.getint('samples_tolerance_retries', 0, - minval=0) # Register z_virtual_endstop pin self.printer.lookup_object('pins').register_chip('probe', self) - # Register homing event handlers + # Register event handlers self.printer.register_event_handler("homing:homing_move_begin", self._handle_homing_move_begin) self.printer.register_event_handler("homing:homing_move_end", @@ -224,11 +199,62 @@ class ProbeSessionHelper: def _handle_home_rails_begin(self, homing_state, rails): endstops = [es for rail in rails for es, name in rail.get_endstops()] if self.mcu_probe in endstops: - self.multi_probe_begin() + self.mcu_probe.multi_probe_begin() + self.multi_probe_pending = True def _handle_home_rails_end(self, homing_state, rails): endstops = [es for rail in rails for es, name in rail.get_endstops()] - if self.mcu_probe in endstops: - self.multi_probe_end() + if self.multi_probe_pending and self.mcu_probe in endstops: + self.multi_probe_pending = False + self.mcu_probe.multi_probe_end() + def _handle_command_error(self): + if self.multi_probe_pending: + self.multi_probe_pending = False + try: + self.mcu_probe.multi_probe_end() + except: + logging.exception("Homing multi-probe end") + def setup_pin(self, pin_type, pin_params): + if pin_type != 'endstop' or pin_params['pin'] != 'z_virtual_endstop': + raise pins.error("Probe virtual endstop only useful as endstop pin") + if pin_params['invert'] or pin_params['pullup']: + raise pins.error("Can not pullup/invert probe virtual endstop") + return self.mcu_probe + +# Helper to track multiple probe attempts in a single command +class ProbeSessionHelper: + def __init__(self, config, mcu_probe): + self.printer = config.get_printer() + self.mcu_probe = mcu_probe + self.multi_probe_pending = False + gcode = self.printer.lookup_object('gcode') + self.dummy_gcode_cmd = gcode.create_gcode_command("", "", {}) + # Infer Z position to move to during a probe + if config.has_section('stepper_z'): + zconfig = config.getsection('stepper_z') + self.z_position = zconfig.getfloat('position_min', 0., + note_valid=False) + else: + pconfig = config.getsection('printer') + self.z_position = pconfig.getfloat('minimum_z_position', 0., + note_valid=False) + self.homing_helper = HomingViaProbeHelper(config, mcu_probe) + # Configurable probing speeds + self.speed = config.getfloat('speed', 5.0, above=0.) + self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.) + # Multi-sample support (for improved accuracy) + self.sample_count = config.getint('samples', 1, minval=1) + self.sample_retract_dist = config.getfloat('sample_retract_dist', 2., + above=0.) + atypes = {'median': 'median', 'average': 'average'} + self.samples_result = config.getchoice('samples_result', atypes, + 'average') + self.samples_tolerance = config.getfloat('samples_tolerance', 0.100, + minval=0.) + self.samples_retries = config.getint('samples_tolerance_retries', 0, + minval=0) + # Register event handlers + self.printer.register_event_handler("gcode:command_error", + self._handle_command_error) def _handle_command_error(self): try: self.multi_probe_end() @@ -241,12 +267,6 @@ class ProbeSessionHelper: if self.multi_probe_pending: self.multi_probe_pending = False self.mcu_probe.multi_probe_end() - def setup_pin(self, pin_type, pin_params): - if pin_type != 'endstop' or pin_params['pin'] != 'z_virtual_endstop': - raise pins.error("Probe virtual endstop only useful as endstop pin") - if pin_params['invert'] or pin_params['pullup']: - raise pins.error("Can not pullup/invert probe virtual endstop") - return self.mcu_probe def get_probe_params(self, gcmd=None): if gcmd is None: gcmd = self.dummy_gcode_cmd From f4adb2999920e203445e0d6e12b0b08650ae5e19 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 23 May 2024 16:43:55 -0400 Subject: [PATCH 039/111] probe: Ensure all external callers always call end_probe_session() Rework ProbeSessionHelper's multi_probe_start() and multi_probe_end() to start_probe_session() and end_probe_session(). Ensure all external callers always invoke these methods prior to running run_probe(). Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 47 +++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 12e6109f6..ca778cc90 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -1,6 +1,6 @@ # Z-Probe support # -# Copyright (C) 2017-2021 Kevin O'Connor +# Copyright (C) 2017-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import logging @@ -128,7 +128,6 @@ class ProbeCommandHelper: fo_gcmd = gcode.create_gcode_command("", "", fo_params) # Probe bed sample_count times probe_session = self.probe.start_probe_session(fo_gcmd) - probe_session.multi_probe_begin() positions = [] while len(positions) < sample_count: # Probe position @@ -137,7 +136,7 @@ class ProbeCommandHelper: # Retract liftpos = [None, None, pos[2] + params['sample_retract_dist']] self._move(liftpos, params['lift_speed']) - probe_session.multi_probe_end() + probe_session.end_probe_session() # Calculate maximum, minimum and average values max_value = max([p[2] for p in positions]) min_value = min([p[2] for p in positions]) @@ -225,7 +224,6 @@ class ProbeSessionHelper: def __init__(self, config, mcu_probe): self.printer = config.get_printer() self.mcu_probe = mcu_probe - self.multi_probe_pending = False gcode = self.printer.lookup_object('gcode') self.dummy_gcode_cmd = gcode.create_gcode_command("", "", {}) # Infer Z position to move to during a probe @@ -252,21 +250,31 @@ class ProbeSessionHelper: minval=0.) self.samples_retries = config.getint('samples_tolerance_retries', 0, minval=0) + # Session state + self.multi_probe_pending = False # Register event handlers self.printer.register_event_handler("gcode:command_error", self._handle_command_error) def _handle_command_error(self): - try: - self.multi_probe_end() - except: - logging.exception("Multi-probe end") - def multi_probe_begin(self): + if self.multi_probe_pending: + try: + self.end_probe_session() + except: + logging.exception("Multi-probe end") + def _probe_state_error(self): + raise self.printer.command_error( + "Internal probe error - start/end probe session mismatch") + def start_probe_session(self, gcmd): + if self.multi_probe_pending: + self._probe_state_error() self.mcu_probe.multi_probe_begin() self.multi_probe_pending = True - def multi_probe_end(self): - if self.multi_probe_pending: - self.multi_probe_pending = False - self.mcu_probe.multi_probe_end() + return self + def end_probe_session(self): + if not self.multi_probe_pending: + self._probe_state_error() + self.multi_probe_pending = False + self.mcu_probe.multi_probe_end() def get_probe_params(self, gcmd=None): if gcmd is None: gcmd = self.dummy_gcode_cmd @@ -315,10 +323,9 @@ class ProbeSessionHelper: % (epos[0], epos[1], epos[2])) return epos[:3] def run_probe(self, gcmd): + if not self.multi_probe_pending: + self._probe_state_error() params = self.get_probe_params(gcmd) - must_notify_multi_probe = not self.multi_probe_pending - if must_notify_multi_probe: - self.multi_probe_begin() toolhead = self.printer.lookup_object('toolhead') probexy = toolhead.get_position()[:2] retries = 0 @@ -341,8 +348,6 @@ class ProbeSessionHelper: toolhead.manual_move( probexy + [pos[2] + params['sample_retract_dist']], params['lift_speed']) - if must_notify_multi_probe: - self.multi_probe_end() # Calculate and return result return calc_probe_z_average(positions, params['samples_result']) @@ -436,14 +441,13 @@ class ProbePointsHelper: raise gcmd.error("horizontal_move_z can't be less than" " probe's z_offset") probe_session = probe.start_probe_session(gcmd) - probe_session.multi_probe_begin() while 1: done = self._move_next() if done: break pos = probe_session.run_probe(gcmd) self.results.append(pos) - probe_session.multi_probe_end() + probe_session.end_probe_session() def _manual_probe_start(self): done = self._move_next() if not done: @@ -460,6 +464,7 @@ class ProbePointsHelper: def run_single_probe(probe, gcmd): probe_session = probe.start_probe_session(gcmd) pos = probe_session.run_probe(gcmd) + probe_session.end_probe_session() return pos @@ -553,7 +558,7 @@ class PrinterProbe: def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): - return self.probe_session + return self.probe_session.start_probe_session(gcmd) def load_config(config): return PrinterProbe(config, ProbeEndstopWrapper(config)) From e780049a74310d23d110d8beac37b980459893a4 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 24 May 2024 21:04:49 -0400 Subject: [PATCH 040/111] probe: Use an event for axis twist compensation updates Instead of directly calling axis_twist_compensation, send an event that can perform the necessary updates. Signed-off-by: Kevin O'Connor --- klippy/extras/axis_twist_compensation.py | 9 ++++++--- klippy/extras/probe.py | 12 +++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index e01951abb..8f4a581a7 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -38,10 +38,13 @@ class AxisTwistCompensation: # setup calibrater self.calibrater = Calibrater(self, config) + # register events + self.printer.register_event_handler("probe:update_results", + self._update_z_compensation_value) - def get_z_compensation_value(self, pos): + def _update_z_compensation_value(self, pos): if not self.z_compensations: - return 0 + return x_coord = pos[0] z_compensations = self.z_compensations @@ -55,7 +58,7 @@ class AxisTwistCompensation: interpolated_z_compensation = BedMesh.lerp( interpolate_t, z_compensations[interpolate_i], z_compensations[interpolate_i + 1]) - return interpolated_z_compensation + pos[2] += interpolated_z_compensation def clear_compensations(self): self.z_compensations = [] diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index ca778cc90..b643dff9c 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -309,15 +309,9 @@ class ProbeSessionHelper: if "Timeout during endstop homing" in reason: reason += HINT_TIMEOUT raise self.printer.command_error(reason) - # get z compensation from axis_twist_compensation - axis_twist_compensation = self.printer.lookup_object( - 'axis_twist_compensation', None) - z_compensation = 0 - if axis_twist_compensation is not None: - z_compensation = ( - axis_twist_compensation.get_z_compensation_value(pos)) - # add z compensation to probe position - epos[2] += z_compensation + # Allow axis_twist_compensation to update results + self.printer.send_event("probe:update_results", epos) + # Report results gcode = self.printer.lookup_object('gcode') gcode.respond_info("probe at %.3f,%.3f is z=%.6f" % (epos[0], epos[1], epos[2])) From abfe3675d69f11567f7592f4b9659d2db46911a8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 25 May 2024 20:46:56 -0400 Subject: [PATCH 041/111] bltouch: Use ppins.setup_pin() helper Signed-off-by: Kevin O'Connor --- klippy/extras/bltouch.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index b01cdb9ea..482578c7d 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -44,10 +44,7 @@ class BLTouchEndstopWrapper: self.next_cmd_time = self.action_end_time = 0. self.finish_home_complete = self.wait_trigger_complete = None # Create an "endstop" object to handle the sensor pin - pin = config.get('sensor_pin') - pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True) - mcu = pin_params['chip'] - self.mcu_endstop = mcu.setup_pin('endstop', pin_params) + self.mcu_endstop = ppins.setup_pin('endstop', config.get('sensor_pin')) # output mode omodes = {'5V': '5V', 'OD': 'OD', None: None} self.output_mode = config.getchoice('set_output_mode', omodes, None) From 58753e58a24678b78379d9fa124e691ffd624394 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 25 May 2024 20:47:40 -0400 Subject: [PATCH 042/111] probe: Use ppins.setup_pin() helper Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index b643dff9c..6f35f5e17 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -480,10 +480,7 @@ class ProbeEndstopWrapper: config, 'deactivate_gcode', '') # Create an "endstop" object to handle the probe pin ppins = self.printer.lookup_object('pins') - pin = config.get('pin') - pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True) - mcu = pin_params['chip'] - self.mcu_endstop = mcu.setup_pin('endstop', pin_params) + self.mcu_endstop = ppins.setup_pin('endstop', config.get('pin')) self.printer.register_event_handler('klippy:mcu_identify', self._handle_mcu_identify) # Wrappers From f72f94e299e33c77e6c1a4a75cf632fc8fc812e0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 25 May 2024 20:52:18 -0400 Subject: [PATCH 043/111] probe: Move add_steppers() logic to HomingViaProbeHelper class Perform the initial add_steppers() configuration in a single location. Signed-off-by: Kevin O'Connor --- klippy/extras/bltouch.py | 7 ------- klippy/extras/probe.py | 14 +++++++------- klippy/extras/probe_eddy_current.py | 7 ------- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 482578c7d..081b92714 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -28,8 +28,6 @@ class BLTouchEndstopWrapper: self.printer = config.get_printer() self.printer.register_event_handler("klippy:connect", self.handle_connect) - self.printer.register_event_handler('klippy:mcu_identify', - self.handle_mcu_identify) self.position_endstop = config.getfloat('z_offset', minval=0.) self.stow_on_each_sample = config.getboolean('stow_on_each_sample', True) @@ -70,11 +68,6 @@ class BLTouchEndstopWrapper: desc=self.cmd_BLTOUCH_STORE_help) # multi probes state self.multi = 'OFF' - def handle_mcu_identify(self): - kin = self.printer.lookup_object('toolhead').get_kinematics() - for stepper in kin.get_steppers(): - if stepper.is_active_axis('z'): - self.add_stepper(stepper) def handle_connect(self): self.sync_mcu_print_time() self.next_cmd_time += 0.200 diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 6f35f5e17..bb09217ac 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -179,6 +179,8 @@ class HomingViaProbeHelper: # Register z_virtual_endstop pin self.printer.lookup_object('pins').register_chip('probe', self) # Register event handlers + self.printer.register_event_handler('klippy:mcu_identify', + self._handle_mcu_identify) self.printer.register_event_handler("homing:homing_move_begin", self._handle_homing_move_begin) self.printer.register_event_handler("homing:homing_move_end", @@ -189,6 +191,11 @@ class HomingViaProbeHelper: self._handle_home_rails_end) self.printer.register_event_handler("gcode:command_error", self._handle_command_error) + def _handle_mcu_identify(self): + kin = self.printer.lookup_object('toolhead').get_kinematics() + for stepper in kin.get_steppers(): + if stepper.is_active_axis('z'): + self.mcu_probe.add_stepper(stepper) def _handle_homing_move_begin(self, hmove): if self.mcu_probe in hmove.get_mcu_endstops(): self.mcu_probe.probe_prepare(hmove) @@ -481,8 +488,6 @@ class ProbeEndstopWrapper: # Create an "endstop" object to handle the probe pin ppins = self.printer.lookup_object('pins') self.mcu_endstop = ppins.setup_pin('endstop', config.get('pin')) - self.printer.register_event_handler('klippy:mcu_identify', - self._handle_mcu_identify) # Wrappers self.get_mcu = self.mcu_endstop.get_mcu self.add_stepper = self.mcu_endstop.add_stepper @@ -492,11 +497,6 @@ class ProbeEndstopWrapper: self.query_endstop = self.mcu_endstop.query_endstop # multi probes state self.multi = 'OFF' - def _handle_mcu_identify(self): - kin = self.printer.lookup_object('toolhead').get_kinematics() - for stepper in kin.get_steppers(): - if stepper.is_active_axis('z'): - self.add_stepper(stepper) def _raise_probe(self): toolhead = self.printer.lookup_object('toolhead') start_pos = toolhead.get_position() diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 3f4a5e206..d2309617f 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -196,13 +196,6 @@ class EddyEndstopWrapper: self._samples = [] self._is_sampling = self._start_from_home = self._need_stop = False self._trigger_time = 0. - self._printer.register_event_handler('klippy:mcu_identify', - self._handle_mcu_identify) - def _handle_mcu_identify(self): - kin = self._printer.lookup_object('toolhead').get_kinematics() - for stepper in kin.get_steppers(): - if stepper.is_active_axis('z'): - self.add_stepper(stepper) # Measurement gathering def _start_measurements(self, is_home=False): self._need_stop = False From 068d2a9f5a70d87dd1158c22c8573e1823ec2fa1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 27 May 2024 14:59:20 -0400 Subject: [PATCH 044/111] bltouch: No need to use PrinterProbe() class Directly register the BLTouchProbe() class as the main probe interface. Signed-off-by: Kevin O'Connor --- klippy/extras/bltouch.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 081b92714..58f668197 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -1,6 +1,6 @@ # BLTouch support # -# Copyright (C) 2018-2021 Kevin O'Connor +# Copyright (C) 2018-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import logging @@ -23,11 +23,9 @@ Commands = { } # BLTouch "endstop" wrapper -class BLTouchEndstopWrapper: +class BLTouchProbe: def __init__(self, config): self.printer = config.get_printer() - self.printer.register_event_handler("klippy:connect", - self.handle_connect) self.position_endstop = config.getfloat('z_offset', minval=0.) self.stow_on_each_sample = config.getboolean('stow_on_each_sample', True) @@ -60,14 +58,30 @@ class BLTouchEndstopWrapper: self.get_steppers = self.mcu_endstop.get_steppers self.home_wait = self.mcu_endstop.home_wait self.query_endstop = self.mcu_endstop.query_endstop + # multi probes state + self.multi = 'OFF' + # Common probe implementation helpers + self.cmd_helper = probe.ProbeCommandHelper( + config, self, self.mcu_endstop.query_endstop) + self.probe_offsets = probe.ProbeOffsetsHelper(config) + self.probe_session = probe.ProbeSessionHelper(config, self) # Register BLTOUCH_DEBUG command self.gcode = self.printer.lookup_object('gcode') self.gcode.register_command("BLTOUCH_DEBUG", self.cmd_BLTOUCH_DEBUG, desc=self.cmd_BLTOUCH_DEBUG_help) self.gcode.register_command("BLTOUCH_STORE", self.cmd_BLTOUCH_STORE, desc=self.cmd_BLTOUCH_STORE_help) - # multi probes state - self.multi = 'OFF' + # Register events + self.printer.register_event_handler("klippy:connect", + self.handle_connect) + def get_probe_params(self, gcmd=None): + return self.probe_session.get_probe_params(gcmd) + def get_offsets(self): + return self.probe_offsets.get_offsets() + def get_status(self, eventtime): + return self.cmd_helper.get_status(eventtime) + def start_probe_session(self, gcmd): + return self.probe_session.start_probe_session(gcmd) def handle_connect(self): self.sync_mcu_print_time() self.next_cmd_time += 0.200 @@ -268,6 +282,6 @@ class BLTouchEndstopWrapper: self.sync_print_time() def load_config(config): - blt = BLTouchEndstopWrapper(config) - config.get_printer().add_object('probe', probe.PrinterProbe(config, blt)) + blt = BLTouchProbe(config) + config.get_printer().add_object('probe', blt) return blt From 93245b3678b4bf64b5aff8322d5d5b3de38d00d2 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 27 May 2024 15:06:59 -0400 Subject: [PATCH 045/111] smart_effector: No need to use PrinterProbe() class Directly register the SmartEffectorProbe() class as the main probe interface. Signed-off-by: Kevin O'Connor --- klippy/extras/smart_effector.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/klippy/extras/smart_effector.py b/klippy/extras/smart_effector.py index c33de5275..726531421 100644 --- a/klippy/extras/smart_effector.py +++ b/klippy/extras/smart_effector.py @@ -48,7 +48,7 @@ class ControlPinHelper: bit_time += bit_step return bit_time -class SmartEffectorEndstopWrapper: +class SmartEffectorProbe: def __init__(self, config): self.printer = config.get_printer() self.gcode = self.printer.lookup_object('gcode') @@ -64,6 +64,11 @@ class SmartEffectorEndstopWrapper: self.query_endstop = self.probe_wrapper.query_endstop self.multi_probe_begin = self.probe_wrapper.multi_probe_begin self.multi_probe_end = self.probe_wrapper.multi_probe_end + # Common probe implementation helpers + self.cmd_helper = probe.ProbeCommandHelper( + config, self, self.probe_wrapper.query_endstop) + self.probe_offsets = probe.ProbeOffsetsHelper(config) + self.probe_session = probe.ProbeSessionHelper(config, self) # SmartEffector control control_pin = config.get('control_pin', None) if control_pin: @@ -78,6 +83,14 @@ class SmartEffectorEndstopWrapper: self.gcode.register_command("SET_SMART_EFFECTOR", self.cmd_SET_SMART_EFFECTOR, desc=self.cmd_SET_SMART_EFFECTOR_help) + def get_probe_params(self, gcmd=None): + return self.probe_session.get_probe_params(gcmd) + def get_offsets(self): + return self.probe_offsets.get_offsets() + def get_status(self, eventtime): + return self.cmd_helper.get_status(eventtime) + def start_probe_session(self, gcmd): + return self.probe_session.start_probe_session(gcmd) def probing_move(self, pos, speed): phoming = self.printer.lookup_object('homing') return phoming.probing_move(self, pos, speed) @@ -151,7 +164,6 @@ class SmartEffectorEndstopWrapper: gcmd.respond_info('SmartEffector sensitivity was reset') def load_config(config): - smart_effector = SmartEffectorEndstopWrapper(config) - config.get_printer().add_object('probe', - probe.PrinterProbe(config, smart_effector)) + smart_effector = SmartEffectorProbe(config) + config.get_printer().add_object('probe', smart_effector) return smart_effector From 931d1ce8f42ed112e6655c9524367941efb86595 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 27 May 2024 15:13:48 -0400 Subject: [PATCH 046/111] probe_eddy_current: No need to use PrinterProbe() class Directly register the PrinterEddyProbe() class as the main probe interface. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index d2309617f..2fbde3960 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -318,11 +318,23 @@ class PrinterEddyProbe: sensor_type = config.getchoice('sensor_type', {s: s for s in sensors}) self.sensor_helper = sensors[sensor_type](config, self.calibration) # Probe interface - self.probe = EddyEndstopWrapper(config, self.sensor_helper, - self.calibration) - self.printer.add_object('probe', probe.PrinterProbe(config, self.probe)) + self.mcu_probe = EddyEndstopWrapper(config, self.sensor_helper, + self.calibration) + self.cmd_helper = probe.ProbeCommandHelper( + config, self, self.mcu_probe.query_endstop) + self.probe_offsets = probe.ProbeOffsetsHelper(config) + self.probe_session = probe.ProbeSessionHelper(config, self.mcu_probe) + self.printer.add_object('probe', self) def add_client(self, cb): self.sensor_helper.add_client(cb) + def get_probe_params(self, gcmd=None): + return self.probe_session.get_probe_params(gcmd) + def get_offsets(self): + return self.probe_offsets.get_offsets() + def get_status(self, eventtime): + return self.cmd_helper.get_status(eventtime) + def start_probe_session(self, gcmd): + return self.probe_session.start_probe_session(gcmd) def load_config_prefix(config): return PrinterEddyProbe(config) From d4bae4dffe8d3996ed1b1dd8f5230a062ae3d033 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 27 May 2024 15:43:46 -0400 Subject: [PATCH 047/111] probe: Simplify PrinterProbe() now that there are no external callers Create the mcu_probe interface locally within PrinterProbe(). Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index bb09217ac..5a976a06c 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -536,12 +536,13 @@ class ProbeEndstopWrapper: # Main external probe interface class PrinterProbe: - def __init__(self, config, mcu_probe): + def __init__(self, config): self.printer = config.get_printer() + self.mcu_probe = ProbeEndstopWrapper(config) self.cmd_helper = ProbeCommandHelper(config, self, - mcu_probe.query_endstop) + self.mcu_probe.query_endstop) self.probe_offsets = ProbeOffsetsHelper(config) - self.probe_session = ProbeSessionHelper(config, mcu_probe) + self.probe_session = ProbeSessionHelper(config, self.mcu_probe) def get_probe_params(self, gcmd=None): return self.probe_session.get_probe_params(gcmd) def get_offsets(self): @@ -552,4 +553,4 @@ class PrinterProbe: return self.probe_session.start_probe_session(gcmd) def load_config(config): - return PrinterProbe(config, ProbeEndstopWrapper(config)) + return PrinterProbe(config) From 17c645f000da6fe99bf75fd79f8e622747b1709e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 5 Jun 2024 20:01:50 -0400 Subject: [PATCH 048/111] msgproto: Support multi-byte command and response ids Update the msgproto.py code so that it can support message ids that are larger than a single byte. (The host C code in klippy/chelper/msgblock.c already supports multi-byte ids.) Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 2 +- klippy/msgproto.py | 61 +++++++++++++++++++++++----------------------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index d7a679acb..6b106245b 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -87,7 +87,7 @@ class CommandWrapper: if cmd_queue is None: cmd_queue = serial.get_default_command_queue() self._cmd_queue = cmd_queue - self._msgtag = msgparser.lookup_msgtag(msgformat) & 0xffffffff + self._msgtag = msgparser.lookup_msgid(msgformat) & 0xffffffff def send(self, data=(), minclock=0, reqclock=0): cmd = self._cmd.encode(data) self._serial.raw_send(cmd, minclock, reqclock, self._cmd_queue) diff --git a/klippy/msgproto.py b/klippy/msgproto.py index f8a12530e..0fe765934 100644 --- a/klippy/msgproto.py +++ b/klippy/msgproto.py @@ -1,6 +1,6 @@ # Protocol definitions for firmware communication # -# Copyright (C) 2016-2021 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import json, zlib, logging @@ -160,8 +160,8 @@ def convert_msg_format(msgformat): return msgformat class MessageFormat: - def __init__(self, msgid, msgformat, enumerations={}): - self.msgid = msgid + def __init__(self, msgid_bytes, msgformat, enumerations={}): + self.msgid_bytes = msgid_bytes self.msgformat = msgformat self.debugformat = convert_msg_format(msgformat) self.name = msgformat.split()[0] @@ -169,19 +169,17 @@ class MessageFormat: self.param_types = [t for name, t in self.param_names] self.name_to_type = dict(self.param_names) def encode(self, params): - out = [] - out.append(self.msgid) + out = list(self.msgid_bytes) for i, t in enumerate(self.param_types): t.encode(out, params[i]) return out def encode_by_name(self, **params): - out = [] - out.append(self.msgid) + out = list(self.msgid_bytes) for name, t in self.param_names: t.encode(out, params[name]) return out def parse(self, s, pos): - pos += 1 + pos += len(self.msgid_bytes) out = {} for name, t in self.param_names: v, pos = t.parse(s, pos) @@ -198,13 +196,13 @@ class MessageFormat: class OutputFormat: name = '#output' - def __init__(self, msgid, msgformat): - self.msgid = msgid + def __init__(self, msgid_bytes, msgformat): + self.msgid_bytes = msgid_bytes self.msgformat = msgformat self.debugformat = convert_msg_format(msgformat) self.param_types = lookup_output_params(msgformat) def parse(self, s, pos): - pos += 1 + pos += len(self.msgid_bytes) out = [] for t in self.param_types: v, pos = t.parse(s, pos) @@ -219,7 +217,7 @@ class OutputFormat: class UnknownFormat: name = '#unknown' def parse(self, s, pos): - msgid = s[pos] + msgid, param_pos = PT_int32().parse(s, pos) msg = bytes(bytearray(s)) return {'#msgid': msgid, '#msg': msg}, len(s)-MESSAGE_TRAILER_SIZE def format_params(self, params): @@ -234,7 +232,8 @@ class MessageParser: self.messages = [] self.messages_by_id = {} self.messages_by_name = {} - self.msgtag_by_format = {} + self.msgid_by_format = {} + self.msgid_parser = PT_int32() self.config = {} self.version = self.build_versions = "" self.raw_identify_data = "" @@ -266,7 +265,7 @@ class MessageParser: out = ["seq: %02x" % (msgseq,)] pos = MESSAGE_HEADER_SIZE while 1: - msgid = s[pos] + msgid, param_pos = self.msgid_parser.parse(s, pos) mid = self.messages_by_id.get(msgid, self.unknown) params, pos = mid.parse(s, pos) out.append(mid.format_params(params)) @@ -283,14 +282,14 @@ class MessageParser: return "%s %s" % (name, msg) return str(params) def parse(self, s): - msgid = s[MESSAGE_HEADER_SIZE] + msgid, param_pos = self.msgid_parser.parse(s, MESSAGE_HEADER_SIZE) mid = self.messages_by_id.get(msgid, self.unknown) params, pos = mid.parse(s, MESSAGE_HEADER_SIZE) if pos != len(s)-MESSAGE_TRAILER_SIZE: self._error("Extra data at end of message") params['#name'] = mid.name return params - def encode(self, seq, cmd): + def encode_msgblock(self, seq, cmd): msglen = MESSAGE_MIN + len(cmd) seq = (seq & MESSAGE_SEQ_MASK) | MESSAGE_DEST out = [msglen, seq] + cmd @@ -317,11 +316,11 @@ class MessageParser: self._error("Command format mismatch: %s vs %s", msgformat, mp.msgformat) return mp - def lookup_msgtag(self, msgformat): - msgtag = self.msgtag_by_format.get(msgformat) - if msgtag is None: + def lookup_msgid(self, msgformat): + msgid = self.msgid_by_format.get(msgformat) + if msgid is None: self._error("Unknown command: %s", msgformat) - return msgtag + return msgid def create_command(self, msg): parts = msg.strip().split() if not parts: @@ -372,22 +371,22 @@ class MessageParser: start_value, count = value for i in range(count): enums[enum_root + str(start_enum + i)] = start_value + i - def _init_messages(self, messages, command_tags=[], output_tags=[]): - for msgformat, msgtag in messages.items(): + def _init_messages(self, messages, command_ids=[], output_ids=[]): + for msgformat, msgid in messages.items(): msgtype = 'response' - if msgtag in command_tags: + if msgid in command_ids: msgtype = 'command' - elif msgtag in output_tags: + elif msgid in output_ids: msgtype = 'output' - self.messages.append((msgtag, msgtype, msgformat)) - if msgtag < -32 or msgtag > 95: - self._error("Multi-byte msgtag not supported") - self.msgtag_by_format[msgformat] = msgtag - msgid = msgtag & 0x7f + self.messages.append((msgid, msgtype, msgformat)) + self.msgid_by_format[msgformat] = msgid + msgid_bytes = [] + self.msgid_parser.encode(msgid_bytes, msgid) if msgtype == 'output': - self.messages_by_id[msgid] = OutputFormat(msgid, msgformat) + self.messages_by_id[msgid] = OutputFormat(msgid_bytes, + msgformat) else: - msg = MessageFormat(msgid, msgformat, self.enumerations) + msg = MessageFormat(msgid_bytes, msgformat, self.enumerations) self.messages_by_id[msgid] = msg self.messages_by_name[msg.name] = msg def process_identify(self, data, decompress=True): From 36b8831c7e7c4e4481704234fbc27dbe43026f73 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 5 Jun 2024 20:20:24 -0400 Subject: [PATCH 049/111] sensor_bulk: Change maximum data size from 52 to 51 bytes Reduce the maximum data size from 52 bytes to 51 bytes. This will enable support for 2-byte response ids. This change would alter the behavior of the ldc1612 sensor support. Force an ldc1612 command name change so that users are alerted that they must rebuild the micro-controller code upon update of the host code. Signed-off-by: Kevin O'Connor --- klippy/extras/bulk_sensor.py | 2 +- klippy/extras/ldc1612.py | 2 +- src/sensor_bulk.h | 2 +- src/sensor_ldc1612.c | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/klippy/extras/bulk_sensor.py b/klippy/extras/bulk_sensor.py index 1720c0522..b0aa320d0 100644 --- a/klippy/extras/bulk_sensor.py +++ b/klippy/extras/bulk_sensor.py @@ -198,7 +198,7 @@ class ClockSyncRegression: inv_freq = clock_to_print_time(base_mcu + inv_cfreq) - base_time return base_time, base_chip, inv_freq -MAX_BULK_MSG_SIZE = 52 +MAX_BULK_MSG_SIZE = 51 # Read sensor_bulk_data and calculate timestamps for devices that take # samples at a fixed frequency (and produce fixed data size samples). diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index cdc01e045..281d34226 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -117,7 +117,7 @@ class LDC1612: cmdqueue = self.i2c.get_command_queue() self.query_ldc1612_cmd = self.mcu.lookup_command( "query_ldc1612 oid=%c rest_ticks=%u", cq=cmdqueue) - self.ffreader.setup_query_command("query_ldc1612_status oid=%c", + self.ffreader.setup_query_command("query_status_ldc1612 oid=%c", oid=self.oid, cq=cmdqueue) self.ldc1612_setup_home_cmd = self.mcu.lookup_command( "ldc1612_setup_home oid=%c clock=%u threshold=%u" diff --git a/src/sensor_bulk.h b/src/sensor_bulk.h index 9c130bea3..c750dbdae 100644 --- a/src/sensor_bulk.h +++ b/src/sensor_bulk.h @@ -4,7 +4,7 @@ struct sensor_bulk { uint16_t sequence, possible_overflows; uint8_t data_count; - uint8_t data[52]; + uint8_t data[51]; }; void sensor_bulk_reset(struct sensor_bulk *sb); diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 6e52c177c..01cf3ee04 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -210,7 +210,7 @@ command_query_ldc1612(uint32_t *args) DECL_COMMAND(command_query_ldc1612, "query_ldc1612 oid=%c rest_ticks=%u"); void -command_query_ldc1612_status(uint32_t *args) +command_query_status_ldc1612(uint32_t *args) { struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); @@ -232,7 +232,7 @@ command_query_ldc1612_status(uint32_t *args) uint32_t fifo = status & 0x08 ? BYTES_PER_SAMPLE : 0; sensor_bulk_status(&ld->sb, args[0], time1, time2-time1, fifo); } -DECL_COMMAND(command_query_ldc1612_status, "query_ldc1612_status oid=%c"); +DECL_COMMAND(command_query_status_ldc1612, "query_status_ldc1612 oid=%c"); void ldc1612_task(void) From 589bd64ce013691ef8989d3dfbc74ffe0822d480 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 5 Jun 2024 21:37:57 -0400 Subject: [PATCH 050/111] command: Support 2-byte message ids Allow command ids, response ids, and output ids to be either 1 or 2 bytes long. This increases the total number of message types from 128 to 16384. Signed-off-by: Kevin O'Connor --- scripts/buildcommands.py | 98 +++++++++++++++++++++++----------------- src/command.c | 30 ++++++++++-- src/command.h | 9 ++-- src/pru/pru0.c | 2 +- 4 files changed, 90 insertions(+), 49 deletions(-) diff --git a/scripts/buildcommands.py b/scripts/buildcommands.py index 236373c2f..b35873840 100644 --- a/scripts/buildcommands.py +++ b/scripts/buildcommands.py @@ -251,8 +251,9 @@ class HandleCommandGeneration: def __init__(self): self.commands = {} self.encoders = [] - self.msg_to_id = dict(msgproto.DefaultMessages) - self.messages_by_name = { m.split()[0]: m for m in self.msg_to_id } + self.msg_to_encid = dict(msgproto.DefaultMessages) + self.encid_to_msgid = {} + self.messages_by_name = { m.split()[0]: m for m in self.msg_to_encid } self.all_param_types = {} self.ctr_dispatch = { 'DECL_COMMAND_FLAGS': self.decl_command, @@ -280,37 +281,47 @@ class HandleCommandGeneration: def decl_output(self, req): msg = req.split(None, 1)[1] self.encoders.append((None, msg)) + def convert_encoded_msgid(self, encoded_msgid): + if encoded_msgid >= 0x80: + data = [(encoded_msgid >> 7) | 0x80, encoded_msgid & 0x7f] + else: + data = [encoded_msgid] + return msgproto.PT_int32().parse(data, 0)[0] def create_message_ids(self): # Create unique ids for each message type - msgid = max(self.msg_to_id.values()) + encoded_msgid = max(self.msg_to_encid.values()) mlist = list(self.commands.keys()) + [m for n, m in self.encoders] for msgname in mlist: msg = self.messages_by_name.get(msgname, msgname) - if msg not in self.msg_to_id: - msgid += 1 - self.msg_to_id[msg] = msgid - if msgid >= 128: - # The mcu currently assumes all message ids encode to one byte + if msg not in self.msg_to_encid: + encoded_msgid += 1 + self.msg_to_encid[msg] = encoded_msgid + if encoded_msgid >= 1<<14: + # The mcu currently assumes all message ids encode to 1 or 2 bytes error("Too many message ids") + self.encid_to_msgid = { + encoded_msgid: self.convert_encoded_msgid(encoded_msgid) + for encoded_msgid in self.msg_to_encid.values() + } def update_data_dictionary(self, data): - # Handle message ids over 96 (they are decoded as negative numbers) - msg_to_tag = {msg: msgid if msgid < 96 else msgid - 128 - for msg, msgid in self.msg_to_id.items()} - command_tags = [msg_to_tag[msg] + # Convert ids to standard form (use both positive and negative numbers) + msg_to_msgid = {msg: self.encid_to_msgid[encoded_msgid] + for msg, encoded_msgid in self.msg_to_encid.items()} + command_ids = [msg_to_msgid[msg] + for msgname, msg in self.messages_by_name.items() + if msgname in self.commands] + response_ids = [msg_to_msgid[msg] for msgname, msg in self.messages_by_name.items() - if msgname in self.commands] - response_tags = [msg_to_tag[msg] - for msgname, msg in self.messages_by_name.items() - if msgname not in self.commands] - data['commands'] = { msg: msgtag for msg, msgtag in msg_to_tag.items() - if msgtag in command_tags } - data['responses'] = { msg: msgtag for msg, msgtag in msg_to_tag.items() - if msgtag in response_tags } - output = {msg: msgtag for msg, msgtag in msg_to_tag.items() - if msgtag not in command_tags and msgtag not in response_tags} + if msgname not in self.commands] + data['commands'] = { msg: msgid for msg, msgid in msg_to_msgid.items() + if msgid in command_ids } + data['responses'] = { msg: msgid for msg, msgid in msg_to_msgid.items() + if msgid in response_ids } + output = {msg: msgid for msg, msgid in msg_to_msgid.items() + if msgid not in command_ids and msgid not in response_ids} if output: data['output'] = output - def build_parser(self, msgid, msgformat, msgtype): + def build_parser(self, encoded_msgid, msgformat, msgtype): if msgtype == "output": param_types = msgproto.lookup_output_params(msgformat) comment = "Output: " + msgformat @@ -327,17 +338,21 @@ class HandleCommandGeneration: params = 'command_parameters%d' % (paramid,) out = """ // %s - .msg_id=%d, + .encoded_msgid=%d, // msgid=%d .num_params=%d, .param_types = %s, -""" % (comment, msgid, len(types), params) +""" % (comment, encoded_msgid, self.encid_to_msgid[encoded_msgid], + len(types), params) if msgtype == 'response': num_args = (len(types) + types.count('PT_progmem_buffer') + types.count('PT_buffer')) out += " .num_args=%d," % (num_args,) else: + msgid_size = 1 + if encoded_msgid >= 0x80: + msgid_size = 2 max_size = min(msgproto.MESSAGE_MAX, - (msgproto.MESSAGE_MIN + 1 + (msgproto.MESSAGE_MIN + msgid_size + sum([t.max_length for t in param_types]))) out += " .max_size=%d," % (max_size,) return out @@ -347,22 +362,23 @@ class HandleCommandGeneration: encoder_code = [] did_output = {} for msgname, msg in self.encoders: - msgid = self.msg_to_id[msg] - if msgid in did_output: + encoded_msgid = self.msg_to_encid[msg] + if encoded_msgid in did_output: continue - did_output[msgid] = True + did_output[encoded_msgid] = True code = (' if (__builtin_strcmp(str, "%s") == 0)\n' - ' return &command_encoder_%s;\n' % (msg, msgid)) + ' return &command_encoder_%s;\n' + % (msg, encoded_msgid)) if msgname is None: - parsercode = self.build_parser(msgid, msg, 'output') + parsercode = self.build_parser(encoded_msgid, msg, 'output') output_code.append(code) else: - parsercode = self.build_parser(msgid, msg, 'command') + parsercode = self.build_parser(encoded_msgid, msg, 'command') encoder_code.append(code) encoder_defs.append( "const struct command_encoder command_encoder_%s PROGMEM = {" " %s\n};\n" % ( - msgid, parsercode)) + encoded_msgid, parsercode)) fmt = """ %s @@ -384,21 +400,21 @@ ctr_lookup_output(const char *str) "".join(encoder_code).strip(), "".join(output_code).strip()) def generate_commands_code(self): - cmd_by_id = { - self.msg_to_id[self.messages_by_name.get(msgname, msgname)]: cmd + cmd_by_encid = { + self.msg_to_encid[self.messages_by_name.get(msgname, msgname)]: cmd for msgname, cmd in self.commands.items() } - max_cmd_msgid = max(cmd_by_id.keys()) + max_cmd_encid = max(cmd_by_encid.keys()) index = [] externs = {} - for msgid in range(max_cmd_msgid+1): - if msgid not in cmd_by_id: + for encoded_msgid in range(max_cmd_encid+1): + if encoded_msgid not in cmd_by_encid: index.append(" {\n},") continue - funcname, flags, msgname = cmd_by_id[msgid] + funcname, flags, msgname = cmd_by_encid[encoded_msgid] msg = self.messages_by_name[msgname] externs[funcname] = 1 - parsercode = self.build_parser(msgid, msg, 'response') + parsercode = self.build_parser(encoded_msgid, msg, 'response') index.append(" {%s\n .flags=%s,\n .func=%s\n}," % ( parsercode, flags, funcname)) index = "".join(index).strip() @@ -411,7 +427,7 @@ const struct command_parser command_index[] PROGMEM = { %s }; -const uint8_t command_index_size PROGMEM = ARRAY_SIZE(command_index); +const uint16_t command_index_size PROGMEM = ARRAY_SIZE(command_index); """ return fmt % (externs, index) def generate_param_code(self): diff --git a/src/command.c b/src/command.c index 39c09458b..d2d05aff9 100644 --- a/src/command.c +++ b/src/command.c @@ -1,6 +1,6 @@ // Code for parsing incoming commands and encoding outgoing messages // -// Copyright (C) 2016,2017 Kevin O'Connor +// Copyright (C) 2016-2024 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -69,6 +69,28 @@ parse_int(uint8_t **pp) return v; } +// Write an encoded msgid (optimized 2-byte VLQ encoder) +static uint8_t * +encode_msgid(uint8_t *p, uint_fast16_t encoded_msgid) +{ + if (encoded_msgid >= 0x80) + *p++ = (encoded_msgid >> 7) | 0x80; + *p++ = encoded_msgid & 0x7f; + return p; +} + +// Parse an encoded msgid (optimized 2-byte parser, return as positive number) +uint_fast16_t +command_parse_msgid(uint8_t **pp) +{ + uint8_t *p = *pp; + uint_fast16_t encoded_msgid = *p++; + if (encoded_msgid & 0x80) + encoded_msgid = ((encoded_msgid & 0x7f) << 7) | (*p++); + *pp = p; + return encoded_msgid; +} + // Parse an incoming command into 'args' uint8_t * command_parsef(uint8_t *p, uint8_t *maxend @@ -119,7 +141,7 @@ command_encodef(uint8_t *buf, const struct command_encoder *ce, va_list args) uint8_t *maxend = &p[max_size - MESSAGE_MIN]; uint_fast8_t num_params = READP(ce->num_params); const uint8_t *param_types = READP(ce->param_types); - *p++ = READP(ce->msg_id); + p = encode_msgid(p, READP(ce->encoded_msgid)); while (num_params--) { if (p > maxend) goto error; @@ -227,7 +249,7 @@ DECL_SHUTDOWN(sendf_shutdown); // Find the command handler associated with a command static const struct command_parser * -command_lookup_parser(uint_fast8_t cmdid) +command_lookup_parser(uint_fast16_t cmdid) { if (!cmdid || cmdid >= READP(command_index_size)) shutdown("Invalid command"); @@ -309,7 +331,7 @@ command_dispatch(uint8_t *buf, uint_fast8_t msglen) uint8_t *p = &buf[MESSAGE_HEADER_SIZE]; uint8_t *msgend = &buf[msglen-MESSAGE_TRAILER_SIZE]; while (p < msgend) { - uint_fast8_t cmdid = *p++; + uint_fast16_t cmdid = command_parse_msgid(&p); const struct command_parser *cp = command_lookup_parser(cmdid); uint32_t args[READP(cp->num_args)]; p = command_parsef(p, msgend, cp, args); diff --git a/src/command.h b/src/command.h index 894114d71..21b3f79b8 100644 --- a/src/command.h +++ b/src/command.h @@ -57,11 +57,13 @@ #define MESSAGE_SYNC 0x7E struct command_encoder { - uint8_t msg_id, max_size, num_params; + uint16_t encoded_msgid; + uint8_t max_size, num_params; const uint8_t *param_types; }; struct command_parser { - uint8_t msg_id, num_args, flags, num_params; + uint16_t encoded_msgid; + uint8_t num_args, flags, num_params; const uint8_t *param_types; void (*func)(uint32_t *args); }; @@ -72,6 +74,7 @@ enum { // command.c void *command_decode_ptr(uint32_t v); +uint_fast16_t command_parse_msgid(uint8_t **pp); uint8_t *command_parsef(uint8_t *p, uint8_t *maxend , const struct command_parser *cp, uint32_t *args); uint_fast8_t command_encode_and_frame( @@ -86,7 +89,7 @@ int_fast8_t command_find_and_dispatch(uint8_t *buf, uint_fast8_t buf_len // out/compile_time_request.c (auto generated file) extern const struct command_parser command_index[]; -extern const uint8_t command_index_size; +extern const uint16_t command_index_size; extern const uint8_t command_identify_data[]; extern const uint32_t command_identify_size; const struct command_encoder *ctr_lookup_encoder(const char *str); diff --git a/src/pru/pru0.c b/src/pru/pru0.c index 57d55d279..8a11e1402 100644 --- a/src/pru/pru0.c +++ b/src/pru/pru0.c @@ -141,7 +141,7 @@ do_dispatch(uint8_t *buf, uint32_t msglen) uint8_t *msgend = &buf[msglen-MESSAGE_TRAILER_SIZE]; while (p < msgend) { // Parse command - uint_fast8_t cmdid = *p++; + uint_fast16_t cmdid = command_parse_msgid(&p); const struct command_parser *cp = &SHARED_MEM->command_index[cmdid]; if (!cmdid || cmdid >= SHARED_MEM->command_index_size || cp->num_args > ARRAY_SIZE(SHARED_MEM->next_command_args)) { From 0d87bec159eba63744d820b5beaef5da20388ba4 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Tue, 11 Jun 2024 16:14:38 -0400 Subject: [PATCH 051/111] ci-install: update gnu-pru to version 2024.05 Signed-off-by: Eric Callahan --- scripts/ci-install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci-install.sh b/scripts/ci-install.sh index a7d2599a2..f5a18612f 100755 --- a/scripts/ci-install.sh +++ b/scripts/ci-install.sh @@ -35,7 +35,7 @@ if [ ! -f ${PRU_FILE} ]; then cd ${BUILD_DIR} git config --global user.email "you@example.com" git config --global user.name "Your Name" - git clone https://github.com/dinuxbg/gnupru -b 2023.01 --depth 1 + git clone https://github.com/dinuxbg/gnupru -b 2024.05 --depth 1 cd gnupru export PREFIX=${PRU_DIR} ./download-and-prepare.sh 2>&1 | pv -nli 30 > ${BUILD_DIR}/gnupru-build.log From 8de7153952c7513adec9f35a8d05b9f8bd00710a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 31 May 2024 13:17:41 -0400 Subject: [PATCH 052/111] probe: Rework ProbePointsHelper to store results locally Store the results of each probe attempt in a local "results" variable (instead of a class variable) when performing "automatic" probes. This is in preparation for gathering the results in the probing implementation. Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 66 +++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 5a976a06c..944956ea7 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -386,7 +386,7 @@ class ProbePointsHelper: # Internal probing state self.lift_speed = self.speed self.probe_offsets = (0., 0., 0.) - self.results = [] + self.manual_results = [] def minimum_points(self,n): if len(self.probe_points) < n: raise self.printer.config_error( @@ -398,34 +398,33 @@ class ProbePointsHelper: self.use_offsets = use_offsets def get_lift_speed(self): return self.lift_speed - def _move_next(self): - toolhead = self.printer.lookup_object('toolhead') - # Lift toolhead + def _move(self, coord, speed): + self.printer.lookup_object('toolhead').manual_move(coord, speed) + def _raise_tool(self, is_first=False): speed = self.lift_speed - if not self.results: + if is_first: # Use full speed to first probe position speed = self.speed - toolhead.manual_move([None, None, self.horizontal_move_z], speed) - # Check if done probing - if len(self.results) >= len(self.probe_points): - toolhead.get_last_move_time() - res = self.finalize_callback(self.probe_offsets, self.results) - if res != "retry": - return True - self.results = [] + self._move([None, None, self.horizontal_move_z], speed) + def _invoke_callback(self, results): + # Flush lookahead queue + toolhead = self.printer.lookup_object('toolhead') + toolhead.get_last_move_time() + # Invoke callback + res = self.finalize_callback(self.probe_offsets, results) + return res != "retry" + def _move_next(self, probe_num): # Move to next XY probe point - nextpos = list(self.probe_points[len(self.results)]) + nextpos = list(self.probe_points[probe_num]) if self.use_offsets: nextpos[0] -= self.probe_offsets[0] nextpos[1] -= self.probe_offsets[1] - toolhead.manual_move(nextpos, self.speed) - return False + self._move(nextpos, self.speed) def start_probe(self, gcmd): manual_probe.verify_no_manual_probe(self.printer) # Lookup objects probe = self.printer.lookup_object('probe', None) method = gcmd.get('METHOD', 'automatic').lower() - self.results = [] def_move_z = self.default_horizontal_move_z self.horizontal_move_z = gcmd.get_float('HORIZONTAL_MOVE_Z', def_move_z) @@ -433,6 +432,7 @@ class ProbePointsHelper: # Manual probe self.lift_speed = self.speed self.probe_offsets = (0., 0., 0.) + self.manual_results = [] self._manual_probe_start() return # Perform automatic probing @@ -441,24 +441,36 @@ class ProbePointsHelper: if self.horizontal_move_z < self.probe_offsets[2]: raise gcmd.error("horizontal_move_z can't be less than" " probe's z_offset") + results = [] probe_session = probe.start_probe_session(gcmd) while 1: - done = self._move_next() - if done: - break + self._raise_tool(not results) + if len(results) >= len(self.probe_points): + done = self._invoke_callback(results) + if done: + break + # Caller wants a "retry" - clear results and restart probing + results = [] + self._move_next(len(results)) pos = probe_session.run_probe(gcmd) - self.results.append(pos) + results.append(pos) probe_session.end_probe_session() def _manual_probe_start(self): - done = self._move_next() - if not done: - gcmd = self.gcode.create_gcode_command("", "", {}) - manual_probe.ManualProbeHelper(self.printer, gcmd, - self._manual_probe_finalize) + self._raise_tool(not self.manual_results) + if len(self.manual_results) >= len(self.probe_points): + done = self._invoke_callback(self.manual_results) + if done: + return + # Caller wants a "retry" - clear results and restart probing + self.manual_results = [] + self._move_next(len(self.manual_results)) + gcmd = self.gcode.create_gcode_command("", "", {}) + manual_probe.ManualProbeHelper(self.printer, gcmd, + self._manual_probe_finalize) def _manual_probe_finalize(self, kin_pos): if kin_pos is None: return - self.results.append(kin_pos) + self.manual_results.append(kin_pos) self._manual_probe_start() # Helper to obtain a single probe measurement From 1591a51f76cff336f0109bf4459bfa46dca1eaa3 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 31 May 2024 14:10:34 -0400 Subject: [PATCH 053/111] probe: Gather multiple results in ProbeSessionHelper Change run_probe() to gather the results locally, and introduce a new pull_probed_results() method that returns the previously probed results. This is in preparation for future probing code that benefits from batching probe results. Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 944956ea7..6a69d1b5a 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -128,14 +128,16 @@ class ProbeCommandHelper: fo_gcmd = gcode.create_gcode_command("", "", fo_params) # Probe bed sample_count times probe_session = self.probe.start_probe_session(fo_gcmd) - positions = [] - while len(positions) < sample_count: + probe_num = 0 + while probe_num < sample_count: # Probe position - pos = probe_session.run_probe(fo_gcmd) - positions.append(pos) + probe_session.run_probe(fo_gcmd) + probe_num += 1 # Retract + pos = toolhead.get_position() liftpos = [None, None, pos[2] + params['sample_retract_dist']] self._move(liftpos, params['lift_speed']) + positions = probe_session.pull_probed_results() probe_session.end_probe_session() # Calculate maximum, minimum and average values max_value = max([p[2] for p in positions]) @@ -259,6 +261,7 @@ class ProbeSessionHelper: minval=0) # Session state self.multi_probe_pending = False + self.results = [] # Register event handlers self.printer.register_event_handler("gcode:command_error", self._handle_command_error) @@ -276,10 +279,12 @@ class ProbeSessionHelper: self._probe_state_error() self.mcu_probe.multi_probe_begin() self.multi_probe_pending = True + self.results = [] return self def end_probe_session(self): if not self.multi_probe_pending: self._probe_state_error() + self.results = [] self.multi_probe_pending = False self.mcu_probe.multi_probe_end() def get_probe_params(self, gcmd=None): @@ -349,8 +354,13 @@ class ProbeSessionHelper: toolhead.manual_move( probexy + [pos[2] + params['sample_retract_dist']], params['lift_speed']) - # Calculate and return result - return calc_probe_z_average(positions, params['samples_result']) + # Calculate result + epos = calc_probe_z_average(positions, params['samples_result']) + self.results.append(epos) + def pull_probed_results(self): + res = self.results + self.results = [] + return res # Helper to read the xyz probe offsets from the config class ProbeOffsetsHelper: @@ -441,19 +451,20 @@ class ProbePointsHelper: if self.horizontal_move_z < self.probe_offsets[2]: raise gcmd.error("horizontal_move_z can't be less than" " probe's z_offset") - results = [] probe_session = probe.start_probe_session(gcmd) + probe_num = 0 while 1: - self._raise_tool(not results) - if len(results) >= len(self.probe_points): + self._raise_tool(not probe_num) + if probe_num >= len(self.probe_points): + results = probe_session.pull_probed_results() done = self._invoke_callback(results) if done: break - # Caller wants a "retry" - clear results and restart probing - results = [] - self._move_next(len(results)) - pos = probe_session.run_probe(gcmd) - results.append(pos) + # Caller wants a "retry" - restart probing + probe_num = 0 + self._move_next(probe_num) + probe_session.run_probe(gcmd) + probe_num += 1 probe_session.end_probe_session() def _manual_probe_start(self): self._raise_tool(not self.manual_results) @@ -476,7 +487,8 @@ class ProbePointsHelper: # Helper to obtain a single probe measurement def run_single_probe(probe, gcmd): probe_session = probe.start_probe_session(gcmd) - pos = probe_session.run_probe(gcmd) + probe_session.run_probe(gcmd) + pos = probe_session.pull_probed_results()[0] probe_session.end_probe_session() return pos From bf1bc1ee0f8b9baac322ffaa00d54afb02c2bd52 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 31 May 2024 15:46:34 -0400 Subject: [PATCH 054/111] probe_eddy_current: Introduce new EddyGatherSamples helper class Split the sample gathering code out of EddyEndstopWrapper class and into a new EddyGatherSamples class. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 147 ++++++++++++++++------------ 1 file changed, 84 insertions(+), 63 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 2fbde3960..848f4d790 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -183,7 +183,81 @@ class EddyCalibration: manual_probe.ManualProbeHelper(self.printer, gcmd, self.post_manual_probe) -# Helper for implementing PROBE style commands +# Tool to gather samples and convert them to probe positions +class EddyGatherSamples: + def __init__(self, printer, sensor_helper, calibration, z_offset): + self._printer = printer + self._sensor_helper = sensor_helper + self._calibration = calibration + self._z_offset = z_offset + # Results storage + self._samples = [] + self._probe_times = [] + self._need_stop = False + # Start samples + if not self._calibration.is_calibrated(): + raise self._printer.command_error( + "Must calibrate probe_eddy_current first") + sensor_helper.add_client(self._add_measurement) + def _add_measurement(self, msg): + if self._need_stop: + del self._samples[:] + return False + self._samples.append(msg) + return True + def finish(self): + self._need_stop = True + def _await_samples(self, end_time): + # Make sure enough samples have been collected + reactor = self._printer.get_reactor() + mcu = self._sensor_helper.get_mcu() + while 1: + if self._samples and self._samples[-1]['data'][-1][0] >= end_time: + break + systime = reactor.monotonic() + est_print_time = mcu.estimated_print_time(systime) + if est_print_time > end_time + 1.0: + raise self._printer.command_error( + "probe_eddy_current sensor outage") + reactor.pause(systime + 0.010) + def _pull_position(self, start_time, end_time): + # Find average sensor position between time range + msg_num = discard_msgs = 0 + samp_sum = 0. + samp_count = 0 + while msg_num < len(self._samples): + msg = self._samples[msg_num] + msg_num += 1 + data = msg['data'] + if data[0][0] > end_time: + break + if data[-1][0] < start_time: + discard_msgs = msg_num + continue + for time, freq, z in data: + if time >= start_time and time <= end_time: + samp_sum += z + samp_count += 1 + del self._samples[:discard_msgs] + if not samp_count: + raise self._printer.command_error( + "Unable to obtain probe_eddy_current sensor readings") + return samp_sum / samp_count + def pull_probed(self): + results = [] + for start_time, end_time, toolhead_pos in self._probe_times: + self._await_samples(end_time) + sensor_z = self._pull_position(start_time, end_time) + # Callers expect position relative to z_offset, so recalculate + bed_deviation = toolhead_pos[2] - sensor_z + toolhead_pos[2] = self._z_offset + bed_deviation + results.append(toolhead_pos) + del self._probe_times[:] + return results + def note_probe(self, start_time, end_time, toolhead_pos): + self._probe_times.append((start_time, end_time, toolhead_pos)) + +# Helper for implementing PROBE style commands (descend until trigger) class EddyEndstopWrapper: REASON_SENSOR_ERROR = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 def __init__(self, config, sensor_helper, calibration): @@ -193,28 +267,8 @@ class EddyEndstopWrapper: self._calibration = calibration self._z_offset = config.getfloat('z_offset', minval=0.) self._dispatch = mcu.TriggerDispatch(self._mcu) - self._samples = [] - self._is_sampling = self._start_from_home = self._need_stop = False self._trigger_time = 0. - # Measurement gathering - def _start_measurements(self, is_home=False): - self._need_stop = False - if self._is_sampling: - return - self._is_sampling = True - self._start_from_home = is_home - self._sensor_helper.add_client(self._add_measurement) - def _stop_measurements(self, is_home=False): - if not self._is_sampling or (is_home and not self._start_from_home): - return - self._need_stop = True - def _add_measurement(self, msg): - if self._need_stop: - del self._samples[:] - self._is_sampling = self._need_stop = False - return False - self._samples.append(msg) - return True + self._gather = None # Interface for MCU_endstop def get_mcu(self): return self._mcu @@ -225,7 +279,6 @@ class EddyEndstopWrapper: def home_start(self, print_time, sample_time, sample_count, rest_time, triggered=True): self._trigger_time = 0. - self._start_measurements(is_home=True) trigger_freq = self._calibration.height_to_freq(self._z_offset) trigger_completion = self._dispatch.start(print_time) self._sensor_helper.setup_home( @@ -235,7 +288,6 @@ class EddyEndstopWrapper: def home_wait(self, home_end_time): self._dispatch.wait_end(home_end_time) trigger_time = self._sensor_helper.clear_home() - self._stop_measurements(is_home=True) res = self._dispatch.stop() if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: @@ -257,50 +309,19 @@ class EddyEndstopWrapper: trig_pos = phoming.probing_move(self, pos, speed) if not self._trigger_time: return trig_pos - # Wait for samples to arrive + # Extract samples start_time = self._trigger_time + 0.050 end_time = start_time + 0.100 - reactor = self._printer.get_reactor() - while 1: - if self._samples and self._samples[-1]['data'][-1][0] >= end_time: - break - systime = reactor.monotonic() - est_print_time = self._mcu.estimated_print_time(systime) - if est_print_time > self._trigger_time + 1.0: - raise self._printer.command_error( - "probe_eddy_current sensor outage") - reactor.pause(systime + 0.010) - # Find position since trigger - samples = self._samples - self._samples = [] - samp_sum = 0. - samp_count = 0 - for msg in samples: - data = msg['data'] - if data[0][0] > end_time: - break - if data[-1][0] < start_time: - continue - for time, freq, z in data: - if time >= start_time and time <= end_time: - samp_sum += z - samp_count += 1 - if not samp_count: - raise self._printer.command_error( - "Unable to obtain probe_eddy_current sensor readings") - halt_z = samp_sum / samp_count - # Calculate reported "trigger" position toolhead = self._printer.lookup_object("toolhead") - new_pos = toolhead.get_position() - new_pos[2] += self._z_offset - halt_z - return new_pos + toolhead_pos = toolhead.get_position() + self._gather.note_probe(start_time, end_time, toolhead_pos) + return self._gather.pull_probed()[0] def multi_probe_begin(self): - if not self._calibration.is_calibrated(): - raise self._printer.command_error( - "Must calibrate probe_eddy_current first") - self._start_measurements() + self._gather = EddyGatherSamples(self._printer, self._sensor_helper, + self._calibration, self._z_offset) def multi_probe_end(self): - self._stop_measurements() + self._gather.finish() + self._gather = None def probe_prepare(self, hmove): pass def probe_finish(self, hmove): From 429aa2b2a6d48246331af38929d358e265e0ef19 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 31 May 2024 16:52:07 -0400 Subject: [PATCH 055/111] probe_eddy_current: Generate Z height from average frequency Calculate the average frequency from a set of samples, and then calculate the estimated Z height from that frequency. This may improve accuracy, as the frequency to Z height is not linear and averaging after the non-linear transform could bias the results. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 848f4d790..dbb3f1d39 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -7,6 +7,8 @@ import logging, math, bisect import mcu from . import ldc1612, probe, manual_probe +OUT_OF_RANGE = 99.9 + # Tool for calibrating the sensor Z detection and applying that calibration class EddyCalibration: def __init__(self, config): @@ -38,9 +40,9 @@ class EddyCalibration: for i, (samp_time, freq, dummy_z) in enumerate(samples): pos = bisect.bisect(self.cal_freqs, freq) if pos >= len(self.cal_zpos): - zpos = -99.9 + zpos = -OUT_OF_RANGE elif pos == 0: - zpos = 99.9 + zpos = OUT_OF_RANGE else: # XXX - could further optimize and avoid div by zero this_freq = self.cal_freqs[pos] @@ -51,6 +53,10 @@ class EddyCalibration: offset = prev_zpos - prev_freq * gain zpos = freq * gain + offset samples[i] = (samp_time, freq, round(zpos, 6)) + def freq_to_height(self, freq): + dummy_sample = [(0., freq, 0.)] + self.apply_calibration(dummy_sample) + return dummy_sample[0][2] def height_to_freq(self, height): # XXX - could optimize lookup rev_zpos = list(reversed(self.cal_zpos)) @@ -220,8 +226,8 @@ class EddyGatherSamples: raise self._printer.command_error( "probe_eddy_current sensor outage") reactor.pause(systime + 0.010) - def _pull_position(self, start_time, end_time): - # Find average sensor position between time range + def _pull_freq(self, start_time, end_time): + # Find average sensor frequency between time range msg_num = discard_msgs = 0 samp_sum = 0. samp_count = 0 @@ -236,7 +242,7 @@ class EddyGatherSamples: continue for time, freq, z in data: if time >= start_time and time <= end_time: - samp_sum += z + samp_sum += freq samp_count += 1 del self._samples[:discard_msgs] if not samp_count: @@ -247,7 +253,11 @@ class EddyGatherSamples: results = [] for start_time, end_time, toolhead_pos in self._probe_times: self._await_samples(end_time) - sensor_z = self._pull_position(start_time, end_time) + freq = self._pull_freq(start_time, end_time) + sensor_z = self._calibration.freq_to_height(freq) + if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: + raise self._printer.command_error( + "probe_eddy_current sensor not in valid range") # Callers expect position relative to z_offset, so recalculate bed_deviation = toolhead_pos[2] - sensor_z toolhead_pos[2] = self._z_offset + bed_deviation From 49f511e6798bae55fd25df8662eb6d5234f17280 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 6 Jun 2024 16:15:51 -0400 Subject: [PATCH 056/111] probe_eddy_current: Process samples as they arrive Convert samples into probe frequencies as the samples arrive. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 34 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index dbb3f1d39..f661560e5 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -199,6 +199,7 @@ class EddyGatherSamples: # Results storage self._samples = [] self._probe_times = [] + self._probe_results = [] self._need_stop = False # Start samples if not self._calibration.is_calibrated(): @@ -210,16 +211,16 @@ class EddyGatherSamples: del self._samples[:] return False self._samples.append(msg) + self._check_samples() return True def finish(self): self._need_stop = True - def _await_samples(self, end_time): + def _await_samples(self): # Make sure enough samples have been collected reactor = self._printer.get_reactor() mcu = self._sensor_helper.get_mcu() - while 1: - if self._samples and self._samples[-1]['data'][-1][0] >= end_time: - break + while self._probe_times: + start_time, end_time, toolhead_pos = self._probe_times[0] systime = reactor.monotonic() est_print_time = mcu.estimated_print_time(systime) if est_print_time > end_time + 1.0: @@ -246,14 +247,24 @@ class EddyGatherSamples: samp_count += 1 del self._samples[:discard_msgs] if not samp_count: - raise self._printer.command_error( - "Unable to obtain probe_eddy_current sensor readings") + # No sensor readings - raise error in pull_probed() + return 0. return samp_sum / samp_count - def pull_probed(self): - results = [] - for start_time, end_time, toolhead_pos in self._probe_times: - self._await_samples(end_time) + def _check_samples(self): + while self._samples and self._probe_times: + start_time, end_time, toolhead_pos = self._probe_times[0] + if self._samples[-1]['data'][-1][0] < end_time: + break freq = self._pull_freq(start_time, end_time) + self._probe_results.append((freq, toolhead_pos)) + self._probe_times.pop(0) + def pull_probed(self): + self._await_samples() + results = [] + for freq, toolhead_pos in self._probe_results: + if not freq: + raise self._printer.command_error( + "Unable to obtain probe_eddy_current sensor readings") sensor_z = self._calibration.freq_to_height(freq) if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: raise self._printer.command_error( @@ -262,10 +273,11 @@ class EddyGatherSamples: bed_deviation = toolhead_pos[2] - sensor_z toolhead_pos[2] = self._z_offset + bed_deviation results.append(toolhead_pos) - del self._probe_times[:] + del self._probe_results[:] return results def note_probe(self, start_time, end_time, toolhead_pos): self._probe_times.append((start_time, end_time, toolhead_pos)) + self._check_samples() # Helper for implementing PROBE style commands (descend until trigger) class EddyEndstopWrapper: From aa0dbf6ee652fe87b4fcc828482309c6830f0efb Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 6 Jun 2024 16:39:33 -0400 Subject: [PATCH 057/111] probe_eddy_current: Calculate toolhead position along with probed position Support calculating the low-level kinematic toolhead position while calculating the probed frequency. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index f661560e5..7ec2305eb 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -220,7 +220,7 @@ class EddyGatherSamples: reactor = self._printer.get_reactor() mcu = self._sensor_helper.get_mcu() while self._probe_times: - start_time, end_time, toolhead_pos = self._probe_times[0] + start_time, end_time, pos_time, toolhead_pos = self._probe_times[0] systime = reactor.monotonic() est_print_time = mcu.estimated_print_time(systime) if est_print_time > end_time + 1.0: @@ -250,12 +250,21 @@ class EddyGatherSamples: # No sensor readings - raise error in pull_probed() return 0. return samp_sum / samp_count + def _lookup_toolhead_pos(self, pos_time): + toolhead = self._printer.lookup_object('toolhead') + kin = toolhead.get_kinematics() + kin_spos = {s.get_name(): s.mcu_to_commanded_position( + s.get_past_mcu_position(pos_time)) + for s in kin.get_steppers()} + return kin.calc_position(kin_spos) def _check_samples(self): while self._samples and self._probe_times: - start_time, end_time, toolhead_pos = self._probe_times[0] + start_time, end_time, pos_time, toolhead_pos = self._probe_times[0] if self._samples[-1]['data'][-1][0] < end_time: break freq = self._pull_freq(start_time, end_time) + if pos_time is not None: + toolhead_pos = self._lookup_toolhead_pos(pos_time) self._probe_results.append((freq, toolhead_pos)) self._probe_times.pop(0) def pull_probed(self): @@ -276,7 +285,10 @@ class EddyGatherSamples: del self._probe_results[:] return results def note_probe(self, start_time, end_time, toolhead_pos): - self._probe_times.append((start_time, end_time, toolhead_pos)) + self._probe_times.append((start_time, end_time, None, toolhead_pos)) + self._check_samples() + def note_probe_and_position(self, start_time, end_time, pos_time): + self._probe_times.append((start_time, end_time, pos_time, None)) self._check_samples() # Helper for implementing PROBE style commands (descend until trigger) From fcf064ba6851042a12a71975c229a9670b34cf31 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 31 May 2024 16:48:55 -0400 Subject: [PATCH 058/111] probe_eddy_current: Add support for probing in "scan" mode When probing in "scan" mode, the toolhead will pause at each position, but does not descend. This can notably reduce the total probing time. Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 2 +- klippy/extras/probe_eddy_current.py | 33 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 6a69d1b5a..88aed25fe 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -438,7 +438,7 @@ class ProbePointsHelper: def_move_z = self.default_horizontal_move_z self.horizontal_move_z = gcmd.get_float('HORIZONTAL_MOVE_Z', def_move_z) - if probe is None or method != 'automatic': + if probe is None or method == 'manual': # Manual probe self.lift_speed = self.speed self.probe_offsets = (0., 0., 0.) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 7ec2305eb..de1f84476 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -363,6 +363,34 @@ class EddyEndstopWrapper: def get_position_endstop(self): return self._z_offset +# Implementing probing with "METHOD=scan" +class EddyScanningProbe: + def __init__(self, printer, sensor_helper, calibration, z_offset, gcmd): + self._printer = printer + self._sensor_helper = sensor_helper + self._calibration = calibration + self._z_offset = z_offset + self._gather = EddyGatherSamples(printer, sensor_helper, + calibration, z_offset) + self._sample_time_delay = 0.050 + self._sample_time = 0.100 + def run_probe(self, gcmd): + toolhead = self._printer.lookup_object("toolhead") + printtime = toolhead.get_last_move_time() + toolhead.dwell(self._sample_time_delay + self._sample_time) + start_time = printtime + self._sample_time_delay + self._gather.note_probe_and_position( + start_time, start_time + self._sample_time, start_time) + def pull_probed_results(self): + results = self._gather.pull_probed() + # Allow axis_twist_compensation to update results + for epos in results: + self._printer.send_event("probe:update_results", epos) + return results + def end_probe_session(self): + self._gather.finish() + self._gather = None + # Main "printer object" class PrinterEddyProbe: def __init__(self, config): @@ -389,6 +417,11 @@ class PrinterEddyProbe: def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): + method = gcmd.get('METHOD', 'automatic').lower() + if method == 'scan': + z_offset = self.get_offsets()[2] + return EddyScanningProbe(self.printer, self.sensor_helper, + self.calibration, z_offset, gcmd) return self.probe_session.start_probe_session(gcmd) def load_config_prefix(config): From beba2c2d3380acc966e876ee02e4902ab3e71976 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 15 Jun 2024 11:03:28 -0400 Subject: [PATCH 059/111] axis_twist_compensation: No need to rename bed_mesh and manual_probe Signed-off-by: Kevin O'Connor --- klippy/extras/axis_twist_compensation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index 8f4a581a7..184e99310 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -5,7 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import math -from . import manual_probe as ManualProbe, bed_mesh as BedMesh +from . import manual_probe, bed_mesh DEFAULT_SAMPLE_COUNT = 3 @@ -53,9 +53,9 @@ class AxisTwistCompensation: / (sample_count - 1)) interpolate_t = (x_coord - self.calibrate_start_x) / spacing interpolate_i = int(math.floor(interpolate_t)) - interpolate_i = BedMesh.constrain(interpolate_i, 0, sample_count - 2) + interpolate_i = bed_mesh.constrain(interpolate_i, 0, sample_count - 2) interpolate_t -= interpolate_i - interpolated_z_compensation = BedMesh.lerp( + interpolated_z_compensation = bed_mesh.lerp( interpolate_t, z_compensations[interpolate_i], z_compensations[interpolate_i + 1]) pos[2] += interpolated_z_compensation @@ -137,7 +137,7 @@ class Calibrater: nozzle_points, self.probe_x_offset, self.probe_y_offset) # verify no other manual probe is in progress - ManualProbe.verify_no_manual_probe(self.printer) + manual_probe.verify_no_manual_probe(self.printer) # begin calibration self.current_point_index = 0 @@ -199,7 +199,7 @@ class Calibrater: self._move_helper((nozzle_points[self.current_point_index])) # start the manual (nozzle) probe - ManualProbe.ManualProbeHelper( + manual_probe.ManualProbeHelper( self.printer, self.gcmd, self._manual_probe_callback_factory( probe_points, nozzle_points, interval)) From 433fcb6f249406c8b5e2f25d1e870809beeafb40 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 15 Jun 2024 11:04:07 -0400 Subject: [PATCH 060/111] axis_twist_compensation: Fix missing probe import Fixes missing import introduced in commit bec47e04. Signed-off-by: Kevin O'Connor --- klippy/extras/axis_twist_compensation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index 184e99310..e7aad52c0 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -5,7 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import math -from . import manual_probe, bed_mesh +from . import manual_probe, bed_mesh, probe DEFAULT_SAMPLE_COUNT = 3 From ae227d485cdac859b22d77da4072f870bce07740 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 11 Jun 2024 08:58:31 -0400 Subject: [PATCH 061/111] armcm_link: Fix build on recent arm gcc/newlibc versions It seems recent arm gcc versions no longer build correctly using the "--specs=nano.specs --specs=nosys.specs" linker flags. Replace those linker flags with "-nostdlib -lgcc -lc_nano". Signed-off-by: Kevin O'Connor --- src/atsam/Makefile | 2 +- src/atsamd/Makefile | 2 +- src/generic/armcm_link.lds.S | 3 +++ src/hc32f460/Makefile | 2 +- src/lpc176x/Makefile | 2 +- src/rp2040/Makefile | 7 +++---- src/rp2040/rp2040_link.lds.S | 3 +++ src/stm32/Makefile | 2 +- 8 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/atsam/Makefile b/src/atsam/Makefile index 7ab69b823..3595d0cef 100644 --- a/src/atsam/Makefile +++ b/src/atsam/Makefile @@ -20,7 +20,7 @@ CFLAGS-$(CONFIG_MACH_SAM4E) += -Ilib/sam4e/include CFLAGS-$(CONFIG_MACH_SAME70) += -Ilib/same70b/include CFLAGS += $(CFLAGS-y) -D__$(MCU)__ -mthumb -Ilib/cmsis-core -Ilib/fast-hash -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld $(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld diff --git a/src/atsamd/Makefile b/src/atsamd/Makefile index d241cd8cb..8b9722b62 100644 --- a/src/atsamd/Makefile +++ b/src/atsamd/Makefile @@ -14,7 +14,7 @@ CFLAGS-$(CONFIG_MACH_SAME51) += -Ilib/same51/include CFLAGS-$(CONFIG_MACH_SAMX5) += -mcpu=cortex-m4 -Ilib/same54/include CFLAGS += $(CFLAGS-y) -D__$(MCU)__ -mthumb -Ilib/cmsis-core -Ilib/fast-hash -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld $(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld diff --git a/src/generic/armcm_link.lds.S b/src/generic/armcm_link.lds.S index 2f789f130..94dd2100d 100644 --- a/src/generic/armcm_link.lds.S +++ b/src/generic/armcm_link.lds.S @@ -69,5 +69,8 @@ SECTIONS // that isn't needed so no need to include them in the binary. *(.init) *(.fini) + // Don't include exception tables + *(.ARM.extab) + *(.ARM.exidx) } } diff --git a/src/hc32f460/Makefile b/src/hc32f460/Makefile index c44267369..85d2fa19d 100644 --- a/src/hc32f460/Makefile +++ b/src/hc32f460/Makefile @@ -7,7 +7,7 @@ dirs-y += src/hc32f460 src/generic lib/hc32f460/driver/src lib/hc32f460/mcu/comm CFLAGS += -mthumb -mcpu=cortex-m4 -Isrc/hc32f460 -Ilib/hc32f460/driver/inc -Ilib/hc32f460/mcu/common -Ilib/cmsis-core -DHC32F460 -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld $(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld diff --git a/src/lpc176x/Makefile b/src/lpc176x/Makefile index 7ed80b26b..6814969c5 100644 --- a/src/lpc176x/Makefile +++ b/src/lpc176x/Makefile @@ -7,7 +7,7 @@ dirs-y += src/lpc176x src/generic lib/lpc176x/device CFLAGS += -mthumb -mcpu=cortex-m3 -Ilib/lpc176x/device -Ilib/cmsis-core -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld $(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld diff --git a/src/rp2040/Makefile b/src/rp2040/Makefile index 641990140..b82503a39 100644 --- a/src/rp2040/Makefile +++ b/src/rp2040/Makefile @@ -46,7 +46,6 @@ $(OUT)klipper.uf2: $(OUT)klipper.elf $(OUT)lib/rp2040/elf2uf2/elf2uf2 $(Q)$(OUT)lib/rp2040/elf2uf2/elf2uf2 $< $@ rptarget-$(CONFIG_RP2040_HAVE_STAGE2) := $(OUT)klipper.uf2 -rplink-$(CONFIG_RP2040_HAVE_STAGE2) := $(OUT)src/rp2040/rp2040_link.ld stage2-$(CONFIG_RP2040_HAVE_STAGE2) := $(OUT)stage2.o # rp2040 building when using a bootloader @@ -55,13 +54,13 @@ $(OUT)klipper.bin: $(OUT)klipper.elf $(Q)$(OBJCOPY) -O binary $< $@ rptarget-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)klipper.bin -rplink-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)src/rp2040/rp2040_link.ld # Set klipper.elf linker rules target-y += $(rptarget-y) -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs -T $(rplink-y) +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano +CFLAGS_klipper.elf += -T $(OUT)src/rp2040/rp2040_link.ld OBJS_klipper.elf += $(stage2-y) -$(OUT)klipper.elf: $(stage2-y) $(rplink-y) +$(OUT)klipper.elf: $(stage2-y) $(OUT)src/rp2040/rp2040_link.ld # Flash rules lib/rp2040_flash/rp2040_flash: diff --git a/src/rp2040/rp2040_link.lds.S b/src/rp2040/rp2040_link.lds.S index 9b0264a2b..6c2db3c9b 100644 --- a/src/rp2040/rp2040_link.lds.S +++ b/src/rp2040/rp2040_link.lds.S @@ -77,5 +77,8 @@ SECTIONS // that isn't needed so no need to include them in the binary. *(.init) *(.fini) + // Don't include exception tables + *(.ARM.extab) + *(.ARM.exidx) } } diff --git a/src/stm32/Makefile b/src/stm32/Makefile index 18af2e9d7..5f4d3af5c 100644 --- a/src/stm32/Makefile +++ b/src/stm32/Makefile @@ -31,7 +31,7 @@ CFLAGS-$(CONFIG_MACH_STM32H7) += -mcpu=cortex-m7 -Ilib/stm32h7/include CFLAGS-$(CONFIG_MACH_STM32L4) += -mcpu=cortex-m4 -Ilib/stm32l4/include CFLAGS += $(CFLAGS-y) -D$(MCU_UPPER) -mthumb -Ilib/cmsis-core -Ilib/fast-hash -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld $(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld From 863a463cb2d07fce2ae09e2b0f925bdf201051dc Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 11 Jun 2024 10:50:57 -0400 Subject: [PATCH 062/111] rp2040_link: Explicitly set klipper.elf output section flags to avoid warning Avoid pointless "LOAD segment with RWX permissions" linker warnings during the rp2040 build. Signed-off-by: Kevin O'Connor --- src/rp2040/rp2040_link.lds.S | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/rp2040/rp2040_link.lds.S b/src/rp2040/rp2040_link.lds.S index 6c2db3c9b..abc5be6c5 100644 --- a/src/rp2040/rp2040_link.lds.S +++ b/src/rp2040/rp2040_link.lds.S @@ -21,6 +21,16 @@ MEMORY ram (rwx) : ORIGIN = CONFIG_RAM_START , LENGTH = CONFIG_RAM_SIZE } +// Force flags for each output section to avoid RWX linker warning +PHDRS +{ + text_segment PT_LOAD FLAGS(5); // RX flags + ram_vectortable_segment PT_LOAD FLAGS(6); // RW flags + data_segment PT_LOAD FLAGS(6); // RW flags + bss_segment PT_LOAD FLAGS(6); // RW flags + stack_segment PT_LOAD FLAGS(6); // RW flags +} + SECTIONS { .text : { @@ -32,7 +42,7 @@ SECTIONS KEEP(*(.vector_table)) _text_vectortable_end = .; *(.text.armcm_boot*) - } > rom + } > rom :text_segment . = ALIGN(4); _data_flash = .; @@ -41,7 +51,7 @@ SECTIONS _ram_vectortable_start = .; . = . + ( _text_vectortable_end - _text_vectortable_start ) ; _ram_vectortable_end = .; - } > ram + } > ram :ram_vectortable_segment .data : AT (_data_flash) { @@ -53,7 +63,7 @@ SECTIONS *(.data .data.*); . = ALIGN(4); _data_end = .; - } > ram + } > ram :data_segment .bss (NOLOAD) : { @@ -63,14 +73,14 @@ SECTIONS *(COMMON) . = ALIGN(4); _bss_end = .; - } > ram + } > ram :bss_segment _stack_start = CONFIG_RAM_START + CONFIG_RAM_SIZE - CONFIG_STACK_SIZE ; .stack _stack_start (NOLOAD) : { . = . + CONFIG_STACK_SIZE; _stack_end = .; - } > ram + } > ram :stack_segment /DISCARD/ : { // The .init/.fini sections are used by __libc_init_array(), but From 11f04ba1bae774d32fd43bed4a0772c58d8df199 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 17 Jun 2024 13:49:05 -0400 Subject: [PATCH 063/111] configfile: Allow getchoice() to take a list If a list is passed to getchoice(), seamlessly convert it to a dict. Signed-off-by: Kevin O'Connor --- klippy/configfile.py | 2 ++ klippy/extras/bltouch.py | 2 +- klippy/extras/display/hd44780.py | 2 +- klippy/extras/display/hd44780_spi.py | 2 +- klippy/extras/display/menu_keys.py | 2 +- klippy/extras/probe.py | 2 +- klippy/extras/replicape.py | 2 +- klippy/kinematics/cartesian.py | 2 +- klippy/kinematics/hybrid_corexy.py | 2 +- klippy/kinematics/hybrid_corexz.py | 2 +- klippy/mcu.py | 3 +-- 11 files changed, 12 insertions(+), 11 deletions(-) diff --git a/klippy/configfile.py b/klippy/configfile.py index 91b555cde..a8a4a4ff7 100644 --- a/klippy/configfile.py +++ b/klippy/configfile.py @@ -69,6 +69,8 @@ class ConfigWrapper: return self._get_wrapper(self.fileconfig.getboolean, option, default, note_valid=note_valid) def getchoice(self, option, choices, default=sentinel, note_valid=True): + if type(choices) == type([]): + choices = {i: i for i in choices} if choices and type(list(choices.keys())[0]) == int: c = self.getint(option, default, note_valid=note_valid) else: diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 58f668197..ae461f4b8 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -42,7 +42,7 @@ class BLTouchProbe: # Create an "endstop" object to handle the sensor pin self.mcu_endstop = ppins.setup_pin('endstop', config.get('sensor_pin')) # output mode - omodes = {'5V': '5V', 'OD': 'OD', None: None} + omodes = ['5V', 'OD', None] self.output_mode = config.getchoice('set_output_mode', omodes, None) # Setup for sensor test self.next_test_time = 0. diff --git a/klippy/extras/display/hd44780.py b/klippy/extras/display/hd44780.py index 9adfa20f7..2da49c51e 100644 --- a/klippy/extras/display/hd44780.py +++ b/klippy/extras/display/hd44780.py @@ -8,7 +8,7 @@ import logging BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000 LINE_LENGTH_DEFAULT=20 -LINE_LENGTH_OPTIONS={16:16, 20:20} +LINE_LENGTH_OPTIONS=[16, 20] TextGlyphs = { 'right_arrow': b'\x7e' } diff --git a/klippy/extras/display/hd44780_spi.py b/klippy/extras/display/hd44780_spi.py index cd1d9e3ea..f21accbb4 100644 --- a/klippy/extras/display/hd44780_spi.py +++ b/klippy/extras/display/hd44780_spi.py @@ -9,7 +9,7 @@ import logging from .. import bus LINE_LENGTH_DEFAULT=20 -LINE_LENGTH_OPTIONS={16:16, 20:20} +LINE_LENGTH_OPTIONS=[16, 20] TextGlyphs = { 'right_arrow': b'\x7e' } diff --git a/klippy/extras/display/menu_keys.py b/klippy/extras/display/menu_keys.py index 91a96e19f..8094c9964 100644 --- a/klippy/extras/display/menu_keys.py +++ b/klippy/extras/display/menu_keys.py @@ -18,7 +18,7 @@ class MenuKeys: # Register rotary encoder encoder_pins = config.get('encoder_pins', None) encoder_steps_per_detent = config.getchoice('encoder_steps_per_detent', - {2: 2, 4: 4}, 4) + [2, 4], 4) if encoder_pins is not None: try: pin1, pin2 = encoder_pins.split(',') diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 88aed25fe..c467e181e 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -252,7 +252,7 @@ class ProbeSessionHelper: self.sample_count = config.getint('samples', 1, minval=1) self.sample_retract_dist = config.getfloat('sample_retract_dist', 2., above=0.) - atypes = {'median': 'median', 'average': 'average'} + atypes = ['median', 'average'] self.samples_result = config.getchoice('samples_result', atypes, 'average') self.samples_tolerance = config.getfloat('samples_tolerance', 0.100, diff --git a/klippy/extras/replicape.py b/klippy/extras/replicape.py index ab501cafc..f7f7bb64b 100644 --- a/klippy/extras/replicape.py +++ b/klippy/extras/replicape.py @@ -160,7 +160,7 @@ class Replicape: printer = config.get_printer() ppins = printer.lookup_object('pins') ppins.register_chip('replicape', self) - revisions = {'B3': 'B3'} + revisions = ['B3'] config.getchoice('revision', revisions) self.host_mcu = mcu.get_printer_mcu(printer, config.get('host_mcu')) # Setup enable pin diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index 9774672e2..0c4bb9255 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -23,7 +23,7 @@ class CartKinematics: self.dc_module = None if config.has_section('dual_carriage'): dc_config = config.getsection('dual_carriage') - dc_axis = dc_config.getchoice('axis', {'x': 'x', 'y': 'y'}) + dc_axis = dc_config.getchoice('axis', ['x', 'y']) self.dual_carriage_axis = {'x': 0, 'y': 1}[dc_axis] # setup second dual carriage rail self.rails.append(stepper.LookupMultiRail(dc_config)) diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index e852826af..265a0e6da 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -27,7 +27,7 @@ class HybridCoreXYKinematics: if config.has_section('dual_carriage'): dc_config = config.getsection('dual_carriage') # dummy for cartesian config users - dc_config.getchoice('axis', {'x': 'x'}, default='x') + dc_config.getchoice('axis', ['x'], default='x') # setup second dual carriage rail self.rails.append(stepper.PrinterRail(dc_config)) self.rails[1].get_endstops()[0][0].add_stepper( diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index 58e6b0d39..2d89e3f7b 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -27,7 +27,7 @@ class HybridCoreXZKinematics: if config.has_section('dual_carriage'): dc_config = config.getsection('dual_carriage') # dummy for cartesian config users - dc_config.getchoice('axis', {'x': 'x'}, default='x') + dc_config.getchoice('axis', ['x'], default='x') # setup second dual carriage rail self.rails.append(stepper.PrinterRail(dc_config)) self.rails[2].get_endstops()[0][0].add_stepper( diff --git a/klippy/mcu.py b/klippy/mcu.py index 6b106245b..23ba07173 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -574,9 +574,8 @@ class MCU: restart_methods = [None, 'arduino', 'cheetah', 'command', 'rpi_usb'] self._restart_method = 'command' if self._baud: - rmethods = {m: m for m in restart_methods} self._restart_method = config.getchoice('restart_method', - rmethods, None) + restart_methods, None) self._reset_cmd = self._config_reset_cmd = None self._is_mcu_bridge = False self._emergency_stop_cmd = None From 0a14e3315084065b19163104c3604d13a7c2dbc8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 6 Jun 2024 17:09:02 -0400 Subject: [PATCH 064/111] probe_eddy_current: Add support for "rapid_scan" mode Add a scanning mode that does not require pausing the toolhead at each probe point. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index de1f84476..345096e60 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -373,15 +373,27 @@ class EddyScanningProbe: self._gather = EddyGatherSamples(printer, sensor_helper, calibration, z_offset) self._sample_time_delay = 0.050 - self._sample_time = 0.100 + self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) + self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' + def _rapid_lookahead_cb(self, printtime): + start_time = printtime - self._sample_time / 2 + self._gather.note_probe_and_position( + start_time, start_time + self._sample_time, printtime) def run_probe(self, gcmd): toolhead = self._printer.lookup_object("toolhead") + if self._is_rapid: + toolhead.register_lookahead_callback(self._rapid_lookahead_cb) + return printtime = toolhead.get_last_move_time() toolhead.dwell(self._sample_time_delay + self._sample_time) start_time = printtime + self._sample_time_delay self._gather.note_probe_and_position( start_time, start_time + self._sample_time, start_time) def pull_probed_results(self): + if self._is_rapid: + # Flush lookahead (so all lookahead callbacks are invoked) + toolhead = self._printer.lookup_object("toolhead") + toolhead.get_last_move_time() results = self._gather.pull_probed() # Allow axis_twist_compensation to update results for epos in results: @@ -418,7 +430,7 @@ class PrinterEddyProbe: return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): method = gcmd.get('METHOD', 'automatic').lower() - if method == 'scan': + if method in ('scan', 'rapid_scan'): z_offset = self.get_offsets()[2] return EddyScanningProbe(self.printer, self.sensor_helper, self.calibration, z_offset, gcmd) From f2df011c68aeff1c9240613837f91f04baa8da83 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Tue, 13 Feb 2024 14:55:14 -0500 Subject: [PATCH 065/111] bed_mesh: optimize rapid travel paths This adds supplemental path generation that implements "overshoot" when a change of direction is performed during a rapid scan. This overshoot reduces measurement error at the extremes of the mesh along the X axis. Signed-off-by: Eric Callahan --- klippy/extras/bed_mesh.py | 689 +++++++++++++++++++++++++++----------- 1 file changed, 495 insertions(+), 194 deletions(-) diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 095ccf1fd..d9ee7dfba 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -298,130 +298,24 @@ class BedMeshCalibrate: self.radius = self.origin = None self.mesh_min = self.mesh_max = (0., 0.) self.adaptive_margin = config.getfloat('adaptive_margin', 0.0) - self.zero_ref_pos = config.getfloatlist( - "zero_reference_position", None, count=2 - ) - self.zero_reference_mode = ZrefMode.DISABLED - self.faulty_regions = [] - self.substituted_indices = collections.OrderedDict() self.bedmesh = bedmesh self.mesh_config = collections.OrderedDict() self._init_mesh_config(config) - self._generate_points(config.error) + self.probe_mgr = ProbeManager( + config, self.orig_config, self.probe_finalize + ) + try: + self.probe_mgr.generate_points( + self.mesh_config, self.mesh_min, self.mesh_max, + self.radius, self.origin + ) + except BedMeshError as e: + raise config.error(str(e)) self._profile_name = "default" - self.probe_helper = probe.ProbePointsHelper( - config, self.probe_finalize, self._get_adjusted_points()) - self.probe_helper.minimum_points(3) - self.probe_helper.use_xy_offsets(True) self.gcode = self.printer.lookup_object('gcode') self.gcode.register_command( 'BED_MESH_CALIBRATE', self.cmd_BED_MESH_CALIBRATE, desc=self.cmd_BED_MESH_CALIBRATE_help) - def _generate_points(self, error, probe_method="automatic"): - x_cnt = self.mesh_config['x_count'] - y_cnt = self.mesh_config['y_count'] - min_x, min_y = self.mesh_min - max_x, max_y = self.mesh_max - x_dist = (max_x - min_x) / (x_cnt - 1) - y_dist = (max_y - min_y) / (y_cnt - 1) - # floor distances down to next hundredth - x_dist = math.floor(x_dist * 100) / 100 - y_dist = math.floor(y_dist * 100) / 100 - if x_dist < 1. or y_dist < 1.: - raise error("bed_mesh: min/max points too close together") - - if self.radius is not None: - # round bed, min/max needs to be recalculated - y_dist = x_dist - new_r = (x_cnt // 2) * x_dist - min_x = min_y = -new_r - max_x = max_y = new_r - else: - # rectangular bed, only re-calc max_x - max_x = min_x + x_dist * (x_cnt - 1) - pos_y = min_y - points = [] - for i in range(y_cnt): - for j in range(x_cnt): - if not i % 2: - # move in positive directon - pos_x = min_x + j * x_dist - else: - # move in negative direction - pos_x = max_x - j * x_dist - if self.radius is None: - # rectangular bed, append - points.append((pos_x, pos_y)) - else: - # round bed, check distance from origin - dist_from_origin = math.sqrt(pos_x*pos_x + pos_y*pos_y) - if dist_from_origin <= self.radius: - points.append( - (self.origin[0] + pos_x, self.origin[1] + pos_y)) - pos_y += y_dist - self.points = points - if self.zero_ref_pos is None or probe_method == "manual": - # Zero Reference Disabled - self.zero_reference_mode = ZrefMode.DISABLED - elif within(self.zero_ref_pos, self.mesh_min, self.mesh_max): - # Zero Reference position within mesh - self.zero_reference_mode = ZrefMode.IN_MESH - else: - # Zero Reference position outside of mesh - self.zero_reference_mode = ZrefMode.PROBE - if not self.faulty_regions: - return - self.substituted_indices.clear() - if self.zero_reference_mode == ZrefMode.PROBE: - # Cannot probe a reference within a faulty region - for min_c, max_c in self.faulty_regions: - if within(self.zero_ref_pos, min_c, max_c): - opt = "zero_reference_position" - raise error( - "bed_mesh: Cannot probe zero reference position at " - "(%.2f, %.2f) as it is located within a faulty region." - " Check the value for option '%s'" - % (self.zero_ref_pos[0], self.zero_ref_pos[1], opt,) - ) - # Check to see if any points fall within faulty regions - if probe_method == "manual": - return - last_y = self.points[0][1] - is_reversed = False - for i, coord in enumerate(self.points): - if not isclose(coord[1], last_y): - is_reversed = not is_reversed - last_y = coord[1] - adj_coords = [] - for min_c, max_c in self.faulty_regions: - if within(coord, min_c, max_c, tol=.00001): - # Point lies within a faulty region - adj_coords = [ - (min_c[0], coord[1]), (coord[0], min_c[1]), - (coord[0], max_c[1]), (max_c[0], coord[1])] - if is_reversed: - # Swap first and last points for zig-zag pattern - first = adj_coords[0] - adj_coords[0] = adj_coords[-1] - adj_coords[-1] = first - break - if not adj_coords: - # coord is not located within a faulty region - continue - valid_coords = [] - for ac in adj_coords: - # make sure that coordinates are within the mesh boundary - if self.radius is None: - if within(ac, (min_x, min_y), (max_x, max_y), .000001): - valid_coords.append(ac) - else: - dist_from_origin = math.sqrt(ac[0]*ac[0] + ac[1]*ac[1]) - if dist_from_origin <= self.radius: - valid_coords.append(ac) - if not valid_coords: - raise error("bed_mesh: Unable to generate coordinates" - " for faulty region at index: %d" % (i)) - self.substituted_indices[i] = valid_coords def print_generated_points(self, print_func): x_offset = y_offset = 0. probe = self.printer.lookup_object('probe', None) @@ -429,20 +323,23 @@ class BedMeshCalibrate: x_offset, y_offset = probe.get_offsets()[:2] print_func("bed_mesh: generated points\nIndex" " | Tool Adjusted | Probe") - for i, (x, y) in enumerate(self.points): + points = self.probe_mgr.get_base_points() + for i, (x, y) in enumerate(points): adj_pt = "(%.1f, %.1f)" % (x - x_offset, y - y_offset) mesh_pt = "(%.1f, %.1f)" % (x, y) print_func( " %-4d| %-16s| %s" % (i, adj_pt, mesh_pt)) - if self.zero_ref_pos is not None: + zero_ref_pos = self.probe_mgr.get_zero_ref_pos() + if zero_ref_pos is not None: print_func( "bed_mesh: zero_reference_position is (%.2f, %.2f)" - % (self.zero_ref_pos[0], self.zero_ref_pos[1]) + % (zero_ref_pos[0], zero_ref_pos[1]) ) - if self.substituted_indices: + substitutes = self.probe_mgr.get_substitutes() + if substitutes: print_func("bed_mesh: faulty region points") - for i, v in self.substituted_indices.items(): - pt = self.points[i] + for i, v in substitutes.items(): + pt = points[i] print_func("%d (%.2f, %.2f), substituted points: %s" % (i, pt[0], pt[1], repr(v))) def _init_mesh_config(self, config): @@ -481,42 +378,6 @@ class BedMeshCalibrate: config.get('algorithm', 'lagrange').strip().lower() orig_cfg['tension'] = mesh_cfg['tension'] = config.getfloat( 'bicubic_tension', .2, minval=0., maxval=2.) - for i in list(range(1, 100, 1)): - start = config.getfloatlist("faulty_region_%d_min" % (i,), None, - count=2) - if start is None: - break - end = config.getfloatlist("faulty_region_%d_max" % (i,), count=2) - # Validate the corners. If necessary reorganize them. - # c1 = min point, c3 = max point - # c4 ---- c3 - # | | - # c1 ---- c2 - c1 = [min([s, e]) for s, e in zip(start, end)] - c3 = [max([s, e]) for s, e in zip(start, end)] - c2 = [c1[0], c3[1]] - c4 = [c3[0], c1[1]] - # Check for overlapping regions - for j, (prev_c1, prev_c3) in enumerate(self.faulty_regions): - prev_c2 = [prev_c1[0], prev_c3[1]] - prev_c4 = [prev_c3[0], prev_c1[1]] - # Validate that no existing corner is within the new region - for coord in [prev_c1, prev_c2, prev_c3, prev_c4]: - if within(coord, c1, c3): - raise config.error( - "bed_mesh: Existing faulty_region_%d %s overlaps " - "added faulty_region_%d %s" - % (j+1, repr([prev_c1, prev_c3]), - i, repr([c1, c3]))) - # Validate that no new corner is within an existing region - for coord in [c1, c2, c3, c4]: - if within(coord, prev_c1, prev_c3): - raise config.error( - "bed_mesh: Added faulty_region_%d %s overlaps " - "existing faulty_region_%d %s" - % (i, repr([c1, c3]), - j+1, repr([prev_c1, prev_c3]))) - self.faulty_regions.append((c1, c3)) self._verify_algorithm(config.error) def _verify_algorithm(self, error): params = self.mesh_config @@ -712,47 +573,36 @@ class BedMeshCalibrate: if need_cfg_update: self._verify_algorithm(gcmd.error) - self._generate_points(gcmd.error, probe_method) + self.probe_mgr.generate_points( + self.mesh_config, self.mesh_min, self.mesh_max, + self.radius, self.origin, probe_method + ) gcmd.respond_info("Generating new points...") self.print_generated_points(gcmd.respond_info) - pts = self._get_adjusted_points() - self.probe_helper.update_probe_points(pts, 3) msg = "\n".join(["%s: %s" % (k, v) for k, v in self.mesh_config.items()]) logging.info("Updated Mesh Configuration:\n" + msg) else: - self._generate_points(gcmd.error, probe_method) - pts = self._get_adjusted_points() - self.probe_helper.update_probe_points(pts, 3) - def _get_adjusted_points(self): - adj_pts = [] - if self.substituted_indices: - last_index = 0 - for i, pts in self.substituted_indices.items(): - adj_pts.extend(self.points[last_index:i]) - adj_pts.extend(pts) - # Add one to the last index to skip the point - # we are replacing - last_index = i + 1 - adj_pts.extend(self.points[last_index:]) - else: - adj_pts = list(self.points) - if self.zero_reference_mode == ZrefMode.PROBE: - adj_pts.append(self.zero_ref_pos) - return adj_pts + self.probe_mgr.generate_points( + self.mesh_config, self.mesh_min, self.mesh_max, + self.radius, self.origin, probe_method + ) cmd_BED_MESH_CALIBRATE_help = "Perform Mesh Bed Leveling" def cmd_BED_MESH_CALIBRATE(self, gcmd): self._profile_name = gcmd.get('PROFILE', "default") if not self._profile_name.strip(): raise gcmd.error("Value for parameter 'PROFILE' must be specified") self.bedmesh.set_mesh(None) - self.update_config(gcmd) - self.probe_helper.start_probe(gcmd) + try: + self.update_config(gcmd) + except BedMeshError as e: + raise gcmd.error(str(e)) + self.probe_mgr.start_probe(gcmd) def probe_finalize(self, offsets, positions): x_offset, y_offset, z_offset = offsets positions = [[round(p[0], 2), round(p[1], 2), p[2]] for p in positions] - if self.zero_reference_mode == ZrefMode.PROBE: + if self.probe_mgr.get_zero_ref_mode() == ZrefMode.PROBE: ref_pos = positions.pop() logging.info( "bed_mesh: z-offset replaced with probed z value at " @@ -768,15 +618,17 @@ class BedMeshCalibrate: x_cnt = params['x_count'] y_cnt = params['y_count'] - if self.substituted_indices: + substitutes = self.probe_mgr.get_substitutes() + base_points = self.probe_mgr.get_base_points() + if substitutes: # Replace substituted points with the original generated # point. Its Z Value is the average probed Z of the # substituted points. corrected_pts = [] idx_offset = 0 start_idx = 0 - for i, pts in self.substituted_indices.items(): - fpt = [p - o for p, o in zip(self.points[i], offsets[:2])] + for i, pts in substitutes.items(): + fpt = [p - o for p, o in zip(base_points[i], offsets[:2])] # offset the index to account for additional samples idx = i + idx_offset # Add "normal" points @@ -793,13 +645,13 @@ class BedMeshCalibrate: corrected_pts.append(fpt) corrected_pts.extend(positions[start_idx:]) # validate corrected positions - if len(self.points) != len(corrected_pts): + if len(base_points) != len(corrected_pts): self._dump_points(positions, corrected_pts, offsets) raise self.gcode.error( "bed_mesh: invalid position list size, " "generated count: %d, probed count: %d" - % (len(self.points), len(corrected_pts))) - for gen_pt, probed in zip(self.points, corrected_pts): + % (len(base_points), len(corrected_pts))) + for gen_pt, probed in zip(base_points, corrected_pts): off_pt = [p - o for p, o in zip(gen_pt, offsets[:2])] if not isclose(off_pt[0], probed[0], abs_tol=.1) or \ not isclose(off_pt[1], probed[1], abs_tol=.1): @@ -866,11 +718,12 @@ class BedMeshCalibrate: z_mesh.build_mesh(probed_matrix) except BedMeshError as e: raise self.gcode.error(str(e)) - if self.zero_reference_mode == ZrefMode.IN_MESH: + if self.probe_mgr.get_zero_ref_mode() == ZrefMode.IN_MESH: # The reference can be anywhere in the mesh, therefore # it is necessary to set the reference after the initial mesh # is generated to lookup the correct z value. - z_mesh.set_zero_reference(*self.zero_ref_pos) + zero_ref_pos = self.probe_mgr.get_zero_ref_pos() + z_mesh.set_zero_reference(*zero_ref_pos) self.bedmesh.set_mesh(z_mesh) self.gcode.respond_info("Mesh Bed Leveling Complete") if self._profile_name is not None: @@ -878,14 +731,15 @@ class BedMeshCalibrate: def _dump_points(self, probed_pts, corrected_pts, offsets): # logs generated points with offset applied, points received # from the finalize callback, and the list of corrected points - max_len = max([len(self.points), len(probed_pts), len(corrected_pts)]) + points = self.probe_mgr.get_base_points() + max_len = max([len(points), len(probed_pts), len(corrected_pts)]) logging.info( "bed_mesh: calibration point dump\nIndex | %-17s| %-25s|" " Corrected Point" % ("Generated Point", "Probed Point")) for i in list(range(max_len)): gen_pt = probed_pt = corr_pt = "" - if i < len(self.points): - off_pt = [p - o for p, o in zip(self.points[i], offsets[:2])] + if i < len(points): + off_pt = [p - o for p, o in zip(points[i], offsets[:2])] gen_pt = "(%.2f, %.2f)" % tuple(off_pt) if i < len(probed_pts): probed_pt = "(%.2f, %.2f, %.4f)" % tuple(probed_pts[i]) @@ -894,6 +748,453 @@ class BedMeshCalibrate: logging.info( " %-4d| %-17s| %-25s| %s" % (i, gen_pt, probed_pt, corr_pt)) +class ProbeManager: + def __init__(self, config, orig_config, finalize_cb): + self.printer = config.get_printer() + self.cfg_overshoot = config.getfloat("scan_overshoot", 0, minval=1.) + self.orig_config = orig_config + self.faulty_regions = [] + self.overshoot = self.cfg_overshoot + self.zero_ref_pos = config.getfloatlist( + "zero_reference_position", None, count=2 + ) + self.zref_mode = ZrefMode.DISABLED + self.base_points = [] + self.substitutes = collections.OrderedDict() + self.is_round = orig_config["radius"] is not None + self.probe_helper = probe.ProbePointsHelper(config, finalize_cb, []) + self.probe_helper.use_xy_offsets(True) + self.rapid_scan_helper = RapidScanHelper(config, self, finalize_cb) + self._init_faulty_regions(config) + + def _init_faulty_regions(self, config): + for i in list(range(1, 100, 1)): + start = config.getfloatlist("faulty_region_%d_min" % (i,), None, + count=2) + if start is None: + break + end = config.getfloatlist("faulty_region_%d_max" % (i,), count=2) + # Validate the corners. If necessary reorganize them. + # c1 = min point, c3 = max point + # c4 ---- c3 + # | | + # c1 ---- c2 + c1 = [min([s, e]) for s, e in zip(start, end)] + c3 = [max([s, e]) for s, e in zip(start, end)] + c2 = [c1[0], c3[1]] + c4 = [c3[0], c1[1]] + # Check for overlapping regions + for j, (prev_c1, prev_c3) in enumerate(self.faulty_regions): + prev_c2 = [prev_c1[0], prev_c3[1]] + prev_c4 = [prev_c3[0], prev_c1[1]] + # Validate that no existing corner is within the new region + for coord in [prev_c1, prev_c2, prev_c3, prev_c4]: + if within(coord, c1, c3): + raise config.error( + "bed_mesh: Existing faulty_region_%d %s overlaps " + "added faulty_region_%d %s" + % (j+1, repr([prev_c1, prev_c3]), + i, repr([c1, c3]))) + # Validate that no new corner is within an existing region + for coord in [c1, c2, c3, c4]: + if within(coord, prev_c1, prev_c3): + raise config.error( + "bed_mesh: Added faulty_region_%d %s overlaps " + "existing faulty_region_%d %s" + % (i, repr([c1, c3]), + j+1, repr([prev_c1, prev_c3]))) + self.faulty_regions.append((c1, c3)) + + def start_probe(self, gcmd): + method = gcmd.get("METHOD", "automatic").lower() + can_scan = False + pprobe = self.printer.lookup_object("probe", None) + if pprobe is not None: + probe_name = pprobe.get_status(None).get("name", "") + can_scan = probe_name.startswith("probe_eddy_current") + if method == "rapid_scan" and can_scan: + self.rapid_scan_helper.perform_rapid_scan(gcmd) + else: + self.probe_helper.start_probe(gcmd) + + def get_zero_ref_pos(self): + return self.zero_ref_pos + + def get_zero_ref_mode(self): + return self.zref_mode + + def get_substitutes(self): + return self.substitutes + + def generate_points( + self, mesh_config, mesh_min, mesh_max, radius, origin, + probe_method="automatic" + ): + x_cnt = mesh_config['x_count'] + y_cnt = mesh_config['y_count'] + min_x, min_y = mesh_min + max_x, max_y = mesh_max + x_dist = (max_x - min_x) / (x_cnt - 1) + y_dist = (max_y - min_y) / (y_cnt - 1) + # floor distances down to next hundredth + x_dist = math.floor(x_dist * 100) / 100 + y_dist = math.floor(y_dist * 100) / 100 + if x_dist < 1. or y_dist < 1.: + raise BedMeshError("bed_mesh: min/max points too close together") + + if radius is not None: + # round bed, min/max needs to be recalculated + y_dist = x_dist + new_r = (x_cnt // 2) * x_dist + min_x = min_y = -new_r + max_x = max_y = new_r + else: + # rectangular bed, only re-calc max_x + max_x = min_x + x_dist * (x_cnt - 1) + pos_y = min_y + points = [] + for i in range(y_cnt): + for j in range(x_cnt): + if not i % 2: + # move in positive directon + pos_x = min_x + j * x_dist + else: + # move in negative direction + pos_x = max_x - j * x_dist + if radius is None: + # rectangular bed, append + points.append((pos_x, pos_y)) + else: + # round bed, check distance from origin + dist_from_origin = math.sqrt(pos_x*pos_x + pos_y*pos_y) + if dist_from_origin <= radius: + points.append( + (origin[0] + pos_x, origin[1] + pos_y)) + pos_y += y_dist + if self.zero_ref_pos is None or probe_method == "manual": + # Zero Reference Disabled + self.zref_mode = ZrefMode.DISABLED + elif within(self.zero_ref_pos, mesh_min, mesh_max): + # Zero Reference position within mesh + self.zref_mode = ZrefMode.IN_MESH + else: + # Zero Reference position outside of mesh + self.zref_mode = ZrefMode.PROBE + self.base_points = points + self.substitutes.clear() + # adjust overshoot + og_min_x = self.orig_config["mesh_min"][0] + og_max_x = self.orig_config["mesh_max"][0] + add_ovs = min(max(0, min_x - og_min_x), max(0, og_max_x - max_x)) + self.overshoot = self.cfg_overshoot + math.floor(add_ovs) + min_pt, max_pt = (min_x, min_y), (max_x, max_y) + self._process_faulty_regions(min_pt, max_pt, radius) + self.probe_helper.update_probe_points(self.get_std_path(), 3) + + def _process_faulty_regions(self, min_pt, max_pt, radius): + if not self.faulty_regions: + return + # Cannot probe a reference within a faulty region + if self.zref_mode == ZrefMode.PROBE: + for min_c, max_c in self.faulty_regions: + if within(self.zero_ref_pos, min_c, max_c): + opt = "zero_reference_position" + raise BedMeshError( + "bed_mesh: Cannot probe zero reference position at " + "(%.2f, %.2f) as it is located within a faulty region." + " Check the value for option '%s'" + % (self.zero_ref_pos[0], self.zero_ref_pos[1], opt,) + ) + # Check to see if any points fall within faulty regions + last_y = self.base_points[0][1] + is_reversed = False + for i, coord in enumerate(self.base_points): + if not isclose(coord[1], last_y): + is_reversed = not is_reversed + last_y = coord[1] + adj_coords = [] + for min_c, max_c in self.faulty_regions: + if within(coord, min_c, max_c, tol=.00001): + # Point lies within a faulty region + adj_coords = [ + (min_c[0], coord[1]), (coord[0], min_c[1]), + (coord[0], max_c[1]), (max_c[0], coord[1])] + if is_reversed: + # Swap first and last points for zig-zag pattern + first = adj_coords[0] + adj_coords[0] = adj_coords[-1] + adj_coords[-1] = first + break + if not adj_coords: + # coord is not located within a faulty region + continue + valid_coords = [] + for ac in adj_coords: + # make sure that coordinates are within the mesh boundary + if radius is None: + if within(ac, min_pt, max_pt, .000001): + valid_coords.append(ac) + else: + dist_from_origin = math.sqrt(ac[0]*ac[0] + ac[1]*ac[1]) + if dist_from_origin <= radius: + valid_coords.append(ac) + if not valid_coords: + raise BedMeshError( + "bed_mesh: Unable to generate coordinates" + " for faulty region at index: %d" % (i) + ) + self.substitutes[i] = valid_coords + + def get_base_points(self): + return self.base_points + + def get_std_path(self): + path = [] + for idx, pt in enumerate(self.base_points): + if idx in self.substitutes: + for sub_pt in self.substitutes[idx]: + path.append(sub_pt) + else: + path.append(pt) + if self.zref_mode == ZrefMode.PROBE: + path.append(self.zero_ref_pos) + return path + + def iter_rapid_path(self): + ascnd_x = True + last_base_pt = last_mv_pt = self.base_points[0] + # Generate initial move point + if self.overshoot: + overshoot = min(8, self.overshoot) + last_mv_pt = (last_base_pt[0] - overshoot, last_base_pt[1]) + yield last_mv_pt, False + for idx, pt in enumerate(self.base_points): + # increasing Y indicates direction change + dir_change = not isclose(pt[1], last_base_pt[1], abs_tol=1e-6) + if idx in self.substitutes: + fp_gen = self._gen_faulty_path( + last_mv_pt, idx, ascnd_x, dir_change + ) + for sub_pt, is_smp in fp_gen: + yield sub_pt, is_smp + last_mv_pt = sub_pt + else: + if dir_change: + for dpt in self._gen_dir_change(last_mv_pt, pt, ascnd_x): + yield dpt, False + yield pt, True + last_mv_pt = pt + last_base_pt = pt + ascnd_x ^= dir_change + if self.zref_mode == ZrefMode.PROBE: + if self.overshoot: + ovs = min(4, self.overshoot) + ovs = ovs if ascnd_x else -ovs + yield (last_mv_pt[0] + ovs, last_mv_pt[1]), False + yield self.zero_ref_pos, True + + def _gen_faulty_path(self, last_pt, idx, ascnd_x, dir_change): + subs = self.substitutes[idx] + sub_cnt = len(subs) + if dir_change: + for dpt in self._gen_dir_change(last_pt, subs[0], ascnd_x): + yield dpt, False + if self.is_round: + # No faulty region path handling for round beds + for pt in subs: + yield pt, True + return + # Check to see if this is the first corner + first_corner = False + sorted_sub_idx = sorted(self.substitutes.keys()) + if sub_cnt == 2 and idx < len(sorted_sub_idx): + first_corner = sorted_sub_idx[idx] == idx + yield subs[0], True + if sub_cnt == 1: + return + last_pt, next_pt = subs[:2] + if sub_cnt == 2: + if first_corner or dir_change: + # horizontal move first + yield (next_pt[0], last_pt[1]), False + else: + yield (last_pt[0], next_pt[1]), False + yield next_pt, True + elif sub_cnt >= 3: + if dir_change: + # first move should be a vertical switch up. If overshoot + # is available, simulate another direction change. Otherwise + # move inward 2 mm, then up through the faulty region. + if self.overshoot: + for dpt in self._gen_dir_change(last_pt, next_pt, ascnd_x): + yield dpt, False + else: + shift = -2 if ascnd_x else 2 + yield (last_pt[0] + shift, last_pt[1]), False + yield (last_pt[0] + shift, next_pt[1]), False + yield next_pt, True + last_pt, next_pt = subs[1:3] + else: + # vertical move + yield (last_pt[0], next_pt[1]), False + yield next_pt, True + last_pt, next_pt = subs[1:3] + if sub_cnt == 4: + # Vertical switch up within faulty region + shift = 2 if ascnd_x else -2 + yield (last_pt[0] + shift, last_pt[1]), False + yield (next_pt[0] - shift, next_pt[1]), False + yield next_pt, True + last_pt, next_pt = subs[2:4] + # horizontal move before final point + yield (next_pt[0], last_pt[1]), False + yield next_pt, True + + def _gen_dir_change(self, last_pt, next_pt, ascnd_x): + if not self.overshoot: + return + # overshoot X beyond the outer point + xdir = 1 if ascnd_x else -1 + overshoot = 2. if self.overshoot >= 3. else self.overshoot + ovr_pt = (last_pt[0] + overshoot * xdir, last_pt[1]) + yield ovr_pt + if self.overshoot < 3.: + # No room to generate an arc, move up to next y + yield (next_pt[0] + overshoot * xdir, next_pt[1]) + else: + # generate arc + STEP_ANGLE = 3 + START_ANGLE = 270 + ydiff = abs(next_pt[1] - last_pt[1]) + xdiff = abs(next_pt[0] - last_pt[0]) + max_radius = min(self.overshoot - 2, 8) + radius = min(ydiff / 2, max_radius) + origin = [ovr_pt[0], last_pt[1] + radius] + next_origin_y = next_pt[1] - radius + # determine angle + if xdiff < .01: + # Move is aligned on the x-axis + angle = 90 + if next_origin_y - origin[1] < .05: + # The move can be completed in a single arc + angle = 180 + else: + angle = int(math.degrees(math.atan(ydiff / xdiff))) + if ( + (ascnd_x and next_pt[0] < last_pt[0]) or + (not ascnd_x and next_pt[0] > last_pt[0]) + ): + angle = 180 - angle + count = int(angle // STEP_ANGLE) + # Gen first arc + step = STEP_ANGLE * xdir + start = START_ANGLE + step + for arc_pt in self._gen_arc(origin, radius, start, step, count): + yield arc_pt + if angle == 180: + # arc complete + return + # generate next arc + origin = [next_pt[0] + overshoot * xdir, next_origin_y] + # start at the angle where the last arc finished + start = START_ANGLE + count * step + # recalculate the count to make sure we generate a full 180 + # degrees. Add a step for the repeated connecting angle + count = 61 - count + for arc_pt in self._gen_arc(origin, radius, start, step, count): + yield arc_pt + + def _gen_arc(self, origin, radius, start, step, count): + end = start + step * count + # create a segent for every 3 degress of travel + for angle in range(start, end, step): + rad = math.radians(angle % 360) + opp = math.sin(rad) * radius + adj = math.cos(rad) * radius + yield (origin[0] + adj, origin[1] + opp) + + +MAX_HIT_DIST = 2. +MM_WIN_SPEED = 125 + +class RapidScanHelper: + def __init__(self, config, probe_mgr, finalize_cb): + self.printer = config.get_printer() + self.probe_manager = probe_mgr + self.speed = config.getfloat("speed", 50., above=0.) + self.scan_height = config.getfloat("horizontal_move_z", 5.) + self.finalize_callback = finalize_cb + + def perform_rapid_scan(self, gcmd): + speed = gcmd.get_float("SCAN_SPEED", self.speed) + scan_height = gcmd.get_float("HORIZONTAL_MOVE_Z", self.scan_height) + gcmd.respond_info( + "Beginning rapid surface scan at height %.2f..." % (scan_height) + ) + pprobe = self.printer.lookup_object("probe") + toolhead = self.printer.lookup_object("toolhead") + # Calculate time window around which a sample is valid. Current + # assumption is anything within 2mm is usable, so: + # window = 2 / max_speed + # + # TODO: validate maximum speed allowed based on sample rate of probe + # Scale the hit distance window for speeds lower than 125mm/s. The + # lower the speed the less the window shrinks. + scale = max(0, 1 - speed / MM_WIN_SPEED) + 1 + hit_dist = min(MAX_HIT_DIST, scale * speed / MM_WIN_SPEED) + half_window = hit_dist / speed + gcmd.respond_info( + "Sample hit distance +/- %.4fmm, time window +/- ms %.4f" + % (hit_dist, half_window * 1000) + ) + gcmd_params = gcmd.get_command_parameters() + gcmd_params["SAMPLE_TIME"] = half_window * 2 + self._raise_tool(gcmd, scan_height) + probe_session = pprobe.start_probe_session(gcmd) + offsets = pprobe.get_offsets() + initial_move = True + for pos, is_probe_pt in self.probe_manager.iter_rapid_path(): + pos = self._apply_offsets(pos[:2], offsets) + toolhead.manual_move(pos, speed) + if initial_move: + initial_move = False + self._move_to_scan_height(gcmd, scan_height) + if is_probe_pt: + probe_session.run_probe(gcmd) + results = probe_session.pull_probed_results() + toolhead.get_last_move_time() + self.finalize_callback(offsets, results) + probe_session.end_probe_session() + + def _raise_tool(self, gcmd, scan_height): + # If the nozzle is below scan height raise the tool + toolhead = self.printer.lookup_object("toolhead") + pprobe = self.printer.lookup_object("probe") + cur_pos = toolhead.get_position() + if cur_pos[2] >= scan_height: + return + pparams = pprobe.get_probe_params(gcmd) + lift_speed = pparams["lift_speed"] + cur_pos[2] = self.scan_height + .5 + toolhead.manual_move(cur_pos, lift_speed) + + def _move_to_scan_height(self, gcmd, scan_height): + time_window = gcmd.get_float("SAMPLE_TIME") + toolhead = self.printer.lookup_object("toolhead") + pprobe = self.printer.lookup_object("probe") + cur_pos = toolhead.get_position() + pparams = pprobe.get_probe_params(gcmd) + lift_speed = pparams["lift_speed"] + probe_speed = pparams["probe_speed"] + cur_pos[2] = scan_height + .5 + toolhead.manual_move(cur_pos, lift_speed) + cur_pos[2] = scan_height + toolhead.manual_move(cur_pos, probe_speed) + toolhead.dwell(time_window / 2 + .01) + + def _apply_offsets(self, point, offsets): + return [(pos - ofs) for pos, ofs in zip(point, offsets)] + class MoveSplitter: def __init__(self, config, gcode): From c7b7c11cc323e7e28521259e615f674990a1701e Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Wed, 14 Feb 2024 08:16:49 -0500 Subject: [PATCH 066/111] bed_mesh: add dump_mesh webhooks API Returns current mesh configuration and state. Includes probed and mesh matrices, saved profiles, current points, and travel paths. Signed-off-by: Eric Callahan --- klippy/extras/bed_mesh.py | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index d9ee7dfba..8aa4958d5 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -121,6 +121,11 @@ class BedMesh: self.gcode.register_command( 'BED_MESH_OFFSET', self.cmd_BED_MESH_OFFSET, desc=self.cmd_BED_MESH_OFFSET_help) + # Register dump webhooks + webhooks = self.printer.lookup_object('webhooks') + webhooks.register_endpoint( + "bed_mesh/dump_mesh", self._handle_dump_request + ) # Register transform gcode_move = self.printer.load_object(config, 'gcode_move') gcode_move.set_move_transform(self) @@ -282,6 +287,31 @@ class BedMesh: gcode_move.reset_last_position() else: gcmd.respond_info("No mesh loaded to offset") + def _handle_dump_request(self, web_request): + eventtime = self.printer.get_reactor().monotonic() + prb = self.printer.lookup_object("probe", None) + th_sts = self.printer.lookup_object("toolhead").get_status(eventtime) + result = {"current_mesh": {}, "profiles": self.pmgr.get_profiles()} + if self.z_mesh is not None: + result["current_mesh"] = { + "name": self.z_mesh.get_profile_name(), + "probed_matrix": self.z_mesh.get_probed_matrix(), + "mesh_matrix": self.z_mesh.get_mesh_matrix(), + "mesh_params": self.z_mesh.get_mesh_params() + } + mesh_args = web_request.get_dict("mesh_args", {}) + gcmd = None + if mesh_args: + gcmd = self.gcode.create_gcode_command("", "", mesh_args) + with self.gcode.get_mutex(): + result["calibration"] = self.bmc.dump_calibration(gcmd) + else: + result["calibration"] = self.bmc.dump_calibration() + offsets = [0, 0, 0] if prb is None else prb.get_offsets() + result["probe_offsets"] = offsets + result["axis_minimum"] = th_sts["axis_minimum"] + result["axis_maximum"] = th_sts["axis_maximum"] + web_request.send(result) class ZrefMode: @@ -587,6 +617,20 @@ class BedMeshCalibrate: self.mesh_config, self.mesh_min, self.mesh_max, self.radius, self.origin, probe_method ) + def dump_calibration(self, gcmd=None): + if gcmd is not None and gcmd.get_command_parameters(): + self.update_config(gcmd) + cfg = dict(self.mesh_config) + cfg["mesh_min"] = self.mesh_min + cfg["mesh_max"] = self.mesh_max + cfg["origin"] = self.origin + cfg["radius"] = self.radius + return { + "points": self.probe_mgr.get_base_points(), + "config": cfg, + "probe_path": self.probe_mgr.get_std_path(), + "rapid_path": list(self.probe_mgr.iter_rapid_path()) + } cmd_BED_MESH_CALIBRATE_help = "Perform Mesh Bed Leveling" def cmd_BED_MESH_CALIBRATE(self, gcmd): self._profile_name = gcmd.get('PROFILE', "default") From fc0f17b920ce0fd2dca90dce0bd7df4573aee568 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Mon, 19 Feb 2024 15:10:56 -0500 Subject: [PATCH 067/111] graph_mesh: script for mesh visualization and analysis Signed-off-by: Eric Callahan --- scripts/graph_mesh.py | 533 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100755 scripts/graph_mesh.py diff --git a/scripts/graph_mesh.py b/scripts/graph_mesh.py new file mode 100755 index 000000000..3a331e5d5 --- /dev/null +++ b/scripts/graph_mesh.py @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 +# Bed Mesh data plotting and analysis +# +# Copyright (C) 2024 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import argparse +import sys +import os +import stat +import errno +import time +import socket +import re +import json +import collections +import numpy as np +import matplotlib +import matplotlib.cm as cm +import matplotlib.pyplot as plt +import matplotlib.animation as ani + +MESH_DUMP_REQUEST = json.dumps( + {"id": 1, "method": "bed_mesh/dump_mesh"} +) + +def sock_error_exit(msg): + sys.stderr.write(msg + "\n") + sys.exit(-1) + +def webhook_socket_create(uds_filename): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + while 1: + try: + sock.connect(uds_filename) + except socket.error as e: + if e.errno == errno.ECONNREFUSED: + time.sleep(0.1) + continue + sock_error_exit( + "Unable to connect socket %s [%d,%s]" + % (uds_filename, e.errno, errno.errorcode[e.errno]) + ) + break + print("Connected") + return sock + +def process_message(msg): + try: + resp = json.loads(msg) + except json.JSONDecodeError: + return None + if resp.get("id", -1) != 1: + return None + if "error" in resp: + err = resp["error"].get("message", "Unknown") + sock_error_exit( + "Error requesting mesh dump: %s" % (err,) + ) + return resp["result"] + + +def request_from_unixsocket(unix_sock_name): + print("Connecting to Unix Socket File '%s'" % (unix_sock_name,)) + whsock = webhook_socket_create(unix_sock_name) + whsock.settimeout(1.) + # send mesh query + whsock.send(MESH_DUMP_REQUEST.encode() + b"\x03") + sock_data = b"" + end_time = time.monotonic() + 10.0 + try: + while time.monotonic() < end_time: + try: + data = whsock.recv(4096) + except TimeoutError: + pass + else: + if not data: + sock_error_exit("Socket closed before mesh received") + parts = data.split(b"\x03") + parts[0] = sock_data + parts[0] + sock_data = parts.pop() + for msg in parts: + result = process_message(msg) + if result is not None: + return result + time.sleep(.1) + finally: + whsock.close() + sock_error_exit("Mesh dump request timed out") + +def request_from_websocket(url): + print("Connecting to websocket url '%s'" % (url,)) + try: + from websockets.sync.client import connect + except ModuleNotFoundError: + sock_error_exit("Python module 'websockets' not installed.") + raise + with connect(url) as websocket: + websocket.send(MESH_DUMP_REQUEST) + end_time = time.monotonic() + 20.0 + while time.monotonic() < end_time: + try: + msg = websocket.recv(10.) + except TimeoutError: + continue + result = process_message(msg) + if result is not None: + return result + time.sleep(.1) + sock_error_exit("Mesh dump request timed out") + +def request_mesh_data(input_name): + url_match = re.match(r"((?:https?)|(?:wss?))://(.+)", input_name.lower()) + if url_match is None: + file_path = os.path.abspath(os.path.expanduser(input_name)) + if not os.path.exists(file_path): + sock_error_exit("Path '%s' does not exist" % (file_path,)) + st_res = os.stat(file_path) + if stat.S_ISSOCK(st_res.st_mode): + return request_from_unixsocket(file_path) + else: + print("Reading mesh data from json file '%s'" % (file_path,)) + with open(file_path, "r") as f: + return json.load(f) + scheme = url_match.group(1) + host = url_match.group(2).rstrip("/") + scheme = scheme.replace("http", "ws") + url = "%s://%s/klippysocket" % (scheme, host) + return request_from_websocket(url) + +class PathAnimation: + instance = None + def __init__(self, artist, x_travel, y_travel): + self.travel_artist = artist + self.x_travel = x_travel + self.y_travel = y_travel + fig = plt.gcf() + self.animation = ani.FuncAnimation( + fig=fig, func=self.update, frames=self.gen_path_position(), + cache_frame_data=False, interval=60 + ) + PathAnimation.instance = self + + def gen_path_position(self): + count = 1 + x_travel, y_travel = self.x_travel, self.y_travel + last_x, last_y = x_travel[0], y_travel[0] + yield count + for xpos, ypos in zip(x_travel[1:], y_travel[1:]): + count += 1 + if xpos == last_x or ypos == last_y: + yield count + last_x, last_y = xpos, ypos + + def update(self, frame): + x_travel, y_travel = self.x_travel, self.y_travel + self.travel_artist.set_xdata(x_travel[:frame]) + self.travel_artist.set_ydata(y_travel[:frame]) + return (self.travel_artist,) + + +def _gen_mesh_coords(min_c, max_c, count): + dist = (max_c - min_c) / (count - 1) + return [min_c + i * dist for i in range(count)] + +def _plot_path(travel_path, probed, diff, cmd_args): + x_travel, y_travel = np.array(travel_path).transpose() + x_probed, y_probed = np.array(probed).transpose() + plt.xlabel("X") + plt.ylabel("Y") + # plot travel + travel_line = plt.plot(x_travel, y_travel, "b-")[0] + # plot intermediate points + plt.plot(x_probed, y_probed, "k.") + # plot start point + plt.plot([x_travel[0]], [y_travel[0]], "g>") + # plot stop point + plt.plot([x_travel[-1]], [y_travel[-1]], "r*") + if diff: + diff_x, diff_y = np.array(diff).transpose() + plt.plot(diff_x, diff_y, "m.") + if cmd_args.animate and cmd_args.output is None: + PathAnimation(travel_line, x_travel, y_travel) + +def _format_mesh_data(matrix, params): + min_pt = (params["min_x"], params["min_y"]) + max_pt = (params["max_x"], params["max_y"]) + xvals = _gen_mesh_coords(min_pt[0], max_pt[0], len(matrix[0])) + yvals = _gen_mesh_coords(min_pt[1], max_pt[0], len(matrix)) + x, y = np.meshgrid(xvals, yvals) + z = np.array(matrix) + return x, y, z + +def _set_xy_limits(mesh_data, cmd_args): + if not cmd_args.scale_plot: + return + ax = plt.gca() + axis_min = mesh_data["axis_minimum"] + axis_max = mesh_data["axis_maximum"] + ax.set_xlim((axis_min[0], axis_max[0])) + ax.set_ylim((axis_min[1], axis_max[1])) + +def _plot_mesh(ax, matrix, params, cmap=cm.viridis, label=None): + x, y, z = _format_mesh_data(matrix, params) + surface = ax.plot_surface(x, y, z, cmap=cmap, label=label) + scale = max(abs(z.min()), abs(z.max())) * 3 + return surface, scale + +def plot_probe_points(mesh_data, cmd_args): + """Plot original generated points""" + calibration = mesh_data["calibration"] + x, y = np.array(calibration["points"]).transpose() + plt.title("Generated Probe Points") + plt.xlabel("X") + plt.ylabel("Y") + plt.plot(x, y, "b.") + _set_xy_limits(mesh_data, cmd_args) + +def plot_probe_path(mesh_data, cmd_args): + """Plot probe travel path""" + calibration = mesh_data["calibration"] + orig_pts = calibration["points"] + path_pts = calibration["probe_path"] + diff = [pt for pt in orig_pts if pt not in path_pts] + plt.title("Probe Travel Path") + _plot_path(path_pts, path_pts[1:-1], diff, cmd_args) + _set_xy_limits(mesh_data, cmd_args) + +def plot_rapid_path(mesh_data, cmd_args): + """Plot rapid scan travel path""" + calibration = mesh_data["calibration"] + orig_pts = calibration["points"] + rapid_pts = calibration["rapid_path"] + rapid_path = [pt[0] for pt in rapid_pts] + probed = [pt for pt, is_ppt in rapid_pts if is_ppt] + diff = [pt for pt in orig_pts if pt not in probed] + plt.title("Rapid Scan Travel Path") + _plot_path(rapid_path, probed, diff, cmd_args) + _set_xy_limits(mesh_data, cmd_args) + +def plot_probed_matrix(mesh_data, cmd_args): + """Plot probed Z values""" + ax = plt.subplot(projection="3d") + profile = cmd_args.profile_name + if profile is not None: + req_mesh = mesh_data["profiles"].get(profile) + if req_mesh is None: + raise Exception("Profile %s not found" % (profile,)) + matrix = req_mesh["points"] + name = profile + else: + req_mesh = mesh_data["current_mesh"] + if not req_mesh: + raise Exception("No current mesh data in dump") + matrix = req_mesh["probed_matrix"] + name = req_mesh["name"] + params = req_mesh["mesh_params"] + surface, scale = _plot_mesh(ax, matrix, params) + ax.set_title("Probed Mesh (%s)" % (name,)) + ax.set(zlim=(-scale, scale)) + plt.gcf().colorbar(surface, shrink=.75) + _set_xy_limits(mesh_data, cmd_args) + +def plot_mesh_matrix(mesh_data, cmd_args): + """Plot mesh Z values""" + ax = plt.subplot(projection="3d") + req_mesh = mesh_data["current_mesh"] + if not req_mesh: + raise Exception("No current mesh data in dump") + matrix = req_mesh["mesh_matrix"] + params = req_mesh["mesh_params"] + surface, scale = _plot_mesh(ax, matrix, params) + name = req_mesh["name"] + ax.set_title("Interpolated Mesh (%s)" % (name,)) + ax.set(zlim=(-scale, scale)) + plt.gcf().colorbar(surface, shrink=.75) + _set_xy_limits(mesh_data, cmd_args) + +def plot_overlay(mesh_data, cmd_args): + """Plots the current probed mesh overlaid with a profile""" + ax = plt.subplot(projection="3d") + # Plot Profile + profile = cmd_args.profile_name + if profile is None: + raise Exception("A profile must be specified to plot an overlay") + req_mesh = mesh_data["profiles"].get(profile) + if req_mesh is None: + raise Exception("Profile %s not found" % (profile,)) + matrix = req_mesh["points"] + params = req_mesh["mesh_params"] + prof_surf, prof_scale = _plot_mesh(ax, matrix, params, label=profile) + # Plot Current + req_mesh = mesh_data["current_mesh"] + if not req_mesh: + raise Exception("No current mesh data in dump") + matrix = req_mesh["probed_matrix"] + params = req_mesh["mesh_params"] + cur_name = req_mesh["name"] + cur_surf, cur_scale = _plot_mesh(ax, matrix, params, cm.inferno, cur_name) + ax.set_title("Probed Mesh Overlay") + scale = max(cur_scale, prof_scale) + ax.set(zlim=(-scale, scale)) + ax.legend(loc='best') + plt.gcf().colorbar(prof_surf, shrink=.75) + _set_xy_limits(mesh_data, cmd_args) + +def plot_delta(mesh_data, cmd_args): + """Plots the delta between current probed mesh and a profile""" + ax = plt.subplot(projection="3d") + # Plot Profile + profile = cmd_args.profile_name + if profile is None: + raise Exception("A profile must be specified to plot an overlay") + req_mesh = mesh_data["profiles"].get(profile) + if req_mesh is None: + raise Exception("Profile %s not found" % (profile,)) + prof_matix = req_mesh["points"] + prof_params = req_mesh["mesh_params"] + req_mesh = mesh_data["current_mesh"] + if not req_mesh: + raise Exception("No current mesh data in dump") + cur_matrix = req_mesh["probed_matrix"] + cur_params = req_mesh["mesh_params"] + cur_name = req_mesh["name"] + # validate that the params match + pfields = ("x_count", "y_count", "min_x", "max_x", "min_y", "max_y") + for field in pfields: + if abs(prof_params[field] - cur_params[field]) >= 1e-6: + raise Exception( + "Values for field %s do not match, cant plot deviation" + ) + delta = np.array(cur_matrix) - np.array(prof_matix) + surface, scale = _plot_mesh(ax, delta, cur_params) + ax.set(zlim=(-scale, scale)) + ax.set_title("Probed Mesh Delta (%s, %s)" % (cur_name, profile)) + _set_xy_limits(mesh_data, cmd_args) + + +PLOT_TYPES = { + "points": plot_probe_points, + "path": plot_probe_path, + "rapid": plot_rapid_path, + "probedz": plot_probed_matrix, + "meshz": plot_mesh_matrix, + "overlay": plot_overlay, + "delta": plot_delta, +} + +def print_types(cmd_args): + typelist = [ + "%-10s%s" % (name, func.__doc__) for name, func in PLOT_TYPES.items() + ] + print("\n".join(typelist)) + +def plot_mesh_data(cmd_args): + mesh_data = request_mesh_data(cmd_args.input) + if cmd_args.output is not None: + matplotlib.use("svg") + + fig = plt.figure() + plot_func = PLOT_TYPES[cmd_args.type] + plot_func(mesh_data, cmd_args) + fig.set_size_inches(10, 8) + fig.tight_layout() + if cmd_args.output is None: + plt.show() + else: + fig.savefig(cmd_args.output) + +def _check_path_unique(name, path): + path = np.array(path) + unique_pts, counts = np.unique(path, return_counts=True, axis=0) + for idx, count in enumerate(counts): + if count != 1: + coord = unique_pts[idx] + print( + " WARNING: Backtracking or duplicate found in %s path at %s, " + "this may be due to multiple samples in a faulty region." + % (name, coord) + ) + +def _analyze_mesh(name, mesh_axes): + print("\nAnalyzing Probed Mesh %s..." % (name,)) + x, y, z = mesh_axes + min_idx, max_idx = z.argmin(), z.argmax() + min_x, min_y = x.flatten()[min_idx], y.flatten()[min_idx] + max_x, max_y = x.flatten()[max_idx], y.flatten()[max_idx] + + print( + " Min Coord (%.2f, %.2f), Max Coord (%.2f, %.2f), " + "Probe Count: (%d, %d)" % + (x.min(), y.min(), x.max(), y.max(), len(z), len(z[0])) + ) + print( + " Mesh range: min %.4f (%.2f, %.2f), max %.4f (%.2f, %.2f)" + % (z.min(), min_x, min_y, z.max(), max_x, max_y) + ) + print(" Mean: %.4f, Standard Deviation: %.4f" % (z.mean(), z.std())) + +def _compare_mesh(name_a, name_b, mesh_a, mesh_b): + ax, ay, az = mesh_a + bx, by, bz = mesh_b + if not np.array_equal(ax, bx) or not np.array_equal(ay, by): + return + delta = az - bz + abs_max = max(abs(delta.max()), abs(delta.min())) + abs_mean = sum([abs(z) for z in delta.flatten()]) / len(delta.flatten()) + min_idx, max_idx = delta.argmin(), delta.argmax() + min_x, min_y = ax.flatten()[min_idx], ay.flatten()[min_idx] + max_x, max_y = ax.flatten()[max_idx], ay.flatten()[max_idx] + print(" Delta from %s to %s..." % (name_a, name_b)) + print( + " Range: min %.4f (%.2f, %.2f), max %.4f (%.2f, %.2f)\n" + " Mean: %.6f, Standard Deviation: %.6f\n" + " Absolute Max: %.6f, Absolute Mean: %.6f" + % (delta.min(), min_x, min_y, delta.max(), max_x, max_y, + delta.mean(), delta.std(), abs_max, abs_mean) + ) + +def analyze(cmd_args): + mesh_data = request_mesh_data(cmd_args.input) + print("Analyzing Travel Path...") + calibration = mesh_data["calibration"] + org_pts = calibration["points"] + probe_path = calibration["probe_path"] + rapid_path = calibration["rapid_path"] + rapid_points = [pt for pt, is_pt in rapid_path if is_pt] + rapid_moves = [pt[0] for pt in rapid_path] + print(" Original point count: %d" % (len(org_pts))) + print(" Probe path count: %d" % (len(probe_path))) + print(" Rapid scan sample count: %d" % (len(probe_path))) + print(" Rapid scan move count: %d" % (len(rapid_moves))) + if np.array_equal(rapid_points, probe_path): + print(" Rapid scan points match probe path points") + else: + diff = [pt for pt in rapid_points if pt not in probe_path] + print( + " ERROR: Rapid scan points do not match probe points\n" + "difference: %s" % (diff,) + ) + _check_path_unique("probe", probe_path) + _check_path_unique("rapid scan", rapid_moves) + req_mesh = mesh_data["current_mesh"] + formatted_data = collections.OrderedDict() + if req_mesh: + matrix = req_mesh["probed_matrix"] + params = req_mesh["mesh_params"] + name = req_mesh["name"] + formatted_data[name] = _format_mesh_data(matrix, params) + profiles = mesh_data["profiles"] + for prof_name, prof_data in profiles.items(): + if prof_name in formatted_data: + continue + matrix = prof_data["points"] + params = prof_data["mesh_params"] + formatted_data[prof_name] = _format_mesh_data(matrix, params) + while formatted_data: + name, current_axes = formatted_data.popitem() + _analyze_mesh(name, current_axes) + for prof_name, prof_axes in formatted_data.items(): + _compare_mesh(name, prof_name, current_axes, prof_axes) + +def dump_request(cmd_args): + mesh_data = request_mesh_data(cmd_args.input) + outfile = cmd_args.output + if outfile is None: + postfix = time.strftime("%Y%m%d_%H%M%S") + outfile = "klipper-bedmesh-%s.json" % (postfix,) + outfile = os.path.abspath(os.path.expanduser(outfile)) + print("Saving Mesh Output to '%s'" % (outfile)) + with open(outfile, "w") as f: + f.write(json.dumps(mesh_data)) + +def main(): + parser = argparse.ArgumentParser(description="Graph Bed Mesh Data") + sub_parsers = parser.add_subparsers() + list_parser = sub_parsers.add_parser( + "list", help="List available plot types" + ) + list_parser.set_defaults(func=print_types) + plot_parser = sub_parsers.add_parser("plot", help="Plot a specified type") + analyze_parser = sub_parsers.add_parser( + "analyze", help="Perform analysis on mesh data" + ) + dump_parser = sub_parsers.add_parser( + "dump", help="Dump API response to json file" + ) + plot_parser.add_argument( + "-a", "--animate", action="store_true", + help="Animate paths in live preview" + ) + plot_parser.add_argument( + "-s", "--scale-plot", action="store_true", + help="Use axis limits reported by Klipper to scale plot X/Y" + ) + plot_parser.add_argument( + "-p", "--profile-name", type=str, default=None, + help="Optional name of a profile to plot for 'probedz'" + ) + plot_parser.add_argument( + "-o", "--output", type=str, default=None, + help="Output file path" + ) + plot_parser.add_argument( + "type", metavar="", type=str, choices=PLOT_TYPES.keys(), + help="Type of data to graph" + ) + plot_parser.add_argument( + "input", metavar="", + help="Path/url to Klipper Socket or path to json file" + ) + plot_parser.set_defaults(func=plot_mesh_data) + analyze_parser.add_argument( + "input", metavar="", + help="Path/url to Klipper Socket or path to json file" + ) + analyze_parser.set_defaults(func=analyze) + dump_parser.add_argument( + "-o", "--output", type=str, default=None, + help="Json output file path" + ) + dump_parser.add_argument( + "input", metavar="", + help="Path or url to Klipper Socket" + ) + dump_parser.set_defaults(func=dump_request) + cmd_args = parser.parse_args() + cmd_args.func(cmd_args) + + +if __name__ == "__main__": + main() From 2c7e09cfa6379e94fcc78774f186bce0bf0a2078 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Sun, 9 Jun 2024 19:21:01 -0400 Subject: [PATCH 068/111] bed_mesh: use generated XY positions in probe_finalize() The scan modes provide kinematic XYZ coordinates in the probe results. These positions may deviate from the requested positions, which can introduce errors in mesh generation when transposing the result into the Z matrix. Rely on the coordinates generated by bed mesh to transpose the matrix, presuming that points at the same index in the list match. Signed-off-by: Eric Callahan --- klippy/extras/bed_mesh.py | 57 +++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 8aa4958d5..9c44692c3 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -643,7 +643,7 @@ class BedMeshCalibrate: raise gcmd.error(str(e)) self.probe_mgr.start_probe(gcmd) def probe_finalize(self, offsets, positions): - x_offset, y_offset, z_offset = offsets + z_offset = offsets[2] positions = [[round(p[0], 2), round(p[1], 2), p[2]] for p in positions] if self.probe_mgr.get_zero_ref_mode() == ZrefMode.PROBE: @@ -654,16 +654,17 @@ class BedMeshCalibrate: % (ref_pos[0], ref_pos[1], ref_pos[2]) ) z_offset = ref_pos[2] + base_points = self.probe_mgr.get_base_points() params = dict(self.mesh_config) - params['min_x'] = min(positions, key=lambda p: p[0])[0] + x_offset - params['max_x'] = max(positions, key=lambda p: p[0])[0] + x_offset - params['min_y'] = min(positions, key=lambda p: p[1])[1] + y_offset - params['max_y'] = max(positions, key=lambda p: p[1])[1] + y_offset + params['min_x'] = min(base_points, key=lambda p: p[0])[0] + params['max_x'] = max(base_points, key=lambda p: p[0])[0] + params['min_y'] = min(base_points, key=lambda p: p[1])[1] + params['max_y'] = max(base_points, key=lambda p: p[1])[1] x_cnt = params['x_count'] y_cnt = params['y_count'] substitutes = self.probe_mgr.get_substitutes() - base_points = self.probe_mgr.get_base_points() + probed_pts = positions if substitutes: # Replace substituted points with the original generated # point. Its Z Value is the average probed Z of the @@ -688,38 +689,42 @@ class BedMeshCalibrate: % (i, fpt[0], fpt[1], avg_z, avg_z - z_offset)) corrected_pts.append(fpt) corrected_pts.extend(positions[start_idx:]) - # validate corrected positions - if len(base_points) != len(corrected_pts): - self._dump_points(positions, corrected_pts, offsets) - raise self.gcode.error( - "bed_mesh: invalid position list size, " - "generated count: %d, probed count: %d" - % (len(base_points), len(corrected_pts))) - for gen_pt, probed in zip(base_points, corrected_pts): - off_pt = [p - o for p, o in zip(gen_pt, offsets[:2])] - if not isclose(off_pt[0], probed[0], abs_tol=.1) or \ - not isclose(off_pt[1], probed[1], abs_tol=.1): - self._dump_points(positions, corrected_pts, offsets) - raise self.gcode.error( - "bed_mesh: point mismatch, orig = (%.2f, %.2f)" - ", probed = (%.2f, %.2f)" - % (off_pt[0], off_pt[1], probed[0], probed[1])) positions = corrected_pts + # validate length of result + if len(base_points) != len(positions): + self._dump_points(probed_pts, positions, offsets) + raise self.gcode.error( + "bed_mesh: invalid position list size, " + "generated count: %d, probed count: %d" + % (len(base_points), len(positions)) + ) + probed_matrix = [] row = [] - prev_pos = positions[0] - for pos in positions: + prev_pos = base_points[0] + for pos, result in zip(base_points, positions): + offset_pos = [p - o for p, o in zip(pos, offsets[:2])] + if ( + not isclose(offset_pos[0], result[0], abs_tol=.5) or + not isclose(offset_pos[1], result[1], abs_tol=.5) + ): + logging.info( + "bed_mesh: point deviation > .5mm: orig pt = (%.2f, %.2f)" + ", probed pt = (%.2f, %.2f)" + % (offset_pos[0], offset_pos[1], result[0], result[1]) + ) + z_pos = result[2] - z_offset if not isclose(pos[1], prev_pos[1], abs_tol=.1): # y has changed, append row and start new probed_matrix.append(row) row = [] if pos[0] > prev_pos[0]: # probed in the positive direction - row.append(pos[2] - z_offset) + row.append(z_pos) else: # probed in the negative direction - row.insert(0, pos[2] - z_offset) + row.insert(0, z_pos) prev_pos = pos # append last row probed_matrix.append(row) From a19af088945fdd33e49477413b90ee061c716e71 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Fri, 16 Feb 2024 06:13:05 -0500 Subject: [PATCH 069/111] bed_mesh: add support for MESH_PPS param in BMC In addition, do not respond with generated points. Signed-off-by: Eric Callahan --- klippy/extras/bed_mesh.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 9c44692c3..bcfd7c74d 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -594,6 +594,12 @@ class BedMeshCalibrate: self.mesh_config['y_count'] = y_cnt need_cfg_update = True + if "MESH_PPS" in params: + xpps, ypps = parse_gcmd_pair(gcmd, 'MESH_PPS', minval=0) + self.mesh_config['mesh_x_pps'] = xpps + self.mesh_config['mesh_y_pps'] = ypps + need_cfg_update = True + if "ALGORITHM" in params: self.mesh_config['algo'] = gcmd.get('ALGORITHM').strip().lower() need_cfg_update = True From a19d64febdbaa7d5ec0a0912bb65c6547c370ec3 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Tue, 11 Jun 2024 14:31:39 -0400 Subject: [PATCH 070/111] docs: add rapid probing documentation Signed-off-by: Eric Callahan --- docs/API_Server.md | 127 ++++++++++++++++++ docs/Bed_Mesh.md | 270 ++++++++++++++++++++++++++++++++++++++- docs/Config_Reference.md | 7 + 3 files changed, 399 insertions(+), 5 deletions(-) diff --git a/docs/API_Server.md b/docs/API_Server.md index 4af1812a3..cc0922e3c 100644 --- a/docs/API_Server.md +++ b/docs/API_Server.md @@ -401,3 +401,130 @@ might return: As with the "gcode/script" endpoint, this endpoint only completes after any pending G-Code commands complete. + +### bed_mesh/dump_mesh + +Dumps the configuration and state for the current mesh and all +saved profiles. + +For example: +`{"id": 123, "method": "bed_mesh/dump_mesh"}` + +might return: + +``` +{ + "current_mesh": { + "name": "eddy-scan-test", + "probed_matrix": [...], + "mesh_matrix": [...], + "mesh_params": { + "x_count": 9, + "y_count": 9, + "mesh_x_pps": 2, + "mesh_y_pps": 2, + "algo": "bicubic", + "tension": 0.5, + "min_x": 20, + "max_x": 330, + "min_y": 30, + "max_y": 320 + } + }, + "profiles": { + "default": { + "points": [...], + "mesh_params": { + "min_x": 20, + "max_x": 330, + "min_y": 30, + "max_y": 320, + "x_count": 9, + "y_count": 9, + "mesh_x_pps": 2, + "mesh_y_pps": 2, + "algo": "bicubic", + "tension": 0.5 + } + }, + "eddy-scan-test": { + "points": [...], + "mesh_params": { + "x_count": 9, + "y_count": 9, + "mesh_x_pps": 2, + "mesh_y_pps": 2, + "algo": "bicubic", + "tension": 0.5, + "min_x": 20, + "max_x": 330, + "min_y": 30, + "max_y": 320 + } + }, + "eddy-rapid-test": { + "points": [...], + "mesh_params": { + "x_count": 9, + "y_count": 9, + "mesh_x_pps": 2, + "mesh_y_pps": 2, + "algo": "bicubic", + "tension": 0.5, + "min_x": 20, + "max_x": 330, + "min_y": 30, + "max_y": 320 + } + } + }, + "calibration": { + "points": [...], + "config": { + "x_count": 9, + "y_count": 9, + "mesh_x_pps": 2, + "mesh_y_pps": 2, + "algo": "bicubic", + "tension": 0.5, + "mesh_min": [ + 20, + 30 + ], + "mesh_max": [ + 330, + 320 + ], + "origin": null, + "radius": null + }, + "probe_path": [...], + "rapid_path": [...] + }, + "probe_offsets": [ + 0, + 25, + 0.5 + ], + "axis_minimum": [ + 0, + 0, + -5, + 0 + ], + "axis_maximum": [ + 351, + 358, + 330, + 0 + ] +} +``` + +The `dump_mesh` endpoint takes one optional parameter, `mesh_args`. +This parameter must be an object, where the keys and values are +parameters available to [BED_MESH_CALIBRATE](#bed_mesh_calibrate). +This will update the mesh configuration and probe points using the +supplied parameters prior to returning the result. It is recommended +to omit mesh parameters unless it is desired to visualize the probe points +and/or travel path before performing `BED_MESH_CALIBRATE`. diff --git a/docs/Bed_Mesh.md b/docs/Bed_Mesh.md index 1538f6257..62f1dee84 100644 --- a/docs/Bed_Mesh.md +++ b/docs/Bed_Mesh.md @@ -421,12 +421,75 @@ have undesirable results when attempting print moves **outside** of the probed a full bed mesh has a variance greater than 1 layer height, caution must be taken when using adaptive bed meshes and attempting print moves outside of the meshed area. +## Surface Scans + +Some probes, such as the [Eddy Current Probe](./Eddy_Probe.md), are capable of +"scanning" the surface of the bed. That is, these probes can sample a mesh +without lifting the tool between samples. To activate scanning mode, the +`METHOD=scan` or `METHOD=rapid_scan` probe parameter should be passed in the +`BED_MESH_CALIBRATE` gcode command. + +### Scan Height + +The scan height is set by the `horizontal_move_z` option in `[bed_mesh]`. In +addition it can be supplied with the `BED_MESH_CALIBRATE` gcode command via the +`HORIZONTAL_MOVE_Z` parameter. + +The scan height must be sufficiently low to avoid scanning errors. Typically +a height of 2mm (ie: `HORIZONTAL_MOVE_Z=2`) should work well, presuming that the +probe is mounted correctly. + +It should be noted that if the probe is more than 4mm above the surface then the +results will be invalid. Thus, scanning is not possible on beds with severe +surface deviation or beds with extreme tilt that hasn't been corrected. + +### Rapid (Continuous) Scanning + +When performing a `rapid_scan` one should keep in mind that the results will +have some amount of error. This error should be low enough to be useful on +large print areas with reasonably thick layer heights. Some probes may be +more prone to error than others. + +It is not recommended that rapid mode be used to scan a "dense" mesh. Some of +the error introduced during a rapid scan may be gaussian noise from the sensor, +and a dense mesh will reflect this noise (ie: there will be peaks and valleys). + +Bed Mesh will attempt to optimize the travel path to provide the best possible +result based on the configuration. This includes avoiding faulty regions +when collecting samples and "overshooting" the mesh when changing direction. +This overshoot improves sampling at the edges of a mesh, however it requires +that the mesh be configured in a way that allows the tool to travel outside +of the mesh. + +``` +[bed_mesh] +speed: 120 +horizontal_move_z: 5 +mesh_min: 35, 6 +mesh_max: 240, 198 +probe_count: 5 +scan_overshoot: 8 +``` + +- `scan_overshoot` + _Default Value: 0 (disabled)_\ + The maximum amount of travel (in mm) available outside of the mesh. + For rectangular beds this applies to travel on the X axis, and for round beds + it applies to the entire radius. The tool must be able to travel the amount + specified outside of the mesh. This value is used to optimize the travel + path when performing a "rapid scan". The minimum value that may be specified + is 1. The default is no overshoot. + +If no scan overshoot is configured then travel path optimization will not +be applied to changes in direction. + ## Bed Mesh Gcodes ### Calibration -`BED_MESH_CALIBRATE PROFILE= METHOD=[manual | automatic] [=] - [=] [ADAPTIVE=[0|1] [ADAPTIVE_MARGIN=]`\ +`BED_MESH_CALIBRATE PROFILE= METHOD=[manual | automatic | scan | rapid_scan] \ +[=] [=] [ADAPTIVE=[0|1] \ +[ADAPTIVE_MARGIN=]`\ _Default Profile: default_\ _Default Method: automatic if a probe is detected, otherwise manual_ \ _Default Adaptive: 0_ \ @@ -435,9 +498,17 @@ _Default Adaptive Margin: 0_ Initiates the probing procedure for Bed Mesh Calibration. The mesh will be saved into a profile specified by the `PROFILE` parameter, -or `default` if unspecified. If `METHOD=manual` is selected then manual probing -will occur. When switching between automatic and manual probing the generated -mesh points will automatically be adjusted. +or `default` if unspecified. The `METHOD` parameter takes one of the following +values: + +- `METHOD=manual`: enables manual probing using the nozzle and the paper test +- `METHOD=automatic`: Automatic (standard) probing. This is the default. +- `METHOD=scan`: Enables surface scanning. The tool will pause over each position + to collect a sample. +- `METHOD=rapid_scan`: Enables continuous surface scanning. + +XY positions are automatically adjusted to include the X and/or Y offsets +when a probing method other than `manual` is selected. It is possible to specify mesh parameters to modify the probed area. The following parameters are available: @@ -451,6 +522,7 @@ following parameters are available: - `MESH_ORIGIN` - `ROUND_PROBE_COUNT` - All beds: + - `MESH_PPS` - `ALGORITHM` - `ADAPTIVE` - `ADAPTIVE_MARGIN` @@ -557,3 +629,191 @@ is intended to compensate for a `gcode offset` when [mesh fade](#mesh-fade) is enabled. For example, if a secondary extruder is higher than the primary and needs a negative gcode offset, ie: `SET_GCODE_OFFSET Z=-.2`, it can be accounted for in `bed_mesh` with `BED_MESH_OFFSET ZFADE=.2`. + +## Bed Mesh Webhooks APIs + +### Dumping mesh data + +`{"id": 123, "method": "bed_mesh/dump_mesh"}` + +Dumps the configuration and state for the current mesh and all +saved profiles. + +The `dump_mesh` endpoint takes one optional parameter, `mesh_args`. +This parameter must be an object, where the keys and values are +parameters available to [BED_MESH_CALIBRATE](#bed_mesh_calibrate). +This will update the mesh configuration and probe points using the +supplied parameters prior to returning the result. It is recommended +to omit mesh parameters unless it is desired to visualize the probe points +and/or travel path before performing `BED_MESH_CALIBRATE`. + +## Visualization and analysis + +Most users will likely find that the visualizers included with +applications such as Mainsail, Fluidd, and Octoprint are sufficient +for basic analysis. However, Klipper's `scripts` folder contains the +`graph_mesh.py` script that may be used to perform additional +visualizations and more detailed analysis, particularly useful +for debugging hardware or the results produced by `bed_mesh`: + +``` +usage: graph_mesh.py [-h] {list,plot,analyze,dump} ... + +Graph Bed Mesh Data + +positional arguments: + {list,plot,analyze,dump} + list List available plot types + plot Plot a specified type + analyze Perform analysis on mesh data + dump Dump API response to json file + +options: + -h, --help show this help message and exit +``` + +### Pre-requisites + +Like most graphing tools provided by Klipper, `graph_mesh.py` requires +the `matplotlib` and `numpy` python dependencies. In addition, connecting +to Klipper via Moonraker's websocket requires the `websockets` python +dependency. While all visualizations can be output to an `svg` file, most of +the visualizations offered by `graph_mesh.py` are better viewed in live +preview mode on a desktop class PC. For example, the 3D visualizations may be +rotated and zoomed in preview mode, and the path visualizations can optionally +be animated in preview mode. + +### Plotting Mesh data + +The `graph_mesh.py` tool can plot several types of visualizations. +Available types can be shown by running `graph_mesh.py list`: + +``` +graph_mesh.py list +points Plot original generated points +path Plot probe travel path +rapid Plot rapid scan travel path +probedz Plot probed Z values +meshz Plot mesh Z values +overlay Plots the current probed mesh overlaid with a profile +delta Plots the delta between current probed mesh and a profile +``` + +Several options are available when plotting visualizations: + +``` +usage: graph_mesh.py plot [-h] [-a] [-s] [-p PROFILE_NAME] [-o OUTPUT] + +positional arguments: + Type of data to graph + Path/url to Klipper Socket or path to json file + +options: + -h, --help show this help message and exit + -a, --animate Animate paths in live preview + -s, --scale-plot Use axis limits reported by Klipper to scale plot X/Y + -p PROFILE_NAME, --profile-name PROFILE_NAME + Optional name of a profile to plot for 'probedz' + -o OUTPUT, --output OUTPUT + Output file path +``` + +Below is a description of each argument: + +- `plot type`: A required positional argument designating the type of + visualization to generate. Must be one of the types output by the + `graph_mesh.py list` command. +- `input`: A required positional argument containing a path or url + to the input source. This must be one of the following: + - A path to Klipper's Unix Domain Socket + - A url to an instance of Moonraker + - A path to a json file produced by `graph_mesh.py dump ` +- `-a`: Optional animation for the `path` and `rapid` visualization types. + Animations only apply to a live preview. +- `-s`: Optionally scales a plot using the `axis_minimum` and `axis_maximum` + values reported by Klipper's `toolhead` object when the dump file was + generated. +- `-p`: A profile name that may be specified when generating the + `probedz` 3D mesh visualization. When generating an `overlay` or + `delta` visualization this argument must be provided. +- `-o`: An optional file path indicating that the script should save the + visualization to this location rather than run in preview mode. Images + are saved in `svg` format. + +For example, to plot an animated rapid path, connecting via Klipper's unix +socket: + +``` +graph_mesh.py plot -a rapid ~/printer_data/comms/klippy.sock +``` + +Or to plot a 3d visualization of the mesh, connecting via Moonraker: + +``` +graph_mesh.py plot meshz http://my-printer.local +``` + +### Bed Mesh Analysis + +The `graph_mesh.py` tool may also be used to perform an analysis on the +data provided by the [bed_mesh/dump_mesh](#dumping-mesh-data) API: + +``` +graph_mesh.py analyze +``` + +As with the `plot` command, the `` must be a path to Klipper's +unix socket, a URL to an instance of Moonraker, or a path to a json file +generated by the dump command. + +To begin, the analysis will perform various checks on the points and +probe paths generated by `bed_mesh` at the time of the dump. This +includes the following: + +- The number of probe points generated, without any additions +- The number of probe points generated including any points generated + as the result faulty regions and/or a configured zero reference position. +- The number of probe points generated when performing a rapid scan. +- The total number of moves generated for a rapid scan. +- A validation that the probe points generated for a rapid scan are + identical to the probe points generated for a standard probing procedure. +- A "backtracking" check for both the standard probe path and a rapid scan + path. Backtracking can be defined as moving to the same position more than + once during the probing procedure. Backtracking should never occur during a + standard probe. Faulty regions *can* result in backtracking during a rapid + scan in an attempt to avoid entering a faulty region when approaching or + leaving a probe location, however should never occur otherwise. + +Next each probed mesh present in the dump will by analyzed, beginning with +the mesh loaded at the time of the dump (if present) and followed by any +saved profiles. The following data is extracted: + +- Mesh shape (Min X,Y, Max X,Y Probe Count) +- Mesh Z range, (Minimum Z, Maximum Z) +- Mean Z value in the mesh +- Standard Deviation of the Z values in the Mesh + +In addition to the above, a delta analysis is performed between meshes +with the same shape, reporting the following: +- The range of the delta between to meshes (Minimum and Maximum) +- The mean delta +- Standard Deviation of the delta +- The absolute maximum difference +- The absolute mean + +### Save mesh data to a file + +The `dump` command may be used to save the response to a file which +can be shared for analysis when troubleshooting: + +``` +graph_mesh.py dump -o +``` + +The `` should be a path to Klipper's unix socket or +a URL to an instance of Moonraker. The `-o` option may be used to +specify the path to the output file. If omitted, the file will be +saved in the working directory, with a file name in the following +format: + +`klipper-bedmesh-{year}{month}{day}{hour}{minute}{second}.json` diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 6b42fe48d..b192e7362 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -998,6 +998,13 @@ Visual Examples: #adaptive_margin: # An optional margin (in mm) to be added around the bed area used by # the defined print objects when generating an adaptive mesh. +#scan_overshoot: +# The maximum amount of travel (in mm) available outside of the mesh. +# For rectangular beds this applies to travel on the X axis, and for round beds +# it applies to the entire radius. The tool must be able to travel the amount +# specified outside of the mesh. This value is used to optimize the travel +# path when performing a "rapid scan". The minimum value that may be specified +# is 1. The default is no overshoot. ``` ### [bed_tilt] From 4ac283cc0e198ad64aad8f321e4f158065828397 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 15 Jun 2024 12:13:35 -0400 Subject: [PATCH 071/111] error_mcu: Move shutdown error message formatting to new error_mcu.py module Create a new module to help format verbose mcu error messages. Move the shutdown message formatting to this module. This moves the error formatting out of the background thread and out of the critical shutdown code path. Signed-off-by: Kevin O'Connor --- klippy/extras/error_mcu.py | 67 ++++++++++++++++++++++++++++++++++++++ klippy/klippy.py | 25 +++++++------- klippy/mcu.py | 41 ++++------------------- 3 files changed, 87 insertions(+), 46 deletions(-) create mode 100644 klippy/extras/error_mcu.py diff --git a/klippy/extras/error_mcu.py b/klippy/extras/error_mcu.py new file mode 100644 index 000000000..ad737f9a7 --- /dev/null +++ b/klippy/extras/error_mcu.py @@ -0,0 +1,67 @@ +# More verbose information on micro-controller errors +# +# Copyright (C) 2024 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging + +message_shutdown = """ +Once the underlying issue is corrected, use the +"FIRMWARE_RESTART" command to reset the firmware, reload the +config, and restart the host software. +Printer is shutdown +""" + +Common_MCU_errors = { + ("Timer too close",): """ +This often indicates the host computer is overloaded. Check +for other processes consuming excessive CPU time, high swap +usage, disk errors, overheating, unstable voltage, or +similar system problems on the host computer.""", + ("Missed scheduling of next ",): """ +This is generally indicative of an intermittent +communication failure between micro-controller and host.""", + ("ADC out of range",): """ +This generally occurs when a heater temperature exceeds +its configured min_temp or max_temp.""", + ("Rescheduled timer in the past", "Stepper too far in past"): """ +This generally occurs when the micro-controller has been +requested to step at a rate higher than it is capable of +obtaining.""", + ("Command request",): """ +This generally occurs in response to an M112 G-Code command +or in response to an internal error in the host software.""", +} + +def error_hint(msg): + for prefixes, help_msg in Common_MCU_errors.items(): + for prefix in prefixes: + if msg.startswith(prefix): + return help_msg + return "" + +class PrinterMCUError: + def __init__(self, config): + self.printer = config.get_printer() + self.printer.register_event_handler("klippy:notify_mcu_shutdown", + self._handle_notify_mcu_shutdown) + def _check_mcu_shutdown(self, msg, details): + mcu_name = details['mcu'] + mcu_msg = details['reason'] + event_type = details['event_type'] + prefix = "MCU '%s' shutdown: " % (mcu_name,) + if event_type == 'is_shutdown': + prefix = "Previous MCU '%s' shutdown: " % (mcu_name,) + # Lookup generic hint + hint = error_hint(msg) + # Update error message + newmsg = "%s%s%s%s" % (prefix, mcu_msg, hint, message_shutdown) + self.printer.update_error_msg(msg, newmsg) + def _handle_notify_mcu_shutdown(self, msg, details): + if msg == "MCU shutdown": + self._check_mcu_shutdown(msg, details) + else: + self.printer.update_error_msg(msg, "%s%s" % (msg, message_shutdown)) + +def load_config(config): + return PrinterMCUError(config) diff --git a/klippy/klippy.py b/klippy/klippy.py index 097cff998..5574063dd 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -1,7 +1,7 @@ #!/usr/bin/env python2 # Main code for host side printer firmware # -# Copyright (C) 2016-2020 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import sys, os, gc, optparse, logging, time, collections, importlib @@ -40,13 +40,6 @@ config, and restart the host software. Error configuring printer """ -message_shutdown = """ -Once the underlying issue is corrected, use the -"FIRMWARE_RESTART" command to reset the firmware, reload the -config, and restart the host software. -Printer is shutdown -""" - class Printer: config_error = configfile.error command_error = gcode.CommandError @@ -85,6 +78,13 @@ class Printer: if (msg != message_ready and self.start_args.get('debuginput') is not None): self.request_exit('error_exit') + def update_error_msg(self, oldmsg, newmsg): + if (self.state_message != oldmsg + or self.state_message in (message_ready, message_startup) + or newmsg in (message_ready, message_startup)): + return + self.state_message = newmsg + logging.error(newmsg) def add_object(self, name, obj): if name in self.objects: raise self.config_error( @@ -241,12 +241,12 @@ class Printer: logging.info(info) if self.bglogger is not None: self.bglogger.set_rollover_info(name, info) - def invoke_shutdown(self, msg): + def invoke_shutdown(self, msg, details={}): if self.in_shutdown_state: return logging.error("Transition to shutdown state: %s", msg) self.in_shutdown_state = True - self._set_state("%s%s" % (msg, message_shutdown)) + self._set_state(msg) for cb in self.event_handlers.get("klippy:shutdown", []): try: cb() @@ -254,9 +254,10 @@ class Printer: logging.exception("Exception during shutdown handler") logging.info("Reactor garbage collection: %s", self.reactor.get_gc_stats()) - def invoke_async_shutdown(self, msg): + self.send_event("klippy:notify_mcu_shutdown", msg, details) + def invoke_async_shutdown(self, msg, details): self.reactor.register_async_callback( - (lambda e: self.invoke_shutdown(msg))) + (lambda e: self.invoke_shutdown(msg, details))) def register_event_handler(self, event, callback): self.event_handlers.setdefault(event, []).append(callback) def send_event(self, event, *params): diff --git a/klippy/mcu.py b/klippy/mcu.py index 23ba07173..feb4856a1 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -1,6 +1,6 @@ # Interface to Klipper micro-controller code # -# Copyright (C) 2016-2023 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import sys, os, zlib, logging, math @@ -605,6 +605,7 @@ class MCU: self._mcu_tick_stddev = 0. self._mcu_tick_awake = 0. # Register handlers + printer.load_object(config, "error_mcu") printer.register_event_handler("klippy:firmware_restart", self._firmware_restart) printer.register_event_handler("klippy:mcu_identify", @@ -631,13 +632,13 @@ class MCU: if clock is not None: self._shutdown_clock = self.clock32_to_clock64(clock) self._shutdown_msg = msg = params['static_string_id'] - logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, params['#name'], + event_type = params['#name'] + self._printer.invoke_async_shutdown( + "MCU shutdown", {"reason": msg, "mcu": self._name, + "event_type": event_type}) + logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, event_type, self._shutdown_msg, self._clocksync.dump_debug(), self._serial.dump_debug()) - prefix = "MCU '%s' shutdown: " % (self._name,) - if params['#name'] == 'is_shutdown': - prefix = "Previous MCU '%s' shutdown: " % (self._name,) - self._printer.invoke_async_shutdown(prefix + msg + error_help(msg)) def _handle_starting(self, params): if not self._is_shutdown: self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart" @@ -1008,34 +1009,6 @@ class MCU: self._get_status_info['last_stats'] = last_stats return False, '%s: %s' % (self._name, stats) -Common_MCU_errors = { - ("Timer too close",): """ -This often indicates the host computer is overloaded. Check -for other processes consuming excessive CPU time, high swap -usage, disk errors, overheating, unstable voltage, or -similar system problems on the host computer.""", - ("Missed scheduling of next ",): """ -This is generally indicative of an intermittent -communication failure between micro-controller and host.""", - ("ADC out of range",): """ -This generally occurs when a heater temperature exceeds -its configured min_temp or max_temp.""", - ("Rescheduled timer in the past", "Stepper too far in past"): """ -This generally occurs when the micro-controller has been -requested to step at a rate higher than it is capable of -obtaining.""", - ("Command request",): """ -This generally occurs in response to an M112 G-Code command -or in response to an internal error in the host software.""", -} - -def error_help(msg): - for prefixes, help_msg in Common_MCU_errors.items(): - for prefix in prefixes: - if msg.startswith(prefix): - return help_msg - return "" - def add_printer_objects(config): printer = config.get_printer() reactor = printer.get_reactor() From ba529996ea18edf87de0bae41d8c458ffc07b889 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 15 Jun 2024 12:27:36 -0400 Subject: [PATCH 072/111] error_mcu: Move mcu protocol error reporting to error_mcu module Signed-off-by: Kevin O'Connor --- klippy/extras/error_mcu.py | 43 +++++++++++++++++++++++++++++++++++++ klippy/klippy.py | 44 ++++---------------------------------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/klippy/extras/error_mcu.py b/klippy/extras/error_mcu.py index ad737f9a7..339eb7313 100644 --- a/klippy/extras/error_mcu.py +++ b/klippy/extras/error_mcu.py @@ -12,6 +12,17 @@ config, and restart the host software. Printer is shutdown """ +message_protocol_error1 = """ +This is frequently caused by running an older version of the +firmware on the MCU(s). Fix by recompiling and flashing the +firmware. +""" + +message_protocol_error2 = """ +Once the underlying issue is corrected, use the "RESTART" +command to reload the config and restart the host software. +""" + Common_MCU_errors = { ("Timer too close",): """ This often indicates the host computer is overloaded. Check @@ -45,6 +56,8 @@ class PrinterMCUError: self.printer = config.get_printer() self.printer.register_event_handler("klippy:notify_mcu_shutdown", self._handle_notify_mcu_shutdown) + self.printer.register_event_handler("klippy:notify_mcu_error", + self._handle_notify_mcu_error) def _check_mcu_shutdown(self, msg, details): mcu_name = details['mcu'] mcu_msg = details['reason'] @@ -62,6 +75,36 @@ class PrinterMCUError: self._check_mcu_shutdown(msg, details) else: self.printer.update_error_msg(msg, "%s%s" % (msg, message_shutdown)) + def _check_protocol_error(self, msg, details): + host_version = self.printer.start_args['software_version'] + msg_update = [] + msg_updated = [] + for mcu_name, mcu in self.printer.lookup_objects('mcu'): + try: + mcu_version = mcu.get_status()['mcu_version'] + except: + logging.exception("Unable to retrieve mcu_version from mcu") + continue + if mcu_version != host_version: + msg_update.append("%s: Current version %s" + % (mcu_name.split()[-1], mcu_version)) + else: + msg_updated.append("%s: Current version %s" + % (mcu_name.split()[-1], mcu_version)) + if not msg_update: + msg_update.append("") + if not msg_updated: + msg_updated.append("") + newmsg = ["MCU Protocol error", + message_protocol_error1, + "Your Klipper version is: %s" % (host_version,), + "MCU(s) which should be updated:"] + newmsg += msg_update + ["Up-to-date MCU(s):"] + msg_updated + newmsg += [message_protocol_error2, details['error']] + self.printer.update_error_msg(msg, "\n".join(newmsg)) + def _handle_notify_mcu_error(self, msg, details): + if msg == "Protocol error": + self._check_protocol_error(msg, details) def load_config(config): return PrinterMCUError(config) diff --git a/klippy/klippy.py b/klippy/klippy.py index 5574063dd..17609b37e 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -22,17 +22,6 @@ command to reload the config and restart the host software. Printer is halted """ -message_protocol_error1 = """ -This is frequently caused by running an older version of the -firmware on the MCU(s). Fix by recompiling and flashing the -firmware. -""" - -message_protocol_error2 = """ -Once the underlying issue is corrected, use the "RESTART" -command to reload the config and restart the host software. -""" - message_mcu_connect_error = """ Once the underlying issue is corrected, use the "FIRMWARE_RESTART" command to reset the firmware, reload the @@ -143,33 +132,6 @@ class Printer: m.add_printer_objects(config) # Validate that there are no undefined parameters in the config file pconfig.check_unused_options(config) - def _build_protocol_error_message(self, e): - host_version = self.start_args['software_version'] - msg_update = [] - msg_updated = [] - for mcu_name, mcu in self.lookup_objects('mcu'): - try: - mcu_version = mcu.get_status()['mcu_version'] - except: - logging.exception("Unable to retrieve mcu_version from mcu") - continue - if mcu_version != host_version: - msg_update.append("%s: Current version %s" - % (mcu_name.split()[-1], mcu_version)) - else: - msg_updated.append("%s: Current version %s" - % (mcu_name.split()[-1], mcu_version)) - if not msg_update: - msg_update.append("") - if not msg_updated: - msg_updated.append("") - msg = ["MCU Protocol error", - message_protocol_error1, - "Your Klipper version is: %s" % (host_version,), - "MCU(s) which should be updated:"] - msg += msg_update + ["Up-to-date MCU(s):"] + msg_updated - msg += [message_protocol_error2, str(e)] - return "\n".join(msg) def _connect(self, eventtime): try: self._read_config() @@ -183,8 +145,10 @@ class Printer: self._set_state("%s\n%s" % (str(e), message_restart)) return except msgproto.error as e: - logging.exception("Protocol error") - self._set_state(self._build_protocol_error_message(e)) + msg = "Protocol error" + logging.exception(msg) + self._set_state(msg) + self.send_event("klippy:notify_mcu_error", msg, {"error": str(e)}) util.dump_mcu_build() return except mcu.error as e: From 7149bb1b6de2616f48b40f060ad8c887cb6a84d2 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 15 Jun 2024 12:34:29 -0400 Subject: [PATCH 073/111] error_mcu: Move formatting of mcu connect errors to error_mcu module Signed-off-by: Kevin O'Connor --- klippy/extras/error_mcu.py | 12 ++++++++++++ klippy/klippy.py | 13 ++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/klippy/extras/error_mcu.py b/klippy/extras/error_mcu.py index 339eb7313..536debbb0 100644 --- a/klippy/extras/error_mcu.py +++ b/klippy/extras/error_mcu.py @@ -23,6 +23,13 @@ Once the underlying issue is corrected, use the "RESTART" command to reload the config and restart the host software. """ +message_mcu_connect_error = """ +Once the underlying issue is corrected, use the +"FIRMWARE_RESTART" command to reset the firmware, reload the +config, and restart the host software. +Error configuring printer +""" + Common_MCU_errors = { ("Timer too close",): """ This often indicates the host computer is overloaded. Check @@ -102,9 +109,14 @@ class PrinterMCUError: newmsg += msg_update + ["Up-to-date MCU(s):"] + msg_updated newmsg += [message_protocol_error2, details['error']] self.printer.update_error_msg(msg, "\n".join(newmsg)) + def _check_mcu_connect_error(self, msg, details): + newmsg = "%s%s" % (details['error'], message_mcu_connect_error) + self.printer.update_error_msg(msg, newmsg) def _handle_notify_mcu_error(self, msg, details): if msg == "Protocol error": self._check_protocol_error(msg, details) + elif msg == "MCU error during connect": + self._check_mcu_connect_error(msg, details) def load_config(config): return PrinterMCUError(config) diff --git a/klippy/klippy.py b/klippy/klippy.py index 17609b37e..75ee6887a 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -22,13 +22,6 @@ command to reload the config and restart the host software. Printer is halted """ -message_mcu_connect_error = """ -Once the underlying issue is corrected, use the -"FIRMWARE_RESTART" command to reset the firmware, reload the -config, and restart the host software. -Error configuring printer -""" - class Printer: config_error = configfile.error command_error = gcode.CommandError @@ -152,8 +145,10 @@ class Printer: util.dump_mcu_build() return except mcu.error as e: - logging.exception("MCU error during connect") - self._set_state("%s%s" % (str(e), message_mcu_connect_error)) + msg = "MCU error during connect" + logging.exception(msg) + self._set_state(msg) + self.send_event("klippy:notify_mcu_error", msg, {"error": str(e)}) util.dump_mcu_build() return except Exception as e: From 9fa0fb1a0ebca3ed4be887417b255b26fc99bbfd Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 18 Jun 2024 12:51:32 -0400 Subject: [PATCH 074/111] error_mcu: Support mechanism to add per-instance context to a shutdown Signed-off-by: Kevin O'Connor --- klippy/extras/error_mcu.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/klippy/extras/error_mcu.py b/klippy/extras/error_mcu.py index 536debbb0..dc91c33a9 100644 --- a/klippy/extras/error_mcu.py +++ b/klippy/extras/error_mcu.py @@ -61,10 +61,13 @@ def error_hint(msg): class PrinterMCUError: def __init__(self, config): self.printer = config.get_printer() + self.clarify_callbacks = {} self.printer.register_event_handler("klippy:notify_mcu_shutdown", self._handle_notify_mcu_shutdown) self.printer.register_event_handler("klippy:notify_mcu_error", self._handle_notify_mcu_error) + def add_clarify(self, msg, callback): + self.clarify_callbacks.setdefault(msg, []).append(callback) def _check_mcu_shutdown(self, msg, details): mcu_name = details['mcu'] mcu_msg = details['reason'] @@ -73,9 +76,17 @@ class PrinterMCUError: if event_type == 'is_shutdown': prefix = "Previous MCU '%s' shutdown: " % (mcu_name,) # Lookup generic hint - hint = error_hint(msg) + hint = error_hint(mcu_msg) + # Add per instance help + clarify = [cb(msg, details) + for cb in self.clarify_callbacks.get(mcu_msg, [])] + clarify = [cm for cm in clarify if cm is not None] + clarify_msg = "" + if clarify: + clarify_msg = "\n".join(["", ""] + clarify + [""]) # Update error message - newmsg = "%s%s%s%s" % (prefix, mcu_msg, hint, message_shutdown) + newmsg = "%s%s%s%s%s" % (prefix, mcu_msg, clarify_msg, + hint, message_shutdown) self.printer.update_error_msg(msg, newmsg) def _handle_notify_mcu_shutdown(self, msg, details): if msg == "MCU shutdown": From d89722056bf58103dc7fc06bc310ac39afa6aaa0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 18 Jun 2024 13:01:34 -0400 Subject: [PATCH 075/111] mcu: Rename setup_minmax() to setup_adc_sample() Rename this method so that it is more distinct from the the common temperature setup_minmax() method. Signed-off-by: Kevin O'Connor --- klippy/extras/adc_scaled.py | 6 ++--- klippy/extras/adc_temperature.py | 8 +++---- klippy/extras/buttons.py | 2 +- klippy/extras/hall_filament_width_sensor.py | 4 ++-- klippy/extras/temperature_mcu.py | 23 ++++++++++--------- .../extras/tsl1401cl_filament_width_sensor.py | 2 +- klippy/mcu.py | 4 ++-- 7 files changed, 24 insertions(+), 25 deletions(-) diff --git a/klippy/extras/adc_scaled.py b/klippy/extras/adc_scaled.py index c2d2cb877..80ea452f3 100644 --- a/klippy/extras/adc_scaled.py +++ b/klippy/extras/adc_scaled.py @@ -7,7 +7,6 @@ SAMPLE_TIME = 0.001 SAMPLE_COUNT = 8 REPORT_TIME = 0.300 -RANGE_CHECK_COUNT = 4 class MCU_scaled_adc: def __init__(self, main, pin_params): @@ -18,7 +17,7 @@ class MCU_scaled_adc: qname = main.name + ":" + pin_params['pin'] query_adc.register_adc(qname, self._mcu_adc) self._callback = None - self.setup_minmax = self._mcu_adc.setup_minmax + self.setup_adc_sample = self._mcu_adc.setup_adc_sample self.get_mcu = self._mcu_adc.get_mcu def _handle_callback(self, read_time, read_value): max_adc = self._main.last_vref[1] @@ -54,8 +53,7 @@ class PrinterADCScaled: ppins = self.printer.lookup_object('pins') mcu_adc = ppins.setup_pin('adc', pin_name) mcu_adc.setup_adc_callback(REPORT_TIME, callback) - mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, minval=0., maxval=1., - range_check_count=RANGE_CHECK_COUNT) + mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT) query_adc = config.get_printer().load_object(config, 'query_adc') query_adc.register_adc(self.name + ":" + name, mcu_adc) return mcu_adc diff --git a/klippy/extras/adc_temperature.py b/klippy/extras/adc_temperature.py index b76e8c66f..260fe2817 100644 --- a/klippy/extras/adc_temperature.py +++ b/klippy/extras/adc_temperature.py @@ -32,10 +32,10 @@ class PrinterADCtoTemperature: temp = self.adc_convert.calc_temp(read_value) self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) def setup_minmax(self, min_temp, max_temp): - adc_range = [self.adc_convert.calc_adc(t) for t in [min_temp, max_temp]] - self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, - minval=min(adc_range), maxval=max(adc_range), - range_check_count=RANGE_CHECK_COUNT) + arange = [self.adc_convert.calc_adc(t) for t in [min_temp, max_temp]] + self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT, + minval=min(arange), maxval=max(arange), + range_check_count=RANGE_CHECK_COUNT) ###################################################################### diff --git a/klippy/extras/buttons.py b/klippy/extras/buttons.py index 70d76a60e..daa998a93 100644 --- a/klippy/extras/buttons.py +++ b/klippy/extras/buttons.py @@ -104,7 +104,7 @@ class MCU_ADC_buttons: self.max_value = 0. ppins = printer.lookup_object('pins') self.mcu_adc = ppins.setup_pin('adc', self.pin) - self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback) query_adc = printer.lookup_object('query_adc') query_adc.register_adc('adc_button:' + pin.strip(), self.mcu_adc) diff --git a/klippy/extras/hall_filament_width_sensor.py b/klippy/extras/hall_filament_width_sensor.py index e08028874..8dab35226 100644 --- a/klippy/extras/hall_filament_width_sensor.py +++ b/klippy/extras/hall_filament_width_sensor.py @@ -49,10 +49,10 @@ class HallFilamentWidthSensor: # Start adc self.ppins = self.printer.lookup_object('pins') self.mcu_adc = self.ppins.setup_pin('adc', self.pin1) - self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback) self.mcu_adc2 = self.ppins.setup_pin('adc', self.pin2) - self.mcu_adc2.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc2.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) self.mcu_adc2.setup_adc_callback(ADC_REPORT_TIME, self.adc2_callback) # extrude factor updating self.extrude_factor_update_timer = self.reactor.register_timer( diff --git a/klippy/extras/temperature_mcu.py b/klippy/extras/temperature_mcu.py index 585ec4c1d..02d91a3a4 100644 --- a/klippy/extras/temperature_mcu.py +++ b/klippy/extras/temperature_mcu.py @@ -35,26 +35,27 @@ class PrinterTemperatureMCU: query_adc.register_adc(config.get_name(), self.mcu_adc) # Register callbacks if self.printer.get_start_args().get('debugoutput') is not None: - self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, - range_check_count=RANGE_CHECK_COUNT) + self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT) return self.printer.register_event_handler("klippy:mcu_identify", - self._mcu_identify) + self.handle_mcu_identify) + # Temperature interface def setup_callback(self, temperature_callback): self.temperature_callback = temperature_callback def get_report_time_delta(self): return REPORT_TIME - def adc_callback(self, read_time, read_value): - temp = self.base_temperature + read_value * self.slope - self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) def setup_minmax(self, min_temp, max_temp): self.min_temp = min_temp self.max_temp = max_temp + # Internal code + def adc_callback(self, read_time, read_value): + temp = self.base_temperature + read_value * self.slope + self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) def calc_adc(self, temp): return (temp - self.base_temperature) / self.slope def calc_base(self, temp, adc): return temp - adc * self.slope - def _mcu_identify(self): + def handle_mcu_identify(self): # Obtain mcu information mcu = self.mcu_adc.get_mcu() self.debug_read_cmd = mcu.lookup_query_command( @@ -89,10 +90,10 @@ class PrinterTemperatureMCU: self.slope = (self.temp2 - self.temp1) / (self.adc2 - self.adc1) self.base_temperature = self.calc_base(self.temp1, self.adc1) # Setup min/max checks - adc_range = [self.calc_adc(t) for t in [self.min_temp, self.max_temp]] - self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, - minval=min(adc_range), maxval=max(adc_range), - range_check_count=RANGE_CHECK_COUNT) + arange = [self.calc_adc(t) for t in [self.min_temp, self.max_temp]] + self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT, + minval=min(arange), maxval=max(arange), + range_check_count=RANGE_CHECK_COUNT) def config_unknown(self): raise self.printer.config_error("MCU temperature not supported on %s" % (self.mcu_type,)) diff --git a/klippy/extras/tsl1401cl_filament_width_sensor.py b/klippy/extras/tsl1401cl_filament_width_sensor.py index fb2d97131..83480f467 100644 --- a/klippy/extras/tsl1401cl_filament_width_sensor.py +++ b/klippy/extras/tsl1401cl_filament_width_sensor.py @@ -33,7 +33,7 @@ class FilamentWidthSensor: # Start adc self.ppins = self.printer.lookup_object('pins') self.mcu_adc = self.ppins.setup_pin('adc', self.pin) - self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback) # extrude factor updating self.extrude_factor_update_timer = self.reactor.register_timer( diff --git a/klippy/mcu.py b/klippy/mcu.py index feb4856a1..1122ff865 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -496,8 +496,8 @@ class MCU_adc: self._inv_max_adc = 0. def get_mcu(self): return self._mcu - def setup_minmax(self, sample_time, sample_count, - minval=0., maxval=1., range_check_count=0): + def setup_adc_sample(self, sample_time, sample_count, + minval=0., maxval=1., range_check_count=0): self._sample_time = sample_time self._sample_count = sample_count self._min_sample = minval From 2d73211190e90f4dac0b8585fa931e8d46d4970e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 18 Jun 2024 13:18:06 -0400 Subject: [PATCH 076/111] adc_temperature: Enhance "ADC out of range" error reports Try to report which ADC is reporting out of range. Signed-off-by: Kevin O'Connor --- klippy/extras/adc_temperature.py | 42 +++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/klippy/extras/adc_temperature.py b/klippy/extras/adc_temperature.py index 260fe2817..c53ae7056 100644 --- a/klippy/extras/adc_temperature.py +++ b/klippy/extras/adc_temperature.py @@ -1,6 +1,6 @@ # Obtain temperature using linear interpolation of ADC values # -# Copyright (C) 2016-2018 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import logging, bisect @@ -22,8 +22,8 @@ class PrinterADCtoTemperature: ppins = config.get_printer().lookup_object('pins') self.mcu_adc = ppins.setup_pin('adc', config.get('sensor_pin')) self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback) - query_adc = config.get_printer().load_object(config, 'query_adc') - query_adc.register_adc(config.get_name(), self.mcu_adc) + self.diag_helper = HelperTemperatureDiagnostics( + config, self.mcu_adc, adc_convert.calc_temp) def setup_callback(self, temperature_callback): self.temperature_callback = temperature_callback def get_report_time_delta(self): @@ -33,9 +33,43 @@ class PrinterADCtoTemperature: self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) def setup_minmax(self, min_temp, max_temp): arange = [self.adc_convert.calc_adc(t) for t in [min_temp, max_temp]] + min_adc, max_adc = sorted(arange) self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT, - minval=min(arange), maxval=max(arange), + minval=min_adc, maxval=max_adc, range_check_count=RANGE_CHECK_COUNT) + self.diag_helper.setup_diag_minmax(min_temp, max_temp, min_adc, max_adc) + +# Tool to register with query_adc and report extra info on ADC range errors +class HelperTemperatureDiagnostics: + def __init__(self, config, mcu_adc, calc_temp_cb): + self.printer = config.get_printer() + self.name = config.get_name() + self.mcu_adc = mcu_adc + self.calc_temp_cb = calc_temp_cb + self.min_temp = self.max_temp = self.min_adc = self.max_adc = None + query_adc = self.printer.load_object(config, 'query_adc') + query_adc.register_adc(self.name, self.mcu_adc) + error_mcu = self.printer.load_object(config, 'error_mcu') + error_mcu.add_clarify("ADC out of range", self._clarify_adc_range) + def setup_diag_minmax(self, min_temp, max_temp, min_adc, max_adc): + self.min_temp, self.max_temp = min_temp, max_temp + self.min_adc, self.max_adc = min_adc, max_adc + def _clarify_adc_range(self, msg, details): + if self.min_temp is None: + return None + last_value, last_read_time = self.mcu_adc.get_last_value() + if not last_read_time: + return None + if last_value >= self.min_adc and last_value <= self.max_adc: + return None + tempstr = "?" + try: + last_temp = self.calc_temp_cb(last_value) + tempstr = "%.3f" % (last_temp,) + except e: + logging.exception("Error in calc_temp callback") + return ("Sensor '%s' temperature %s not in range %.3f:%.3f" + % (self.name, tempstr, self.min_temp, self.max_temp)) ###################################################################### From 6d70050261ec3290f3c2e4015438e4910fd430d0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 18 Jun 2024 13:22:48 -0400 Subject: [PATCH 077/111] temperature_mcu: Enhance "ADC out of range" error reports Try to report which ADC is reporting out of range. Signed-off-by: Kevin O'Connor --- klippy/extras/temperature_mcu.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/klippy/extras/temperature_mcu.py b/klippy/extras/temperature_mcu.py index 02d91a3a4..be2cd145c 100644 --- a/klippy/extras/temperature_mcu.py +++ b/klippy/extras/temperature_mcu.py @@ -1,10 +1,11 @@ # Support for micro-controller chip based temperature sensors # -# Copyright (C) 2020 Kevin O'Connor +# Copyright (C) 2020-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import logging import mcu +from . import adc_temperature SAMPLE_TIME = 0.001 SAMPLE_COUNT = 8 @@ -31,8 +32,8 @@ class PrinterTemperatureMCU: self.mcu_adc = ppins.setup_pin('adc', '%s:ADC_TEMPERATURE' % (mcu_name,)) self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback) - query_adc = config.get_printer().load_object(config, 'query_adc') - query_adc.register_adc(config.get_name(), self.mcu_adc) + self.diag_helper = adc_temperature.HelperTemperatureDiagnostics( + config, self.mcu_adc, self.calc_temp) # Register callbacks if self.printer.get_start_args().get('debugoutput') is not None: self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT) @@ -51,6 +52,8 @@ class PrinterTemperatureMCU: def adc_callback(self, read_time, read_value): temp = self.base_temperature + read_value * self.slope self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) + def calc_temp(self, adc): + return self.base_temperature + adc * self.slope def calc_adc(self, temp): return (temp - self.base_temperature) / self.slope def calc_base(self, temp, adc): @@ -91,9 +94,12 @@ class PrinterTemperatureMCU: self.base_temperature = self.calc_base(self.temp1, self.adc1) # Setup min/max checks arange = [self.calc_adc(t) for t in [self.min_temp, self.max_temp]] + min_adc, max_adc = sorted(arange) self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT, - minval=min(arange), maxval=max(arange), + minval=min_adc, maxval=max_adc, range_check_count=RANGE_CHECK_COUNT) + self.diag_helper.setup_diag_minmax(self.min_temp, self.max_temp, + min_adc, max_adc) def config_unknown(self): raise self.printer.config_error("MCU temperature not supported on %s" % (self.mcu_type,)) From 4d21ffc1d67d4aa9886cc691441afccc057b975d Mon Sep 17 00:00:00 2001 From: elmo Date: Thu, 27 Jun 2024 18:59:48 +0200 Subject: [PATCH 078/111] config: Adds support for the Tronxy Crux1 printer (#6627) Signed-off-by: Louis West --- config/printer-tronxy-crux1-2022.cfg | 138 +++++++++++++++++++++++++++ test/klippy/printers.test | 1 + 2 files changed, 139 insertions(+) create mode 100644 config/printer-tronxy-crux1-2022.cfg diff --git a/config/printer-tronxy-crux1-2022.cfg b/config/printer-tronxy-crux1-2022.cfg new file mode 100644 index 000000000..e3254d85b --- /dev/null +++ b/config/printer-tronxy-crux1-2022.cfg @@ -0,0 +1,138 @@ +# Klipper configuration for the TronXY Crux1 printer +# CXY-V10.1-220921 mainboard, GD32F4XX or STM32F446 MCU +# +# ======================= +# BUILD AND FLASH OPTIONS +# ======================= +# +# MCU-architecture: STMicroelectronics +# Processor model: STM32F446 +# Bootloader offset: 64KiB +# Comms interface: Serial on USART1 PA10/PA9 +# +# Build the firmware with these options +# Rename the resulting klipper.bin into fmw_tronxy.bin +# Put the file into a directory called "update" on a FAT32 formatted SD card. +# Turn off the printer, plug in the SD card and turn the printer back on +# Flashing will start automatically and progress will be indicated on the LCD +# Once the flashing is completed the display will get stuck on the white Tronxy logo bootscreen +# The LCD display will NOT work anymore after flashing Klipper onto this printer + +[mcu] +serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 +restart_method: command + +[printer] +kinematics: cartesian +max_velocity: 250 +max_accel: 1500 +square_corner_velocity: 5 +max_z_velocity: 15 +max_z_accel: 100 + +[controller_fan drivers_fan] +pin: PD7 + +[pwm_cycle_time BEEPER_pin] +pin: PA8 +value: 0 +shutdown_value: 0 +cycle_time: 0.001 + +[safe_z_home] +home_xy_position: 0, 0 +speed: 100 +z_hop: 10 +z_hop_speed: 5 + +[stepper_x] +step_pin: PE5 +dir_pin: PF1 +enable_pin: !PF0 +microsteps: 16 +rotation_distance: 20 +endstop_pin: ^!PC15 +position_endstop: -1 +position_min: -1 +position_max: 180 +homing_speed: 100 +homing_retract_dist: 10 +second_homing_speed: 25 + +[stepper_y] +step_pin: PF9 +dir_pin: !PF3 +enable_pin: !PF5 +microsteps: 16 +rotation_distance: 20 +endstop_pin: ^!PC14 +position_endstop: -3 +position_min: -3 +position_max: 180 +homing_retract_dist: 10 +homing_speed: 100 +second_homing_speed: 25 + +[stepper_z] +step_pin: PA6 +dir_pin: !PF15 +enable_pin: !PA5 +microsteps: 16 +rotation_distance: 4 +endstop_pin: ^!PC13 +position_endstop: 0 +position_max: 180 +position_min: 0 + +[extruder] +step_pin: PB1 +dir_pin: PF13 +enable_pin: !PF14 +microsteps: 16 +rotation_distance: 16.75 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PG7 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC3 +control: pid +pid_kp: 22.2 +pid_ki: 1.08 +pid_kd: 114.00 +min_temp: 0 +max_temp: 250 +min_extrude_temp: 170 +max_extrude_only_distance: 450 + +[heater_fan hotend_fan] +heater: extruder +heater_temp: 50.0 +pin: PG9 + +[fan] +pin: PG0 + +[filament_switch_sensor filament_sensor] +pause_on_runout: True +switch_pin: ^!PE6 + +[heater_bed] +heater_pin: PE2 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC2 +min_temp: 0 +max_temp: 130 +control: pid +pid_kp: 10.00 +pid_ki: 0.023 +pid_kd: 305.4 + +[bed_screws] +screw1: 17.5, 11 +screw1_name: front_left +screw2: 162.5, 11 +screw2_name: front_right +screw3: 162.5, 162.5 +screw3_name: back_right +screw4: 17.5, 162.5 +screw4_name: back_left diff --git a/test/klippy/printers.test b/test/klippy/printers.test index ba7adb614..9f7ab3c20 100644 --- a/test/klippy/printers.test +++ b/test/klippy/printers.test @@ -247,6 +247,7 @@ CONFIG ../../config/generic-fysetc-spider.cfg CONFIG ../../config/generic-ldo-leviathan-v1.2.cfg CONFIG ../../config/generic-mks-rumba32-v1.0.cfg CONFIG ../../config/printer-ratrig-v-minion-2021.cfg +CONFIG ../../config/printer-tronxy-crux1-2022.cfg # Printers using the stm32h723 DICTIONARY stm32h723.dict From 9318901f19de1cbee1cbf142f2f4bcccd86d053b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 30 Jun 2024 15:39:46 -0400 Subject: [PATCH 079/111] mkdocs: Update id The Google UA ids are deprecated - update to assigned GA4 id. Signed-off-by: Kevin O'Connor --- docs/_klipper3d/mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_klipper3d/mkdocs.yml b/docs/_klipper3d/mkdocs.yml index c5da747b5..4bfdd1694 100644 --- a/docs/_klipper3d/mkdocs.yml +++ b/docs/_klipper3d/mkdocs.yml @@ -71,7 +71,7 @@ extra: # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-analytics/#site-search-tracking analytics: provider: google - property: UA-138371409-1 + property: G-VEN1PGNQL4 # Language Selection alternate: - name: English From 34732f857afa3df399120ce7ae8362469cd219f5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 4 Jul 2024 18:36:39 -0400 Subject: [PATCH 080/111] smart_effector: Define get_position_endstop() wrapper Reported by @noisyfox. Signed-off-by: Kevin O'Connor --- klippy/extras/smart_effector.py | 1 + 1 file changed, 1 insertion(+) diff --git a/klippy/extras/smart_effector.py b/klippy/extras/smart_effector.py index 726531421..6e5867893 100644 --- a/klippy/extras/smart_effector.py +++ b/klippy/extras/smart_effector.py @@ -64,6 +64,7 @@ class SmartEffectorProbe: self.query_endstop = self.probe_wrapper.query_endstop self.multi_probe_begin = self.probe_wrapper.multi_probe_begin self.multi_probe_end = self.probe_wrapper.multi_probe_end + self.get_position_endstop = self.probe_wrapper.get_position_endstop # Common probe implementation helpers self.cmd_helper = probe.ProbeCommandHelper( config, self, self.probe_wrapper.query_endstop) From 00cb683def53be4b437bfb3e3a637d2d5879946c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 4 Jul 2024 17:20:36 -0400 Subject: [PATCH 081/111] serialhdl: Catch IOError in connect_canbus() It seems the can library on Python2 can sometimes raise an IOError exception on a failure. Signed-off-by: Kevin O'Connor --- klippy/serialhdl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/serialhdl.py b/klippy/serialhdl.py index 6aee56481..30db61707 100644 --- a/klippy/serialhdl.py +++ b/klippy/serialhdl.py @@ -136,7 +136,7 @@ class SerialReader: can_filters=filters, bustype='socketcan') bus.send(set_id_msg) - except (can.CanError, os.error) as e: + except (can.CanError, os.error, IOError) as e: logging.warning("%sUnable to open CAN port: %s", self.warn_prefix, e) self.reactor.pause(self.reactor.monotonic() + 5.) From 248d3dbf8bcda69a51e3a303862bbcc64a32119d Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 7 Jul 2024 00:28:45 +0200 Subject: [PATCH 082/111] sht3x: use periodic report mode (#6634) Signed-off-by: Timofey Titovets --- klippy/extras/bus.py | 3 +++ klippy/extras/sht3x.py | 35 ++++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 9b2ec371f..28bfcdf0d 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -192,6 +192,9 @@ class MCU_I2C: return self.i2c_write_cmd.send([self.oid, data], minclock=minclock, reqclock=reqclock) + def i2c_write_wait_ack(self, data, minclock=0, reqclock=0): + self.i2c_write_cmd.send_wait_ack([self.oid, data], + minclock=minclock, reqclock=reqclock) def i2c_read(self, write, read_len): return self.i2c_read_cmd.send([self.oid, write, read_len]) def i2c_modify_bits(self, reg, clear_bits, set_bits, diff --git a/klippy/extras/sht3x.py b/klippy/extras/sht3x.py index 699d3f209..5a8785e8d 100644 --- a/klippy/extras/sht3x.py +++ b/klippy/extras/sht3x.py @@ -27,6 +27,13 @@ SHT3X_CMD = { 'LOW_REP': [0x24, 0x16], }, }, + 'PERIODIC': { + '2HZ': { + 'HIGH_REP': [0x22, 0x36], + 'MED_REP': [0x22, 0x20], + 'LOW_REP': [0x22, 0x2B], + }, + }, 'OTHER': { 'STATUS': { 'READ': [0xF3, 0x2D], @@ -72,10 +79,12 @@ class SHT3X: def _init_sht3x(self): # Device Soft Reset - self.i2c.i2c_write(SHT3X_CMD['OTHER']['SOFTRESET']) - - # Wait 2ms after reset - self.reactor.pause(self.reactor.monotonic() + .02) + self.i2c.i2c_write_wait_ack(SHT3X_CMD['OTHER']['BREAK']) + # Break takes ~ 1ms + self.reactor.pause(self.reactor.monotonic() + .0015) + self.i2c.i2c_write_wait_ack(SHT3X_CMD['OTHER']['SOFTRESET']) + # Wait <=1.5ms after reset + self.reactor.pause(self.reactor.monotonic() + .0015) status = self.i2c.i2c_read(SHT3X_CMD['OTHER']['STATUS']['READ'], 3) response = bytearray(status['response']) @@ -86,17 +95,17 @@ class SHT3X: if self._crc8(status) != checksum: logging.warning("sht3x: Reading status - checksum error!") + # Enable periodic mode + self.i2c.i2c_write_wait_ack( + SHT3X_CMD['PERIODIC']['2HZ']['HIGH_REP'] + ) + # Wait <=15.5ms for first measurment + self.reactor.pause(self.reactor.monotonic() + .0155) + def _sample_sht3x(self, eventtime): try: - # Read Temeprature - params = self.i2c.i2c_write( - SHT3X_CMD['MEASURE']['STRETCH_ENABLED']['HIGH_REP'] - ) - # Wait - self.reactor.pause(self.reactor.monotonic() - + .20) - - params = self.i2c.i2c_read([], 6) + # Read measurment + params = self.i2c.i2c_read(SHT3X_CMD['OTHER']['FETCH'], 6) response = bytearray(params['response']) rtemp = response[0] << 8 From c84d78f3f169bc5163d11b74837f9880b0b7dba4 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Thu, 11 Jul 2024 20:43:21 +0200 Subject: [PATCH 083/111] extruder: Allow dynamic adjustment of pressure advance (#6635) Signed-off-by: Dmitry Butyugin --- klippy/chelper/__init__.py | 3 +- klippy/chelper/kin_extruder.c | 83 +++++++++++++++++++++++++------ klippy/kinematics/extruder.py | 11 ++-- test/klippy/commands.test | 2 +- test/klippy/pressure_advance.cfg | 68 +++++++++++++++++++++++++ test/klippy/pressure_advance.test | 38 ++++++++++++++ 6 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 test/klippy/pressure_advance.cfg create mode 100644 test/klippy/pressure_advance.test diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index e4199561d..fa1261be9 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -142,8 +142,9 @@ defs_kin_winch = """ defs_kin_extruder = """ struct stepper_kinematics *extruder_stepper_alloc(void); + void extruder_stepper_free(struct stepper_kinematics *sk); void extruder_set_pressure_advance(struct stepper_kinematics *sk - , double pressure_advance, double smooth_time); + , double print_time, double pressure_advance, double smooth_time); """ defs_kin_shaper = """ diff --git a/klippy/chelper/kin_extruder.c b/klippy/chelper/kin_extruder.c index b8d1cc221..0cd6523c3 100644 --- a/klippy/chelper/kin_extruder.c +++ b/klippy/chelper/kin_extruder.c @@ -9,9 +9,15 @@ #include // memset #include "compiler.h" // __visible #include "itersolve.h" // struct stepper_kinematics +#include "list.h" // list_node #include "pyhelper.h" // errorf #include "trapq.h" // move_get_distance +struct pa_params { + double pressure_advance, active_print_time; + struct list_node node; +}; + // Without pressure advance, the extruder stepper position is: // extruder_position(t) = nominal_position(t) // When pressure advance is enabled, additional filament is pushed @@ -52,17 +58,25 @@ extruder_integrate_time(double base, double start_v, double half_accel // Calculate the definitive integral of extruder for a given move static double -pa_move_integrate(struct move *m, double pressure_advance +pa_move_integrate(struct move *m, struct list_head *pa_list , double base, double start, double end, double time_offset) { if (start < 0.) start = 0.; if (end > m->move_t) end = m->move_t; - // Calculate base position and velocity with pressure advance + // Determine pressure_advance value int can_pressure_advance = m->axes_r.y != 0.; - if (!can_pressure_advance) - pressure_advance = 0.; + double pressure_advance = 0.; + if (can_pressure_advance) { + struct pa_params *pa = list_last_entry(pa_list, struct pa_params, node); + while (unlikely(pa->active_print_time > m->print_time) && + !list_is_first(&pa->node, pa_list)) { + pa = list_prev_entry(pa, node); + } + pressure_advance = pa->pressure_advance; + } + // Calculate base position and velocity with pressure advance base += pressure_advance * m->start_v; double start_v = m->start_v + pressure_advance * 2. * m->half_accel; // Calculate definitive integral @@ -75,20 +89,20 @@ pa_move_integrate(struct move *m, double pressure_advance // Calculate the definitive integral of the extruder over a range of moves static double pa_range_integrate(struct move *m, double move_time - , double pressure_advance, double hst) + , struct list_head *pa_list, double hst) { // Calculate integral for the current move double res = 0., start = move_time - hst, end = move_time + hst; double start_base = m->start_pos.x; - res += pa_move_integrate(m, pressure_advance, 0., start, move_time, start); - res -= pa_move_integrate(m, pressure_advance, 0., move_time, end, end); + res += pa_move_integrate(m, pa_list, 0., start, move_time, start); + res -= pa_move_integrate(m, pa_list, 0., move_time, end, end); // Integrate over previous moves struct move *prev = m; while (unlikely(start < 0.)) { prev = list_prev_entry(prev, node); start += prev->move_t; double base = prev->start_pos.x - start_base; - res += pa_move_integrate(prev, pressure_advance, base, start + res += pa_move_integrate(prev, pa_list, base, start , prev->move_t, start); } // Integrate over future moves @@ -96,14 +110,15 @@ pa_range_integrate(struct move *m, double move_time end -= m->move_t; m = list_next_entry(m, node); double base = m->start_pos.x - start_base; - res -= pa_move_integrate(m, pressure_advance, base, 0., end, end); + res -= pa_move_integrate(m, pa_list, base, 0., end, end); } return res; } struct extruder_stepper { struct stepper_kinematics sk; - double pressure_advance, half_smooth_time, inv_half_smooth_time2; + struct list_head pa_list; + double half_smooth_time, inv_half_smooth_time2; }; static double @@ -116,22 +131,45 @@ extruder_calc_position(struct stepper_kinematics *sk, struct move *m // Pressure advance not enabled return m->start_pos.x + move_get_distance(m, move_time); // Apply pressure advance and average over smooth_time - double area = pa_range_integrate(m, move_time, es->pressure_advance, hst); + double area = pa_range_integrate(m, move_time, &es->pa_list, hst); return m->start_pos.x + area * es->inv_half_smooth_time2; } void __visible -extruder_set_pressure_advance(struct stepper_kinematics *sk +extruder_set_pressure_advance(struct stepper_kinematics *sk, double print_time , double pressure_advance, double smooth_time) { struct extruder_stepper *es = container_of(sk, struct extruder_stepper, sk); - double hst = smooth_time * .5; + double hst = smooth_time * .5, old_hst = es->half_smooth_time; es->half_smooth_time = hst; es->sk.gen_steps_pre_active = es->sk.gen_steps_post_active = hst; + + // Cleanup old pressure advance parameters + double cleanup_time = sk->last_flush_time - (old_hst > hst ? old_hst : hst); + struct pa_params *first_pa = list_first_entry( + &es->pa_list, struct pa_params, node); + while (!list_is_last(&first_pa->node, &es->pa_list)) { + struct pa_params *next_pa = list_next_entry(first_pa, node); + if (next_pa->active_print_time >= cleanup_time) break; + list_del(&first_pa->node); + first_pa = next_pa; + } + if (! hst) return; es->inv_half_smooth_time2 = 1. / (hst * hst); - es->pressure_advance = pressure_advance; + + if (list_last_entry(&es->pa_list, struct pa_params, node)->pressure_advance + == pressure_advance) { + // Retain old pa_params + return; + } + // Add new pressure advance parameters + struct pa_params *pa = malloc(sizeof(*pa)); + memset(pa, 0, sizeof(*pa)); + pa->pressure_advance = pressure_advance; + pa->active_print_time = print_time; + list_add_tail(&pa->node, &es->pa_list); } struct stepper_kinematics * __visible @@ -141,5 +179,22 @@ extruder_stepper_alloc(void) memset(es, 0, sizeof(*es)); es->sk.calc_position_cb = extruder_calc_position; es->sk.active_flags = AF_X; + list_init(&es->pa_list); + struct pa_params *pa = malloc(sizeof(*pa)); + memset(pa, 0, sizeof(*pa)); + list_add_tail(&pa->node, &es->pa_list); return &es->sk; } + +void __visible +extruder_stepper_free(struct stepper_kinematics *sk) +{ + struct extruder_stepper *es = container_of(sk, struct extruder_stepper, sk); + while (!list_empty(&es->pa_list)) { + struct pa_params *pa = list_first_entry( + &es->pa_list, struct pa_params, node); + list_del(&pa->node); + free(pa); + } + free(sk); +} diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 692400378..7fb2e7ed5 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -18,7 +18,7 @@ class ExtruderStepper: self.stepper = stepper.PrinterStepper(config) ffi_main, ffi_lib = chelper.get_ffi() self.sk_extruder = ffi_main.gc(ffi_lib.extruder_stepper_alloc(), - ffi_lib.free) + ffi_lib.extruder_stepper_free) self.stepper.set_stepper_kinematics(self.sk_extruder) self.motion_queue = None # Register commands @@ -71,11 +71,14 @@ class ExtruderStepper: if not pressure_advance: new_smooth_time = 0. toolhead = self.printer.lookup_object("toolhead") - toolhead.note_step_generation_scan_time(new_smooth_time * .5, - old_delay=old_smooth_time * .5) + if new_smooth_time != old_smooth_time: + toolhead.note_step_generation_scan_time( + new_smooth_time * .5, old_delay=old_smooth_time * .5) ffi_main, ffi_lib = chelper.get_ffi() espa = ffi_lib.extruder_set_pressure_advance - espa(self.sk_extruder, pressure_advance, new_smooth_time) + toolhead.register_lookahead_callback( + lambda print_time: espa(self.sk_extruder, print_time, + pressure_advance, new_smooth_time)) self.pressure_advance = pressure_advance self.pressure_advance_smooth_time = smooth_time cmd_SET_PRESSURE_ADVANCE_help = "Set pressure advance parameters" diff --git a/test/klippy/commands.test b/test/klippy/commands.test index 50e71ab3c..33c599614 100644 --- a/test/klippy/commands.test +++ b/test/klippy/commands.test @@ -40,7 +40,7 @@ SET_VELOCITY_LIMIT ACCEL=100 VELOCITY=20 SQUARE_CORNER_VELOCITY=1 ACCEL_TO_DECEL M204 S500 SET_PRESSURE_ADVANCE EXTRUDER=extruder ADVANCE=.001 -SET_PRESSURE_ADVANCE ADVANCE=.002 ADVANCE_LOOKAHEAD_TIME=.001 +SET_PRESSURE_ADVANCE ADVANCE=.002 SMOOTH_TIME=.001 # Restart command (must be last in test) RESTART diff --git a/test/klippy/pressure_advance.cfg b/test/klippy/pressure_advance.cfg new file mode 100644 index 000000000..d7123d08e --- /dev/null +++ b/test/klippy/pressure_advance.cfg @@ -0,0 +1,68 @@ +# Config for extruder testing +[stepper_x] +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PE5 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PJ1 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: ^PD3 +position_endstop: 0.5 +position_max: 200 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.500 +filament_diameter: 3.500 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 210 + +[extruder_stepper my_extra_stepper] +extruder: extruder +step_pin: PH5 +dir_pin: PH6 +enable_pin: !PB5 +microsteps: 16 +rotation_distance: 28.2 + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 diff --git a/test/klippy/pressure_advance.test b/test/klippy/pressure_advance.test new file mode 100644 index 000000000..c1ef052ad --- /dev/null +++ b/test/klippy/pressure_advance.test @@ -0,0 +1,38 @@ +# Extruder tests +DICTIONARY atmega2560.dict +CONFIG pressure_advance.cfg + +SET_PRESSURE_ADVANCE ADVANCE=0.1 +# Home and extrusion moves +G28 +G1 X20 Y20 Z1 F6000 +G1 E7 +G1 X25 Y25 E7.5 + +# Update pressure advance for my_extra_stepper +SET_PRESSURE_ADVANCE EXTRUDER=my_extra_stepper ADVANCE=0.02 +G1 X30 Y30 E8.0 + +# Unsync my_extra_stepper from extruder +SYNC_EXTRUDER_MOTION EXTRUDER=my_extra_stepper MOTION_QUEUE= + +# Update pressure advance for primary extruder +SET_PRESSURE_ADVANCE ADVANCE=0.01 +G1 X35 Y35 E8.5 + +# Update pressure advance both extruders +SET_PRESSURE_ADVANCE EXTRUDER=my_extra_stepper ADVANCE=0.05 +SET_PRESSURE_ADVANCE ADVANCE=0.05 +# Sync my_extra_stepper to extruder +SYNC_EXTRUDER_MOTION EXTRUDER=my_extra_stepper MOTION_QUEUE=extruder +G1 X40 Y40 E9.0 + +# Update smooth_time +SET_PRESSURE_ADVANCE SMOOTH_TIME=0.02 +SET_PRESSURE_ADVANCE EXTRUDER=my_extra_stepper SMOOTH_TIME=0.02 +G1 X45 Y45 E9.5 + +# Updating both smooth_time and pressure advance +SET_PRESSURE_ADVANCE SMOOTH_TIME=0.03 ADVANCE=0.1 +SET_PRESSURE_ADVANCE EXTRUDER=my_extra_stepper SMOOTH_TIME=0.03 ADVANCE=0.1 +G1 X50 Y50 E10.0 From 0087f04cc370cd542069940f0ed10c583de158ee Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 11 Jul 2024 15:01:32 -0400 Subject: [PATCH 084/111] gcode: Minor change to suppress python warning on '\s' Reported by @matdibu. Signed-off-by: Kevin O'Connor --- klippy/gcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/gcode.py b/klippy/gcode.py index 7d980585f..15ab624aa 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -406,7 +406,7 @@ class GCodeIO: self._dump_debug() if self.is_fileinput: self.printer.request_exit('error_exit') - m112_r = re.compile('^(?:[nN][0-9]+)?\s*[mM]112(?:\s|$)') + m112_r = re.compile(r'^(?:[nN][0-9]+)?\s*[mM]112(?:\s|$)') def _process_data(self, eventtime): # Read input, separate by newline, and add to pending_commands try: From 7603953ef746d36a8144d9043d2b67c04ce432cc Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Tue, 5 Mar 2024 05:42:18 -0500 Subject: [PATCH 085/111] temperature_probe: probe temperature sensor Add temperature sensor with thermal drift calibration. Currently only Eddy Current based probes support calibration. Signed-off-by: Eric Callahan --- klippy/extras/temperature_probe.py | 716 +++++++++++++++++++++++++++++ 1 file changed, 716 insertions(+) create mode 100644 klippy/extras/temperature_probe.py diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py new file mode 100644 index 000000000..ae285ce36 --- /dev/null +++ b/klippy/extras/temperature_probe.py @@ -0,0 +1,716 @@ +# Probe temperature sensor and drift calibration +# +# Copyright (C) 2024 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from . import manual_probe + +KELVIN_TO_CELSIUS = -273.15 + +###################################################################### +# Polynomial Helper Classes and Functions +###################################################################### + +def calc_determinant(matrix): + m = matrix + aei = m[0][0] * m[1][1] * m[2][2] + bfg = m[1][0] * m[2][1] * m[0][2] + cdh = m[2][0] * m[0][1] * m[1][2] + ceg = m[2][0] * m[1][1] * m[0][2] + bdi = m[1][0] * m[0][1] * m[2][2] + afh = m[0][0] * m[2][1] * m[1][2] + return aei + bfg + cdh - ceg - bdi - afh + +class Polynomial2d: + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c + + def __call__(self, xval): + return self.c * xval * xval + self.b * xval + self.a + + def get_coefs(self): + return (self.a, self.b, self.c) + + def __str__(self): + return "%f, %f, %f" % (self.a, self.b, self.c) + + def __repr__(self): + parts = ["y(x) ="] + deg = 2 + for i, coef in enumerate((self.c, self.b, self.a)): + if round(coef, 8) == int(coef): + coef = int(coef) + if abs(coef) < 1e-10: + continue + cur_deg = deg - i + x_str = "x^%d" % (cur_deg,) if cur_deg > 1 else "x" * cur_deg + if len(parts) == 1: + parts.append("%f%s" % (coef, x_str)) + else: + sym = "-" if coef < 0 else "+" + parts.append("%s %f%s" % (sym, abs(coef), x_str)) + return " ".join(parts) + + @classmethod + def fit(cls, coords): + xlist = [c[0] for c in coords] + ylist = [c[1] for c in coords] + count = len(coords) + sum_x = sum(xlist) + sum_y = sum(ylist) + sum_x2 = sum([x**2 for x in xlist]) + sum_x3 = sum([x**3 for x in xlist]) + sum_x4 = sum([x**4 for x in xlist]) + sum_xy = sum([x * y for x, y in coords]) + sum_x2y = sum([y*x**2 for x, y in coords]) + vector_b = [sum_y, sum_xy, sum_x2y] + m = [ + [count, sum_x, sum_x2], + [sum_x, sum_x2, sum_x3], + [sum_x2, sum_x3, sum_x4] + ] + m0 = [vector_b, m[1], m[2]] + m1 = [m[0], vector_b, m[2]] + m2 = [m[0], m[1], vector_b] + det_m = calc_determinant(m) + a0 = calc_determinant(m0) / det_m + a1 = calc_determinant(m1) / det_m + a2 = calc_determinant(m2) / det_m + return cls(a0, a1, a2) + +class TemperatureProbe: + def __init__(self, config): + self.name = config.get_name() + self.printer = config.get_printer() + self.gcode = self.printer.lookup_object("gcode") + self.speed = config.getfloat("speed", None, above=0.) + self.horizontal_move_z = config.getfloat( + "horizontal_move_z", 2., above=0. + ) + self.resting_z = config.getfloat("resting_z", .4, above=0.) + self.cal_pos = config.getfloatlist( + "calibration_position", None, count=3 + ) + self.cal_bed_temp = config.getfloat( + "calibration_bed_temp", None, above=50. + ) + self.cal_extruder_temp = config.getfloat( + "calibration_extruder_temp", None, above=50. + ) + self.cal_extruder_z = config.getfloat( + "extruder_heating_z", 50., above=0. + ) + # Setup temperature sensor + smooth_time = config.getfloat("smooth_time", 2., above=0.) + self.inv_smooth_time = 1. / smooth_time + self.min_temp = config.getfloat( + "min_temp", KELVIN_TO_CELSIUS, minval=KELVIN_TO_CELSIUS + ) + self.max_temp = config.getfloat( + "max_temp", 99999999.9, above=self.min_temp + ) + pheaters = self.printer.load_object(config, "heaters") + self.sensor = pheaters.setup_sensor(config) + self.sensor.setup_minmax(self.min_temp, self.max_temp) + self.sensor.setup_callback(self._temp_callback) + pheaters.register_sensor(config, self) + self.last_temp_read_time = 0. + self.last_measurement = (0., 99999999., 0.,) + # Calibration State + self.cal_helper = None + self.next_auto_temp = 99999999. + self.target_temp = 0 + self.expected_count = 0 + self.sample_count = 0 + self.in_calibration = False + self.step = 2. + self.last_zero_pos = None + self.total_expansion = 0 + self.start_pos = [] + + # Register GCode Commands + pname = self.name.split(maxsplit=1)[-1] + self.gcode.register_mux_command( + "TEMPERATURE_PROBE_CALIBRATE", "PROBE", pname, + self.cmd_TEMPERATURE_PROBE_CALIBRATE, + desc=self.cmd_TEMPERATURE_PROBE_CALIBRATE_help + ) + + self.gcode.register_mux_command( + "TEMPERATURE_PROBE_ENABLE", "PROBE", pname, + self.cmd_TEMPERATURE_PROBE_ENABLE, + desc=self.cmd_TEMPERATURE_PROBE_ENABLE_help + ) + + # Register Drift Compensation Helper with probe + full_probe_name = "probe_eddy_current %s" % (pname,) + if config.has_section(full_probe_name): + pprobe = self.printer.load_object(config, full_probe_name) + self.cal_helper = EddyDriftCompensation(config, self) + pprobe.register_drift_compensation(self.cal_helper) + logging.info( + "%s: registered drift compensation with probe [%s]" + % (self.name, full_probe_name) + ) + else: + logging.info( + "%s: No probe named %s configured, thermal drift compensation " + "disabled." % (self.name, pname) + ) + + def _temp_callback(self, read_time, temp): + smoothed_temp, measured_min, measured_max = self.last_measurement + time_diff = read_time - self.last_temp_read_time + self.last_temp_read_time = read_time + temp_diff = temp - smoothed_temp + adj_time = min(time_diff * self.inv_smooth_time, 1.) + smoothed_temp += temp_diff * adj_time + measured_min = min(measured_min, smoothed_temp) + measured_max = max(measured_max, smoothed_temp) + self.last_measurement = (smoothed_temp, measured_min, measured_max) + if self.in_calibration and smoothed_temp >= self.next_auto_temp: + self.printer.get_reactor().register_async_callback( + self._check_kick_next + ) + + def _check_kick_next(self, eventtime): + smoothed_temp = self.last_measurement[0] + if self.in_calibration and smoothed_temp >= self.next_auto_temp: + self.next_auto_temp = 99999999. + self.gcode.run_script("TEMPERATURE_PROBE_NEXT") + + def get_temp(self, eventtime=None): + return self.last_measurement[0], self.target_temp + + def _collect_sample(self, kin_pos, tool_zero_z): + probe = self._get_probe() + x_offset, y_offset, _ = probe.get_offsets() + speeds = self._get_speeds() + lift_speed, _, move_speed = speeds + toolhead = self.printer.lookup_object("toolhead") + cur_pos = toolhead.get_position() + # Move to probe to sample collection position + cur_pos[2] += self.horizontal_move_z + toolhead.manual_move(cur_pos, lift_speed) + cur_pos[0] -= x_offset + cur_pos[1] -= y_offset + toolhead.manual_move(cur_pos, move_speed) + return self.cal_helper.collect_sample(kin_pos, tool_zero_z, speeds) + + def _prepare_next_sample(self, last_temp, tool_zero_z): + # Register our own abort command now that the manual + # probe has finished and unregistered + self.gcode.register_command( + "ABORT", self.cmd_TEMPERATURE_PROBE_ABORT, + desc=self.cmd_TEMPERATURE_PROBE_ABORT_help + ) + probe_speed = self._get_speeds()[1] + # Move tool down to the resting position + toolhead = self.printer.lookup_object("toolhead") + cur_pos = toolhead.get_position() + cur_pos[2] = tool_zero_z + self.resting_z + toolhead.manual_move(cur_pos, probe_speed) + cnt, exp_cnt = self.sample_count, self.expected_count + self.next_auto_temp = last_temp + self.step + self.gcode.respond_info( + "%s: collected sample %d/%d at temp %.2fC, next sample scheduled " + "at temp %.2fC" + % (self.name, cnt, exp_cnt, last_temp, self.next_auto_temp) + ) + + def _manual_probe_finalize(self, kin_pos): + if kin_pos is None: + # Calibration aborted + self._finalize_drift_cal(False) + return + if self.last_zero_pos is not None: + z_diff = self.last_zero_pos[2] - kin_pos[2] + self.total_expansion += z_diff + logging.info( + "Estimated Total Thermal Expansion: %.6f" + % (self.total_expansion,) + ) + self.last_zero_pos = kin_pos + toolhead = self.printer.lookup_object("toolhead") + tool_zero_z = toolhead.get_position()[2] + try: + last_temp = self._collect_sample(kin_pos, tool_zero_z) + except Exception: + self._finalize_drift_cal(False) + raise + self.sample_count += 1 + if last_temp >= self.target_temp: + # Calibration Done + self._finalize_drift_cal(True) + else: + try: + self._prepare_next_sample(last_temp, tool_zero_z) + if self.sample_count == 1: + self._set_bed_temp(self.cal_bed_temp) + except Exception: + self._finalize_drift_cal(False) + raise + + def _finalize_drift_cal(self, success, msg=None): + self.next_auto_temp = 99999999. + self.target_temp = 0 + self.expected_count = 0 + self.sample_count = 0 + self.step = 2. + self.in_calibration = False + self.last_zero_pos = None + self.total_expansion = 0 + self.start_pos = [] + # Unregister Temporary Commands + self.gcode.register_command("ABORT", None) + self.gcode.register_command("TEMPERATURE_PROBE_NEXT", None) + self.gcode.register_command("TEMPERATURE_PROBE_COMPLETE", None) + # Turn off heaters + self._set_extruder_temp(0) + self._set_bed_temp(0) + try: + self.cal_helper.finish_calibration(success) + except self.gcode.error as e: + success = False + msg = str(e) + if not success: + msg = msg or "%s: calibration aborted" % (self.name,) + self.gcode.respond_info(msg) + + def _get_probe(self): + probe = self.printer.lookup_object("probe") + if probe is None: + raise self.gcode.error("No probe configured") + return probe + + def _set_extruder_temp(self, temp, wait=False): + if self.cal_extruder_temp is None: + # Extruder temperature not configured + return + toolhead = self.printer.lookup_object("toolhead") + extr_name = toolhead.get_extruder().get_name() + self.gcode.run_script_from_command( + "SET_HEATER_TEMPERATURE HEATER=%s TARGET=%f" + % (extr_name, temp) + ) + if wait: + self.gcode.run_script_from_command( + "TEMPERATURE_WAIT SENSOR=%s MINIMUM=%f" + % (extr_name, temp) + ) + def _set_bed_temp(self, temp): + if self.cal_bed_temp is None: + # Bed temperature not configured + return + self.gcode.run_script_from_command( + "SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=%f" + % (temp,) + ) + + def _check_homed(self): + toolhead = self.printer.lookup_object("toolhead") + reactor = self.printer.get_reactor() + status = toolhead.get_status(reactor.monotonic()) + h_axes = status["homed_axes"] + for axis in "xyz": + if axis not in h_axes: + raise self.gcode.error( + "Printer must be homed before calibration" + ) + + def _move_to_start(self): + toolhead = self.printer.lookup_object("toolhead") + cur_pos = toolhead.get_position() + move_speed = self._get_speeds()[2] + if self.cal_pos is not None: + if self.cal_extruder_temp is not None: + # Move to extruder heating z position + cur_pos[2] = self.cal_extruder_z + toolhead.manual_move(cur_pos, move_speed) + toolhead.manual_move(self.cal_pos[:2], move_speed) + self._set_extruder_temp(self.cal_extruder_temp, True) + toolhead.manual_move(self.cal_pos, move_speed) + elif self.cal_extruder_temp is not None: + cur_pos[2] = self.cal_extruder_z + toolhead.manual_move(cur_pos, move_speed) + self._set_extruder_temp(self.cal_extruder_temp, True) + + def _get_speeds(self): + pparams = self._get_probe().get_probe_params() + probe_speed = pparams["probe_speed"] + lift_speed = pparams["lift_speed"] + move_speed = self.speed or max(probe_speed, lift_speed) + return lift_speed, probe_speed, move_speed + + cmd_TEMPERATURE_PROBE_CALIBRATE_help = ( + "Calibrate probe temperature drift compensation" + ) + def cmd_TEMPERATURE_PROBE_CALIBRATE(self, gcmd): + if self.cal_helper is None: + raise gcmd.error( + "No calibration helper registered for [%s]" + % (self.name,) + ) + self._check_homed() + probe = self._get_probe() + probe_name = probe.get_status(None)["name"] + short_name = probe_name.split(maxsplit=1)[-1] + if short_name != self.name.split(maxsplit=1)[-1]: + raise self.gcode.error( + "[%s] not linked to registered probe [%s]." + % (self.name, probe_name) + ) + manual_probe.verify_no_manual_probe(self.printer) + if self.in_calibration: + raise gcmd.error( + "Already in probe drift calibration. Use " + "TEMPERATURE_PROBE_COMPLETE or ABORT to exit." + ) + cur_temp = self.last_measurement[0] + target_temp = gcmd.get_float("TARGET", above=cur_temp) + step = gcmd.get_float("STEP", 2., minval=1.0) + expected_count = int( + (target_temp - cur_temp) / step + .5 + ) + if expected_count < 3: + raise gcmd.error( + "Invalid STEP and/or TARGET parameters resulted " + "in too few expected samples: %d" + % (expected_count,) + ) + try: + self.gcode.register_command( + "TEMPERATURE_PROBE_NEXT", self.cmd_TEMPERATURE_PROBE_NEXT, + desc=self.cmd_TEMPERATURE_PROBE_NEXT_help + ) + self.gcode.register_command( + "TEMPERATURE_PROBE_COMPLETE", + self.cmd_TEMPERATURE_PROBE_COMPLETE, + desc=self.cmd_TEMPERATURE_PROBE_NEXT_help + ) + except self.printer.config_error: + raise gcmd.error( + "Auxiliary Probe Drift Commands already registered. Use " + "TEMPERATURE_PROBE_COMPLETE or ABORT to exit." + ) + self.in_calibration = True + self.cal_helper.start_calibration() + self.target_temp = target_temp + self.step = step + self.sample_count = 0 + self.expected_count = expected_count + # If configured move to heating position and turn on extruder + try: + self._move_to_start() + except self.printer.command_error: + self._finalize_drift_cal(False, "Error during initial move") + raise + # Caputure start position and begin initial probe + toolhead = self.printer.lookup_object("toolhead") + self.start_pos = toolhead.get_position()[:2] + manual_probe.ManualProbeHelper( + self.printer, gcmd, self._manual_probe_finalize + ) + + cmd_TEMPERATURE_PROBE_NEXT_help = "Sample next probe drift temperature" + def cmd_TEMPERATURE_PROBE_NEXT(self, gcmd): + manual_probe.verify_no_manual_probe(self.printer) + self.next_auto_temp = 99999999. + toolhead = self.printer.lookup_object("toolhead") + # Lift and Move to nozzle back to start position + curpos = toolhead.get_position() + start_z = curpos[2] + lift_speed, probe_speed, move_speed = self._get_speeds() + # Move nozzle to the manual probing position + curpos[2] += self.horizontal_move_z + toolhead.manual_move(curpos, lift_speed) + curpos[0] = self.start_pos[0] + curpos[1] = self.start_pos[1] + toolhead.manual_move(curpos, move_speed) + curpos[2] = start_z + toolhead.manual_move(curpos, probe_speed) + self.gcode.register_command("ABORT", None) + manual_probe.ManualProbeHelper( + self.printer, gcmd, self._manual_probe_finalize + ) + + cmd_TEMPERATURE_PROBE_COMPLETE_help = "Finish Probe Drift Calibration" + def cmd_TEMPERATURE_PROBE_COMPLETE(self, gcmd): + manual_probe.verify_no_manual_probe(self.printer) + self._finalize_drift_cal(self.sample_count >= 3) + + cmd_TEMPERATURE_PROBE_ABORT_help = "Abort Probe Drift Calibration" + def cmd_TEMPERATURE_PROBE_ABORT(self, gcmd): + self._finalize_drift_cal(False) + + cmd_TEMPERATURE_PROBE_ENABLE_help = ( + "Set adjustment factor applied to drift correction" + ) + def cmd_TEMPERATURE_PROBE_ENABLE(self, gcmd): + if self.cal_helper is not None: + self.cal_helper.set_enabled(gcmd) + + def is_in_calibration(self): + return self.in_calibration + + def get_status(self, eventtime=None): + smoothed_temp, measured_min, measured_max = self.last_measurement + dcomp_enabled = False + if self.cal_helper is not None: + dcomp_enabled = self.cal_helper.is_enabled() + return { + "temperature": smoothed_temp, + "measured_min_temp": round(measured_min, 2), + "measured_max_temp": round(measured_max, 2), + "in_calibration": self.in_calibration, + "estimated_expansion": self.total_expansion, + "compensation_enabled": dcomp_enabled + } + + def stats(self, eventtime): + return False, '%s: temp=%.1f' % (self.name, self.last_measurement[0]) + + +##################################################################### +# +# Eddy Current Probe Drift Compensation Helper +# +##################################################################### + +DRIFT_SAMPLE_COUNT = 9 + +class EddyDriftCompensation: + def __init__(self, config, sensor): + self.printer = config.get_printer() + self.temp_sensor = sensor + self.name = config.get_name() + self.cal_temp = config.getfloat("calibration_temp", 0.) + self.drift_calibration = None + self.calibration_samples = None + self.dc_min_temp = config.getfloat("drift_calibration_min_temp", 0.) + dc = config.getlists( + "drift_calibration", None, seps=(',', '\n'), parser=float + ) + self.min_freq = 999999999999. + if dc is not None: + for coefs in dc: + if len(coefs) != 3: + raise config.error( + "Invalid polynomial in drift calibration" + ) + self.drift_calibration = [Polynomial2d(*coefs) for coefs in dc] + cal = self.drift_calibration + self._check_calibration(cal, self.dc_min_temp, config.error) + low_poly = self.drift_calibration[-1] + self.min_freq = min([low_poly(temp) for temp in range(121)]) + cal_str = "\n".join([repr(p) for p in cal]) + logging.info( + "%s: loaded temperature drift calibration. Min Temp: %.2f," + " Min Freq: %.6f\n%s" + % (self.name, self.dc_min_temp, self.min_freq, cal_str) + ) + else: + logging.info( + "%s: No drift calibration configured, disabling temperature " + "drift compensation" + % (self.name,) + ) + self.enabled = has_dc = self.drift_calibration is not None + if self.cal_temp < 1e-6 and has_dc: + self.enabled = False + logging.info( + "%s: No temperature saved for eddy probe calibration, " + "disabling temperature drift compensation." + % (self.name,) + ) + + def is_enabled(self): + return self.enabled + + def set_enabled(self, gcmd): + enabled = gcmd.get_int("ENABLE") + if enabled: + if self.drift_calibration is None: + raise gcmd.error( + "No drift calibration configured, cannot enable " + "temperature drift compensation" + ) + if self.cal_temp < 1e-6: + raise gcmd.error( + "Z Calibration temperature not configured, cannot enable " + "temperature drift compensation" + ) + self.enabled = enabled + + def note_z_calibration_start(self): + self.cal_temp = self.get_temperature() + + def note_z_calibration_finish(self): + self.cal_temp = (self.cal_temp + self.get_temperature()) / 2.0 + configfile = self.printer.lookup_object('configfile') + configfile.set(self.name, "calibration_temp", "%.6f " % (self.cal_temp)) + gcode = self.printer.lookup_object("gcode") + gcode.respond_info( + "%s: Z Calibration Temperature set to %.2f. " + "The SAVE_CONFIG command will update the printer config " + "file and restart the printer." + % (self.name, self.cal_temp) + ) + + def collect_sample(self, kin_pos, tool_zero_z, speeds): + if self.calibration_samples is None: + self.calibration_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)] + move_times = [] + temps = [0. for _ in range(DRIFT_SAMPLE_COUNT)] + probe_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)] + toolhead = self.printer.lookup_object("toolhead") + cur_pos = toolhead.get_position() + lift_speed, probe_speed, _ = speeds + + def _on_bulk_data_recd(msg): + if move_times: + idx, start_time, end_time = move_times[0] + cur_temp = self.get_temperature() + for sample in msg["data"]: + ptime = sample[0] + while ptime > end_time: + move_times.pop(0) + if not move_times: + return idx >= DRIFT_SAMPLE_COUNT - 1 + idx, start_time, end_time = move_times[0] + if ptime < start_time: + continue + temps[idx] = cur_temp + probe_samples[idx].append(sample) + return True + sect_name = "probe_eddy_current " + self.name.split(maxsplit=1)[-1] + self.printer.lookup_object(sect_name).add_client(_on_bulk_data_recd) + for i in range(DRIFT_SAMPLE_COUNT): + if i == 0: + # Move down to first sample location + cur_pos[2] = tool_zero_z + .05 + else: + # Sample each .5mm in z + cur_pos[2] += 1. + toolhead.manual_move(cur_pos, lift_speed) + cur_pos[2] -= .5 + toolhead.manual_move(cur_pos, probe_speed) + start = toolhead.get_last_move_time() + .05 + end = start + .1 + move_times.append((i, start, end)) + toolhead.dwell(.2) + toolhead.wait_moves() + # Wait for sample collection to finish + reactor = self.printer.get_reactor() + evttime = reactor.monotonic() + while move_times: + evttime = reactor.pause(evttime + .1) + sample_temp = sum(temps) / len(temps) + for i, data in enumerate(probe_samples): + freqs = [d[1] for d in data] + zvals = [d[2] for d in data] + avg_freq = sum(freqs) / len(freqs) + avg_z = sum(zvals) / len(zvals) + kin_z = i * .5 + .05 + kin_pos[2] + logging.info( + "Probe Values at Temp %.2fC, Z %.4fmm: Avg Freq = %.6f, " + "Avg Measured Z = %.6f" + % (sample_temp, kin_z, avg_freq, avg_z) + ) + self.calibration_samples[i].append((sample_temp, avg_freq)) + return sample_temp + + def start_calibration(self): + self.enabled = False + self.calibration_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)] + + def finish_calibration(self, success): + cal_samples = self.calibration_samples + self.calibration_samples = None + if not success: + return + gcode = self.printer.lookup_object("gcode") + if len(cal_samples) < 3: + raise gcode.error( + "calbration error, not enough samples" + ) + min_temp, _ = cal_samples[0][0] + polynomials = [] + for i, coords in enumerate(cal_samples): + height = .05 + i * .5 + poly = Polynomial2d.fit(coords) + polynomials.append(poly) + logging.info("Polynomial at Z=%.2f: %s" % (height, repr(poly))) + self._check_calibration(polynomials, min_temp) + coef_cfg = "\n" + "\n".join([str(p) for p in polynomials]) + configfile = self.printer.lookup_object('configfile') + configfile.set(self.name, "drift_calibration", coef_cfg) + configfile.set(self.name, "drift_calibration_min_temp", min_temp) + gcode.respond_info( + "%s: generated %d 2D polynomials\n" + "The SAVE_CONFIG command will update the printer config " + "file and restart the printer." + % (self.name, len(polynomials)) + ) + + def _check_calibration(self, calibration, start_temp, error=None): + error = error or self.printer.command_error + start = int(start_temp) + for temp in range(start, 121, 1): + last_freq = calibration[0](temp) + for i, poly in enumerate(calibration[1:]): + next_freq = poly(temp) + if next_freq >= last_freq: + # invalid polynomial + raise error( + "%s: invalid calibration detected, curve at index " + "%d overlaps previous curve at temp %dC." + % (self.name, i + 1, temp) + ) + last_freq = next_freq + + def adjust_freq(self, freq, origin_temp=None): + # Adjusts frequency from current temperature toward + # destination temperature + if not self.enabled or freq < self.min_freq: + return freq + if origin_temp is None: + origin_temp = self.get_temperature() + return self._calc_freq(freq, origin_temp, self.cal_temp) + + def unadjust_freq(self, freq, dest_temp=None): + # Given a frequency and its orignal sampled temp, find the + # offset frequency based on the current temp + if not self.enabled or freq < self.min_freq: + return freq + if dest_temp is None: + dest_temp = self.get_temperature() + return self._calc_freq(freq, self.cal_temp, dest_temp) + + def _calc_freq(self, freq, origin_temp, dest_temp): + high_freq = low_freq = None + dc = self.drift_calibration + for pos, poly in enumerate(dc): + high_freq = low_freq + low_freq = poly(origin_temp) + if freq >= low_freq: + if high_freq is None: + # Freqency above max calibration value + err = poly(dest_temp) - low_freq + return freq + err + t = min(1., max(0., (freq - low_freq) / (high_freq - low_freq))) + low_tgt_freq = poly(dest_temp) + high_tgt_freq = dc[pos-1](dest_temp) + return (1 - t) * low_tgt_freq + t * high_tgt_freq + # Frequency below minimum, no correction + return freq + + def get_temperature(self): + return self.temp_sensor.get_temp()[0] + + +def load_config_prefix(config): + return TemperatureProbe(config) From bd1dbc8af341bdaafc480d5363471937337c2af1 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Mon, 1 Apr 2024 07:44:31 -0400 Subject: [PATCH 086/111] probe_eddy_current: support thermal compensation Signed-off-by: Eric Callahan --- klippy/extras/probe_eddy_current.py | 38 +++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 345096e60..932d1bfa3 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -14,6 +14,7 @@ class EddyCalibration: def __init__(self, config): self.printer = config.get_printer() self.name = config.get_name() + self.drift_comp = DummyDriftCompensation() # Current calibration data self.cal_freqs = [] self.cal_zpos = [] @@ -37,8 +38,10 @@ class EddyCalibration: self.cal_freqs = [c[0] for c in cal] self.cal_zpos = [c[1] for c in cal] def apply_calibration(self, samples): + cur_temp = self.drift_comp.get_temperature() for i, (samp_time, freq, dummy_z) in enumerate(samples): - pos = bisect.bisect(self.cal_freqs, freq) + adj_freq = self.drift_comp.adjust_freq(freq, cur_temp) + pos = bisect.bisect(self.cal_freqs, adj_freq) if pos >= len(self.cal_zpos): zpos = -OUT_OF_RANGE elif pos == 0: @@ -51,7 +54,7 @@ class EddyCalibration: prev_zpos = self.cal_zpos[pos - 1] gain = (this_zpos - prev_zpos) / (this_freq - prev_freq) offset = prev_zpos - prev_freq * gain - zpos = freq * gain + offset + zpos = adj_freq * gain + offset samples[i] = (samp_time, freq, round(zpos, 6)) def freq_to_height(self, freq): dummy_sample = [(0., freq, 0.)] @@ -71,7 +74,8 @@ class EddyCalibration: prev_zpos = rev_zpos[pos - 1] gain = (this_freq - prev_freq) / (this_zpos - prev_zpos) offset = prev_freq - prev_zpos * gain - return height * gain + offset + freq = height * gain + offset + return self.drift_comp.unadjust_freq(freq) def do_calibration_moves(self, move_speed): toolhead = self.printer.lookup_object('toolhead') kin = toolhead.get_kinematics() @@ -86,6 +90,7 @@ class EddyCalibration: return True self.printer.lookup_object(self.name).add_client(handle_batch) toolhead.dwell(1.) + self.drift_comp.note_z_calibration_start() # Move to each 40um position max_z = 4.0 samp_dist = 0.040 @@ -112,6 +117,7 @@ class EddyCalibration: times.append((start_query_time, end_query_time, kin_pos[2])) toolhead.dwell(1.0) toolhead.wait_moves() + self.drift_comp.note_z_calibration_finish() # Finish data collection is_finished = True # Correlate query responses @@ -188,6 +194,8 @@ class EddyCalibration: # Start manual probe manual_probe.ManualProbeHelper(self.printer, gcmd, self.post_manual_probe) + def register_drift_compensation(self, comp): + self.drift_comp = comp # Tool to gather samples and convert them to probe positions class EddyGatherSamples: @@ -265,16 +273,18 @@ class EddyGatherSamples: freq = self._pull_freq(start_time, end_time) if pos_time is not None: toolhead_pos = self._lookup_toolhead_pos(pos_time) - self._probe_results.append((freq, toolhead_pos)) + sensor_z = None + if freq: + sensor_z = self._calibration.freq_to_height(freq) + self._probe_results.append((sensor_z, toolhead_pos)) self._probe_times.pop(0) def pull_probed(self): self._await_samples() results = [] - for freq, toolhead_pos in self._probe_results: - if not freq: + for sensor_z, toolhead_pos in self._probe_results: + if sensor_z is None: raise self._printer.command_error( "Unable to obtain probe_eddy_current sensor readings") - sensor_z = self._calibration.freq_to_height(freq) if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: raise self._printer.command_error( "probe_eddy_current sensor not in valid range") @@ -435,6 +445,20 @@ class PrinterEddyProbe: return EddyScanningProbe(self.printer, self.sensor_helper, self.calibration, z_offset, gcmd) return self.probe_session.start_probe_session(gcmd) + def register_drift_compensation(self, comp): + self.calibration.register_drift_compensation(comp) + +class DummyDriftCompensation: + def get_temperature(self): + return 0. + def note_z_calibration_start(self): + pass + def note_z_calibration_finish(self): + pass + def adjust_freq(self, freq, temp=None): + return freq + def unadjust_freq(self, freq, temp=None): + return freq def load_config_prefix(config): return PrinterEddyProbe(config) From 6848843224009df3594efbaf3a4c3acc2de107e2 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Tue, 9 Apr 2024 11:26:39 -0400 Subject: [PATCH 087/111] docs: add initial temperature_probe documentation Signed-off-by: Eric Callahan --- docs/Config_Reference.md | 59 +++++++++++++++++++++++++++ docs/Eddy_Probe.md | 88 ++++++++++++++++++++++++++++++++++++++++ docs/G-Codes.md | 36 ++++++++++++++++ 3 files changed, 183 insertions(+) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index b192e7362..9be53dfaa 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2401,6 +2401,65 @@ temperature sensors that are reported via the M105 command. # parameter. ``` +### [temperature_probe] + +Reports probe coil temperature. Includes optional thermal drift +calibration for eddy current based probes. A `[temperature_probe]` +section may be linked to a `[probe_eddy_current]` by using the same +postfix for both sections. + +``` +[temperature_probe my_probe] +#sensor_type: +#sensor_pin: +#min_temp: +#max_temp: +# Temperature sensor configuration. +# See the "extruder" section for the definition of the above +# parameters. +#smooth_time: +# A time value (in seconds) over which temperature measurements will +# be smoothed to reduce the impact of measurement noise. The default +# is 2.0 seconds. +#gcode_id: +# See the "heater_generic" section for the definition of this +# parameter. +#speed: +# The travel speed [mm/s] for xy moves during calibration. Default +# is the speed defined by the probe. +#horizontal_move_z: +# The z distance [mm] from the bed at which xy moves will occur +# during calibration. Default is 2mm. +#resting_z: +# The z distance [mm] from the bed at which the tool will rest +# to heat the probe coil during calibration. Default is .4mm +#calibration_position: +# The X, Y, Z position where the tool should be moved when +# probe drift calibration initializes. This is the location +# where the first manual probe will occur. If omitted, the +# default behavior is not to move the tool prior to the first +# manual probe. +#calibration_bed_temp: +# The maximum safe bed temperature (in C) used to heat the probe +# during probe drift calibration. When set, the calibration +# procedure will turn on the bed after the first sample is +# taken. When the calibration procedure is complete the bed +# temperature will be set to zero. When omitted the default +# behavior is not to set the bed temperature. +#calibration_extruder_temp: +# The extruder temperature (in C) set probe during drift calibration. +# When this option is supplied the procedure will wait for until the +# specified temperature is reached before requesting the first manual +# probe. When the calibration procedure is complete the extruder +# temperature will be set to 0. When omitted the default behavior is +# not to set the extruder temperature. +#extruder_heating_z: 50. +# The Z location where extruder heating will occur if the +# "calibration_extruder_temp" option is set. Its recommended to heat +# the extruder some distance from the bed to minimize its impact on +# the probe coil temperature. The default is 50. +``` + ## Temperature sensors Klipper includes definitions for many types of temperature sensors. diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 221c855b6..5fa7fc4d3 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -54,3 +54,91 @@ result in changes in reported Z height. Changes in either the bed surface temperature or sensor hardware temperature can skew the results. It is important that calibration and probing is only done when the printer is at a stable temperature. + +## Thermal Drift Calibration + +As with all inductive probes, eddy current probes are subject to +significant thermal drift. If the eddy probe has a temperature +sensor on the coil it is possible to configure a `[temperature_probe]` +to report coil temperature and enable software drift compensation. To +link a temperature probe to an eddy current probe the +`[temperature_probe]` section must share a name with the +`[probe_eddy_current]` section. For example: + +``` +[probe_eddy_current my_probe] +# eddy probe configuration... + +[temperature_probe my_probe] +# temperature probe configuration... +``` + +See the [configuration reference](Config_Reference.md#temperature_probe) +for further details on how to configure a `temperature_probe`. It is +advised to configure the `calibration_position`, +`calibration_extruder_temp`, `extruder_heating_z`, and +`calibration_bed_temp` options, as doing so will automate some of the +steps outlined below. + +Eddy probe manufacturers may offer a stock drift calibration that can be +manually added to `drift_calibration` option of the `[probe_eddy_current]` +section. If they do not, or if the stock calibration does not perform well on +your system, the `temperature_probe` module offers a manual calibration +procedure via the `TEMPERATURE_PROBE_CALIBRATE` gcode command. + +Prior to performing calibration the user should have an idea of what the +maximum attainable temperature probe coil temperature is. This temperature +should be used to set the `TARGET` parameter of the +`TEMPERATURE_PROBE_CALIBRATE` command. The goal is to calibrate across the +widest temperature range possible, thus its desirable to start with the printer +cold and finish with the coil at the maximum temperature it can reach. + +Once a `[temperature_probe]` is configured, the following steps may be taken +to perform thermal drift calibration: + +- The probe must be calibrated using `PROBE_EDDY_CURRENT_CALIBRATE` + when a `[temperature_probe]` is configured and linked. This captures + the temperature during calibration which is necessary to perform + thermal drift compensation. +- Make sure the nozzle is free of debris and filament. +- The bed, nozzle, and probe coil should be cold prior to calibration. +- The following steps are required if the `calibration_position`, + `calibration_extruder_temp`, and `extruder_heating_z` options in + `[temperature_probe]` are **NOT** configured: + - Move the tool to the center of the bed. Z should be 30mm+ above the bed. + - Heat the extruder to a temperature above the maximum safe bed temperature. + 150-170C should be sufficient for most configurations. The purpose of + heating the extruder is to avoid nozzle expansion during calibration. + - When the extruder temperature has settled, move the Z axis down to about 1mm + above the bed. +- Start drift calibration. If the probe's name is `my_probe` and the maximum + probe temperature we can achieve is 80C, the appropriate gcode command is + `TEMPERATURE_PROBE_CALIBRATE PROBE=my_probe TARGET=80`. If configured, the + tool will move to the X,Y coordinate specified by the `calibration_position` + and the Z value specified by `extruder_heating_z`. After heating the extruder + to the specified temperature the tool will move to the Z value specified + by the`calibration_position`. +- The procedure will request a manual probe. Perform the manual probe with + the paper test and `ACCEPT`. The calibration procedure will take the first + set of samples with the probe then park the probe in the heating position. +- If the `calibration_bed_temp` is **NOT** configured turn on the bed heat + to the maximum safe temperature. Otherwise this step will be performed + automatically. +- By default the calibration procedure will request a manual probe every + 2C between samples until the `TARGET` is reached. The temperature delta + between samples can be customized by setting the `STEP` parameter in + `TEMPERATURE_PROBE_CALIBRATE`. Care should be taken when setting a custom + `STEP` value, a value too high may request too few samples resulting in + a poor calibration. +- The following additional gcode commands are available during drift + calibration: + - `TEMPERATURE_PROBE_NEXT` may be used to force a new sample before the step + delta has been reached. + - `TEMPERATURE_PROBE_COMPLETE` may be used to complete calibration before the + `TARGET` has been reached. + - `ABORT` may be used to end calibration and discard results. +- When calibration is finished use `SAVE_CONFIG` to store the drift + calibration. + +As one may conclude, the calibration process outlined above is more challenging +and time consuming than most other procedures. It may require practice and several attempts to achieve an optimal calibration. diff --git a/docs/G-Codes.md b/docs/G-Codes.md index e55fba35d..2444c53b1 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -1415,3 +1415,39 @@ command will probe the points specified in the config and then make independent adjustments to each Z stepper to compensate for tilt. See the PROBE command for details on the optional probe parameters. The optional `HORIZONTAL_MOVE_Z` value overrides the `horizontal_move_z` option specified in the config file. + +### [temperature_probe] + +The following commands are available when a +[temperature_probe config section](Config_Reference.md#temperature_probe) +is enabled. + +#### TEMPERATURE_PROBE_CALIBRATE +`TEMPERATURE_PROBE_CALIBRATE [PROBE=] [TARGET=] [STEP=]`: +Initiates probe drift calibration for eddy current based probes. The `TARGET` +is a target temperature for the last sample. When the temperature recorded +during a sample exceeds the `TARGET` calibration will complete. The `STEP` +parameter sets temperature delta (in C) between samples. After a sample has +been taken, this delta is used to schedule a call to `TEMPERATURE_PROBE_NEXT`. +The default `STEP` is 2. + +#### TEMPERATURE_PROBE_NEXT +`TEMPERATURE_PROBE_NEXT`: After calibration has started this command is run to +take the next sample. It is automatically scheduled to run when the delta +specified by `STEP` has been reached, however its also possible to manually run +this command to force a new sample. This command is only available during +calibration. + +#### TEMPERATURE_PROBE_COMPLETE: +`TEMPERATURE_PROBE_COMPLETE`: Can be used to end calibration and save the +current result before the `TARGET` temperature is reached. This command +is only available during calibration. + +#### ABORT +`ABORT`: Aborts the calibration process, discarding the current results. +This command is only available during drift calibration. + +### TEMPERATURE_PROBE_ENABLE +`TEMPERATURE_PROBE_ENABLE ENABLE=[0|1]`: Sets temperature drift +compensation on or off. If ENABLE is set to 0, drift compensation +will be disabled, if set to 1 it is enabled. From 12cd1d9e81c32b26ccc319af1dfc3633438908f1 Mon Sep 17 00:00:00 2001 From: bryan065 <30362590+bryan065@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:45:51 -0400 Subject: [PATCH 088/111] spi_flash: Add stm32g0b0xx to board_defs.py (#6646) Added board definition for stm32g0b0xx variant of the SKR Mini v3.0. Signed-off-by: Bryan Le --- scripts/spi_flash/board_defs.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index 4f84d7229..c0a8b5772 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -31,6 +31,11 @@ BOARD_DEFS = { 'spi_bus': "spi1", "cs_pin": "PA4" }, + 'btt-skr-mini-v3-b0': { + 'mcu': "stm32g0b0xx", + 'spi_bus': "spi1", + "cs_pin": "PA4" + }, 'flyboard-mini': { 'mcu': "stm32f103xe", 'spi_bus': "spi2", @@ -152,6 +157,7 @@ BOARD_ALIASES = { 'btt-skr-mini-e3-v1.2': BOARD_DEFS['btt-skr-mini'], 'btt-skr-mini-e3-v2': BOARD_DEFS['btt-skr-mini'], 'btt-skr-mini-e3-v3': BOARD_DEFS['btt-skr-mini-v3'], + 'btt-skr-mini-e3-v3-b0': BOARD_DEFS['btt-skr-mini-v3-b0'], 'btt-skr-mini-mz': BOARD_DEFS['btt-skr-mini'], 'btt-skr-e3-dip': BOARD_DEFS['btt-skr-mini'], 'btt002-v1': BOARD_DEFS['btt-skr-mini'], From 0844388d70f225f6458382c1c5d4e7eb37767758 Mon Sep 17 00:00:00 2001 From: Miles Pawar Date: Sat, 27 Jul 2024 01:14:22 +0100 Subject: [PATCH 089/111] config: Update generic-bigtreetech-skr-mini-e3-v3.0.cfg (#6654) - Removed stealth from Extruder to stop under extrusion issues - Changed Bed sensor to correct one for Ender 3 * Update generic-bigtreetech-skr-mini-e3-v3.0.cfg Signed-off-by: Miles Pawar --- config/generic-bigtreetech-skr-mini-e3-v3.0.cfg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/generic-bigtreetech-skr-mini-e3-v3.0.cfg b/config/generic-bigtreetech-skr-mini-e3-v3.0.cfg index 2ea064b09..b6a98bb02 100644 --- a/config/generic-bigtreetech-skr-mini-e3-v3.0.cfg +++ b/config/generic-bigtreetech-skr-mini-e3-v3.0.cfg @@ -85,11 +85,10 @@ uart_pin: PC11 tx_pin: PC10 uart_address: 3 run_current: 0.650 -stealthchop_threshold: 999999 [heater_bed] heater_pin: PC9 -sensor_type: ATC Semitec 104GT-2 +sensor_type: EPCOS 100K B57560G104F sensor_pin: PC4 control: pid pid_Kp: 54.027 From c0095812ff18687ed25ce0f1ed468ebed8f81cfe Mon Sep 17 00:00:00 2001 From: Gareth Farrington Date: Sun, 2 Jun 2024 22:31:28 -0700 Subject: [PATCH 090/111] hx71x: Load Cell Skeleton and HX71x bulk ADC * Create the load_cell host module skeleton to create the sensors and start taking samples. * Add support for the HX717 and HX711 ADC sensors. Signed-off-by: Gareth Farrington --- docs/API_Server.md | 15 +++ docs/Config_Reference.md | 57 ++++++++ klippy/extras/hx71x.py | 170 +++++++++++++++++++++++ klippy/extras/load_cell.py | 36 +++++ src/Kconfig | 9 +- src/Makefile | 1 + src/sensor_hx71x.c | 245 ++++++++++++++++++++++++++++++++++ test/configs/ar100.config | 1 + test/configs/stm32f042.config | 1 + 9 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 klippy/extras/hx71x.py create mode 100644 klippy/extras/load_cell.py create mode 100644 src/sensor_hx71x.c diff --git a/docs/API_Server.md b/docs/API_Server.md index cc0922e3c..36d67b1ac 100644 --- a/docs/API_Server.md +++ b/docs/API_Server.md @@ -364,6 +364,21 @@ and might later produce asynchronous messages such as: The "header" field in the initial query response is used to describe the fields found in later "data" responses. +### hx71x/dump_hx71x + +This endpoint is used to subscribe to raw HX711 and HX717 ADC data. +Obtaining these low-level ADC updates may be useful for diagnostic +and debugging purposes. Using this endpoint may increase Klipper's +system load. + +A request may look like: +`{"id": 123, "method":"hx71x/dump_hx71x", +"params": {"sensor": "load_cell", "response_template": {}}}` +and might return: +`{"id": 123,"result":{"header":["time","counts"]}}` +and might later produce asynchronous messages such as: +`{"params":{"data":[[3292.432935, 562534], [3292.4394937, 5625322]]}}` + ### pause_resume/cancel This endpoint is similar to running the "PRINT_CANCEL" G-Code command. diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 9be53dfaa..03b10da1c 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -4645,6 +4645,63 @@ adc2: # above parameters. ``` +## Load Cells + +### [load_cell] +Load Cell. Uses an ADC sensor attached to a load cell to create a digital +scale. + +``` +[load_cell] +sensor_type: +# This must be one of the supported Sensor types, see `Load Cell Sensors` +``` + +### Load Cell Sensors + +Load Cell supports dedicated Analog to Digital Converter (ADC) chips that sample +at a high data rate. + +#### XH711 +This is a 24 bit low sample rate chip using "bit-bang" communications. It is +suitable for filament scales. +``` +sensor_type: hx711 +sclk_pin: +# The pin connected to the HX711 clock line. This parameter must be provided. +dout_pin: +# The pin connected to the HX711 data output line. This parameter must be +# provided. +#gain: A-128 +# Valid values for gain are: A-128, A-64, B-32. The default is A-128. +# 'A' denotes the input channel and the number denotes the gain. Only the 3 +# listed combinations are supported by the chip. Note that changing the gain +# setting also selects the channel being read. +#sample_rate: 80 +# Valid values for sample_rate are 80 or 10. The default value is 80. +# This must match the wiring of the chip. The sample rate cannot be changed +# in software. +``` + +### HX717 +This is the 4x higher sample rate version of the HX711, suitable for probing. +``` +sensor_type: hx717 +sclk_pin: +# The pin connected to the HX717 clock line. This parameter must be provided. +dout_pin: +# The pin connected to the HX717 data output line. This parameter must be +# provided. +#gain: A-128 +# Valid values for gain are A-128, B-64, A-64, B-8. +# 'A' denotes the input channel and the number denotes the gain setting. +# Only the 4 listed combinations are supported by the chip. Note that +# changing the gain setting also selects the channel being read. +#sample_rate: 320 +# Valid values for sample_rate are: 10, 20, 80, 320. The default is 320. +# This must match the wiring of the chip. The sample rate cannot be changed +# in software. +``` ## Board specific hardware support ### [sx1509] diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py new file mode 100644 index 000000000..1b1128ac8 --- /dev/null +++ b/klippy/extras/hx71x.py @@ -0,0 +1,170 @@ +# HX711/HX717 Support +# +# Copyright (C) 2024 Gareth Farrington +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from . import bulk_sensor + +# +# Constants +# +UPDATE_INTERVAL = 0.10 +SAMPLE_ERROR_DESYNC = -0x80000000 +SAMPLE_ERROR_LONG_READ = 0x40000000 + +# Implementation of HX711 and HX717 +class HX71xBase(): + def __init__(self, config, sensor_type, + sample_rate_options, default_sample_rate, + gain_options, default_gain): + self.printer = printer = config.get_printer() + self.name = config.get_name().split()[-1] + self.last_error_count = 0 + self.consecutive_fails = 0 + self.sensor_type = sensor_type + # Chip options + dout_pin_name = config.get('dout_pin') + sclk_pin_name = config.get('sclk_pin') + ppins = printer.lookup_object('pins') + dout_ppin = ppins.lookup_pin(dout_pin_name) + sclk_ppin = ppins.lookup_pin(sclk_pin_name) + self.mcu = mcu = dout_ppin['chip'] + self.oid = mcu.create_oid() + if sclk_ppin['chip'] is not mcu: + raise config.error("%s config error: All pins must be " + "connected to the same MCU" % (self.name,)) + self.dout_pin = dout_ppin['pin'] + self.sclk_pin = sclk_ppin['pin'] + # Samples per second choices + self.sps = config.getchoice('sample_rate', sample_rate_options, + default=default_sample_rate) + # 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, " 0: + logging.error("%s: Forced sensor restart due to error", self.name) + self._finish_measurements() + self._start_measurements() + elif overflows > 0: + self.consecutive_fails += 1 + if self.consecutive_fails > 4: + logging.error("%s: Forced sensor restart due to overflows", + self.name) + self._finish_measurements() + self._start_measurements() + else: + self.consecutive_fails = 0 + return {'data': samples, 'errors': self.last_error_count, + 'overflows': self.ffreader.get_last_overflows()} + + +class HX711(HX71xBase): + def __init__(self, config): + super(HX711, self).__init__(config, "hx711", + # HX711 sps options + {80: 80, 10: 10}, 80, + # HX711 gain/channel options + {'A-128': 1, 'B-32': 2, 'A-64': 3}, 'A-128') + + +class HX717(HX71xBase): + def __init__(self, config): + super(HX717, self).__init__(config, "hx717", + # HX717 sps options + {320: 320, 80: 80, 20: 20, 10: 10}, 320, + # HX717 gain/channel options + {'A-128': 1, 'B-64': 2, 'A-64': 3, + 'B-8': 4}, 'A-128') + + +HX71X_SENSOR_TYPES = { + "hx711": HX711, + "hx717": HX717 +} diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py new file mode 100644 index 000000000..7210f230c --- /dev/null +++ b/klippy/extras/load_cell.py @@ -0,0 +1,36 @@ +# Load Cell Implementation +# +# Copyright (C) 2024 Gareth Farrington +# +# This file may be distributed under the terms of the GNU GPLv3 license. +from . import hx71x + +# Printer class that controls a load cell +class LoadCell: + def __init__(self, config, sensor): + self.printer = printer = config.get_printer() + self.sensor = sensor # must implement BulkAdcSensor + # startup, when klippy is ready, start capturing data + printer.register_event_handler("klippy:ready", self._handle_ready) + + def _handle_ready(self): + self.sensor.add_client(self._on_sample) + + def _on_sample(self, msg): + return True + + def get_sensor(self): + return self.sensor + + def get_status(self, eventtime): + return {} + +def load_config(config): + # Sensor types + sensors = {} + sensors.update(hx71x.HX71X_SENSOR_TYPES) + sensor_class = config.getchoice('sensor_type', sensors) + return LoadCell(config, sensor_class(config)) + +def load_config_prefix(config): + return load_config(config) diff --git a/src/Kconfig b/src/Kconfig index 7dcea3bab..4fb5268a2 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -108,6 +108,10 @@ config WANT_LDC1612 bool depends on HAVE_GPIO_I2C default y +config WANT_HX71X + bool + depends on WANT_GPIO_BITBANGING + default y config WANT_SOFTWARE_I2C bool depends on HAVE_GPIO && HAVE_GPIO_I2C @@ -118,7 +122,7 @@ config WANT_SOFTWARE_SPI default y config NEED_SENSOR_BULK bool - depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612 + depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612 || WANT_HX71X default y menu "Optional features (to reduce code size)" depends on HAVE_LIMITED_CODE_SIZE @@ -137,6 +141,9 @@ config WANT_LIS2DW config WANT_LDC1612 bool "Support ldc1612 eddy current sensor" depends on HAVE_GPIO_I2C +config WANT_HX71X + bool "Support HX711 and HX717 ADC chips" + depends on WANT_GPIO_BITBANGING config WANT_SOFTWARE_I2C bool "Support software based I2C \"bit-banging\"" depends on HAVE_GPIO && HAVE_GPIO_I2C diff --git a/src/Makefile b/src/Makefile index ed98172e4..4a1d24361 100644 --- a/src/Makefile +++ b/src/Makefile @@ -20,4 +20,5 @@ sensors-src-$(CONFIG_HAVE_GPIO_I2C) += sensor_mpu9250.c src-$(CONFIG_WANT_SENSORS) += $(sensors-src-y) src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c src-$(CONFIG_WANT_LDC1612) += sensor_ldc1612.c +src-$(CONFIG_WANT_HX71X) += sensor_hx71x.c src-$(CONFIG_NEED_SENSOR_BULK) += sensor_bulk.c diff --git a/src/sensor_hx71x.c b/src/sensor_hx71x.c new file mode 100644 index 000000000..4f0a8c5bb --- /dev/null +++ b/src/sensor_hx71x.c @@ -0,0 +1,245 @@ +// Support for bit-banging commands to HX711 and HX717 ADC chips +// +// Copyright (C) 2024 Gareth Farrington +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include "autoconf.h" // CONFIG_MACH_AVR +#include "board/gpio.h" // gpio_out_write +#include "board/irq.h" // irq_poll +#include "board/misc.h" // timer_read_time +#include "basecmd.h" // oid_alloc +#include "command.h" // DECL_COMMAND +#include "sched.h" // sched_add_timer +#include "sensor_bulk.h" // sensor_bulk_report +#include +#include + +struct hx71x_adc { + struct timer timer; + uint8_t gain_channel; // the gain+channel selection (1-4) + uint8_t pending_flag; + uint32_t rest_ticks; + uint32_t last_error; + struct gpio_in dout; // pin used to receive data from the hx71x + struct gpio_out sclk; // pin used to generate clock for the hx71x + struct sensor_bulk sb; +}; + +#define BYTES_PER_SAMPLE 4 +#define SAMPLE_ERROR_DESYNC 1 << 31 +#define SAMPLE_ERROR_READ_TOO_LONG 1 << 30 + +static struct task_wake wake_hx71x; + + +/**************************************************************** + * Low-level bit-banging + ****************************************************************/ + +#define MIN_PULSE_TIME nsecs_to_ticks(200) + +static uint32_t +nsecs_to_ticks(uint32_t ns) +{ + return timer_from_us(ns * 1000) / 1000000; +} + +// Pause for 200ns +static void +hx71x_delay_noirq(void) +{ + if (CONFIG_MACH_AVR) { + // Optimize avr, as calculating time takes longer than needed delay + asm("nop\n nop"); + return; + } + uint32_t end = timer_read_time() + MIN_PULSE_TIME; + while (timer_is_before(timer_read_time(), end)) + ; +} + +// Pause for a minimum of 200ns +static void +hx71x_delay(void) +{ + if (CONFIG_MACH_AVR) + // Optimize avr, as calculating time takes longer than needed delay + return; + uint32_t end = timer_read_time() + MIN_PULSE_TIME; + while (timer_is_before(timer_read_time(), end)) + irq_poll(); +} + +// Read 'num_bits' from the sensor +static uint32_t +hx71x_raw_read(struct gpio_in dout, struct gpio_out sclk, int num_bits) +{ + uint32_t bits_read = 0; + while (num_bits--) { + irq_disable(); + gpio_out_toggle_noirq(sclk); + hx71x_delay_noirq(); + gpio_out_toggle_noirq(sclk); + uint_fast8_t bit = gpio_in_read(dout); + irq_enable(); + hx71x_delay(); + bits_read = (bits_read << 1) | bit; + } + return bits_read; +} + + +/**************************************************************** + * HX711 and HX717 Sensor Support + ****************************************************************/ + +// Check if data is ready +static uint_fast8_t +hx71x_is_data_ready(struct hx71x_adc *hx71x) +{ + return !gpio_in_read(hx71x->dout); +} + +// Event handler that wakes wake_hx71x() periodically +static uint_fast8_t +hx71x_event(struct timer *timer) +{ + struct hx71x_adc *hx71x = container_of(timer, struct hx71x_adc, timer); + uint32_t rest_ticks = hx71x->rest_ticks; + if (hx71x->pending_flag) { + hx71x->sb.possible_overflows++; + rest_ticks *= 4; + } else if (hx71x_is_data_ready(hx71x)) { + // New sample pending + hx71x->pending_flag = 1; + sched_wake_task(&wake_hx71x); + rest_ticks *= 8; + } + hx71x->timer.waketime += rest_ticks; + return SF_RESCHEDULE; +} + +static void +add_sample(struct hx71x_adc *hx71x, uint8_t oid, uint32_t counts, + uint8_t force_flush) { + // Add measurement to buffer + hx71x->sb.data[hx71x->sb.data_count] = counts; + hx71x->sb.data[hx71x->sb.data_count + 1] = counts >> 8; + hx71x->sb.data[hx71x->sb.data_count + 2] = counts >> 16; + hx71x->sb.data[hx71x->sb.data_count + 3] = counts >> 24; + hx71x->sb.data_count += BYTES_PER_SAMPLE; + + if (hx71x->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(hx71x->sb.data) + || force_flush) + sensor_bulk_report(&hx71x->sb, oid); +} + +// hx71x ADC query +static void +hx71x_read_adc(struct hx71x_adc *hx71x, uint8_t oid) +{ + uint32_t start = timer_read_time(); + // Read from sensor + uint_fast8_t gain_channel = hx71x->gain_channel; + uint32_t adc = hx71x_raw_read(hx71x->dout, hx71x->sclk, 24 + gain_channel); + hx71x->pending_flag = 0; + barrier(); + + // Extract report from raw data + uint32_t counts = adc >> gain_channel; + if (counts & 0x800000) + counts |= 0xFF000000; + + // Check for errors + uint_fast8_t extras_mask = (1 << gain_channel) - 1; + if ((adc & extras_mask) != extras_mask) { + // Transfer did not complete correctly + hx71x->last_error = SAMPLE_ERROR_DESYNC; + } else if ((timer_read_time() - start) > (hx71x->rest_ticks * 8)) { + // Transfer took too long + hx71x->last_error = SAMPLE_ERROR_READ_TOO_LONG; + } + + // forever send errors until reset + if (hx71x->last_error != 0) { + counts = hx71x->last_error; + } + + // Add measurement to buffer + add_sample(hx71x, oid, counts, false); +} + +// Create a hx71x sensor +void +command_config_hx71x(uint32_t *args) +{ + struct hx71x_adc *hx71x = oid_alloc(args[0] + , command_config_hx71x, sizeof(*hx71x)); + hx71x->timer.func = hx71x_event; + hx71x->pending_flag = 0; + uint8_t gain_channel = args[1]; + if (gain_channel < 1 || gain_channel > 4) { + shutdown("HX71x gain/channel out of range 1-4"); + } + hx71x->gain_channel = gain_channel; + hx71x->dout = gpio_in_setup(args[2], 1); + hx71x->sclk = gpio_out_setup(args[3], 0); + gpio_out_write(hx71x->sclk, 1); // put chip in power down state +} +DECL_COMMAND(command_config_hx71x, "config_hx71x oid=%c gain_channel=%c" + " dout_pin=%u sclk_pin=%u"); + +// start/stop capturing ADC data +void +command_query_hx71x(uint32_t *args) +{ + uint8_t oid = args[0]; + struct hx71x_adc *hx71x = oid_lookup(oid, command_config_hx71x); + sched_del_timer(&hx71x->timer); + hx71x->pending_flag = 0; + hx71x->last_error = 0; + hx71x->rest_ticks = args[1]; + if (!hx71x->rest_ticks) { + // End measurements + gpio_out_write(hx71x->sclk, 1); // put chip in power down state + return; + } + // Start new measurements + gpio_out_write(hx71x->sclk, 0); // wake chip from power down + sensor_bulk_reset(&hx71x->sb); + irq_disable(); + hx71x->timer.waketime = timer_read_time() + hx71x->rest_ticks; + sched_add_timer(&hx71x->timer); + irq_enable(); +} +DECL_COMMAND(command_query_hx71x, "query_hx71x oid=%c rest_ticks=%u"); + +void +command_query_hx71x_status(const uint32_t *args) +{ + uint8_t oid = args[0]; + struct hx71x_adc *hx71x = oid_lookup(oid, command_config_hx71x); + irq_disable(); + const uint32_t start_t = timer_read_time(); + uint8_t is_data_ready = hx71x_is_data_ready(hx71x); + irq_enable(); + uint8_t pending_bytes = is_data_ready ? BYTES_PER_SAMPLE : 0; + sensor_bulk_status(&hx71x->sb, oid, start_t, 0, pending_bytes); +} +DECL_COMMAND(command_query_hx71x_status, "query_hx71x_status oid=%c"); + +// Background task that performs measurements +void +hx71x_capture_task(void) +{ + if (!sched_check_wake(&wake_hx71x)) + return; + uint8_t oid; + struct hx71x_adc *hx71x; + foreach_oid(oid, hx71x, command_config_hx71x) { + if (hx71x->pending_flag) + hx71x_read_adc(hx71x, oid); + } +} +DECL_TASK(hx71x_capture_task); diff --git a/test/configs/ar100.config b/test/configs/ar100.config index 6c9174824..61734ab9b 100644 --- a/test/configs/ar100.config +++ b/test/configs/ar100.config @@ -4,3 +4,4 @@ CONFIG_WANT_DISPLAYS=n CONFIG_WANT_SOFTWARE_I2C=n CONFIG_WANT_SOFTWARE_SPI=n CONFIG_WANT_LIS2DW=n +CONFIG_WANT_HX71X=n diff --git a/test/configs/stm32f042.config b/test/configs/stm32f042.config index 12cc0922e..3e0b2a552 100644 --- a/test/configs/stm32f042.config +++ b/test/configs/stm32f042.config @@ -4,3 +4,4 @@ CONFIG_MACH_STM32F042=y CONFIG_WANT_SOFTWARE_I2C=n CONFIG_WANT_LIS2DW=n CONFIG_WANT_LDC1612=n +CONFIG_WANT_HX71X=n From 055f07c6389fdae1be0ecafbe5b114ab08813fdd Mon Sep 17 00:00:00 2001 From: Gareth Farrington Date: Sun, 2 Jun 2024 22:33:45 -0700 Subject: [PATCH 091/111] ads1220: Add ADS1220 bulk sensor to load_cell Add support for the ADS1220 as an alternative to HX71x that supports SPI and higher sample rates. Signed-off-by: Gareth Farrington --- docs/API_Server.md | 15 +++ docs/Config_Reference.md | 34 +++++++ klippy/extras/ads1220.py | 187 ++++++++++++++++++++++++++++++++++ klippy/extras/load_cell.py | 2 + src/Kconfig | 10 +- src/Makefile | 1 + src/sensor_ads1220.c | 161 +++++++++++++++++++++++++++++ test/configs/ar100.config | 1 + test/configs/stm32f042.config | 1 + 9 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 klippy/extras/ads1220.py create mode 100644 src/sensor_ads1220.c diff --git a/docs/API_Server.md b/docs/API_Server.md index 36d67b1ac..f29bbeba5 100644 --- a/docs/API_Server.md +++ b/docs/API_Server.md @@ -379,6 +379,21 @@ and might return: and might later produce asynchronous messages such as: `{"params":{"data":[[3292.432935, 562534], [3292.4394937, 5625322]]}}` +### ads1220/dump_ads1220 + +This endpoint is used to subscribe to raw ADS1220 ADC data. +Obtaining these low-level ADC updates may be useful for diagnostic +and debugging purposes. Using this endpoint may increase Klipper's +system load. + +A request may look like: +`{"id": 123, "method":"ads1220/dump_ads1220", +"params": {"sensor": "load_cell", "response_template": {}}}` +and might return: +`{"id": 123,"result":{"header":["time","counts"]}}` +and might later produce asynchronous messages such as: +`{"params":{"data":[[3292.432935, 562534], [3292.4394937, 5625322]]}}` + ### pause_resume/cancel This endpoint is similar to running the "PRINT_CANCEL" G-Code command. diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 03b10da1c..63779df95 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -4702,6 +4702,40 @@ dout_pin: # This must match the wiring of the chip. The sample rate cannot be changed # in software. ``` + +### ADS1220 +The ADS1220 is a 24 bit ADC supporting up to a 2Khz sample rate configurable in +software. +``` +sensor_type: ads1220 +cs_pin: +# The pin connected to the ADS1220 chip select line. This parameter must +# be provided. +#spi_speed: 512000 +# This chip supports 2 speeds: 256000 or 512000. The faster speed is only +# enabled when one of the Turbo sample rates is used. The correct spi_speed +# is selected based on the sample rate. +#spi_bus: +#spi_software_sclk_pin: +#spi_software_mosi_pin: +#spi_software_miso_pin: +# See the "common SPI settings" section for a description of the +# above parameters. +data_ready_pin: +# Pin connected to the ADS1220 data ready line. This parameter must be +# provided. +#gain: 128 +# Valid gain values are 128, 64, 32, 16, 8, 4, 2, 1 +# The default is 128 +#sample_rate: 660 +# This chip supports two ranges of sample rates, Normal and Turbo. In turbo +# mode the chips c internal clock runs twice as fast and the SPI communication +# speed is also doubled. +# Normal sample rates: 20, 45, 90, 175, 330, 600, 1000 +# Turbo sample rates: 40, 90, 180, 350, 660, 1200, 2000 +# The default is 660 +``` + ## Board specific hardware support ### [sx1509] diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py new file mode 100644 index 000000000..fba741818 --- /dev/null +++ b/klippy/extras/ads1220.py @@ -0,0 +1,187 @@ +# ADS1220 Support +# +# Copyright (C) 2024 Gareth Farrington +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from . import bulk_sensor, bus + +# +# Constants +# +BYTES_PER_SAMPLE = 4 # samples are 4 byte wide unsigned integers +MAX_SAMPLES_PER_MESSAGE = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE +UPDATE_INTERVAL = 0.10 +RESET_CMD = 0x06 +START_SYNC_CMD = 0x08 +RREG_CMD = 0x20 +WREG_CMD = 0x40 +NOOP_CMD = 0x0 +RESET_STATE = bytearray([0x0, 0x0, 0x0, 0x0]) + +# turn bytearrays into pretty hex strings: [0xff, 0x1] +def hexify(byte_array): + return "[%s]" % (", ".join([hex(b) for b in byte_array])) + + +class ADS1220(): + def __init__(self, config): + self.printer = printer = config.get_printer() + self.name = config.get_name().split()[-1] + self.last_error_count = 0 + self.consecutive_fails = 0 + # Chip options + # Gain + self.gain_options = {'1': 0x0, '2': 0x1, '4': 0x2, '8': 0x3, '16': 0x4, + '32': 0x5, '64': 0x6, '128': 0x7} + self.gain = config.getchoice('gain', self.gain_options, default='128') + # Sample rate + self.sps_normal = {'20': 20, '45': 45, '90': 90, '175': 175, + '330': 330, '600': 600, '1000': 1000} + self.sps_turbo = {'40': 40, '90': 90, '180': 180, '350': 350, + '660': 660, '1200': 1200, '2000': 2000} + self.sps_options = self.sps_normal.copy() + self.sps_options.update(self.sps_turbo) + self.sps = config.getchoice('sps', self.sps_options, default='660') + self.is_turbo = str(self.sps) in self.sps_turbo + # SPI Setup + spi_speed = 512000 if self.is_turbo else 256000 + self.spi = bus.MCU_SPI_from_config(config, 1, default_speed=spi_speed) + self.mcu = mcu = self.spi.get_mcu() + self.oid = mcu.create_oid() + # Data Ready (DRDY) Pin + drdy_pin = config.get('data_ready_pin') + ppins = printer.lookup_object('pins') + drdy_ppin = ppins.lookup_pin(drdy_pin) + self.data_ready_pin = drdy_ppin['pin'] + drdy_pin_mcu = drdy_ppin['chip'] + 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 + self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, " +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include "board/irq.h" // irq_disable +#include "board/gpio.h" // gpio_out_write +#include "board/misc.h" // timer_read_time +#include "basecmd.h" // oid_alloc +#include "command.h" // DECL_COMMAND +#include "sched.h" // sched_add_timer +#include "sensor_bulk.h" // sensor_bulk_report +#include "spicmds.h" // spidev_transfer +#include + +struct ads1220_adc { + struct timer timer; + uint32_t rest_ticks; + struct gpio_in data_ready; + struct spidev_s *spi; + uint8_t pending_flag, data_count; + struct sensor_bulk sb; +}; + +// Flag types +enum { + FLAG_PENDING = 1 << 0 +}; + +#define BYTES_PER_SAMPLE 4 + +static struct task_wake wake_ads1220; + +/**************************************************************** + * ADS1220 Sensor Support + ****************************************************************/ + +int8_t +ads1220_is_data_ready(struct ads1220_adc *ads1220) { + return gpio_in_read(ads1220->data_ready) == 0; +} + +// Event handler that wakes wake_ads1220() periodically +static uint_fast8_t +ads1220_event(struct timer *timer) +{ + struct ads1220_adc *ads1220 = container_of(timer, struct ads1220_adc, + timer); + uint32_t rest_ticks = ads1220->rest_ticks; + if (ads1220->pending_flag) { + ads1220->sb.possible_overflows++; + rest_ticks *= 4; + } else if (ads1220_is_data_ready(ads1220)) { + ads1220->pending_flag = 1; + sched_wake_task(&wake_ads1220); + rest_ticks *= 8; + } + ads1220->timer.waketime += rest_ticks; + return SF_RESCHEDULE; +} + +// Add a measurement to the buffer +static void +add_sample(struct ads1220_adc *ads1220, uint8_t oid, uint_fast32_t counts) +{ + ads1220->sb.data[ads1220->sb.data_count] = counts; + ads1220->sb.data[ads1220->sb.data_count + 1] = counts >> 8; + ads1220->sb.data[ads1220->sb.data_count + 2] = counts >> 16; + ads1220->sb.data[ads1220->sb.data_count + 3] = counts >> 24; + ads1220->sb.data_count += BYTES_PER_SAMPLE; + + if ((ads1220->sb.data_count + BYTES_PER_SAMPLE) > + ARRAY_SIZE(ads1220->sb.data)) { + sensor_bulk_report(&ads1220->sb, oid); + } +} + +// ADS1220 ADC query +void +ads1220_read_adc(struct ads1220_adc *ads1220, uint8_t oid) +{ + uint8_t msg[3] = {0, 0, 0}; + spidev_transfer(ads1220->spi, 1, sizeof(msg), msg); + ads1220->pending_flag = 0; + barrier(); + + // create 24 bit int from bytes + int32_t counts = (msg[0] << 16) | (msg[1] << 8) | msg[2]; + + // extend 2's complement 24 bits to 32bits + if (counts & 0x800000) + counts |= 0xFF000000; + + add_sample(ads1220, oid, counts); +} + +// Create an ads1220 sensor +void +command_config_ads1220(uint32_t *args) +{ + struct ads1220_adc *ads1220 = oid_alloc(args[0] + , command_config_ads1220, sizeof(*ads1220)); + ads1220->timer.func = ads1220_event; + ads1220->pending_flag = 0; + ads1220->spi = spidev_oid_lookup(args[1]); + ads1220->data_ready = gpio_in_setup(args[2], 0); +} +DECL_COMMAND(command_config_ads1220, "config_ads1220 oid=%c" + " spi_oid=%c data_ready_pin=%u"); + +// start/stop capturing ADC data +void +command_query_ads1220(uint32_t *args) +{ + uint8_t oid = args[0]; + struct ads1220_adc *ads1220 = oid_lookup(oid, command_config_ads1220); + sched_del_timer(&ads1220->timer); + ads1220->pending_flag = 0; + ads1220->rest_ticks = args[1]; + if (!ads1220->rest_ticks) { + // End measurements + return; + } + // Start new measurements + sensor_bulk_reset(&ads1220->sb); + irq_disable(); + ads1220->timer.waketime = timer_read_time() + ads1220->rest_ticks; + sched_add_timer(&ads1220->timer); + irq_enable(); +} +DECL_COMMAND(command_query_ads1220, "query_ads1220 oid=%c rest_ticks=%u"); + +void +command_query_ads1220_status(const uint32_t *args) +{ + uint8_t oid = args[0]; + struct ads1220_adc *ads1220 = oid_lookup(oid, command_config_ads1220); + irq_disable(); + const uint32_t start_t = timer_read_time(); + uint8_t is_data_ready = ads1220_is_data_ready(ads1220); + irq_enable(); + uint8_t pending_bytes = is_data_ready ? BYTES_PER_SAMPLE : 0; + sensor_bulk_status(&ads1220->sb, oid, start_t, 0, pending_bytes); +} +DECL_COMMAND(command_query_ads1220_status, "query_ads1220_status oid=%c"); + +// Background task that performs measurements +void +ads1220_capture_task(void) +{ + if (!sched_check_wake(&wake_ads1220)) + return; + uint8_t oid; + struct ads1220_adc *ads1220; + foreach_oid(oid, ads1220, command_config_ads1220) { + if (ads1220->pending_flag) + ads1220_read_adc(ads1220, oid); + } +} +DECL_TASK(ads1220_capture_task); diff --git a/test/configs/ar100.config b/test/configs/ar100.config index 61734ab9b..a1335176f 100644 --- a/test/configs/ar100.config +++ b/test/configs/ar100.config @@ -5,3 +5,4 @@ CONFIG_WANT_SOFTWARE_I2C=n CONFIG_WANT_SOFTWARE_SPI=n CONFIG_WANT_LIS2DW=n CONFIG_WANT_HX71X=n +CONFIG_WANT_ADS1220=n diff --git a/test/configs/stm32f042.config b/test/configs/stm32f042.config index 3e0b2a552..53cf1281e 100644 --- a/test/configs/stm32f042.config +++ b/test/configs/stm32f042.config @@ -5,3 +5,4 @@ CONFIG_WANT_SOFTWARE_I2C=n CONFIG_WANT_LIS2DW=n CONFIG_WANT_LDC1612=n CONFIG_WANT_HX71X=n +CONFIG_WANT_ADS1220=n From cb15d0fec69317ed2023dd8aad19f749f527fe77 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 31 Jul 2024 21:26:10 -0400 Subject: [PATCH 092/111] load_cell: Don't start sensor on startup Also, don't report an empty status. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py index f6725d180..14f3c2983 100644 --- a/klippy/extras/load_cell.py +++ b/klippy/extras/load_cell.py @@ -11,11 +11,6 @@ class LoadCell: def __init__(self, config, sensor): self.printer = printer = config.get_printer() self.sensor = sensor # must implement BulkAdcSensor - # startup, when klippy is ready, start capturing data - printer.register_event_handler("klippy:ready", self._handle_ready) - - def _handle_ready(self): - self.sensor.add_client(self._on_sample) def _on_sample(self, msg): return True @@ -23,9 +18,6 @@ class LoadCell: def get_sensor(self): return self.sensor - def get_status(self, eventtime): - return {} - def load_config(config): # Sensor types sensors = {} From 13c75ea876ae66d5f33d79afe4e30dffed3e999c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 31 Jul 2024 21:31:22 -0400 Subject: [PATCH 093/111] docs: Fix heading hierarchy for load_cell in Config_Reference.md Signed-off-by: Kevin O'Connor --- docs/Config_Reference.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 63779df95..f19f0ebc1 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -4654,18 +4654,14 @@ scale. ``` [load_cell] sensor_type: -# This must be one of the supported Sensor types, see `Load Cell Sensors` +# This must be one of the supported sensor types, see below. ``` -### Load Cell Sensors - -Load Cell supports dedicated Analog to Digital Converter (ADC) chips that sample -at a high data rate. - #### XH711 This is a 24 bit low sample rate chip using "bit-bang" communications. It is suitable for filament scales. ``` +[load_cell] sensor_type: hx711 sclk_pin: # The pin connected to the HX711 clock line. This parameter must be provided. @@ -4683,9 +4679,10 @@ dout_pin: # in software. ``` -### HX717 +#### HX717 This is the 4x higher sample rate version of the HX711, suitable for probing. ``` +[load_cell] sensor_type: hx717 sclk_pin: # The pin connected to the HX717 clock line. This parameter must be provided. @@ -4703,10 +4700,11 @@ dout_pin: # in software. ``` -### ADS1220 +#### ADS1220 The ADS1220 is a 24 bit ADC supporting up to a 2Khz sample rate configurable in software. ``` +[load_cell] sensor_type: ads1220 cs_pin: # The pin connected to the ADS1220 chip select line. This parameter must From ba2a149e9ab093a36120226c2e76ba8f3f4e7ed9 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sun, 14 Jul 2024 20:12:32 +0200 Subject: [PATCH 094/111] idex_modes: Improved restoring position in RESTORE_DUAL_CARRIAGE_STATE Previous implementation could crash the idex carriages into each other. The new code moves the idex carriages together, eliminating this risk and decreasing the time needed to restore the carriages positions. Signed-off-by: Dmitry Butyugin --- klippy/kinematics/idex_modes.py | 34 +++++++++++++++++++++++++++------ test/klippy/dual_carriage.cfg | 2 +- test/klippy/dual_carriage.test | 12 ++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/klippy/kinematics/idex_modes.py b/klippy/kinematics/idex_modes.py index f2618d080..2f2da4168 100644 --- a/klippy/kinematics/idex_modes.py +++ b/klippy/kinematics/idex_modes.py @@ -4,7 +4,7 @@ # Copyright (C) 2023 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. -import math +import logging, math import chelper INACTIVE = 'INACTIVE' @@ -202,14 +202,31 @@ class DualCarriages: move_speed = gcmd.get_float('MOVE_SPEED', 0., above=0.) toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() - pos = toolhead.get_position() if gcmd.get_int('MOVE', 1): + homing_speed = 99999999. + cur_pos = [] for i, dc in enumerate(self.dc): self.toggle_active_dc_rail(i) - saved_pos = saved_state['axes_positions'][i] - toolhead.manual_move( - pos[:self.axis] + [saved_pos] + pos[self.axis+1:], - move_speed or dc.get_rail().homing_speed) + homing_speed = min(homing_speed, dc.get_rail().homing_speed) + cur_pos.append(toolhead.get_position()) + move_pos = list(cur_pos[0]) + dl = [saved_state['axes_positions'][i] - cur_pos[i][self.axis] + for i in range(2)] + primary_ind = 0 if abs(dl[0]) >= abs(dl[1]) else 1 + self.toggle_active_dc_rail(primary_ind) + move_pos[self.axis] = saved_state['axes_positions'][primary_ind] + dc_mode = INACTIVE if min(abs(dl[0]), abs(dl[1])) < 0.000000001 \ + else COPY if dl[0] * dl[1] > 0 else MIRROR + if dc_mode != INACTIVE: + self.dc[1-primary_ind].activate(dc_mode, cur_pos[primary_ind]) + self.dc[1-primary_ind].override_axis_scaling( + abs(dl[1-primary_ind] / dl[primary_ind]), + cur_pos[primary_ind]) + toolhead.manual_move(move_pos, move_speed or homing_speed) + toolhead.flush_step_generation() + # Make sure the scaling coefficients are restored with the mode + self.dc[0].inactivate(move_pos) + self.dc[1].inactivate(move_pos) for i, dc in enumerate(self.dc): saved_mode = saved_state['carriage_modes'][i] self.activate_dc_mode(i, saved_mode) @@ -257,3 +274,8 @@ class DualCarriagesRail: self.scale = 0. self.apply_transform() self.mode = INACTIVE + def override_axis_scaling(self, new_scale, position): + old_axis_position = self.get_axis_position(position) + self.scale = math.copysign(new_scale, self.scale) + self.offset = old_axis_position - position[self.axis] * self.scale + self.apply_transform() diff --git a/test/klippy/dual_carriage.cfg b/test/klippy/dual_carriage.cfg index 9ae01c2bc..93c574440 100644 --- a/test/klippy/dual_carriage.cfg +++ b/test/klippy/dual_carriage.cfg @@ -61,7 +61,7 @@ pid_Kd: 114 min_temp: 0 max_temp: 250 -[gcode_macro PARK_extruder0] +[gcode_macro PARK_extruder] gcode: G90 G1 X0 diff --git a/test/klippy/dual_carriage.test b/test/klippy/dual_carriage.test index 5b2f9e65d..ed40c236e 100644 --- a/test/klippy/dual_carriage.test +++ b/test/klippy/dual_carriage.test @@ -17,6 +17,18 @@ G1 X190 F6000 SET_DUAL_CARRIAGE CARRIAGE=0 G1 X20 F6000 +# Save dual carriage state +SAVE_DUAL_CARRIAGE_STATE + +G1 X50 F6000 + +# Go back to alternate carriage +SET_DUAL_CARRIAGE CARRIAGE=1 +G1 X170 F6000 + +# Restore dual carriage state +RESTORE_DUAL_CARRIAGE_STATE + # Test changing extruders G1 X5 T1 From d7d9092a920b3bd2bede4b570c66ddaa52df3f19 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Tue, 30 Jul 2024 20:51:28 +0200 Subject: [PATCH 095/111] servo: Asynchronous adjustments of servo position This change follows the same approach as implemented for fan control. The change removes the move queue flushing when changing servo position, which does not appear to be necessary. This can be beneficial, for example, for WS7040-based cooling on IDEX setups where the servo can be used to control the air flow between the toolheads, with this change eliminating micro-stutters of the toolhead on servo position adjustment. Signed-off-by: Dmitry Butyugin --- klippy/extras/servo.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/klippy/extras/servo.py b/klippy/extras/servo.py index c05c9f819..344d6a31d 100644 --- a/klippy/extras/servo.py +++ b/klippy/extras/servo.py @@ -58,13 +58,15 @@ class PrinterServo: return width * self.width_to_value cmd_SET_SERVO_help = "Set servo angle" def cmd_SET_SERVO(self, gcmd): - print_time = self.printer.lookup_object('toolhead').get_last_move_time() width = gcmd.get_float('WIDTH', None) if width is not None: - self._set_pwm(print_time, self._get_pwm_from_pulse_width(width)) + value = self._get_pwm_from_pulse_width(width) else: angle = gcmd.get_float('ANGLE') - self._set_pwm(print_time, self._get_pwm_from_angle(angle)) + value = self._get_pwm_from_angle(angle) + toolhead = self.printer.lookup_object('toolhead') + toolhead.register_lookahead_callback((lambda pt: + self._set_pwm(pt, value))) def load_config_prefix(config): return PrinterServo(config) From 025ae2349de8ef18a24f284397e1e2cea93e4223 Mon Sep 17 00:00:00 2001 From: JamesH1978 <87171443+JamesH1978@users.noreply.github.com> Date: Fri, 9 Aug 2024 03:43:21 +0100 Subject: [PATCH 096/111] docs: Update Installation.md (#6650) Added links for Fluidd/Mainsail/Octoprint Added references to overview.md and mkdocs.yml and spelling errors. Signed-off-by: James Hartley --- docs/Installation.md | 158 ++++++++++++++++++++----------------- docs/OctoPrint.md | 79 +++++++++++++++++++ docs/Overview.md | 1 + docs/_klipper3d/mkdocs.yml | 4 +- 4 files changed, 169 insertions(+), 73 deletions(-) create mode 100644 docs/OctoPrint.md diff --git a/docs/Installation.md b/docs/Installation.md index 004d963a0..ca64259aa 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -1,15 +1,20 @@ # Installation -These instructions assume the software will run on a Raspberry Pi -computer in conjunction with OctoPrint. It is recommended that a -Raspberry Pi 2 (or later) be used as the host machine (see the +These instructions assume the software will run on a linux based host +running a Klipper compatible front end. It is recommended that a +SBC(Small Board Computer) such as a Raspberry Pi or Debian based Linux +device be used as the host machine (see the [FAQ](FAQ.md#can-i-run-klipper-on-something-other-than-a-raspberry-pi-3) -for other machines). +for other options). + +For the purposes of these instructions host relates to the Linux device and +mcu relates to the printboard. SBC relates to the term Small Board Computer +such as the Raspberry Pi. ## Obtain a Klipper Configuration File Most Klipper settings are determined by a "printer configuration file" -that will be stored on the Raspberry Pi. An appropriate configuration +printer.cfg, that will be stored on the host. An appropriate configuration file can often be found by looking in the Klipper [config directory](../config/) for a file starting with a "printer-" prefix that corresponds to the target printer. The Klipper @@ -35,38 +40,51 @@ printer configuration file, then start with the closest example [config file](../config/) and use the Klipper [config reference](Config_Reference.md) for further information. -## Prepping an OS image +## Interacting with Klipper -Start by installing [OctoPi](https://github.com/guysoft/OctoPi) on the -Raspberry Pi computer. Use OctoPi v0.17.0 or later - see the -[OctoPi releases](https://github.com/guysoft/OctoPi/releases) for -release information. One should verify that OctoPi boots and that the -OctoPrint web server works. After connecting to the OctoPrint web -page, follow the prompt to upgrade OctoPrint to v1.4.2 or later. +Klipper is a 3d printer firmware, so it needs some way for the user to +interact with it. -After installing OctoPi and upgrading OctoPrint, it will be necessary -to ssh into the target machine to run a handful of system commands. If -using a Linux or MacOS desktop, then the "ssh" software should already -be installed on the desktop. There are free ssh clients available for -other desktops (eg, -[PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/)). Use the -ssh utility to connect to the Raspberry Pi (`ssh pi@octopi` -- password -is "raspberry") and run the following commands: +Currently the best choices are front ends that retrieve information through +the [Moonraker web API](https://moonraker.readthedocs.io/) and there is also +the option to use [Octoprint](https://octoprint.org/) to control Klipper. -``` -git clone https://github.com/Klipper3d/klipper -./klipper/scripts/install-octopi.sh -``` +The choice is up to the user on what to use, but the underlying Klipper is the +same in all cases. We encourage users to research the options available and +make an informed decision. -The above will download Klipper, install some system dependencies, -setup Klipper to run at system startup, and start the Klipper host -software. It will require an internet connection and it may take a few -minutes to complete. +## Obtaining an OS image for SBC's + +There are many ways to obtain an OS image for Klipper for SBC use, most depend on +what front end you wish to use. Some manafactures of these SBC boards also provide +their own Klipper-centric images. + +The two main Moonraker based front ends are [Fluidd](https://docs.fluidd.xyz/) +and [Mainsail](https://docs.mainsail.xyz/), the latter of which has a premade install +image ["MainsailOS"](http://docs.mainsailOS.xyz), this has the option for Raspberry Pi +and some OrangePi varianta. + +Fluidd can be installed via KIAUH(Klipper Install And Update Helper), which +is explained below and is a 3rd party installer for all things Klipper. + +OctoPrint can be installed via the popular OctoPi image or via KIAUH, this +process is explained in [OctoPrint.md](OctoPrint.md) + +## Installing via KIAUH + +Normally you would start with a base image for your SBC, RPiOS Lite for example, +or in the case of a x86 Linux device, Ubuntu Server. Please note that Desktop +variants are not recommended due to certain helper programs that can stop some +Klipper functions working and even mask access to some print boards. + +KIAUH can be used to install Klipper and its associated programs on a variety +of Linux based systems that run a form of Debian. More information can be found +at https://github.com/dw-0/kiauh ## Building and flashing the micro-controller To compile the micro-controller code, start by running these commands -on the Raspberry Pi: +on your host device: ``` cd ~/klipper/ @@ -108,10 +126,21 @@ It should report something similar to the following: It's common for each printer to have its own unique serial port name. This unique name will be used when flashing the micro-controller. It's possible there may be multiple lines in the above output - if so, -choose the line corresponding to the micro-controller (see the +choose the line corresponding to the micro-controller. If many +items are listed and the choice is ambiguous, unplug the board and +run the command again, the missing item will be your print board(see the [FAQ](FAQ.md#wheres-my-serial-port) for more information). -For common micro-controllers, the code can be flashed with something +For common micro-controllers with STM32 or clone chips, LPC chips and +others it is usual that these need an initial Klipper flash via SD card. + +When flashing with this method, it is important to make sure that the +print board is not connected with USB to the host, due to some boards +being able to feed power back to the board and stopping a flash from +occuring. + +For common micro-controllers using Atmega chips, for example the 2560, +the code can be flashed with something similar to: ``` @@ -123,53 +152,38 @@ sudo service klipper start Be sure to update the FLASH_DEVICE with the printer's unique serial port name. -When flashing for the first time, make sure that OctoPrint is not -connected directly to the printer (from the OctoPrint web page, under -the "Connection" section, click "Disconnect"). +For common micro-controllers using RP2040 chips, the code can be flashed +with something similar to: -## Configuring OctoPrint to use Klipper +``` +sudo service klipper stop +make flash FLASH_DEVICE=first +sudo service klipper start +``` -The OctoPrint web server needs to be configured to communicate with -the Klipper host software. Using a web browser, login to the OctoPrint -web page and then configure the following items: +It is important to note that RP2040 chips may need to be put into Boot mode +before this operation. -Navigate to the Settings tab (the wrench icon at the top of the -page). Under "Serial Connection" in "Additional serial ports" add -`/tmp/printer`. Then click "Save". - -Enter the Settings tab again and under "Serial Connection" change the -"Serial Port" setting to `/tmp/printer`. - -In the Settings tab, navigate to the "Behavior" sub-tab and select the -"Cancel any ongoing prints but stay connected to the printer" -option. Click "Save". - -From the main page, under the "Connection" section (at the top left of -the page) make sure the "Serial Port" is set to `/tmp/printer` and -click "Connect". (If `/tmp/printer` is not an available selection then -try reloading the page.) - -Once connected, navigate to the "Terminal" tab and type "status" -(without the quotes) into the command entry box and click "Send". The -terminal window will likely report there is an error opening the -config file - that means OctoPrint is successfully communicating with -Klipper. Proceed to the next section. ## Configuring Klipper The next step is to copy the [printer configuration file](#obtain-a-klipper-configuration-file) to -the Raspberry Pi. +the host. -Arguably the easiest way to set the Klipper configuration file is to -use a desktop editor that supports editing files over the "scp" and/or -"sftp" protocols. There are freely available tools that support this -(eg, Notepad++, WinSCP, and Cyberduck). Load the printer config file -in the editor and then save it as a file named `printer.cfg` in the -home directory of the pi user (ie, `/home/pi/printer.cfg`). +Arguably the easiest way to set the Klipper configuration file is using the +built in editors in Mainsail or Fluidd. These will allow the user to open +the configuration examples and save them to be printer.cfg. + +Another option is to use a desktop editor that supports editing files +over the "scp" and/or "sftp" protocols. There are freely available tools +that support this (eg, Notepad++, WinSCP, and Cyberduck). +Load the printer config file in the editor and then save it as a file +named "printer.cfg" in the home directory of the pi user +(ie, /home/pi/printer.cfg). Alternatively, one can also copy and edit the file directly on the -Raspberry Pi via ssh. That may look something like the following (be +host via ssh. That may look something like the following (be sure to update the command to use the appropriate printer config filename): @@ -201,7 +215,7 @@ serial: /dev/serial/by-id/usb-1a86_USB2.0-Serial-if00-port0 ``` After creating and editing the file it will be necessary to issue a -"restart" command in the OctoPrint web terminal to load the config. A +"restart" command in the command console to load the config. A "status" command will report the printer is ready if the Klipper config file is successfully read and the micro-controller is successfully found and configured. @@ -211,10 +225,10 @@ Klipper to report a configuration error. If an error occurs, make any necessary corrections to the printer config file and issue "restart" until "status" reports the printer is ready. -Klipper reports error messages via the OctoPrint terminal tab. The -"status" command can be used to re-report error messages. The default -Klipper startup script also places a log in **/tmp/klippy.log** which -provides more detailed information. +Klipper reports error messages via the command console and via pop up in +Fluidd and Mainsail. The "status" command can be used to re-report error +messages. A log is available and usually located in ~/printer_data/logs +this is named klippy.log After Klipper reports that the printer is ready, proceed to the [config check document](Config_checks.md) to perform some basic checks diff --git a/docs/OctoPrint.md b/docs/OctoPrint.md new file mode 100644 index 000000000..4fa1b0217 --- /dev/null +++ b/docs/OctoPrint.md @@ -0,0 +1,79 @@ +# OctoPrint for Klipper + +Klipper has a few options for its front ends, Octoprint was the first +and original front end for Klipper. This document will give +a brief overview of installing with this option. + +## Install with OctoPi + +Start by installing [OctoPi](https://github.com/guysoft/OctoPi) on the +Raspberry Pi computer. Use OctoPi v0.17.0 or later - see the +[OctoPi releases](https://github.com/guysoft/OctoPi/releases) for +release information. + +One should verify that OctoPi boots and that the +OctoPrint web server works. After connecting to the OctoPrint web +page, follow the prompt to upgrade OctoPrint if needed. + +After installing OctoPi and upgrading OctoPrint, it will be necessary +to ssh into the target machine to run a handful of system commands. + +Start by running these commands on your host device: + +__If you do not have git installed, please do so with:__ +``` +sudo apt install git +``` +then proceed: +``` +cd ~ +git clone https://github.com/Klipper3d/klipper +./klipper/scripts/install-octopi.sh +``` + +The above will download Klipper, install the needed system dependencies, +setup Klipper to run at system startup, and start the Klipper host +software. It will require an internet connection and it may take a few +minutes to complete. + +## Installing with KIAUH + +KIAUH can be used to install OctoPrint on a variety of Linux based systems +that run a form of Debian. More information can be found +at https://github.com/dw-0/kiauh + +## Configuring OctoPrint to use Klipper + +The OctoPrint web server needs to be configured to communicate with the Klipper +host software. Using a web browser, login to the OctoPrint web page and then +configure the following items: + +Navigate to the Settings tab (the wrench icon at the top of the page). +Under "Serial Connection" in "Additional serial ports" add: + +``` +~/printer_data/comms/klippy.sock +``` +Then click "Save". + +_In some older setups this address may be `/tmp/printer`_ + + +Enter the Settings tab again and under "Serial Connection" change the "Serial Port" +setting to the one added above. + +In the Settings tab, navigate to the "Behavior" sub-tab and select the +"Cancel any ongoing prints but stay connected to the printer" option. Click "Save". + +From the main page, under the "Connection" section (at the top left of the page) +make sure the "Serial Port" is set to the new additional one added +and click "Connect". (If it is not in the available selection then +try reloading the page.) + +Once connected, navigate to the "Terminal" tab and type "status" (without the quotes) +into the command entry box and click "Send". The terminal window will likely report +there is an error opening the config file - that means OctoPrint is successfully +communicating with Klipper. + +Please proceed to [Installation.md](Installation.md) and the +_Building and flashing the micro-controller_ section diff --git a/docs/Overview.md b/docs/Overview.md index 2b9253c26..1ab948910 100644 --- a/docs/Overview.md +++ b/docs/Overview.md @@ -17,6 +17,7 @@ communication with the Klipper developers. ## Installation and Configuration - [Installation](Installation.md): Guide to installing Klipper. + - [Octoprint](OctoPrint.md): Guide to installing Octoprint with Klipper. - [Config Reference](Config_Reference.md): Description of config parameters. - [Rotation Distance](Rotation_Distance.md): Calculating the diff --git a/docs/_klipper3d/mkdocs.yml b/docs/_klipper3d/mkdocs.yml index 4bfdd1694..02d32fadc 100644 --- a/docs/_klipper3d/mkdocs.yml +++ b/docs/_klipper3d/mkdocs.yml @@ -88,7 +88,9 @@ nav: - Config_Changes.md - Contact.md - Installation and Configuration: - - Installation.md + - Installation: + - Installation.md + - OctoPrint.md - Configuration Reference: - Config_Reference.md - Rotation_Distance.md From ca815f52c8420225f93f90698768a5c4714654a9 Mon Sep 17 00:00:00 2001 From: Nicholas Huskie <59385028+Huskia@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:45:12 +0800 Subject: [PATCH 097/111] stm32: Fix getting wrong ADC value on PA0 of STM32G431 (#6660) * Fix getting wrong ADC value on PA0 * Fix invalid/unused pin being used as adc channel on STM32H7/G431/L4 Signed-off-by: Nicholas Huskie --- src/stm32/stm32h7_adc.c | 44 +++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/stm32/stm32h7_adc.c b/src/stm32/stm32h7_adc.c index 3c217ca27..6740edd3e 100644 --- a/src/stm32/stm32h7_adc.c +++ b/src/stm32/stm32h7_adc.c @@ -12,6 +12,8 @@ #include "internal.h" // GPIO #include "sched.h" // sched_shutdown +#define ADC_INVALID_PIN 0xFF + #define ADC_TEMPERATURE_PIN 0xfe DECL_ENUMERATION("pin", "ADC_TEMPERATURE", ADC_TEMPERATURE_PIN); @@ -24,8 +26,8 @@ DECL_CONSTANT("ADC_MAX", 4095); static const uint8_t adc_pins[] = { #if CONFIG_MACH_STM32H7 // ADC1 - 0, // PA0_C ADC12_INP0 - 0, // PA1_C ADC12_INP1 + ADC_INVALID_PIN, // PA0_C ADC12_INP0 + ADC_INVALID_PIN, // PA1_C ADC12_INP1 GPIO('F', 11), // ADC1_INP2 GPIO('A', 6), // ADC12_INP3 GPIO('C', 4), // ADC12_INP4 @@ -45,8 +47,8 @@ static const uint8_t adc_pins[] = { GPIO('A', 4), // ADC12_INP18 GPIO('A', 5), // ADC12_INP19 // ADC2 - 0, // PA0_C ADC12_INP0 - 0, // PA1_C ADC12_INP1 + ADC_INVALID_PIN, // PA0_C ADC12_INP0 + ADC_INVALID_PIN, // PA1_C ADC12_INP1 GPIO('F', 13), // ADC2_INP2 GPIO('A', 6), // ADC12_INP3 GPIO('C', 4), // ADC12_INP4 @@ -61,13 +63,13 @@ static const uint8_t adc_pins[] = { GPIO('C', 3), // ADC12_INP13 GPIO('A', 2), // ADC12_INP14 GPIO('A', 3), // ADC12_INP15 - 0, // dac_out1 - 0, // dac_out2 + ADC_INVALID_PIN, // dac_out1 + ADC_INVALID_PIN, // dac_out2 GPIO('A', 4), // ADC12_INP18 GPIO('A', 5), // ADC12_INP19 // ADC3 - 0, // PC2_C ADC3_INP0 - 0, // PC3_C ADC3_INP1 + ADC_INVALID_PIN, // PC2_C ADC3_INP0 + ADC_INVALID_PIN, // PC3_C ADC3_INP1 GPIO('F', 9) , // ADC3_INP2 GPIO('F', 7), // ADC3_INP3 GPIO('F', 5), // ADC3_INP4 @@ -85,14 +87,14 @@ static const uint8_t adc_pins[] = { GPIO('H', 5), // ADC3_INP16 #if CONFIG_MACH_STM32H723 ADC_TEMPERATURE_PIN, - 0, + ADC_INVALID_PIN, #else - 0, // Vbat/4 + ADC_INVALID_PIN, // Vbat/4 ADC_TEMPERATURE_PIN,// VSENSE #endif - 0, // VREFINT + ADC_INVALID_PIN, // VREFINT #elif CONFIG_MACH_STM32G4 - 0, // [0] vssa + ADC_INVALID_PIN, // [0] vssa GPIO('A', 0), // [1] GPIO('A', 1), // [2] GPIO('A', 2), // [3] @@ -105,14 +107,14 @@ static const uint8_t adc_pins[] = { GPIO('F', 0), // [10] GPIO('B', 12), // [11] GPIO('B', 1), // [12] - 0, // [13] opamp + ADC_INVALID_PIN, // [13] opamp GPIO('B', 11), // [14] GPIO('B', 0), // [15] ADC_TEMPERATURE_PIN, // [16] vtemp - 0, // [17] vbat/3 - 0, // [18] vref - 0, - 0, // [0] vssa ADC 2 + ADC_INVALID_PIN, // [17] vbat/3 + ADC_INVALID_PIN, // [18] vref + ADC_INVALID_PIN, + ADC_INVALID_PIN, // [0] vssa ADC 2 GPIO('A', 0), // [1] GPIO('A', 1), // [2] GPIO('A', 6), // [3] @@ -128,11 +130,11 @@ static const uint8_t adc_pins[] = { GPIO('A', 5), // [13] GPIO('B', 11), // [14] GPIO('B', 15), // [15] - 0, // [16] opamp + ADC_INVALID_PIN, // [16] opamp GPIO('A', 4), // [17] - 0, // [18] opamp + ADC_INVALID_PIN, // [18] opamp #else // stm32l4 - 0, // vref + ADC_INVALID_PIN, // vref GPIO('C', 0), // ADC12_IN1 .. 16 GPIO('C', 1), GPIO('C', 2), @@ -150,7 +152,7 @@ static const uint8_t adc_pins[] = { GPIO('B', 0), GPIO('B', 1), ADC_TEMPERATURE_PIN, // temp - 0, // vbat + ADC_INVALID_PIN, // vbat #endif }; From 503e7e368b1ea1660e2ff12346d8fccd7bb6a864 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 3 Aug 2024 20:54:07 +0200 Subject: [PATCH 098/111] gcode_arc: refactor simplify Signed-off-by: Timofey Titovets --- klippy/extras/gcode_arcs.py | 42 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/klippy/extras/gcode_arcs.py b/klippy/extras/gcode_arcs.py index 76c165dd5..df68954e7 100644 --- a/klippy/extras/gcode_arcs.py +++ b/klippy/extras/gcode_arcs.py @@ -39,8 +39,6 @@ class ArcSupport: self.gcode.register_command("G18", self.cmd_G18) self.gcode.register_command("G19", self.cmd_G19) - self.Coord = self.gcode.Coord - # backwards compatibility, prior implementation only supported XY self.plane = ARC_PLANE_X_Y @@ -64,24 +62,28 @@ class ArcSupport: if not gcodestatus['absolute_coordinates']: raise gcmd.error("G2/G3 does not support relative move mode") currentPos = gcodestatus['gcode_position'] + absolut_extrude = gcodestatus['absolute_extrude'] # Parse parameters - asTarget = self.Coord(x=gcmd.get_float("X", currentPos[0]), - y=gcmd.get_float("Y", currentPos[1]), - z=gcmd.get_float("Z", currentPos[2]), - e=None) + asTarget = [gcmd.get_float("X", currentPos[0]), + gcmd.get_float("Y", currentPos[1]), + gcmd.get_float("Z", currentPos[2])] if gcmd.get_float("R", None) is not None: raise gcmd.error("G2/G3 does not support R moves") # determine the plane coordinates and the helical axis - asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('IJ') ] + I = gcmd.get_float('I', 0.) + J = gcmd.get_float('J', 0.) + asPlanar = (I, J) axes = (X_AXIS, Y_AXIS, Z_AXIS) if self.plane == ARC_PLANE_X_Z: - asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('IK') ] + K = gcmd.get_float('K', 0.) + asPlanar = (I, K) axes = (X_AXIS, Z_AXIS, Y_AXIS) elif self.plane == ARC_PLANE_Y_Z: - asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('JK') ] + K = gcmd.get_float('K', 0.) + asPlanar = (J, K) axes = (Y_AXIS, Z_AXIS, X_AXIS) if not (asPlanar[0] or asPlanar[1]): @@ -91,20 +93,20 @@ class ArcSupport: asF = gcmd.get_float("F", None) # Build list of linear coordinates to move - coords = self.planArc(currentPos, asTarget, asPlanar, - clockwise, *axes) + segments, coords = self.planArc(currentPos, asTarget, asPlanar, + clockwise, *axes) e_per_move = e_base = 0. if asE is not None: - if gcodestatus['absolute_extrude']: + if absolut_extrude: e_base = currentPos[3] - e_per_move = (asE - e_base) / len(coords) + e_per_move = (asE - e_base) / segments # Convert coords into G1 commands for coord in coords: g1_params = {'X': coord[0], 'Y': coord[1], 'Z': coord[2]} if e_per_move: g1_params['E'] = e_base + e_per_move - if gcodestatus['absolute_extrude']: + if absolut_extrude: e_base += e_per_move if asF is not None: g1_params['F'] = asF @@ -162,20 +164,20 @@ class ArcSupport: coords = [] for i in range(1, int(segments)): dist_Helical = i * linear_per_segment - cos_Ti = math.cos(i * theta_per_segment) - sin_Ti = math.sin(i * theta_per_segment) + c_theta = i * theta_per_segment + cos_Ti = math.cos(c_theta) + sin_Ti = math.sin(c_theta) r_P = -offset[0] * cos_Ti + offset[1] * sin_Ti r_Q = -offset[0] * sin_Ti - offset[1] * cos_Ti - # Coord doesn't support index assignment, create list - c = [None, None, None, None] + c = [None, None, None] c[alpha_axis] = center_P + r_P c[beta_axis] = center_Q + r_Q c[helical_axis] = currentPos[helical_axis] + dist_Helical - coords.append(self.Coord(*c)) + coords.append(c) coords.append(targetPos) - return coords + return segments, coords def load_config(config): return ArcSupport(config) From 3f2ef88eb9f8a24ba870933108b570da616428e2 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 1 Aug 2024 22:34:10 +0200 Subject: [PATCH 099/111] gcode_arc: merge coords gen & G1 emit Chopping lines from arc can take significant time. Merge cycles to make the event loop progress and optimize performance. Signed-off-by: Timofey Titovets --- klippy/extras/gcode_arcs.py | 56 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/klippy/extras/gcode_arcs.py b/klippy/extras/gcode_arcs.py index df68954e7..3917dac30 100644 --- a/klippy/extras/gcode_arcs.py +++ b/klippy/extras/gcode_arcs.py @@ -89,29 +89,9 @@ class ArcSupport: if not (asPlanar[0] or asPlanar[1]): raise gcmd.error("G2/G3 requires IJ, IK or JK parameters") - asE = gcmd.get_float("E", None) - asF = gcmd.get_float("F", None) - - # Build list of linear coordinates to move - segments, coords = self.planArc(currentPos, asTarget, asPlanar, - clockwise, *axes) - e_per_move = e_base = 0. - if asE is not None: - if absolut_extrude: - e_base = currentPos[3] - e_per_move = (asE - e_base) / segments - - # Convert coords into G1 commands - for coord in coords: - g1_params = {'X': coord[0], 'Y': coord[1], 'Z': coord[2]} - if e_per_move: - g1_params['E'] = e_base + e_per_move - if absolut_extrude: - e_base += e_per_move - if asF is not None: - g1_params['F'] = asF - g1_gcmd = self.gcode.create_gcode_command("G1", "G1", g1_params) - self.gcode_move.cmd_G1(g1_gcmd) + # Build linear coordinates to move + self.planArc(currentPos, asTarget, asPlanar, clockwise, + gcmd, absolut_extrude, *axes) # function planArc() originates from marlin plan_arc() # https://github.com/MarlinFirmware/Marlin @@ -122,6 +102,7 @@ class ArcSupport: # # alpha and beta axes are the current plane, helical axis is linear travel def planArc(self, currentPos, targetPos, offset, clockwise, + gcmd, absolut_extrude, alpha_axis, beta_axis, helical_axis): # todo: sometimes produces full circles @@ -161,8 +142,17 @@ class ArcSupport: # Generate coordinates theta_per_segment = angular_travel / segments linear_per_segment = linear_travel / segments - coords = [] - for i in range(1, int(segments)): + + asE = gcmd.get_float("E", None) + asF = gcmd.get_float("F", None) + + e_per_move = e_base = 0. + if asE is not None: + if absolut_extrude: + e_base = currentPos[3] + e_per_move = (asE - e_base) / segments + + for i in range(1, int(segments) + 1): dist_Helical = i * linear_per_segment c_theta = i * theta_per_segment cos_Ti = math.cos(c_theta) @@ -174,10 +164,20 @@ class ArcSupport: c[alpha_axis] = center_P + r_P c[beta_axis] = center_Q + r_Q c[helical_axis] = currentPos[helical_axis] + dist_Helical - coords.append(c) - coords.append(targetPos) - return segments, coords + + if i == segments: + c = targetPos + # Convert coords into G1 commands + g1_params = {'X': c[0], 'Y': c[1], 'Z': c[2]} + if e_per_move: + g1_params['E'] = e_base + e_per_move + if absolut_extrude: + e_base += e_per_move + if asF is not None: + g1_params['F'] = asF + g1_gcmd = self.gcode.create_gcode_command("G1", "G1", g1_params) + self.gcode_move.cmd_G1(g1_gcmd) def load_config(config): return ArcSupport(config) From c0edfbc4ea02d7d0fb5824a686b4376a05972266 Mon Sep 17 00:00:00 2001 From: Bevan Weiss Date: Thu, 15 Aug 2024 12:14:19 +1000 Subject: [PATCH 100/111] src: Current code produces warnings for possible value overflows. (#6665) As the input values are uint8_t types, any shift may result in value loss. Explicit promotion to the output type (uint32_t) keeps things safe. Have also changed the int32_t in ads1220_read_adc to uint32_t, type promotion and bit manipulation are a bit 'weird' on signed integers, so keep it as an unsigned to align with following function call parameter type. Have retained the prior explicit sign extension logic however. Signed-off-by: Bevan Weiss --- src/sensor_ads1220.c | 4 +++- src/sensor_hx71x.c | 4 ++-- src/sensor_ldc1612.c | 5 ++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/sensor_ads1220.c b/src/sensor_ads1220.c index 044980c75..ea33379a0 100644 --- a/src/sensor_ads1220.c +++ b/src/sensor_ads1220.c @@ -86,7 +86,9 @@ ads1220_read_adc(struct ads1220_adc *ads1220, uint8_t oid) barrier(); // create 24 bit int from bytes - int32_t counts = (msg[0] << 16) | (msg[1] << 8) | msg[2]; + uint32_t counts = ((uint32_t)msg[0] << 16) + | ((uint32_t)msg[1] << 8) + | ((uint32_t)msg[2]); // extend 2's complement 24 bits to 32bits if (counts & 0x800000) diff --git a/src/sensor_hx71x.c b/src/sensor_hx71x.c index 4f0a8c5bb..90c964019 100644 --- a/src/sensor_hx71x.c +++ b/src/sensor_hx71x.c @@ -27,8 +27,8 @@ struct hx71x_adc { }; #define BYTES_PER_SAMPLE 4 -#define SAMPLE_ERROR_DESYNC 1 << 31 -#define SAMPLE_ERROR_READ_TOO_LONG 1 << 30 +#define SAMPLE_ERROR_DESYNC 1L << 31 +#define SAMPLE_ERROR_READ_TOO_LONG 1L << 30 static struct task_wake wake_hx71x; diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 01cf3ee04..45e8b84e2 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -180,7 +180,10 @@ ldc1612_query(struct ldc1612 *ld, uint8_t oid) ld->sb.data_count += BYTES_PER_SAMPLE; // Check for endstop trigger - uint32_t data = (d[0] << 24L) | (d[1] << 16L) | (d[2] << 8) | d[3]; + uint32_t data = ((uint32_t)d[0] << 24) + | ((uint32_t)d[1] << 16) + | ((uint32_t)d[2] << 8) + | ((uint32_t)d[3]); check_home(ld, data); // Flush local buffer if needed From d5e5a6da2defb357ac986fb2a79003a8e1dd738d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 2 Aug 2024 13:44:26 -0400 Subject: [PATCH 101/111] hx71x: Update api header and docs to correctly note "value" field Update both hx71x and ads1220 to reflect that there is a third "value" field in the reported data. Signed-off-by: Kevin O'Connor --- docs/API_Server.md | 10 ++++++---- klippy/extras/ads1220.py | 4 ++-- klippy/extras/hx71x.py | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/API_Server.md b/docs/API_Server.md index f29bbeba5..3837f737e 100644 --- a/docs/API_Server.md +++ b/docs/API_Server.md @@ -375,9 +375,10 @@ A request may look like: `{"id": 123, "method":"hx71x/dump_hx71x", "params": {"sensor": "load_cell", "response_template": {}}}` and might return: -`{"id": 123,"result":{"header":["time","counts"]}}` +`{"id": 123,"result":{"header":["time","counts","value"]}}` and might later produce asynchronous messages such as: -`{"params":{"data":[[3292.432935, 562534], [3292.4394937, 5625322]]}}` +`{"params":{"data":[[3292.432935, 562534, 0.067059278], +[3292.4394937, 5625322, 0.670590639]]}}` ### ads1220/dump_ads1220 @@ -390,9 +391,10 @@ A request may look like: `{"id": 123, "method":"ads1220/dump_ads1220", "params": {"sensor": "load_cell", "response_template": {}}}` and might return: -`{"id": 123,"result":{"header":["time","counts"]}}` +`{"id": 123,"result":{"header":["time","counts","value"]}}` and might later produce asynchronous messages such as: -`{"params":{"data":[[3292.432935, 562534], [3292.4394937, 5625322]]}}` +`{"params":{"data":[[3292.432935, 562534, 0.067059278], +[3292.4394937, 5625322, 0.670590639]]}}` ### pause_resume/cancel diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index fba741818..14d47581e 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -69,9 +69,9 @@ class ADS1220(): self.printer, self._process_batch, self._start_measurements, self._finish_measurements, UPDATE_INTERVAL) # publish raw samples to the socket + hdr = {'header': ('time', 'counts', 'value')} self.batch_bulk.add_mux_endpoint("ads1220/dump_ads1220", "sensor", - self.name, - {'header': ('time', 'counts')}) + self.name, hdr) # Command Configuration mcu.add_config_cmd( "config_ads1220 oid=%d spi_oid=%d data_ready_pin=%s" diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index 1b1128ac8..85eff85f9 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -53,8 +53,8 @@ class HX71xBase(): self._finish_measurements, UPDATE_INTERVAL) # publish raw samples to the socket dump_path = "%s/dump_%s" % (sensor_type, sensor_type) - self.batch_bulk.add_mux_endpoint(dump_path, "sensor", self.name, - {'header': ('time', 'counts')}) + hdr = {'header': ('time', 'counts', 'value')} + self.batch_bulk.add_mux_endpoint(dump_path, "sensor", self.name, hdr) # Command Configuration self.query_hx71x_cmd = None mcu.add_config_cmd( From d81eb557d7f2aad5e22e3633fbcd53a4d60ac5d1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 2 Aug 2024 13:54:06 -0400 Subject: [PATCH 102/111] sensor_hx71x: Signal an overflow from the timer handler Check for overflows in the timer handler instead of checking the elapsed query time. This should be a better check as it also accounts for task delays that occur before the query starts. Signed-off-by: Kevin O'Connor --- src/sensor_hx71x.c | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/sensor_hx71x.c b/src/sensor_hx71x.c index 90c964019..f20d88072 100644 --- a/src/sensor_hx71x.c +++ b/src/sensor_hx71x.c @@ -18,7 +18,7 @@ struct hx71x_adc { struct timer timer; uint8_t gain_channel; // the gain+channel selection (1-4) - uint8_t pending_flag; + uint8_t flags; uint32_t rest_ticks; uint32_t last_error; struct gpio_in dout; // pin used to receive data from the hx71x @@ -26,6 +26,10 @@ struct hx71x_adc { struct sensor_bulk sb; }; +enum { + HX_PENDING = 1<<0, HX_OVERFLOW = 1<<1, +}; + #define BYTES_PER_SAMPLE 4 #define SAMPLE_ERROR_DESYNC 1L << 31 #define SAMPLE_ERROR_READ_TOO_LONG 1L << 30 @@ -107,12 +111,14 @@ hx71x_event(struct timer *timer) { struct hx71x_adc *hx71x = container_of(timer, struct hx71x_adc, timer); uint32_t rest_ticks = hx71x->rest_ticks; - if (hx71x->pending_flag) { + uint8_t flags = hx71x->flags; + if (flags & HX_PENDING) { hx71x->sb.possible_overflows++; + hx71x->flags = HX_PENDING | HX_OVERFLOW; rest_ticks *= 4; } else if (hx71x_is_data_ready(hx71x)) { // New sample pending - hx71x->pending_flag = 1; + hx71x->flags = HX_PENDING; sched_wake_task(&wake_hx71x); rest_ticks *= 8; } @@ -139,12 +145,15 @@ add_sample(struct hx71x_adc *hx71x, uint8_t oid, uint32_t counts, static void hx71x_read_adc(struct hx71x_adc *hx71x, uint8_t oid) { - uint32_t start = timer_read_time(); // Read from sensor uint_fast8_t gain_channel = hx71x->gain_channel; uint32_t adc = hx71x_raw_read(hx71x->dout, hx71x->sclk, 24 + gain_channel); - hx71x->pending_flag = 0; - barrier(); + + // Clear pending flag (and note if an overflow occurred) + irq_disable(); + uint8_t flags = hx71x->flags; + hx71x->flags = 0; + irq_enable(); // Extract report from raw data uint32_t counts = adc >> gain_channel; @@ -156,7 +165,7 @@ hx71x_read_adc(struct hx71x_adc *hx71x, uint8_t oid) if ((adc & extras_mask) != extras_mask) { // Transfer did not complete correctly hx71x->last_error = SAMPLE_ERROR_DESYNC; - } else if ((timer_read_time() - start) > (hx71x->rest_ticks * 8)) { + } else if (flags & HX_OVERFLOW) { // Transfer took too long hx71x->last_error = SAMPLE_ERROR_READ_TOO_LONG; } @@ -177,7 +186,6 @@ command_config_hx71x(uint32_t *args) struct hx71x_adc *hx71x = oid_alloc(args[0] , command_config_hx71x, sizeof(*hx71x)); hx71x->timer.func = hx71x_event; - hx71x->pending_flag = 0; uint8_t gain_channel = args[1]; if (gain_channel < 1 || gain_channel > 4) { shutdown("HX71x gain/channel out of range 1-4"); @@ -197,7 +205,7 @@ command_query_hx71x(uint32_t *args) uint8_t oid = args[0]; struct hx71x_adc *hx71x = oid_lookup(oid, command_config_hx71x); sched_del_timer(&hx71x->timer); - hx71x->pending_flag = 0; + hx71x->flags = 0; hx71x->last_error = 0; hx71x->rest_ticks = args[1]; if (!hx71x->rest_ticks) { @@ -238,7 +246,7 @@ hx71x_capture_task(void) uint8_t oid; struct hx71x_adc *hx71x; foreach_oid(oid, hx71x, command_config_hx71x) { - if (hx71x->pending_flag) + if (hx71x->flags) hx71x_read_adc(hx71x, oid); } } From 3e55008323a87d11ba8833319759e6219b293432 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 14 Jul 2024 00:35:22 +0200 Subject: [PATCH 103/111] bme280.py: iir_filter mask input value Signed-off-by: Timofey Titovets --- klippy/extras/bme280.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/klippy/extras/bme280.py b/klippy/extras/bme280.py index 262dc130f..c09e4b142 100644 --- a/klippy/extras/bme280.py +++ b/klippy/extras/bme280.py @@ -143,6 +143,7 @@ class BME280: pow(2, self.os_temp - 1), pow(2, self.os_hum - 1), pow(2, self.os_pres - 1))) logging.info("BMxx80: IIR: %dx" % (pow(2, self.iir_filter) - 1)) + self.iir_filter = self.iir_filter & 0x07 self.temp = self.pressure = self.humidity = self.gas = self.t_fine = 0. self.min_temp = self.max_temp = self.range_switching_error = 0. @@ -326,7 +327,7 @@ class BME280: self.chip_registers = BME280_REGS if self.chip_type in ('BME680', 'BME280'): - self.write_register('CONFIG', (self.iir_filter & 0x07) << 2) + self.write_register('CONFIG', self.iir_filter << 2) # Read out and calculate the trimming parameters if self.chip_type == 'BMP180': From 9e45ec222e34b48217f7423a4a8c64a8d21a80c2 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 14 Jul 2024 00:37:55 +0200 Subject: [PATCH 104/111] bme280.py: drop unused max_sample_time Signed-off-by: Timofey Titovets --- klippy/extras/bme280.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/klippy/extras/bme280.py b/klippy/extras/bme280.py index c09e4b142..7fda219cd 100644 --- a/klippy/extras/bme280.py +++ b/klippy/extras/bme280.py @@ -298,11 +298,9 @@ class BME280: self.sample_timer = self.reactor.register_timer(self._sample_bme680) self.chip_registers = BME680_REGS elif self.chip_type == 'BMP180': - self.max_sample_time = (1.25 + ((2.3 * self.os_pres) + .575)) / 1000 self.sample_timer = self.reactor.register_timer(self._sample_bmp180) self.chip_registers = BMP180_REGS elif self.chip_type == 'BMP388': - self.max_sample_time = 0.5 self.chip_registers = BMP388_REGS self.write_register( "PWR_CTRL", From ff3eed2ad8ca54394b96b238ab48fe0b72171c64 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 13 Jul 2024 22:40:23 +0200 Subject: [PATCH 105/111] bme280: use periodic mode for BM[PE]280 Signed-off-by: Timofey Titovets --- klippy/extras/bme280.py | 78 +++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/klippy/extras/bme280.py b/klippy/extras/bme280.py index 7fda219cd..e4fc358ba 100644 --- a/klippy/extras/bme280.py +++ b/klippy/extras/bme280.py @@ -83,6 +83,7 @@ BMP180_REGS = { STATUS_MEASURING = 1 << 3 STATUS_IM_UPDATE = 1 MODE = 1 +MODE_PERIODIC = 3 RUN_GAS = 1 << 4 NB_CONV_0 = 0 EAS_NEW_DATA = 1 << 7 @@ -282,7 +283,7 @@ class BME280: self.chip_type, self.i2c.i2c_address)) # Reset chip - self.write_register('RESET', [RESET_CHIP_VALUE]) + self.write_register('RESET', [RESET_CHIP_VALUE], wait=True) self.reactor.pause(self.reactor.monotonic() + .5) # Make sure non-volatile memory has been copied to registers @@ -317,14 +318,20 @@ class BME280: self.write_register("INT_CTRL", [BMP388_REG_VAL_DRDY_EN]) self.sample_timer = self.reactor.register_timer(self._sample_bmp388) - else: + elif self.chip_type == 'BME280': self.max_sample_time = \ (1.25 + (2.3 * self.os_temp) + ((2.3 * self.os_pres) + .575) + ((2.3 * self.os_hum) + .575)) / 1000 self.sample_timer = self.reactor.register_timer(self._sample_bme280) self.chip_registers = BME280_REGS + else: + self.max_sample_time = \ + (1.25 + (2.3 * self.os_temp) + + ((2.3 * self.os_pres) + .575)) / 1000 + self.sample_timer = self.reactor.register_timer(self._sample_bme280) + self.chip_registers = BME280_REGS - if self.chip_type in ('BME680', 'BME280'): + if self.chip_type == 'BME680': self.write_register('CONFIG', self.iir_filter << 2) # Read out and calculate the trimming parameters @@ -346,21 +353,53 @@ class BME280: elif self.chip_type == 'BMP388': self.dig = read_calibration_data_bmp388(cal_1) + if self.chip_type in ('BME280', 'BMP280'): + max_standby_time = REPORT_TIME - self.max_sample_time + # 0.5 ms + t_sb = 0 + if self.chip_type == 'BME280': + if max_standby_time > 1: + t_sb = 5 + elif max_standby_time > 0.5: + t_sb = 4 + elif max_standby_time > 0.25: + t_sb = 3 + elif max_standby_time > 0.125: + t_sb = 2 + elif max_standby_time > 0.0625: + t_sb = 1 + elif max_standby_time > 0.020: + t_sb = 7 + elif max_standby_time > 0.010: + t_sb = 6 + else: + if max_standby_time > 4: + t_sb = 7 + elif max_standby_time > 2: + t_sb = 6 + elif max_standby_time > 1: + t_sb = 5 + elif max_standby_time > 0.5: + t_sb = 4 + elif max_standby_time > 0.25: + t_sb = 3 + elif max_standby_time > 0.125: + t_sb = 2 + elif max_standby_time > 0.0625: + t_sb = 1 + + cfg = t_sb << 5 | self.iir_filter << 2 + self.write_register('CONFIG', cfg) + if self.chip_type == 'BME280': + self.write_register('CTRL_HUM', self.os_hum) + # Enter normal (periodic) mode + meas = self.os_temp << 5 | self.os_pres << 2 | MODE_PERIODIC + self.write_register('CTRL_MEAS', meas, wait=True) + def _sample_bme280(self, eventtime): - # Enter forced mode - if self.chip_type == 'BME280': - self.write_register('CTRL_HUM', self.os_hum) - meas = self.os_temp << 5 | self.os_pres << 2 | MODE - self.write_register('CTRL_MEAS', meas) - + # In normal mode data shadowing is performed + # So reading can be done while measurements are in process try: - # wait until results are ready - status = self.read_register('STATUS', 1)[0] - while status & STATUS_MEASURING: - self.reactor.pause( - self.reactor.monotonic() + self.max_sample_time) - status = self.read_register('STATUS', 1)[0] - if self.chip_type == 'BME280': data = self.read_register('PRESSURE_MSB', 8) elif self.chip_type == 'BMP280': @@ -718,12 +757,15 @@ class BME280: params = self.i2c.i2c_read(regs, read_len) return bytearray(params['response']) - def write_register(self, reg_name, data): + def write_register(self, reg_name, data, wait = False): if type(data) is not list: data = [data] reg = self.chip_registers[reg_name] data.insert(0, reg) - self.i2c.i2c_write(data) + if not wait: + self.i2c.i2c_write(data) + else: + self.i2c.i2c_write_wait_ack(data) def get_status(self, eventtime): data = { From f9d7a71195c8d2d4b423a6abeb30d287ada84095 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 13 Jul 2024 23:27:22 +0200 Subject: [PATCH 106/111] bme680: select mode once Signed-off-by: Timofey Titovets --- klippy/extras/bme280.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/klippy/extras/bme280.py b/klippy/extras/bme280.py index e4fc358ba..f55ee5187 100644 --- a/klippy/extras/bme280.py +++ b/klippy/extras/bme280.py @@ -295,7 +295,10 @@ class BME280: status = self.read_register('STATUS', 1)[0] if self.chip_type == 'BME680': - self.max_sample_time = 0.5 + self.max_sample_time = \ + (1.25 + (2.3 * self.os_temp) + ((2.3 * self.os_pres) + .575) + + ((2.3 * self.os_hum) + .575) + + self.gas_heat_duration) / 1000 self.sample_timer = self.reactor.register_timer(self._sample_bme680) self.chip_registers = BME680_REGS elif self.chip_type == 'BMP180': @@ -331,9 +334,6 @@ class BME280: self.sample_timer = self.reactor.register_timer(self._sample_bme280) self.chip_registers = BME280_REGS - if self.chip_type == 'BME680': - self.write_register('CONFIG', self.iir_filter << 2) - # Read out and calculate the trimming parameters if self.chip_type == 'BMP180': cal_1 = self.read_register('CAL_1', 22) @@ -396,6 +396,19 @@ class BME280: meas = self.os_temp << 5 | self.os_pres << 2 | MODE_PERIODIC self.write_register('CTRL_MEAS', meas, wait=True) + if self.chip_type == 'BME680': + self.write_register('CONFIG', self.iir_filter << 2) + # Should be set once and reused on every mode register write + self.write_register('CTRL_HUM', self.os_hum & 0x07) + gas_wait_0 = self._calc_gas_heater_duration(self.gas_heat_duration) + self.write_register('GAS_WAIT_0', [gas_wait_0]) + res_heat_0 = self._calc_gas_heater_resistance(self.gas_heat_temp) + self.write_register('RES_HEAT_0', [res_heat_0]) + gas_config = RUN_GAS | NB_CONV_0 + self.write_register('CTRL_GAS_1', [gas_config]) + # Set initial heater current to reach Gas heater target on start + self.write_register('IDAC_HEAT_0', 96) + def _sample_bme280(self, eventtime): # In normal mode data shadowing is performed # So reading can be done while measurements are in process @@ -500,17 +513,6 @@ class BME280: return comp_press def _sample_bme680(self, eventtime): - self.write_register('CTRL_HUM', self.os_hum & 0x07) - meas = self.os_temp << 5 | self.os_pres << 2 - self.write_register('CTRL_MEAS', [meas]) - - gas_wait_0 = self._calculate_gas_heater_duration(self.gas_heat_duration) - self.write_register('GAS_WAIT_0', [gas_wait_0]) - res_heat_0 = self._calculate_gas_heater_resistance(self.gas_heat_temp) - self.write_register('RES_HEAT_0', [res_heat_0]) - gas_config = RUN_GAS | NB_CONV_0 - self.write_register('CTRL_GAS_1', [gas_config]) - def data_ready(stat): new_data = (stat & EAS_NEW_DATA) gas_done = not (stat & GAS_DONE) @@ -518,8 +520,9 @@ class BME280: return new_data and gas_done and meas_done # Enter forced mode - meas = meas | MODE - self.write_register('CTRL_MEAS', meas) + meas = self.os_temp << 5 | self.os_pres << 2 | MODE + self.write_register('CTRL_MEAS', meas, wait=True) + self.reactor.pause(self.reactor.monotonic() + self.max_sample_time) try: # wait until results are ready status = self.read_register('EAS_STATUS_0', 1)[0] @@ -681,7 +684,7 @@ class BME280: gas_raw - 512. + var1) return gas - def _calculate_gas_heater_resistance(self, target_temp): + def _calc_gas_heater_resistance(self, target_temp): amb_temp = self.temp heater_data = self.read_register('RES_HEAT_VAL', 3) res_heat_val = get_signed_byte(heater_data[0]) @@ -696,7 +699,7 @@ class BME280: * (1. / (1. + (res_heat_val * 0.002)))) - 25)) return int(res_heat) - def _calculate_gas_heater_duration(self, duration_ms): + def _calc_gas_heater_duration(self, duration_ms): if duration_ms >= 4032: duration_reg = 0xff else: From 81de9a8615df667ae2ea6d2b0f9204e7f3b09bcc Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 21 Jul 2024 15:35:55 +0200 Subject: [PATCH 107/111] bme680: measure gas VOC once a while Signed-off-by: Timofey Titovets --- klippy/extras/bme280.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/klippy/extras/bme280.py b/klippy/extras/bme280.py index f55ee5187..1c26bbee7 100644 --- a/klippy/extras/bme280.py +++ b/klippy/extras/bme280.py @@ -157,6 +157,7 @@ class BME280: return self.printer.register_event_handler("klippy:connect", self.handle_connect) + self.last_gas_time = 0 def handle_connect(self): self._init_bmxx80() @@ -297,8 +298,7 @@ class BME280: if self.chip_type == 'BME680': self.max_sample_time = \ (1.25 + (2.3 * self.os_temp) + ((2.3 * self.os_pres) + .575) - + ((2.3 * self.os_hum) + .575) - + self.gas_heat_duration) / 1000 + + ((2.3 * self.os_hum) + .575)) / 1000 self.sample_timer = self.reactor.register_timer(self._sample_bme680) self.chip_registers = BME680_REGS elif self.chip_type == 'BMP180': @@ -404,8 +404,6 @@ class BME280: self.write_register('GAS_WAIT_0', [gas_wait_0]) res_heat_0 = self._calc_gas_heater_resistance(self.gas_heat_temp) self.write_register('RES_HEAT_0', [res_heat_0]) - gas_config = RUN_GAS | NB_CONV_0 - self.write_register('CTRL_GAS_1', [gas_config]) # Set initial heater current to reach Gas heater target on start self.write_register('IDAC_HEAT_0', 96) @@ -513,26 +511,40 @@ class BME280: return comp_press def _sample_bme680(self, eventtime): - def data_ready(stat): + def data_ready(stat, run_gas): new_data = (stat & EAS_NEW_DATA) gas_done = not (stat & GAS_DONE) meas_done = not (stat & MEASURE_DONE) + if not run_gas: + gas_done = True return new_data and gas_done and meas_done + run_gas = False + # Check VOC once a while + if self.reactor.monotonic() - self.last_gas_time > 3: + gas_config = RUN_GAS | NB_CONV_0 + self.write_register('CTRL_GAS_1', [gas_config]) + run_gas = True + # Enter forced mode meas = self.os_temp << 5 | self.os_pres << 2 | MODE self.write_register('CTRL_MEAS', meas, wait=True) - self.reactor.pause(self.reactor.monotonic() + self.max_sample_time) + max_sample_time = self.max_sample_time + if run_gas: + max_sample_time += self.gas_heat_duration / 1000 + self.reactor.pause(self.reactor.monotonic() + max_sample_time) try: # wait until results are ready status = self.read_register('EAS_STATUS_0', 1)[0] - while not data_ready(status): + while not data_ready(status, run_gas): self.reactor.pause( self.reactor.monotonic() + self.max_sample_time) status = self.read_register('EAS_STATUS_0', 1)[0] data = self.read_register('PRESSURE_MSB', 8) - gas_data = self.read_register('GAS_R_MSB', 2) + gas_data = [0, 0] + if run_gas: + gas_data = self.read_register('GAS_R_MSB', 2) except Exception: logging.exception("BME680: Error reading data") self.temp = self.pressure = self.humidity = self.gas = .0 @@ -556,6 +568,10 @@ class BME280: gas_raw = (gas_data[0] << 2) | ((gas_data[1] & 0xC0) >> 6) gas_range = (gas_data[1] & 0x0F) self.gas = self._compensate_gas(gas_raw, gas_range) + # Disable gas measurement on success + gas_config = NB_CONV_0 + self.write_register('CTRL_GAS_1', [gas_config]) + self.last_gas_time = self.reactor.monotonic() if self.temp < self.min_temp or self.temp > self.max_temp: self.printer.invoke_shutdown( From f71d2c7cfc97d3e7964403463e513ac7f9b90050 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 29 Aug 2024 22:31:13 -0400 Subject: [PATCH 108/111] stm32: Fix setting USB clock with USB to CANbus mode on stm32g4/stm32l4 Signed-off-by: Kevin O'Connor --- src/stm32/stm32g4.c | 2 +- src/stm32/stm32l4.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stm32/stm32g4.c b/src/stm32/stm32g4.c index 1eed3ec18..5255cb19a 100644 --- a/src/stm32/stm32g4.c +++ b/src/stm32/stm32g4.c @@ -104,7 +104,7 @@ enable_clock_stm32g4(void) RCC->CR |= RCC_CR_PLLON; // Enable 48Mhz USB clock using clock recovery - if (CONFIG_USBSERIAL) { + if (CONFIG_USB) { RCC->CRRCR |= RCC_CRRCR_HSI48ON; while (!(RCC->CRRCR & RCC_CRRCR_HSI48RDY)) ; diff --git a/src/stm32/stm32l4.c b/src/stm32/stm32l4.c index 7db15fff0..ae099d6bc 100644 --- a/src/stm32/stm32l4.c +++ b/src/stm32/stm32l4.c @@ -96,7 +96,7 @@ enable_clock_stm32l4(void) RCC->CR |= RCC_CR_PLLON; // Enable 48Mhz USB clock using clock recovery - if (CONFIG_USBSERIAL) { + if (CONFIG_USB) { RCC->CRRCR |= RCC_CRRCR_HSI48ON; while (!(RCC->CRRCR & RCC_CRRCR_HSI48RDY)) ; From 40d6a06f8f4b55e089c96cb16cce563e3a5f1fcf Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Mon, 12 Aug 2024 10:49:55 -0400 Subject: [PATCH 109/111] temperature_probe: add max_valid_temp option Signed-off-by: Eric Callahan --- klippy/extras/temperature_probe.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py index ae285ce36..05eac34ef 100644 --- a/klippy/extras/temperature_probe.py +++ b/klippy/extras/temperature_probe.py @@ -490,6 +490,7 @@ class EddyDriftCompensation: self.cal_temp = config.getfloat("calibration_temp", 0.) self.drift_calibration = None self.calibration_samples = None + self.max_valid_temp = config.getfloat("max_validation_temp", 60.) self.dc_min_temp = config.getfloat("drift_calibration_min_temp", 0.) dc = config.getlists( "drift_calibration", None, seps=(',', '\n'), parser=float @@ -503,7 +504,8 @@ class EddyDriftCompensation: ) self.drift_calibration = [Polynomial2d(*coefs) for coefs in dc] cal = self.drift_calibration - self._check_calibration(cal, self.dc_min_temp, config.error) + start_temp, end_temp = self.dc_min_temp, self.max_valid_temp + self._check_calibration(cal, start_temp, end_temp, config.error) low_poly = self.drift_calibration[-1] self.min_freq = min([low_poly(temp) for temp in range(121)]) cal_str = "\n".join([repr(p) for p in cal]) @@ -638,13 +640,15 @@ class EddyDriftCompensation: "calbration error, not enough samples" ) min_temp, _ = cal_samples[0][0] + max_temp, _ = cal_samples[-1][0] polynomials = [] for i, coords in enumerate(cal_samples): height = .05 + i * .5 poly = Polynomial2d.fit(coords) polynomials.append(poly) logging.info("Polynomial at Z=%.2f: %s" % (height, repr(poly))) - self._check_calibration(polynomials, min_temp) + end_vld_temp = max(self.max_valid_temp, max_temp) + self._check_calibration(polynomials, min_temp, end_vld_temp) coef_cfg = "\n" + "\n".join([str(p) for p in polynomials]) configfile = self.printer.lookup_object('configfile') configfile.set(self.name, "drift_calibration", coef_cfg) @@ -656,10 +660,11 @@ class EddyDriftCompensation: % (self.name, len(polynomials)) ) - def _check_calibration(self, calibration, start_temp, error=None): + def _check_calibration(self, calibration, start_temp, end_temp, error=None): error = error or self.printer.command_error start = int(start_temp) - for temp in range(start, 121, 1): + end = int(end_temp) + 1 + for temp in range(start, end, 1): last_freq = calibration[0](temp) for i, poly in enumerate(calibration[1:]): next_freq = poly(temp) From 08a1c9f12760ee6d89db2b82e76c7d93453212db Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Mon, 12 Aug 2024 10:56:49 -0400 Subject: [PATCH 110/111] docs: update temperature_probe documentation Add documentation for the "max_valid_temp" option. Signed-off-by: Eric Callahan --- docs/Config_Reference.md | 4 ++++ docs/Eddy_Probe.md | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index f19f0ebc1..e4c709375 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2458,6 +2458,10 @@ postfix for both sections. # "calibration_extruder_temp" option is set. Its recommended to heat # the extruder some distance from the bed to minimize its impact on # the probe coil temperature. The default is 50. +#max_validation_temp: 60. +# The maximum temperature used to validate the calibration. It is +# recommended to set this to a value between 100 and 120 for enclosed +# printers. The default is 60. ``` ## Temperature sensors diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 5fa7fc4d3..3c36a613c 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -78,7 +78,9 @@ for further details on how to configure a `temperature_probe`. It is advised to configure the `calibration_position`, `calibration_extruder_temp`, `extruder_heating_z`, and `calibration_bed_temp` options, as doing so will automate some of the -steps outlined below. +steps outlined below. If the printer to be calibrated is enclosed, it +is strongly recommended to set the `max_validation_temp` option to a value +between 100 and 120. Eddy probe manufacturers may offer a stock drift calibration that can be manually added to `drift_calibration` option of the `[probe_eddy_current]` From 14a83103c32fe1590b5b0416f71d1445648c58ed Mon Sep 17 00:00:00 2001 From: Bevan Weiss Date: Fri, 6 Sep 2024 06:50:32 +1000 Subject: [PATCH 111/111] flashsd: Add support for chitu-v6 (#6671) Add flashsd configuration for Tronxy x5sa and other printers based on Chitu v6 board. These boards should support sdio (this is what the schematic details), however I couldn't get this working from a quick try. Signed-off-by: Bevan Weiss --- scripts/spi_flash/board_defs.py | 14 +++++++++++++- scripts/spi_flash/spi_flash.py | 23 +++++++++++------------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index c0a8b5772..9924fefcd 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -46,6 +46,7 @@ BOARD_DEFS = { 'mcu': "stm32f103xe", 'spi_bus': "spi2", "cs_pin": "PA15", + "conversion_script": "scripts/update_mks_robin.py", "firmware_path": "Robin_e3.bin", "current_firmware_path": "Robin_e3.cur" }, @@ -133,6 +134,16 @@ BOARD_DEFS = { 'mcu': "stm32g0b1xx", 'spi_bus': "spi1", "cs_pin": "PB8" + }, + 'chitu-v6': { + 'mcu': "stm32f103xe", + 'spi_bus': "swspi", + 'spi_pins': "PC8,PD2,PC12", + "cs_pin": "PC11", + #'sdio_bus': 'sdio', + "conversion_script": "scripts/update_chitu.py", + "firmware_path": "update.cbd", + 'skip_verify': True } } @@ -182,7 +193,8 @@ BOARD_ALIASES = { 'fysetc-s6-v1.2': BOARD_DEFS['fysetc-spider'], 'fysetc-s6-v2': BOARD_DEFS['fysetc-spider'], 'robin_v3': BOARD_DEFS['monster8'], - 'btt-skrat-v1.0': BOARD_DEFS['btt-skrat'] + 'btt-skrat-v1.0': BOARD_DEFS['btt-skrat'], + 'chitu-v6': BOARD_DEFS['chitu-v6'] } def list_boards(): diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index a3231b693..cbe769e57 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -74,20 +74,19 @@ def translate_serial_to_tty(device): return ttyname, ttyname def check_need_convert(board_name, config): - if board_name.lower().startswith('mks-robin-e3'): - # we need to convert this file - robin_util = os.path.join( - fatfs_lib.KLIPPER_DIR, "scripts/update_mks_robin.py") - klipper_bin = config['klipper_bin_path'] - robin_bin = os.path.join( + conv_script = config.get("conversion_script") + if conv_script is None: + return + conv_util = os.path.join(fatfs_lib.KLIPPER_DIR, conv_script) + klipper_bin = config['klipper_bin_path'] + dest_bin = os.path.join( os.path.dirname(klipper_bin), os.path.basename(config['firmware_path'])) - cmd = "%s %s %s %s" % (sys.executable, robin_util, klipper_bin, - robin_bin) - output("Converting Klipper binary to MKS Robin format...") - os.system(cmd) - output_line("Done") - config['klipper_bin_path'] = robin_bin + cmd = "%s %s %s %s" % (sys.executable, conv_util, klipper_bin, dest_bin) + output("Converting Klipper binary to custom format...") + os.system(cmd) + output_line("Done") + config['klipper_bin_path'] = dest_bin ###########################################################