From b97b4bdbf583e8d3932467f627a420632e92e995 Mon Sep 17 00:00:00 2001 From: Rogerio Goncalves Date: Tue, 23 Apr 2024 16:26:00 -0400 Subject: [PATCH 1/2] update to latest Klipper3d/klipper (2f6e94c) (#216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * stm32: fix support for USARTs on STM32G0B0 Signed-off-by: Robert Cambridge * makefile: Replace CFLAGS -I with -iquote The -iquote tells GCC to only search that path when resolving a quoted "include" (vs ) which by convention imples a include from the projects own soruce tree. This prevents a conflict between Klippers "sched.h" and "gpio.h" and and glibc . Signed-off-by: Michael 'ASAP' Weinrich * linux: Don't use absolute paths for include Not all systems (i.e. Nix) repect the standard Linux filesystem hierarchy, instead relative paths should be used and allowing GCC to rely on it's builtin search paths. Signed-off-by: Michael 'ASAP' Weinrich * manual_stepper: Add basic status. (#6527) Adding position and enabled in manual_stepper status. Enabled is already available through stepper_enable object. But this makes it more straightforward to access it. Signed-off-by: Viesturs Zarins * klippy: remove a few unused variable assignments (#6504) Signed-off-by: Kamil Domański * mcu: Separate trdispatch handling from MCU_endstop class Create a new TriggerDispatch class to track the low-level handling of the trdispatch mechanism. Signed-off-by: Kevin O'Connor * probe: Add a probing_move() wrapper to low-level mcu_probe class This allows the low-level probe class more control on the probing implementation. Signed-off-by: Kevin O'Connor * sensor_ldc1612: Initial support for bulk reading ldc1612 sensor Signed-off-by: Alan.Ma from BigTreeTech Signed-off-by: Kevin O'Connor * ldc1612: Initial host support for reading ldc1612 bulk sensor data Signed-off-by: Kevin O'Connor * ldc1612: Add LDC_CALIBRATE_DRIVE_CURRENT calibration command Add a command to calibrate the sensor DRIVE_CURRENT0 register. Signed-off-by: Kevin O'Connor * probe_eddy_current: Support calibrating Z height to sensor frequency Add a calibration tool that can be used to correlate sensor frequency to bed Z height. Signed-off-by: Kevin O'Connor * probe_eddy_current: Initial support for PROBE command Signed-off-by: Kevin O'Connor * probe_eddy_current: Use sensor value at halt position for "trigger" position Calculate the sensor Z position after the probe halts and return that as the "probed position". This sensor position provides a more accurate measurement. Signed-off-by: Kevin O'Connor * docs: Add documentation for probe_eddy_current Signed-off-by: Kevin O'Connor * docs: Add a new Eddy_Probe.md document Signed-off-by: Kevin O'Connor * motan: Add support for graphing ldc1612 coil frequencies Signed-off-by: Kevin O'Connor * manual_stepper: Revert "manual_stepper: Add basic status. (#6527)" This reverts commit b029d0466841b90b54279500f70a92deacfd6c5a. The MCU_Stepper class does not have a is_motor_enabled() method, so the change above results in an internal exception. Signed-off-by: Kevin O'Connor * homing_override: Adds rawparams support Signed-off-by: Pedro Lamas * docs: Fix typo in Resonance_Compensation.md Signed-off-by: Plynskiy Nikita * config: Artillery Sidewinder X3 (#6534) Signed-off-by: Phil Timpson * virtual_sdcard: Define a default for on_gcode_error If on_gcode_error is not specified, default to running the TURN_OFF_HEATERS command. Signed-off-by: Kevin O'Connor * docs: Recommend using "ip" instead of "ifconfig" in CANBUS.md Some Linux systems do not install ifconfig, while ip should always be available. So, update the canbus documentation to recommend that. Signed-off-by: Kevin O'Connor * docs: Add information on txqueuelen to CANBUS_Troubleshooting.md Provide some background information on the Linux can interface txqueuelen parameter, errors that it can cause, and considerations when configuring it. Signed-off-by: Kevin O'Connor * adxl345: Move sample timestamp calculation to reusable code Add a new extract_samples() method to the ChipClockUpdater class that calculates the sample timestamp for each sample in a list of bulk sensor reports. Update the adxl345 code to use that extract_samples() code. Signed-off-by: Kevin O'Connor * lis2dw: Use extract_samples() for sample timestamp calculation Signed-off-by: Kevin O'Connor * mpu9250: Use extract_samples() for sample timestamp calculation Signed-off-by: Kevin O'Connor * ldc1612: Use extract_samples() for sample timestamp calculation Signed-off-by: Kevin O'Connor * bulk_sensor: Refactor ChipClockUpdater constructor Build the clock_sync and struct.Struct() in the ChipClockUpdater constructor. Signed-off-by: Kevin O'Connor * bulk_sensor: Rework ChipClockUpdater class into FixedFreqReader Move the sensor_bulk_data message queuing into the class, and then rename that class. This simplifies the users of the code. Signed-off-by: Kevin O'Connor * bulk_sensor: Rename BulkDataQueue methods Rename pull_samples() to pull_queue() and rename clear_sample() to clear_queue(). This avoids confusion between the queue of response messages and the larger list of samples stored within those messages. Signed-off-by: Kevin O'Connor * docs: Update CANBUS_Troubleshooting.md to avoid formatting error Avoid starting a line with "128." as that confused markdown. Signed-off-by: Kevin O'Connor * sht3x: Add sht31 support (#6560) Signed-off-by: Timofey Titovets * docs: Fix typo in Bed_Mesh.md (#6572) Signed-off-by: Maggi Alessandro * format --------- Signed-off-by: Robert Cambridge Signed-off-by: Michael 'ASAP' Weinrich Signed-off-by: Viesturs Zarins Signed-off-by: Kamil Domański Signed-off-by: Kevin O'Connor Signed-off-by: Alan.Ma from BigTreeTech Signed-off-by: Pedro Lamas Signed-off-by: Plynskiy Nikita Signed-off-by: Phil Timpson Signed-off-by: Timofey Titovets Signed-off-by: Maggi Alessandro Co-authored-by: Robert Cambridge Co-authored-by: Michael 'ASAP' Weinrich Co-authored-by: Viesturs Zariņš Co-authored-by: Kamil Domański Co-authored-by: Kevin O'Connor Co-authored-by: Pedro Lamas Co-authored-by: trofen <39155883+trofen@users.noreply.github.com> Co-authored-by: TheFeralEngineer <74030381+TheFeralEngineer@users.noreply.github.com> Co-authored-by: Timofey Titovets Co-authored-by: Alessandro Maggi <59124971+DicyRoll@users.noreply.github.com> --- .github/pull_request_template.md | 1 - Makefile | 5 +- ...nter-artillery-sidewinder-x3-plus-2024.cfg | 188 +++++++++ docs/Bed_Mesh.md | 7 +- docs/CANBUS.md | 4 +- docs/CANBUS_Troubleshooting.md | 50 +++ docs/Config_Changes.md | 6 + docs/Config_Reference.md | 75 +++- docs/Eddy_Probe.md | 56 +++ docs/G-Codes.md | 22 + docs/Overview.md | 1 + docs/Resonance_Compensation.md | 2 +- docs/Status_Reference.md | 3 +- docs/_klipper3d/mkdocs.yml | 1 + klippy/configfile.py | 1 - klippy/extras/adxl345.py | 79 ++-- klippy/extras/angle.py | 6 +- klippy/extras/bltouch.py | 5 + klippy/extras/bulk_sensor.py | 83 +++- klippy/extras/dotstar.py | 1 - klippy/extras/endstop_phase.py | 2 +- klippy/extras/homing_override.py | 1 + klippy/extras/idle_timeout.py | 2 +- klippy/extras/input_shaper.py | 1 - klippy/extras/ldc1612.py | 240 +++++++++++ klippy/extras/lis2dw.py | 73 +--- klippy/extras/mpu9250.py | 70 +--- klippy/extras/probe.py | 17 +- klippy/extras/probe_eddy_current.py | 382 ++++++++++++++++++ klippy/extras/sht3x.py | 168 ++++++++ klippy/extras/smart_effector.py | 4 + klippy/extras/temperature_sensors.cfg | 2 + klippy/extras/virtual_sdcard.py | 9 +- klippy/kinematics/hybrid_corexy.py | 2 +- klippy/kinematics/hybrid_corexz.py | 2 +- klippy/mcu.py | 116 ++++-- klippy/toolhead.py | 1 - scripts/motan/data_logger.py | 6 +- scripts/motan/readlog.py | 81 ++++ src/Kconfig | 9 +- src/Makefile | 1 + src/linux/gpio.c | 2 +- src/linux/main.c | 2 +- src/sensor_ldc1612.c | 207 ++++++++++ src/stm32/Kconfig | 2 +- src/stm32/stm32f0_serial.c | 7 + test/klippy/printers.test | 4 + 47 files changed, 1738 insertions(+), 271 deletions(-) create mode 100644 config/printer-artillery-sidewinder-x3-plus-2024.cfg create mode 100644 docs/Eddy_Probe.md create mode 100644 klippy/extras/ldc1612.py create mode 100644 klippy/extras/probe_eddy_current.py create mode 100644 klippy/extras/sht3x.py create mode 100644 src/sensor_ldc1612.c diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d71bc2ff9..292578f93 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,4 +7,3 @@ _Enter a good description of whats being changed and WHY - [ ] added a test case if possible - [ ] if new feature, added to the readme - [ ] ci is happy and green - diff --git a/Makefile b/Makefile index 6d0184037..735d18633 100644 --- a/Makefile +++ b/Makefile @@ -29,8 +29,9 @@ dirs-y = src cc-option=$(shell if test -z "`$(1) $(2) -S -o /dev/null -xc /dev/null 2>&1`" \ ; then echo "$(2)"; else echo "$(3)"; fi ;) -CFLAGS := -I$(OUT) -Isrc -I$(OUT)board-generic/ -std=gnu11 -O2 -MD \ - -Wall -Wold-style-definition $(call cc-option,$(CC),-Wtype-limits,) \ +CFLAGS := -iquote $(OUT) -iquote src -iquote $(OUT)board-generic/ \ + -std=gnu11 -O2 -MD -Wall \ + -Wold-style-definition $(call cc-option,$(CC),-Wtype-limits,) \ -ffunction-sections -fdata-sections -fno-delete-null-pointer-checks CFLAGS += -flto=auto -fwhole-program -fno-use-linker-plugin -ggdb3 diff --git a/config/printer-artillery-sidewinder-x3-plus-2024.cfg b/config/printer-artillery-sidewinder-x3-plus-2024.cfg new file mode 100644 index 000000000..eea1a980a --- /dev/null +++ b/config/printer-artillery-sidewinder-x3-plus-2024.cfg @@ -0,0 +1,188 @@ +# For the Artillery Sidewinder X3 Pro/Plus that came factory installed with V1.29 firmware, follow these steps. +# - Compile with the processor model STM32F401. +# - Select the 48KiB bootloader, +# - Select USB PA11/PA12 for USB communication interface. +# - Select USART2 PA3/PA2 for UART communication via the Wi-Fi Tx/Rx pins +# To set 48KiB bootloader, you need to make a change to make menuconfig Kconfig file +# Here is a link to a how-to video: https://youtu.be/dpc76zN7Dh0 +# Rename klipper.bin to yuntu.bin +# Copy the file out/yuntu.bin to an SD card and then restart the printer with that SD card +# +# For models that did not come with V1.29 installed +# - Compile with the processor model STM32F401. +# - Select the NO BOOTLOADER +# - Select USB PA11/PA12 for USB communication interface. +# - Select USART2 PA3/PA2 for UART communication via the Wi-Fi Tx/Rx pins +# - quit, save, make +# - Connect your printer to a computer running Pronterface, Octoprint, Repetier, BedLeveler5000 (anything with Console capability) +# - Power on the machine and send M997 through console into Marlin, this will put the board into "DFU" mode +# - DO NOT TURN OFF THE PRINTER +# - Connect your Linux/Klipper device to the USB port +# - Run lsusb and verify that the STM32 DFU device is visible (Bus 001 Device 006: ID 0483:df11 STMicroelectronics STM Device in DFU Mode) +# - Run sudo make flash 0483:df11 +# - Run lsusb again and there should be two devices: +# Bus 001 Device 007: ID 1d50:614e OpenMoko, Inc. stm32f401xc +# Bus 001 Device 003: ID 0cf3:e010 Qualcomm Atheros Communications stm32f401xc +# See docs/Config_Reference.md for a description of parameters. + +[mcu] +serial: /dev/ttyACM0 +restart_method: command + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 15 +max_z_accel: 100 +square_corner_velocity: 5 + +[led LED_Light] +white_pin: PC2 +initial_white: 1.0 + +[neopixel hotend_neopixel] +pin: PD2 +color_order: GRB +initial_RED: 1.0 +initial_GREEN: 1.0 +initial_BLUE: 1.0 + +[stepper_x] +step_pin: PA8 +dir_pin: PC9 +enable_pin: !PA15 +microsteps: 16 +rotation_distance: 40 +endstop_pin: !PB9 +position_min: 0 +position_endstop: 0 +position_max: 315 +homing_speed: 50 + +[stepper_y] +step_pin: PC7 +dir_pin: !PC6 +enable_pin: !PC8 +microsteps: 16 +rotation_distance: 40 +endstop_pin: !PB8 +position_endstop: 0 +position_max: 315 +homing_speed: 50 + +[stepper_z] +step_pin: PB10 +dir_pin: !PA4 +enable_pin: !PC4 +rotation_distance: 8 +microsteps: 16 +position_min: -1 +position_max: 400 +endstop_pin: probe:z_virtual_endstop # Use Z- as endstop +#homing_speed: 10.0 + +[extruder] +max_extrude_only_distance: 100.0 +step_pin: PC11 +dir_pin: !PC10 +enable_pin: !PC12 +microsteps: 64 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PA6 +sensor_type: EPCOS 100K B57560G104F #Generic 3950 +sensor_pin: PC5 +min_extrude_temp: 170 +min_temp: 0 +max_temp: 300 +# Calibrate E-Steps https://www.klipper3d.org/Rotation_Distance.html#calibrating-rotation_distance-on-extruders +rotation_distance: 17.75 +# Calibrate PID: https://www.klipper3d.org/Config_checks.html#calibrate-pid-settings +# - Example: PID_CALIBRATE HEATER=extruder TARGET=200 +control: pid +pid_kp: 30.356 +pid_ki: 1.857 +pid_kd: 124.081 +# Calibrate PA: https://www.klipper3d.org/Pressure_Advance.html + +[heater_bed] +heater_pin: PA7 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC0 +max_temp: 100 +min_temp: 0 +# Calibrate PID: https://www.klipper3d.org/Config_checks.html#calibrate-pid-settings +# - Example: PID_CALIBRATE HEATER=heater_bed TARGET=60 +control: pid +pid_kp: 64.230 +pid_ki: 0.723 +pid_kd: 1425.905 + +[heater_fan hotend_fan] +pin: PB1 +heater: extruder +heater_temp: 50.0 + +[fan] +pin: PB0 + +[temperature_fan Artillery_MCU] +sensor_type: temperature_mcu +pin: PA5 +max_temp: 60.0 +target_temp: 40.0 +min_temp: 0 +shutdown_speed: 0.0 +kick_start_time: 0.5 +off_below: 0.19 +max_speed: 1.0 +min_speed: 0.0 +control: watermark + +[filament_switch_sensor filament_sensor] +pause_on_runout: true +switch_pin: PC1 + +[probe] +pin: PC14 +x_offset:45.2 +y_offset:11.6 +speed:5 +lift_speed:15 +z_offset: 2.350 + +[safe_z_home] +home_xy_position: 110, 145 # X, Y coordinate (e.g. 100, 100) where the Z homing should be +speed: 300.0 +z_hop: 10 +z_hop_speed: 15.0 + +[bed_mesh] +speed: 300 +horizontal_move_z: 6 +mesh_min: 46,15 +mesh_max: 300,300 +probe_count: 10, 10 +fade_start: 1.0 +fade_end: 0.0 +algorithm: bicubic + +[screws_tilt_adjust] +screw1: 120, 153 +screw1_name: center reference +screw2: 7, 45 +screw2_name: front left +screw3: 210, 45 +screw3_name: front right +screw4: 227, 145 +screw4_name: right center +screw5: 210, 245 +screw5_name: rear right +screw6: 7, 245 +screw6_name: rear left +screw7: 7, 145 +screw7_name: left center +horizontal_move_z: 8 +speed: 300 +screw_thread: CW-M4 diff --git a/docs/Bed_Mesh.md b/docs/Bed_Mesh.md index 9ee8df507..1538f6257 100644 --- a/docs/Bed_Mesh.md +++ b/docs/Bed_Mesh.md @@ -44,10 +44,9 @@ probe_count: 5, 3 - `mesh_max: 240, 198`\ _Required_\ - The probed coordinate farthest farthest from the origin. This is not - necessarily the last point probed, as the probing process occurs in a - zig-zag fashion. As with `mesh_min`, this coordinate is relative to - the probe's location. + The probed coordinate farthest from the origin. This is not necessarily + the last point probed, as the probing process occurs in a zig-zag fashion. + As with `mesh_min`, this coordinate is relative to the probe's location. - `probe_count: 5, 3`\ _Default Value: 3, 3_\ diff --git a/docs/CANBUS.md b/docs/CANBUS.md index e80141a95..321f8e891 100644 --- a/docs/CANBUS.md +++ b/docs/CANBUS.md @@ -31,7 +31,7 @@ adapter. This is typically done by creating a new file named allow-hotplug can0 iface can0 can static bitrate 1000000 - up ifconfig $IFACE txqueuelen 128 + up ip link set $IFACE txqueuelen 128 ``` ## Terminating Resistors @@ -113,7 +113,7 @@ Some important notes when using this mode: allow-hotplug can0 iface can0 can static bitrate 1000000 - up ifconfig $IFACE txqueuelen 128 + up ip link set $IFACE txqueuelen 128 ``` * The "bridge mcu" is not actually on the CAN bus. Messages to and diff --git a/docs/CANBUS_Troubleshooting.md b/docs/CANBUS_Troubleshooting.md index bd9ef0456..de0deaf74 100644 --- a/docs/CANBUS_Troubleshooting.md +++ b/docs/CANBUS_Troubleshooting.md @@ -52,6 +52,56 @@ Reordered messages is a severe problem that must be fixed. It will result in unstable behavior and can lead to confusing errors at any part of a print. +## Use an appropriate txqueuelen setting + +The Klipper code uses the Linux kernel to manage CAN bus traffic. By +default, the kernel will only queue 10 CAN transmit packets. It is +recommended to [configure the can0 device](CANBUS.md#host-hardware) +with a `txqueuelen 128` to increase that size. + +If Klipper transmits a packet and Linux has filled all of its transmit +queue space then Linux will drop that packet and messages like the +following will appear in the Klipper log: +``` +Got error -1 in can write: (105)No buffer space available +``` +Klipper will automatically retransmit the lost messages as part of its +normal application level message retransmit system. Thus, this log +message is a warning and it does not indicate an unrecoverable error. + +If a complete CAN bus failure occurs (such as a CAN wire break) then +Linux will not be able to transmit any messages on the CAN bus and it +is common to find the above message in the Klipper log. In this case, +the log message is a symptom of a larger problem (the inability to +transmit any messages) and is not directly related to Linux +`txqueuelen`. + +One may check the current queue size by running the Linux command `ip +link show can0`. It should report a bunch of text including the +snippet `qlen 128`. If one sees something like `qlen 10` then it +indicates the CAN device has not been properly configured. + +It is not recommended to use a `txqueuelen` significantly larger +than 128. A CAN bus running at a frequency of 1000000 will typically +take around 120us to transmit a CAN packet. Thus a queue of 128 +packets is likely to take around 15-20ms to drain. A substantially +larger queue could cause excessive spikes in message round-trip-time +which could lead to unrecoverable errors. Said another way, Klipper's +application retransmit system is more robust if it does not have to +wait for Linux to drain an excessively large queue of possibly stale +data. This is analogous to the problem of +[bufferbloat](https://en.wikipedia.org/wiki/Bufferbloat) on internet +routers. + +Under normal circumstances Klipper may utilize ~25 queue slots per +MCU - typically only utilizing more slots during retransmits. +(Specifically, the Klipper host may transmit up to 192 bytes to each +Klipper MCU before receiving an acknowledgment from that MCU.) If a +single CAN bus has 5 or more Klipper MCUs on it, then it might be +necessary to increase the `txqueuelen` above the recommended value +of 128. However, as above, care should be taken when selecting a new +value to avoid excessive round-trip-time latency. + ## Obtaining candump logs The CAN bus messages sent to and from the micro-controller are handled diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 58d16d995..cdbfe1d8a 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,12 @@ All dates in this document are approximate. ## Changes +20240415: The `on_error_gcode` parameter in the `[virtual_sdcard]` +config section now has a default. If this parameter is not specified +it now defaults to `TURN_OFF_HEATERS`. If the previous behavior is +desired (take no default action on an error during a virtual_sdcard +print) then define `on_error_gcode` with an empty value. + 20240313: The `max_accel_to_decel` parameter in the `[printer]` config section has been deprecated. The `ACCEL_TO_DECEL` parameter of the `SET_VELOCITY_LIMIT` command has been deprecated. The diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index f5bcb880b..1a88b0e09 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -120,7 +120,7 @@ A collection of DangerKlipper-specific system options # for conflicts to autosave data. Any configurations updated will be backed # up to configs/config_backups. #bgflush_extra_time: 0.250 -# This allows to set extra flush time (in seconds). Under certain conditions, +# This allows to set extra flush time (in seconds). Under certain conditions, # a low value will result in an error if message is not get flushed, a high value # (0.250) will result in homing/probing latency. The default is 0.250 ``` @@ -1688,9 +1688,11 @@ path: # be provided. #on_error_gcode: # A list of G-Code commands to execute when an error is reported. +# See docs/Command_Templates.md for G-Code format. The default is to +# run TURN_OFF_HEATERS. #with_subdirs: False -# Enable scanning of subdirectories for the menu and for the M20 and M23 commands. The default is False. - +# Enable scanning of subdirectories for the menu and for the +# M20 and M23 commands. The default is False. ``` ### [sdcard_loop] @@ -2316,6 +2318,40 @@ z_offset: # See the "probe" section for more information on the parameters above. ``` +### [probe_eddy_current] + +Support for eddy current inductive probes. One may define this section +(instead of a probe section) to enable this probe. See the +[command reference](G-Codes.md#probe_eddy_current) for further information. + +``` +[probe_eddy_current my_eddy_probe] +sensor_type: ldc1612 +# The sensor chip used to perform eddy current measurements. This +# parameter must be provided and must be set to ldc1612. +#z_offset: +# The nominal distance (in mm) between the nozzle and bed that a +# probing attempt should stop at. This parameter must be provided. +#i2c_address: +#i2c_mcu: +#i2c_bus: +#i2c_software_scl_pin: +#i2c_software_sda_pin: +#i2c_speed: +# The i2c settings for the sensor chip. See the "common I2C +# settings" section for a description of the above parameters. +#x_offset: +#y_offset: +#speed: +#lift_speed: +#samples: +#sample_retract_dist: +#samples_result: +#samples_tolerance: +#samples_tolerance_retries: +# See the "probe" section for information on these parameters. +``` + ### [axis_twist_compensation] A tool to compensate for inaccurate probe readings due to twist in X gantry. See @@ -2944,6 +2980,25 @@ sensor_type: # Interval in seconds between readings. Default is 30 ``` +### SHT3X sensor + +SHT3X family two wire interface (I2C) environmental sensor. These sensors +have a range of -55~125 C, so are usable for e.g. chamber temperature +monitoring. They can also function as simple fan/heater controllers. + +``` +sensor_type: SHT3X +#i2c_address: +# Default is 68 (0x44). +#i2c_mcu: +#i2c_bus: +#i2c_software_scl_pin: +#i2c_software_sda_pin: +#i2c_speed: +# See the "common I2C settings" section for a description of the +# above parameters. +``` + ### LM75 temperature sensor LM75/LM75A two wire (I2C) connected temperature sensors. These sensors @@ -5197,25 +5252,25 @@ TradRack repo for additional information: ``` [trad_rack] selector_max_velocity: -# Maximum velocity (in mm/s) of the selector. +# Maximum velocity (in mm/s) of the selector. # This parameter must be specified. selector_max_accel: -# Maximum acceleration (in mm/s^2) of the selector. +# Maximum acceleration (in mm/s^2) of the selector. # This parameter must be specified. #filament_max_velocity: -# Maximum velocity (in mm/s) for filament movement. +# Maximum velocity (in mm/s) for filament movement. # Defaults to buffer_pull_speed. #filament_max_accel: 1500.0 # Maximum acceleration (in mm/s^2) for filament movement. # The default is 1500.0. toolhead_fil_sensor_pin: # The pin on which the toolhead filament sensor is connected. -# If a pin is not specified, no toolhead filament sensor will +# If a pin is not specified, no toolhead filament sensor will # be used. lane_count: # The number of filament lanes. This parameter must be specified. lane_spacing: -# Spacing (in mm) between filament lanes. +# Spacing (in mm) between filament lanes. # This parameter must be specified. #lane_offset_: # Options with a "lane_offset_" prefix may be specified for any of @@ -5299,7 +5354,7 @@ toolhead_unload_length: # segment into the lane module. #spool_pull_speed: 100.0 # Speed (in mm/s) to move filament through the bowden tube when -# loading from a spool. See Tuning.md for details. +# loading from a spool. See Tuning.md for details. # The default is 100.0. #buffer_pull_speed: # Speed (in mm/s) to move filament through the bowden tube when @@ -5321,7 +5376,7 @@ toolhead_unload_length: #load_with_toolhead_sensor: True # Whether to use the toolhead sensor when loading the toolhead. # See Tuning.md for details. Defaults to True but is ignored if -# toolhead_fil_sensor_pin is not specified. +# toolhead_fil_sensor_pin is not specified. #unload_with_toolhead_sensor: True # Whether to use the toolhead sensor when unloading the toolhead. # See Tuning.md for details. Defaults to True but is ignored if diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md new file mode 100644 index 000000000..221c855b6 --- /dev/null +++ b/docs/Eddy_Probe.md @@ -0,0 +1,56 @@ +# Eddy Current Inductive probe + +This document describes how to use an +[eddy current](https://en.wikipedia.org/wiki/Eddy_current) inductive +probe in Klipper. + +Currently, an eddy current probe can not be used for Z homing. The +sensor can only be used for Z probing. + +Start by declaring a +[probe_eddy_current config section](Config_Reference.md#probe_eddy_current) +in the printer.cfg file. It is recommended to set the `z_offset` to +0.5mm. It is typical for the sensor to require an `x_offset` and +`y_offset`. If these values are not known, one should estimate the +values during initial calibration. + +The first step in calibration is to determine the appropriate +DRIVE_CURRENT for the sensor. Home the printer and navigate the +toolhead so that the sensor is near the center of the bed and is about +20mm above the bed. Then issue an `LDC_CALIBRATE_DRIVE_CURRENT +CHIP=` command. For example, if the config section was +named `[probe_eddy_current my_eddy_probe]` then one would run +`LDC_CALIBRATE_DRIVE_CURRENT CHIP=my_eddy_probe`. This command should +complete in a few seconds. After it completes, issue a `SAVE_CONFIG` +command to save the results to the printer.cfg and restart. + +The second step in calibration is to correlate the sensor readings to +the corresponding Z heights. Home the printer and navigate the +toolhead so that the nozzle is near the center of the bed. Then run an +`PROBE_EDDY_CURRENT_CALIBRATE CHIP=my_eddy_probe` command. Once the +tool starts, follow the steps described at +["the paper test"](Bed_Level.md#the-paper-test) to determine the +actual distance between the nozzle and bed at the given location. Once +those steps are complete one can `ACCEPT` the position. The tool will +then move the the toolhead so that the sensor is above the point where +the nozzle used to be and run a series of movements to correlate the +sensor to Z positions. This will take a couple of minutes. After the +tool completes, issue a `SAVE_CONFIG` command to save the results to +the printer.cfg and restart. + +After initial calibration it is a good idea to verify that the +`x_offset` and `y_offset` are accurate. Follow the steps to +[calibrate probe x and y offsets](Probe_Calibrate.md#calibrating-probe-x-and-y-offsets). +If either the `x_offset` or `y_offset` is modified then be sure to run +the `PROBE_EDDY_CURRENT_CALIBRATE` command (as described above) after +making the change. + +Once calibration is complete, one may use all the standard Klipper +tools that use a Z probe. + +Note that eddy current sensors (and inductive probes in general) are +susceptible to "thermal drift". That is, changes in temperature can +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. diff --git a/docs/G-Codes.md b/docs/G-Codes.md index ec639b117..2f2b02d89 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -1170,6 +1170,28 @@ babystepping), and subtract if from the probe's z_offset. This acts to take a frequently used babystepping value, and "make it permanent". Requires a `SAVE_CONFIG` to take effect. +### [probe_eddy_current] + +The following commands are available when a +[probe_eddy_current config section](Config_Reference.md#probe_eddy_current) +is enabled. + +#### PROBE_EDDY_CURRENT_CALIBRATE +`PROBE_EDDY_CURRENT_CALIBRATE CHIP=`: This starts a tool +that calibrates the sensor resonance frequencies to corresponding Z +heights. The tool will take a couple of minutes to complete. After +completion, use the SAVE_CONFIG command to store the results in the +printer.cfg file. + +#### LDC_CALIBRATE_DRIVE_CURRENT +`LDC_CALIBRATE_DRIVE_CURRENT CHIP=` This tool will +calibrate the ldc1612 DRIVE_CURRENT0 register. Prior to using this +tool, move the sensor so that it is near the center of the bed and +about 20mm above the bed surface. Run this command to determine an +appropriate DRIVE_CURRENT for the sensor. After running this command +use the SAVE_CONFIG command to store that new setting in the +printer.cfg config file. + ### [pwm_cycle_time] The following command is available when a diff --git a/docs/Overview.md b/docs/Overview.md index 5d1a87342..ef951fb2e 100644 --- a/docs/Overview.md +++ b/docs/Overview.md @@ -99,3 +99,4 @@ communication with the Klipper developers. troubleshooting CAN bus. - [TSL1401CL filament width sensor](TSL1401CL_Filament_Width_Sensor.md) - [Hall filament width sensor](Hall_Filament_Width_Sensor.md) +- [Eddy Current Inductive probe](Eddy_Probe.md) diff --git a/docs/Resonance_Compensation.md b/docs/Resonance_Compensation.md index 31f1b35e3..8f2d2b643 100644 --- a/docs/Resonance_Compensation.md +++ b/docs/Resonance_Compensation.md @@ -48,7 +48,7 @@ First, measure the **ringing frequency**. to 5.0. It is not advised to increase it when using input shaper because it can cause more smoothing in parts - it is better to use higher acceleration value instead. -2. Disable the `miminum_cruise_ratio` feature by issuing the following +2. Disable the `minimum_cruise_ratio` feature by issuing the following command: `SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0` 3. Disable Pressure Advance: `SET_PRESSURE_ADVANCE ADVANCE=0` 4. If you have already added `[input_shaper]` section to the printer.cfg, diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index 0d833da52..a368c49fa 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -474,6 +474,7 @@ The following information is available in [bme280 config_section_name](Config_Reference.md#bmp280bme280bme680-temperature-sensor), [htu21d config_section_name](Config_Reference.md#htu21d-sensor), +[sht3x config_section_name](Config_Reference.md#sht31-sensor), [lm75 config_section_name](Config_Reference.md#lm75-temperature-sensor), [temperature_host config_section_name](Config_Reference.md#host-temperature-sensor) and @@ -481,7 +482,7 @@ and objects: - `temperature`: The last read temperature from the sensor. - `humidity`, `pressure`, `gas`: The last read values from the sensor - (only on bme280, htu21d, and lm75 sensors). + (only on bme280, htu21d, sht3x and lm75 sensors). ## temperature_fan diff --git a/docs/_klipper3d/mkdocs.yml b/docs/_klipper3d/mkdocs.yml index 9373fb6f7..cbe3d4181 100644 --- a/docs/_klipper3d/mkdocs.yml +++ b/docs/_klipper3d/mkdocs.yml @@ -138,4 +138,5 @@ nav: - CANBUS_Troubleshooting.md - TSL1401CL_Filament_Width_Sensor.md - Hall_Filament_Width_Sensor.md + - Eddy_Probe.md - Sponsors.md diff --git a/klippy/configfile.py b/klippy/configfile.py index e8fe649f0..71bc4c8c8 100644 --- a/klippy/configfile.py +++ b/klippy/configfile.py @@ -324,7 +324,6 @@ def _find_autosave_data(self, data): value_r = re.compile("[^A-Za-z0-9_].*$") def _strip_duplicates(self, data, config): - fileconfig = config.fileconfig # Comment out fields in 'data' that are defined in 'config' lines = data.split("\n") section = None diff --git a/klippy/extras/adxl345.py b/klippy/extras/adxl345.py index 7de45c570..4d5325f71 100644 --- a/klippy/extras/adxl345.py +++ b/klippy/extras/adxl345.py @@ -259,9 +259,6 @@ def read_axes_map(config): return [am[a.strip()] for a in axes_map] -BYTES_PER_SAMPLE = 5 -SAMPLES_PER_BLOCK = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE - BATCH_UPDATES = 0.100 @@ -286,13 +283,9 @@ def __init__(self, config): "query_adxl345 oid=%d rest_ticks=0" % (oid,), on_restart=True ) mcu.register_config_callback(self._build_config) - self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, oid=oid) - # Clock tracking + # Bulk sample message reading chip_smooth = self.data_rate * BATCH_UPDATES * 2 - self.clock_sync = bulk_sensor.ClockSyncRegression(mcu, chip_smooth) - self.clock_updater = bulk_sensor.ChipClockUpdater( - self.clock_sync, BYTES_PER_SAMPLE - ) + self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, "BBBBB") self.last_error_count = 0 # Process messages in batches self.batch_bulk = bulk_sensor.BatchBulkHelper( @@ -313,8 +306,8 @@ def _build_config(self): self.query_adxl345_cmd = self.mcu.lookup_command( "query_adxl345 oid=%c rest_ticks=%u", cq=cmdqueue ) - self.clock_updater.setup_query_command( - self.mcu, "query_adxl345_status oid=%c", oid=self.oid, cq=cmdqueue + self.ffreader.setup_query_command( + "query_adxl345_status oid=%c", oid=self.oid, cq=cmdqueue ) def read_reg(self, reg): @@ -339,41 +332,25 @@ def start_internal_client(self): return aqh # Measurement decoding - def _extract_samples(self, raw_samples): - # Load variables to optimize inner loop below + def _convert_samples(self, samples): (x_pos, x_scale), (y_pos, y_scale), (z_pos, z_scale) = self.axes_map - last_sequence = self.clock_updater.get_last_sequence() - time_base, chip_base, inv_freq = self.clock_sync.get_time_translation() - # Process every message in raw_samples - count = seq = 0 - samples = [None] * (len(raw_samples) * SAMPLES_PER_BLOCK) - for params in raw_samples: - seq_diff = (params["sequence"] - last_sequence) & 0xFFFF - seq_diff -= (seq_diff & 0x8000) << 1 - seq = last_sequence + seq_diff - d = bytearray(params["data"]) - msg_cdiff = seq * SAMPLES_PER_BLOCK - chip_base - for i in range(len(d) // BYTES_PER_SAMPLE): - d_xyz = d[i * BYTES_PER_SAMPLE : (i + 1) * BYTES_PER_SAMPLE] - xlow, ylow, zlow, xzhigh, yzhigh = d_xyz - if yzhigh & 0x80: - self.last_error_count += 1 - continue - rx = (xlow | ((xzhigh & 0x1F) << 8)) - ((xzhigh & 0x10) << 9) - ry = (ylow | ((yzhigh & 0x1F) << 8)) - ((yzhigh & 0x10) << 9) - rz = ( - zlow | ((xzhigh & 0xE0) << 3) | ((yzhigh & 0xE0) << 6) - ) - ((yzhigh & 0x40) << 7) - raw_xyz = (rx, ry, rz) - x = round(raw_xyz[x_pos] * x_scale, 6) - y = round(raw_xyz[y_pos] * y_scale, 6) - z = round(raw_xyz[z_pos] * z_scale, 6) - ptime = round(time_base + (msg_cdiff + i) * inv_freq, 6) - samples[count] = (ptime, x, y, z) - count += 1 - self.clock_sync.set_last_chip_clock(seq * SAMPLES_PER_BLOCK + i) + count = 0 + for ptime, xlow, ylow, zlow, xzhigh, yzhigh in samples: + if yzhigh & 0x80: + self.last_error_count += 1 + continue + rx = (xlow | ((xzhigh & 0x1F) << 8)) - ((xzhigh & 0x10) << 9) + ry = (ylow | ((yzhigh & 0x1F) << 8)) - ((yzhigh & 0x10) << 9) + rz = (zlow | ((xzhigh & 0xE0) << 3) | ((yzhigh & 0xE0) << 6)) - ( + (yzhigh & 0x40) << 7 + ) + raw_xyz = (rx, ry, rz) + x = round(raw_xyz[x_pos] * x_scale, 6) + y = round(raw_xyz[y_pos] * y_scale, 6) + z = round(raw_xyz[z_pos] * z_scale, 6) + samples[count] = (round(ptime, 6), x, y, z) + count += 1 del samples[count:] - return samples # Start, stop, and process message batches def _start_measurements(self): @@ -394,34 +371,30 @@ def _start_measurements(self): self.set_reg(REG_BW_RATE, QUERY_RATES[self.data_rate]) self.set_reg(REG_FIFO_CTL, SET_FIFO_CTL) # Start bulk reading - self.bulk_queue.clear_samples() rest_ticks = self.mcu.seconds_to_clock(4.0 / self.data_rate) self.query_adxl345_cmd.send([self.oid, rest_ticks]) self.set_reg(REG_POWER_CTL, 0x08) logging.info("ADXL345 starting '%s' measurements", self.name) # Initialize clock tracking - self.clock_updater.note_start() + self.ffreader.note_start() self.last_error_count = 0 def _finish_measurements(self): # Halt bulk reading self.set_reg(REG_POWER_CTL, 0x00) self.query_adxl345_cmd.send_wait_ack([self.oid, 0]) - self.bulk_queue.clear_samples() + self.ffreader.note_end() logging.info("ADXL345 finished '%s' measurements", self.name) def _process_batch(self, eventtime): - self.clock_updater.update_clock() - raw_samples = self.bulk_queue.pull_samples() - if not raw_samples: - return {} - samples = self._extract_samples(raw_samples) + samples = self.ffreader.pull_samples() + self._convert_samples(samples) if not samples: return {} return { "data": samples, "errors": self.last_error_count, - "overflows": self.clock_updater.get_last_overflows(), + "overflows": self.ffreader.get_last_overflows(), } diff --git a/klippy/extras/angle.py b/klippy/extras/angle.py index 56a9658a4..9fb58d955 100644 --- a/klippy/extras/angle.py +++ b/klippy/extras/angle.py @@ -621,7 +621,7 @@ def _start_measurements(self): logging.info("Starting angle '%s' measurements", self.name) self.sensor_helper.start() # Start bulk reading - self.bulk_queue.clear_samples() + self.bulk_queue.clear_queue() self.last_sequence = 0 systime = self.printer.get_reactor().monotonic() print_time = self.mcu.estimated_print_time(systime) + MIN_MSG_TIME @@ -635,14 +635,14 @@ def _start_measurements(self): def _finish_measurements(self): # Halt bulk reading self.query_spi_angle_cmd.send_wait_ack([self.oid, 0, 0, 0]) - self.bulk_queue.clear_samples() + self.bulk_queue.clear_queue() self.sensor_helper.last_temperature = None logging.info("Stopped angle '%s' measurements", self.name) def _process_batch(self, eventtime): if self.sensor_helper.is_tcode_absolute: self.sensor_helper.update_clock() - raw_samples = self.bulk_queue.pull_samples() + raw_samples = self.bulk_queue.pull_queue() if not raw_samples: return {} samples, error_count = self._extract_samples(raw_samples) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index f7cf9b582..ba796223b 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -26,6 +26,7 @@ "output_mode_store": 0.001884, } + # BLTouch "endstop" wrapper class BLTouchEndstopWrapper: def __init__(self, config): @@ -217,6 +218,10 @@ def multi_probe_end(self): self.sync_print_time() 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() diff --git a/klippy/extras/bulk_sensor.py b/klippy/extras/bulk_sensor.py index 71fd64c05..fba14cb91 100644 --- a/klippy/extras/bulk_sensor.py +++ b/klippy/extras/bulk_sensor.py @@ -3,7 +3,7 @@ # Copyright (C) 2020-2023 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import logging, threading +import logging, threading, struct # This "bulk sensor" module facilitates the processing of sensor chip # measurements that do not require the host to respond with low @@ -147,14 +147,14 @@ def _handle_data(self, params): with self.lock: self.raw_samples.append(params) - def pull_samples(self): + def pull_queue(self): with self.lock: raw_samples = self.raw_samples self.raw_samples = [] return raw_samples - def clear_samples(self): - self.pull_samples() + def clear_queue(self): + self.pull_queue() ###################################################################### @@ -238,18 +238,22 @@ def get_time_translation(self): MAX_BULK_MSG_SIZE = 52 -# Handle common periodic chip status query responses -class ChipClockUpdater: - def __init__(self, clock_sync, bytes_per_sample): - self.clock_sync = clock_sync - self.bytes_per_sample = bytes_per_sample - self.samples_per_block = MAX_BULK_MSG_SIZE // bytes_per_sample +# Read sensor_bulk_data and calculate timestamps for devices that take +# samples at a fixed frequency (and produce fixed data size samples). +class FixedFreqReader: + def __init__(self, mcu, chip_clock_smooth, unpack_fmt): + self.mcu = mcu + self.clock_sync = ClockSyncRegression(mcu, chip_clock_smooth) + unpack = struct.Struct(unpack_fmt) + self.unpack_from = unpack.unpack_from + self.bytes_per_sample = unpack.size + self.samples_per_block = MAX_BULK_MSG_SIZE // self.bytes_per_sample self.last_sequence = self.max_query_duration = 0 self.last_overflows = 0 - self.mcu = self.oid = self.query_status_cmd = None + self.bulk_queue = self.oid = self.query_status_cmd = None - def setup_query_command(self, mcu, msgformat, oid, cq): - self.mcu = mcu + def setup_query_command(self, msgformat, oid, cq): + # Lookup sensor query command (that responds with sensor_bulk_status) self.oid = oid self.query_status_cmd = self.mcu.lookup_query_command( msgformat, @@ -258,25 +262,30 @@ def setup_query_command(self, mcu, msgformat, oid, cq): oid=oid, cq=cq, ) - - def get_last_sequence(self): - return self.last_sequence + # Read sensor_bulk_data messages and store in a queue + self.bulk_queue = BulkDataQueue(self.mcu, oid=oid) def get_last_overflows(self): return self.last_overflows - def clear_duration_filter(self): + def _clear_duration_filter(self): self.max_query_duration = 1 << 31 def note_start(self): self.last_sequence = 0 self.last_overflows = 0 + # Clear local queue (clear any stale samples from previous session) + self.bulk_queue.clear_queue() # Set initial clock - self.clear_duration_filter() - self.update_clock(is_reset=True) - self.clear_duration_filter() + self._clear_duration_filter() + self._update_clock(is_reset=True) + self._clear_duration_filter() - def update_clock(self, is_reset=False): + def note_end(self): + # Clear local queue (free no longer needed memory) + self.bulk_queue.clear_queue() + + def _update_clock(self, is_reset=False): params = self.query_status_cmd.send([self.oid]) mcu_clock = self.mcu.clock32_to_clock64(params["clock"]) seq_diff = (params["next_sequence"] - self.last_sequence) & 0xFFFF @@ -305,3 +314,35 @@ def update_clock(self, is_reset=False): self.clock_sync.reset(avg_mcu_clock, chip_clock) else: self.clock_sync.update(avg_mcu_clock, chip_clock) + + # Convert sensor_bulk_data responses into list of samples + def pull_samples(self): + # Query MCU for sample timing and update clock synchronization + self._update_clock() + # Pull sensor_bulk_data messages from local queue + raw_samples = self.bulk_queue.pull_queue() + if not raw_samples: + return [] + # Load variables to optimize inner loop below + last_sequence = self.last_sequence + time_base, chip_base, inv_freq = self.clock_sync.get_time_translation() + unpack_from = self.unpack_from + bytes_per_sample = self.bytes_per_sample + samples_per_block = self.samples_per_block + # Process every message in raw_samples + count = seq = 0 + samples = [None] * (len(raw_samples) * samples_per_block) + for params in raw_samples: + seq_diff = (params["sequence"] - last_sequence) & 0xFFFF + seq_diff -= (seq_diff & 0x8000) << 1 + seq = last_sequence + seq_diff + msg_cdiff = seq * samples_per_block - chip_base + data = params["data"] + for i in range(len(data) // bytes_per_sample): + ptime = time_base + (msg_cdiff + i) * inv_freq + udata = unpack_from(data, i * bytes_per_sample) + samples[count] = (ptime,) + udata + count += 1 + self.clock_sync.set_last_chip_clock(seq * samples_per_block + i) + del samples[count:] + return samples diff --git a/klippy/extras/dotstar.py b/klippy/extras/dotstar.py index 11d6d1dfe..68a181653 100644 --- a/klippy/extras/dotstar.py +++ b/klippy/extras/dotstar.py @@ -11,7 +11,6 @@ class PrinterDotstar: def __init__(self, config): self.printer = printer = config.get_printer() - name = config.get_name().split()[1] # Configure a software spi bus ppins = printer.lookup_object("pins") data_pin_params = ppins.lookup_pin(config.get("data_pin")) diff --git a/klippy/extras/endstop_phase.py b/klippy/extras/endstop_phase.py index 6aab53a66..221b9b99e 100644 --- a/klippy/extras/endstop_phase.py +++ b/klippy/extras/endstop_phase.py @@ -15,6 +15,7 @@ "tmc5160", ] + # Calculate the trigger phase of a stepper motor class PhaseCalc: def __init__(self, printer, name, phases=None): @@ -228,7 +229,6 @@ def cmd_ENDSTOP_PHASE_CALIBRATE(self, gcmd): def generate_stats(self, stepper_name, phase_calc): phase_history = phase_calc.phase_history wph = phase_history + phase_history - count = sum(phase_history) phases = len(phase_history) half_phases = phases // 2 res = [] diff --git a/klippy/extras/homing_override.py b/klippy/extras/homing_override.py index e4b570179..d99e78889 100644 --- a/klippy/extras/homing_override.py +++ b/klippy/extras/homing_override.py @@ -58,6 +58,7 @@ def cmd_G28(self, gcmd): # Perform homing context = self.template.create_template_context() context["params"] = gcmd.get_command_parameters() + context["rawparams"] = gcmd.get_raw_command_parameters() try: self.in_script = True self.template.run_gcode_from_command(context) diff --git a/klippy/extras/idle_timeout.py b/klippy/extras/idle_timeout.py index 3e28f3c05..9d8321580 100644 --- a/klippy/extras/idle_timeout.py +++ b/klippy/extras/idle_timeout.py @@ -53,7 +53,7 @@ def transition_idle_state(self, eventtime): self.state = "Printing" try: script = self.idle_gcode.render() - res = self.gcode.run_script(script) + self.gcode.run_script(script) except: logging.exception("idle timeout gcode execution") self.state = "Ready" diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py index daaa2e683..99179895e 100644 --- a/klippy/extras/input_shaper.py +++ b/klippy/extras/input_shaper.py @@ -78,7 +78,6 @@ def get_shaper(self): def update(self, gcmd): self.params.update(gcmd) - old_n, old_A, old_T = self.n, self.A, self.T self.n, self.A, self.T = self.params.get_shaper() def set_shaper_kinematics(self, sk): diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py new file mode 100644 index 000000000..11fd59264 --- /dev/null +++ b/klippy/extras/ldc1612.py @@ -0,0 +1,240 @@ +# Support for reading frequency samples from ldc1612 +# +# Copyright (C) 2020-2024 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from . import bus, bulk_sensor + +MIN_MSG_TIME = 0.100 + +BATCH_UPDATES = 0.100 + +LDC1612_ADDR = 0x2A + +LDC1612_FREQ = 12000000 +SETTLETIME = 0.005 +DRIVECUR = 15 +DEGLITCH = 0x05 # 10 Mhz + +LDC1612_MANUF_ID = 0x5449 +LDC1612_DEV_ID = 0x3055 + +REG_RCOUNT0 = 0x08 +REG_OFFSET0 = 0x0C +REG_SETTLECOUNT0 = 0x10 +REG_CLOCK_DIVIDERS0 = 0x14 +REG_ERROR_CONFIG = 0x19 +REG_CONFIG = 0x1A +REG_MUX_CONFIG = 0x1B +REG_DRIVE_CURRENT0 = 0x1E +REG_MANUFACTURER_ID = 0x7E +REG_DEVICE_ID = 0x7F + + +# Tool for determining appropriate DRIVE_CURRENT register +class DriveCurrentCalibrate: + def __init__(self, config, sensor): + self.printer = config.get_printer() + self.sensor = sensor + self.drive_cur = config.getint( + "reg_drive_current", DRIVECUR, minval=0, maxval=31 + ) + self.name = config.get_name() + gcode = self.printer.lookup_object("gcode") + gcode.register_mux_command( + "LDC_CALIBRATE_DRIVE_CURRENT", + "CHIP", + self.name.split()[-1], + self.cmd_LDC_CALIBRATE, + desc=self.cmd_LDC_CALIBRATE_help, + ) + + def get_drive_current(self): + return self.drive_cur + + cmd_LDC_CALIBRATE_help = "Calibrate LDC1612 DRIVE_CURRENT register" + + def cmd_LDC_CALIBRATE(self, gcmd): + is_in_progress = True + + def handle_batch(msg): + return is_in_progress + + self.sensor.add_client(handle_batch) + toolhead = self.printer.lookup_object("toolhead") + toolhead.dwell(0.100) + toolhead.wait_moves() + old_config = self.sensor.read_reg(REG_CONFIG) + self.sensor.set_reg(REG_CONFIG, 0x001 | (1 << 9)) + toolhead.wait_moves() + toolhead.dwell(0.100) + toolhead.wait_moves() + reg_drive_current0 = self.sensor.read_reg(REG_DRIVE_CURRENT0) + self.sensor.set_reg(REG_CONFIG, old_config) + is_in_progress = False + # Report found value to user + drive_cur = (reg_drive_current0 >> 6) & 0x1F + gcmd.respond_info( + "%s: reg_drive_current: %d\n" + "The SAVE_CONFIG command will update the printer config file\n" + "with the above and restart the printer." % (self.name, drive_cur) + ) + configfile = self.printer.lookup_object("configfile") + configfile.set(self.name, "reg_drive_current", "%d" % (drive_cur,)) + + +# Interface class to LDC1612 mcu support +class LDC1612: + def __init__(self, config, calibration=None): + self.printer = config.get_printer() + self.calibration = calibration + self.dccal = DriveCurrentCalibrate(config, self) + self.data_rate = 250 + # Setup mcu sensor_ldc1612 bulk query code + self.i2c = bus.MCU_I2C_from_config( + config, default_addr=LDC1612_ADDR, default_speed=400000 + ) + self.mcu = mcu = self.i2c.get_mcu() + 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()) + ) + mcu.add_config_cmd( + "query_ldc1612 oid=%d rest_ticks=0" % (oid,), on_restart=True + ) + mcu.register_config_callback(self._build_config) + # Bulk sample message reading + chip_smooth = self.data_rate * BATCH_UPDATES * 2 + self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, ">I") + self.last_error_count = 0 + # Process messages in batches + self.batch_bulk = bulk_sensor.BatchBulkHelper( + self.printer, + self._process_batch, + self._start_measurements, + self._finish_measurements, + BATCH_UPDATES, + ) + self.name = config.get_name().split()[-1] + hdr = ("time", "frequency", "z") + self.batch_bulk.add_mux_endpoint( + "ldc1612/dump_ldc1612", "sensor", self.name, {"header": hdr} + ) + + def _build_config(self): + cmdqueue = self.i2c.get_command_queue() + self.query_ldc1612_cmd = self.mcu.lookup_command( + "query_ldc1612 oid=%c rest_ticks=%u", cq=cmdqueue + ) + self.ffreader.setup_query_command( + "query_ldc1612_status oid=%c", oid=self.oid, cq=cmdqueue + ) + self.ldc1612_setup_home_cmd = self.mcu.lookup_command( + "ldc1612_setup_home oid=%c clock=%u threshold=%u" + " trsync_oid=%c trigger_reason=%c", + cq=cmdqueue, + ) + self.query_ldc1612_home_state_cmd = self.mcu.lookup_query_command( + "query_ldc1612_home_state oid=%c", + "ldc1612_home_state oid=%c homing=%c trigger_clock=%u", + oid=self.oid, + cq=cmdqueue, + ) + + def get_mcu(self): + return self.i2c.get_mcu() + + def read_reg(self, reg): + params = self.i2c.i2c_read([reg], 2) + response = bytearray(params["response"]) + return (response[0] << 8) | response[1] + + def set_reg(self, reg, val, minclock=0): + self.i2c.i2c_write( + [reg, (val >> 8) & 0xFF, val & 0xFF], minclock=minclock + ) + + def add_client(self, cb): + self.batch_bulk.add_client(cb) + + # Homing + def setup_home(self, print_time, trigger_freq, trsync_oid, 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] + ) + + def clear_home(self): + self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0]) + if self.mcu.is_fileoutput(): + return 0.0 + params = self.query_ldc1612_home_state_cmd.send([self.oid]) + tclock = self.mcu.clock32_to_clock64(params["trigger_clock"]) + return self.mcu.clock_to_print_time(tclock) + + # Measurement decoding + def _convert_samples(self, samples): + freq_conv = float(LDC1612_FREQ) / (1 << 28) + count = 0 + for ptime, val in samples: + mv = val & 0x0FFFFFFF + if mv != val: + self.last_error_count += 1 + samples[count] = (round(ptime, 6), round(freq_conv * mv, 3), 999.9) + count += 1 + + # Start, stop, and process message batches + def _start_measurements(self): + # In case of miswiring, testing LDC1612 device ID prevents treating + # noise or wrong signal as a correctly initialized device + manuf_id = self.read_reg(REG_MANUFACTURER_ID) + dev_id = self.read_reg(REG_DEVICE_ID) + if manuf_id != LDC1612_MANUF_ID or dev_id != LDC1612_DEV_ID: + raise self.printer.command_error( + "Invalid ldc1612 id (got %x,%x vs %x,%x).\n" + "This is generally indicative of connection problems\n" + "(e.g. faulty wiring) or a faulty ldc1612 chip." + % (manuf_id, dev_id, LDC1612_MANUF_ID, LDC1612_DEV_ID) + ) + # Setup chip in requested query rate + rcount0 = LDC1612_FREQ / (16.0 * (self.data_rate - 4)) + self.set_reg(REG_RCOUNT0, int(rcount0 + 0.5)) + self.set_reg(REG_OFFSET0, 0) + self.set_reg( + REG_SETTLECOUNT0, int(SETTLETIME * LDC1612_FREQ / 16.0 + 0.5) + ) + self.set_reg(REG_CLOCK_DIVIDERS0, (1 << 12) | 1) + self.set_reg(REG_ERROR_CONFIG, (0x1F << 11) | 1) + self.set_reg(REG_MUX_CONFIG, 0x0208 | DEGLITCH) + self.set_reg(REG_CONFIG, 0x001 | (1 << 12) | (1 << 10) | (1 << 9)) + self.set_reg(REG_DRIVE_CURRENT0, self.dccal.get_drive_current() << 11) + # Start bulk reading + rest_ticks = self.mcu.seconds_to_clock(0.5 / self.data_rate) + self.query_ldc1612_cmd.send([self.oid, rest_ticks]) + logging.info("LDC1612 starting '%s' measurements", self.name) + # Initialize clock tracking + self.ffreader.note_start() + self.last_error_count = 0 + + def _finish_measurements(self): + # Halt bulk reading + self.query_ldc1612_cmd.send_wait_ack([self.oid, 0]) + self.ffreader.note_end() + logging.info("LDC1612 finished '%s' measurements", self.name) + + def _process_batch(self, eventtime): + samples = self.ffreader.pull_samples() + self._convert_samples(samples) + if not samples: + return {} + if self.calibration is not None: + self.calibration.apply_calibration(samples) + return { + "data": samples, + "errors": self.last_error_count, + "overflows": self.ffreader.get_last_overflows(), + } diff --git a/klippy/extras/lis2dw.py b/klippy/extras/lis2dw.py index 5e72ee5b0..b2f4f6fc5 100644 --- a/klippy/extras/lis2dw.py +++ b/klippy/extras/lis2dw.py @@ -30,9 +30,6 @@ FREEFALL_ACCEL = 9.80665 SCALE = FREEFALL_ACCEL * 1.952 / 4 -BYTES_PER_SAMPLE = 6 -SAMPLES_PER_BLOCK = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE - BATCH_UPDATES = 0.100 @@ -55,13 +52,9 @@ def __init__(self, config): "query_lis2dw oid=%d rest_ticks=0" % (oid,), on_restart=True ) mcu.register_config_callback(self._build_config) - self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, oid=oid) - # Clock tracking + # Bulk sample message reading chip_smooth = self.data_rate * BATCH_UPDATES * 2 - self.clock_sync = bulk_sensor.ClockSyncRegression(mcu, chip_smooth) - self.clock_updater = bulk_sensor.ChipClockUpdater( - self.clock_sync, BYTES_PER_SAMPLE - ) + self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, "hhh") self.last_error_count = 0 # Process messages in batches self.batch_bulk = bulk_sensor.BatchBulkHelper( @@ -108,8 +101,8 @@ def _build_config(self): self.query_mpu9250_cmd = self.mcu.lookup_command( "query_mpu9250 oid=%c rest_ticks=%u", cq=cmdqueue ) - self.clock_updater.setup_query_command( - self.mcu, "query_mpu9250_status oid=%c", oid=self.oid, cq=cmdqueue + self.ffreader.setup_query_command( + "query_mpu9250_status oid=%c", oid=self.oid, cq=cmdqueue ) def read_reg(self, reg): @@ -125,39 +118,16 @@ def start_internal_client(self): return aqh # Measurement decoding - def _extract_samples(self, raw_samples): - # Load variables to optimize inner loop below + def _convert_samples(self, samples): (x_pos, x_scale), (y_pos, y_scale), (z_pos, z_scale) = self.axes_map - last_sequence = self.clock_updater.get_last_sequence() - time_base, chip_base, inv_freq = self.clock_sync.get_time_translation() - # Process every message in raw_samples - count = seq = 0 - samples = [None] * (len(raw_samples) * SAMPLES_PER_BLOCK) - for params in raw_samples: - seq_diff = (params["sequence"] - last_sequence) & 0xFFFF - seq_diff -= (seq_diff & 0x8000) << 1 - seq = last_sequence + seq_diff - d = bytearray(params["data"]) - msg_cdiff = seq * SAMPLES_PER_BLOCK - chip_base - - for i in range(len(d) // BYTES_PER_SAMPLE): - d_xyz = d[i * BYTES_PER_SAMPLE : (i + 1) * BYTES_PER_SAMPLE] - xhigh, xlow, yhigh, ylow, zhigh, zlow = d_xyz - # Merge and perform twos-complement - rx = ((xhigh << 8) | xlow) - ((xhigh & 0x80) << 9) - ry = ((yhigh << 8) | ylow) - ((yhigh & 0x80) << 9) - rz = ((zhigh << 8) | zlow) - ((zhigh & 0x80) << 9) - - raw_xyz = (rx, ry, rz) - x = round(raw_xyz[x_pos] * x_scale, 6) - y = round(raw_xyz[y_pos] * y_scale, 6) - z = round(raw_xyz[z_pos] * z_scale, 6) - ptime = round(time_base + (msg_cdiff + i) * inv_freq, 6) - samples[count] = (ptime, x, y, z) - count += 1 - self.clock_sync.set_last_chip_clock(seq * SAMPLES_PER_BLOCK + i) - del samples[count:] - return samples + count = 0 + for ptime, rx, ry, rz in samples: + raw_xyz = (rx, ry, rz) + x = round(raw_xyz[x_pos] * x_scale, 6) + y = round(raw_xyz[y_pos] * y_scale, 6) + z = round(raw_xyz[z_pos] * z_scale, 6) + samples[count] = (round(ptime, 6), x, y, z) + count += 1 # Start, stop, and process message batches def _start_measurements(self): @@ -194,36 +164,32 @@ def _start_measurements(self): self.read_reg(REG_INT_STATUS) # clear FIFO overflow flag # Start bulk reading - self.bulk_queue.clear_samples() rest_ticks = self.mcu.seconds_to_clock(4.0 / self.data_rate) self.query_mpu9250_cmd.send([self.oid, rest_ticks]) self.set_reg(REG_FIFO_EN, SET_ENABLE_FIFO) logging.info("MPU9250 starting '%s' measurements", self.name) # Initialize clock tracking - self.clock_updater.note_start() + self.ffreader.note_start() self.last_error_count = 0 def _finish_measurements(self): # Halt bulk reading self.set_reg(REG_FIFO_EN, SET_DISABLE_FIFO) self.query_mpu9250_cmd.send_wait_ack([self.oid, 0]) - self.bulk_queue.clear_samples() + self.ffreader.note_end() logging.info("MPU9250 finished '%s' measurements", self.name) self.set_reg(REG_PWR_MGMT_1, SET_PWR_MGMT_1_SLEEP) self.set_reg(REG_PWR_MGMT_2, SET_PWR_MGMT_2_OFF) def _process_batch(self, eventtime): - self.clock_updater.update_clock() - raw_samples = self.bulk_queue.pull_samples() - if not raw_samples: - return {} - samples = self._extract_samples(raw_samples) + samples = self.ffreader.pull_samples() + self._convert_samples(samples) if not samples: return {} return { "data": samples, "errors": self.last_error_count, - "overflows": self.clock_updater.get_last_overflows(), + "overflows": self.ffreader.get_last_overflows(), } diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 1e581b291..98178517c 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -151,11 +151,10 @@ def _probe(self, speed): curtime = self.printer.get_reactor().monotonic() if "z" not in toolhead.get_status(curtime)["homed_axes"]: raise self.printer.command_error("Must home before probe") - phoming = self.printer.lookup_object("homing") pos = toolhead.get_position() pos[2] = self.z_position try: - epos = phoming.probing_move(self.mcu_probe, pos, speed) + epos = self.mcu_probe.probing_move(pos, speed) except self.printer.command_error as e: reason = str(e) if "Timeout during endstop homing" in reason: @@ -416,7 +415,7 @@ def _handle_mcu_identify(self): if stepper.is_active_axis("z"): self.add_stepper(stepper) - def raise_probe(self): + def _raise_probe(self): toolhead = self.printer.lookup_object("toolhead") start_pos = toolhead.get_position() self.deactivate_gcode.run_gcode_from_command() @@ -425,7 +424,7 @@ def raise_probe(self): "Toolhead moved during probe activate_gcode script" ) - def lower_probe(self): + def _lower_probe(self): toolhead = self.printer.lookup_object("toolhead") start_pos = toolhead.get_position() self.activate_gcode.run_gcode_from_command() @@ -442,18 +441,22 @@ def multi_probe_begin(self): def multi_probe_end(self): if self.stow_on_each_sample: return - self.raise_probe() + 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() + self._lower_probe() if self.multi == "FIRST": self.multi = "ON" def probe_finish(self, hmove): if self.multi == "OFF": - self.raise_probe() + self._raise_probe() def get_position_endstop(self): return self.position_endstop diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py new file mode 100644 index 000000000..f4c7df56e --- /dev/null +++ b/klippy/extras/probe_eddy_current.py @@ -0,0 +1,382 @@ +# Support for eddy current based Z probes +# +# Copyright (C) 2021-2024 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import math, bisect +import mcu +from . import ldc1612, probe, manual_probe + + +# Tool for calibrating the sensor Z detection and applying that calibration +class EddyCalibration: + def __init__(self, config): + self.printer = config.get_printer() + self.name = config.get_name() + # Current calibration data + self.cal_freqs = [] + self.cal_zpos = [] + cal = config.get("calibrate", None) + if cal is not None: + cal = [ + list(map(float, d.strip().split(":", 1))) + for d in cal.split(",") + ] + self.load_calibration(cal) + # Probe calibrate state + self.probe_speed = 0.0 + # Register commands + cname = self.name.split()[-1] + gcode = self.printer.lookup_object("gcode") + gcode.register_mux_command( + "PROBE_EDDY_CURRENT_CALIBRATE", + "CHIP", + cname, + self.cmd_EDDY_CALIBRATE, + desc=self.cmd_EDDY_CALIBRATE_help, + ) + + def is_calibrated(self): + return len(self.cal_freqs) > 2 + + def load_calibration(self, cal): + cal = sorted([(c[1], c[0]) for c in cal]) + self.cal_freqs = [c[0] for c in cal] + self.cal_zpos = [c[1] for c in cal] + + def apply_calibration(self, samples): + 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 + elif pos == 0: + zpos = 99.9 + else: + # XXX - could further optimize and avoid div by zero + this_freq = self.cal_freqs[pos] + prev_freq = self.cal_freqs[pos - 1] + this_zpos = self.cal_zpos[pos] + 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 + samples[i] = (samp_time, freq, round(zpos, 6)) + + def height_to_freq(self, height): + # XXX - could optimize lookup + rev_zpos = list(reversed(self.cal_zpos)) + rev_freqs = list(reversed(self.cal_freqs)) + pos = bisect.bisect(rev_zpos, height) + if pos == 0 or pos >= len(rev_zpos): + raise self.printer.command_error( + "Invalid probe_eddy_current height" + ) + this_freq = rev_freqs[pos] + prev_freq = rev_freqs[pos - 1] + this_zpos = rev_zpos[pos] + 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 + + def do_calibration_moves(self, move_speed): + toolhead = self.printer.lookup_object("toolhead") + kin = toolhead.get_kinematics() + move = toolhead.manual_move + # Start data collection + msgs = [] + is_finished = False + + def handle_batch(msg): + if is_finished: + return False + msgs.append(msg) + return True + + self.printer.lookup_object(self.name).add_client(handle_batch) + toolhead.dwell(1.0) + # Move to each 50um position + max_z = 4 + samp_dist = 0.050 + num_steps = int(max_z / samp_dist + 0.5) + 1 + start_pos = toolhead.get_position() + times = [] + for i in range(num_steps): + # Move to next position (always descending to reduce backlash) + hop_pos = list(start_pos) + hop_pos[2] += i * samp_dist + 0.500 + move(hop_pos, move_speed) + next_pos = list(start_pos) + next_pos[2] += i * samp_dist + move(next_pos, move_speed) + # Note sample timing + start_query_time = toolhead.get_last_move_time() + 0.050 + end_query_time = start_query_time + 0.100 + toolhead.dwell(0.200) + # Find Z position based on actual commanded stepper position + toolhead.flush_step_generation() + kin_spos = { + s.get_name(): s.get_commanded_position() + for s in kin.get_steppers() + } + kin_pos = kin.calc_position(kin_spos) + times.append((start_query_time, end_query_time, kin_pos[2])) + toolhead.dwell(1.0) + toolhead.wait_moves() + # Finish data collection + is_finished = True + # Correlate query responses + cal = {} + step = 0 + for msg in msgs: + for query_time, freq, old_z in msg["data"]: + # Add to step tracking + while step < len(times) and query_time > times[step][1]: + step += 1 + if step < len(times) and query_time >= times[step][0]: + cal.setdefault(times[step][2], []).append(freq) + if len(cal) != len(times): + raise self.printer.command_error( + "Failed calibration - incomplete sensor data" + ) + return cal + + def calc_freqs(self, meas): + total_count = total_variance = 0 + positions = {} + for pos, freqs in meas.items(): + count = len(freqs) + freq_avg = float(sum(freqs)) / count + positions[pos] = freq_avg + total_count += count + total_variance += sum([(f - freq_avg) ** 2 for f in freqs]) + return positions, math.sqrt(total_variance / total_count), total_count + + def post_manual_probe(self, kin_pos): + if kin_pos is None: + # Manual Probe was aborted + return + curpos = list(kin_pos) + move = self.printer.lookup_object("toolhead").manual_move + # Move away from the bed + probe_calibrate_z = curpos[2] + curpos[2] += 5.0 + move(curpos, self.probe_speed) + # Move sensor over nozzle position + pprobe = self.printer.lookup_object("probe") + x_offset, y_offset, z_offset = pprobe.get_offsets() + curpos[0] -= x_offset + curpos[1] -= y_offset + move(curpos, self.probe_speed) + # Descend back to bed + curpos[2] -= 5.0 - 0.050 + move(curpos, self.probe_speed) + # Perform calibration movement and capture + cal = self.do_calibration_moves(self.probe_speed) + # Calculate each sample position average and variance + positions, std, total = self.calc_freqs(cal) + last_freq = 0.0 + for pos, freq in reversed(sorted(positions.items())): + if freq <= last_freq: + raise self.printer.command_error( + "Failed calibration - frequency not increasing each step" + ) + last_freq = freq + gcode = self.printer.lookup_object("gcode") + gcode.respond_info( + "probe_eddy_current: stddev=%.3f in %d queries\n" + "The SAVE_CONFIG command will update the printer config file\n" + "and restart the printer." % (std, total) + ) + # Save results + cal_contents = [] + for i, (pos, freq) in enumerate(sorted(positions.items())): + if not i % 3: + cal_contents.append("\n") + cal_contents.append("%.6f:%.3f" % (pos - probe_calibrate_z, freq)) + cal_contents.append(",") + cal_contents.pop() + configfile = self.printer.lookup_object("configfile") + configfile.set(self.name, "calibrate", "".join(cal_contents)) + + cmd_EDDY_CALIBRATE_help = "Calibrate eddy current probe" + + def cmd_EDDY_CALIBRATE(self, gcmd): + self.probe_speed = gcmd.get_float("PROBE_SPEED", 5.0, above=0.0) + # Start manual probe + manual_probe.ManualProbeHelper( + self.printer, gcmd, self.post_manual_probe + ) + + +# Helper for implementing PROBE style commands +class EddyEndstopWrapper: + def __init__(self, config, sensor_helper, calibration): + self._printer = config.get_printer() + self._sensor_helper = sensor_helper + self._mcu = sensor_helper.get_mcu() + self._calibration = calibration + self._z_offset = config.getfloat("z_offset", minval=0.0) + self._dispatch = mcu.TriggerDispatch(self._mcu) + self._samples = [] + self._is_sampling = self._start_from_home = self._need_stop = False + self._trigger_time = 0.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 + if self._is_sampling: + return + self._is_sampling = True + self._is_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 + + # Interface for MCU_endstop + def get_mcu(self): + return self._mcu + + def add_stepper(self, stepper): + self._dispatch.add_stepper(stepper) + + def get_steppers(self): + return self._dispatch.get_steppers() + + def home_start( + self, print_time, sample_time, sample_count, rest_time, triggered=True + ): + self._trigger_time = 0.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( + print_time, + trigger_freq, + self._dispatch.get_oid(), + mcu.MCU_trsync.REASON_ENDSTOP_HIT, + ) + return trigger_completion + + 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: + return -1.0 + if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: + return 0.0 + if self._mcu.is_fileoutput(): + return home_end_time + self._trigger_time = trigger_time + return trigger_time + + def query_endstop(self, print_time): + return False # XXX + + # Interface for ProbeEndstopWrapper + def probing_move(self, pos, speed): + # Perform probing move + phoming = self._printer.lookup_object("homing") + trig_pos = phoming.probing_move(self, pos, speed) + if not self._trigger_time: + return trig_pos + # Wait for 200ms to elapse since trigger time + reactor = self._printer.get_reactor() + while 1: + 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.0: + break + reactor.pause(systime + need_delay) + # 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.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 + + 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() + + def multi_probe_end(self): + self._stop_measurements() + + def probe_prepare(self, hmove): + pass + + def probe_finish(self, hmove): + pass + + def get_position_endstop(self): + return self._z_offset + + +# Main "printer object" +class PrinterEddyProbe: + def __init__(self, config): + self.printer = config.get_printer() + self.calibration = EddyCalibration(config) + # Sensor type + sensors = {"ldc1612": ldc1612.LDC1612} + sensor_type = config.getchoice("sensor_type", {s: s for s in sensors}) + self.sensor_helper = sensors[sensor_type](config, self.calibration) + # Probe interface + self.probe = EddyEndstopWrapper( + config, self.sensor_helper, self.calibration + ) + self.printer.add_object("probe", probe.PrinterProbe(config, self.probe)) + + def add_client(self, cb): + self.sensor_helper.add_client(cb) + + +def load_config_prefix(config): + return PrinterEddyProbe(config) diff --git a/klippy/extras/sht3x.py b/klippy/extras/sht3x.py new file mode 100644 index 000000000..52fc7c845 --- /dev/null +++ b/klippy/extras/sht3x.py @@ -0,0 +1,168 @@ +# SHT3X i2c based temperature sensors support +# +# Copyright (C) 2024 Timofey Titovets +# Based on htu21d.py code +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from . import bus + +###################################################################### +# Compatible Sensors: +# SHT31 - Tested on octopus pro and Linux MCU +# +###################################################################### +SHT3X_I2C_ADDR = 0x44 + +SHT3X_CMD = { + "MEASURE": { + "STRETCH_ENABLED": { + "HIGH_REP": [0x2C, 0x06], # High (15ms) repeatability measurement + "MED_REP": [0x2C, 0x0D], # Medium (6ms) repeatability measurement + "LOW_REP": [0x2C, 0x10], # Low (4ms) repeatability measurement + }, + "STRETCH_DISABLED": { + "HIGH_REP": [0x24, 0x00], + "MED_REP": [0x24, 0x0B], + "LOW_REP": [0x24, 0x16], + }, + }, + "OTHER": { + "STATUS": { + "READ": [0xF3, 0x2D], + "CLEAN": [0x30, 0x41], + }, + "SOFTRESET": [0x30, 0xA2], # Soft reset + "HEATER": { + "ENABLE": [0x30, 0x6D], + "DISABLE": [0x30, 0x66], + }, + "FETCH": [0xE0, 0x00], + "BREAK": [0x30, 0x93], + }, +} + + +class SHT3X: + def __init__(self, config): + self.printer = config.get_printer() + self.name = config.get_name().split()[-1] + self.reactor = self.printer.get_reactor() + self.i2c = bus.MCU_I2C_from_config( + config, default_addr=SHT3X_I2C_ADDR, default_speed=100000 + ) + self.report_time = config.getint("sht3x_report_time", 1, minval=1) + self.deviceId = config.get("sensor_type") + self.temp = self.min_temp = self.max_temp = self.humidity = 0.0 + self.sample_timer = self.reactor.register_timer(self._sample_sht3x) + self.printer.add_object("sht3x " + self.name, self) + self.printer.register_event_handler( + "klippy:connect", self.handle_connect + ) + + def handle_connect(self): + self._init_sht3x() + self.reactor.update_timer(self.sample_timer, self.reactor.NOW) + + def setup_minmax(self, min_temp, max_temp): + self.min_temp = min_temp + self.max_temp = max_temp + + def setup_callback(self, cb): + self._callback = cb + + def get_report_time_delta(self): + return self.report_time + + 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() + 0.02) + + status = self.i2c.i2c_read(SHT3X_CMD["OTHER"]["STATUS"]["READ"], 3) + response = bytearray(status["response"]) + status = response[0] << 8 + status |= response[1] + checksum = response[2] + + if self._crc8(status) != checksum: + logging.warning("sht3x: Reading status - checksum error!") + + 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() + 0.20) + + params = self.i2c.i2c_read([], 6) + + response = bytearray(params["response"]) + rtemp = response[0] << 8 + rtemp |= response[1] + if self._crc8(rtemp) != response[2]: + logging.warning("sht3x: Checksum error on Temperature reading!") + else: + self.temp = -45 + (175 * rtemp / 65535) + logging.debug("sht3x: Temperature %.2f " % self.temp) + + rhumid = response[3] << 8 + rhumid |= response[4] + if self._crc8(rhumid) != response[5]: + logging.warning("sht3x: Checksum error on Humidity reading!") + else: + self.humidity = 100 * rhumid / 65535 + logging.debug("sht3x: Humidity %.2f " % self.humidity) + + except Exception: + logging.exception("sht3x: Error reading data") + self.temp = self.humidity = 0.0 + return self.reactor.NEVER + + if self.temp < self.min_temp or self.temp > self.max_temp: + self.printer.invoke_shutdown( + "sht3x: temperature %0.1f outside range of %0.1f:%.01f" + % (self.temp, self.min_temp, self.max_temp) + ) + + measured_time = self.reactor.monotonic() + print_time = self.i2c.get_mcu().estimated_print_time(measured_time) + self._callback(print_time, self.temp) + return measured_time + self.report_time + + def _split_bytes(self, data): + bytes = [] + for i in range((data.bit_length() + 7) // 8): + bytes.append((data >> i * 8) & 0xFF) + bytes.reverse() + return bytes + + def _crc8(self, data): + # crc8 polynomial for 16bit value, CRC8 -> x^8 + x^5 + x^4 + 1 + SHT3X_CRC8_POLYNOMINAL = 0x31 + crc = 0xFF + data_bytes = self._split_bytes(data) + for byte in data_bytes: + crc ^= byte + for _ in range(8): + if crc & 0x80: + crc = (crc << 1) ^ SHT3X_CRC8_POLYNOMINAL + else: + crc <<= 1 + return crc & 0xFF + + def get_status(self, eventtime): + return { + "temperature": round(self.temp, 2), + "humidity": round(self.humidity, 1), + } + + +def load_config(config): + # Register sensor + pheater = config.get_printer().lookup_object("heaters") + pheater.add_sensor_factory("SHT3X", SHT3X) diff --git a/klippy/extras/smart_effector.py b/klippy/extras/smart_effector.py index 3765d1b61..cf7ee1307 100644 --- a/klippy/extras/smart_effector.py +++ b/klippy/extras/smart_effector.py @@ -88,6 +88,10 @@ def __init__(self, config): desc=self.cmd_SET_SMART_EFFECTOR_help, ) + def probing_move(self, pos, speed): + phoming = self.printer.lookup_object("homing") + return phoming.probing_move(self, pos, speed) + def probe_prepare(self, hmove): toolhead = self.printer.lookup_object("toolhead") self.probe_wrapper.probe_prepare(hmove) diff --git a/klippy/extras/temperature_sensors.cfg b/klippy/extras/temperature_sensors.cfg index 107fcd24b..4fbe5492c 100644 --- a/klippy/extras/temperature_sensors.cfg +++ b/klippy/extras/temperature_sensors.cfg @@ -18,6 +18,8 @@ # Load "SI7013", "SI7020", "SI7021", "SHT21", and "HTU21D" sensors [htu21d] +[sht3x] + # Load "AHT10" [aht10] diff --git a/klippy/extras/virtual_sdcard.py b/klippy/extras/virtual_sdcard.py index 286743083..f9617e8b2 100644 --- a/klippy/extras/virtual_sdcard.py +++ b/klippy/extras/virtual_sdcard.py @@ -8,6 +8,13 @@ VALID_GCODE_EXTS = ["gcode", "g", "gco"] +DEFAULT_ERROR_GCODE = """ +{% if 'heaters' in printer %} + TURN_OFF_HEATERS +{% endif %} +""" + + class VirtualSD: def __init__(self, config): self.printer = config.get_printer() @@ -30,7 +37,7 @@ def __init__(self, config): # Error handling gcode_macro = self.printer.load_object(config, "gcode_macro") self.on_error_gcode = gcode_macro.load_template( - config, "on_error_gcode", "" + config, "on_error_gcode", DEFAULT_ERROR_GCODE ) # Register commands self.gcode = self.printer.lookup_object("gcode") diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index 437b71683..e514f6813 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -6,11 +6,11 @@ import stepper from . import idex_modes + # The hybrid-corexy kinematic is also known as Markforged kinematics class HybridCoreXYKinematics: def __init__(self, toolhead, config): self.printer = config.get_printer() - printer_config = config.getsection("printer") # itersolve parameters self.rails = [ stepper.PrinterRail(config.getsection("stepper_x")), diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index 6ce0c15d6..b0e885d73 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -6,11 +6,11 @@ import stepper from . import idex_modes + # The hybrid-corexz kinematic is also known as Markforged kinematics class HybridCoreXZKinematics: def __init__(self, toolhead, config): self.printer = config.get_printer() - printer_config = config.getsection("printer") # itersolve parameters self.rails = [ stepper.PrinterRail(config.getsection("stepper_x")), diff --git a/klippy/mcu.py b/klippy/mcu.py index 33897717d..f0464d81b 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -297,25 +297,19 @@ def stop(self): TRSYNC_SINGLE_MCU_TIMEOUT = 0.250 -class MCU_endstop: - RETRY_QUERY = 1.000 - - def __init__(self, mcu, pin_params): +class TriggerDispatch: + def __init__(self, mcu): self._mcu = mcu - self._pin = pin_params["pin"] - self._pullup = pin_params["pullup"] - self._invert = pin_params["invert"] - self._oid = self._mcu.create_oid() - self._home_cmd = self._query_cmd = None - self._mcu.register_config_callback(self._build_config) self._trigger_completion = None - self._rest_ticks = 0 ffi_main, ffi_lib = chelper.get_ffi() self._trdispatch = ffi_main.gc(ffi_lib.trdispatch_alloc(), ffi_lib.free) self._trsyncs = [MCU_trsync(mcu, self._trdispatch)] - def get_mcu(self): - return self._mcu + def get_oid(self): + return self._trsyncs[0].get_oid() + + def get_command_queue(self): + return self._trsyncs[0].get_command_queue() def add_stepper(self, stepper): trsyncs = {trsync.get_mcu(): trsync for trsync in self._trsyncs} @@ -339,6 +333,62 @@ def add_stepper(self, stepper): def get_steppers(self): return [s for trsync in self._trsyncs for s in trsync.get_steppers()] + def start(self, print_time): + reactor = self._mcu.get_printer().get_reactor() + self._trigger_completion = reactor.completion() + expire_timeout = get_danger_options().multi_mcu_trsync_timeout + if len(self._trsyncs) == 1: + expire_timeout = TRSYNC_SINGLE_MCU_TIMEOUT + for i, trsync in enumerate(self._trsyncs): + report_offset = float(i) / len(self._trsyncs) + trsync.start( + print_time, + report_offset, + self._trigger_completion, + expire_timeout, + ) + etrsync = self._trsyncs[0] + ffi_main, ffi_lib = chelper.get_ffi() + ffi_lib.trdispatch_start(self._trdispatch, etrsync.REASON_HOST_REQUEST) + return self._trigger_completion + + def wait_end(self, end_time): + etrsync = self._trsyncs[0] + etrsync.set_home_end_time(end_time) + if self._mcu.is_fileoutput(): + self._trigger_completion.complete(True) + self._trigger_completion.wait() + + def stop(self): + 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 + return res[0] + + +class MCU_endstop: + def __init__(self, mcu, pin_params): + self._mcu = mcu + self._pin = pin_params["pin"] + self._pullup = pin_params["pullup"] + self._invert = pin_params["invert"] + self._oid = self._mcu.create_oid() + self._home_cmd = self._query_cmd = None + self._mcu.register_config_callback(self._build_config) + self._rest_ticks = 0 + self._dispatch = TriggerDispatch(mcu) + + def get_mcu(self): + return self._mcu + + def add_stepper(self, stepper): + self._dispatch.add_stepper(stepper) + + def get_steppers(self): + return self._dispatch.get_steppers() + def _build_config(self): # Setup config self._mcu.add_config_cmd( @@ -352,7 +402,7 @@ def _build_config(self): on_restart=True, ) # Lookup commands - cmd_queue = self._trsyncs[0].get_command_queue() + cmd_queue = self._dispatch.get_command_queue() self._home_cmd = self._mcu.lookup_command( "endstop_home oid=%c clock=%u sample_ticks=%u sample_count=%c" " rest_ticks=%u pin_value=%c trsync_oid=%c trigger_reason=%c", @@ -373,22 +423,7 @@ def home_start( self._mcu.print_time_to_clock(print_time + rest_time) - clock ) self._rest_ticks = rest_ticks - reactor = self._mcu.get_printer().get_reactor() - self._trigger_completion = reactor.completion() - expire_timeout = get_danger_options().multi_mcu_trsync_timeout - if len(self._trsyncs) == 1: - expire_timeout = TRSYNC_SINGLE_MCU_TIMEOUT - for i, trsync in enumerate(self._trsyncs): - report_offset = float(i) / len(self._trsyncs) - trsync.start( - print_time, - report_offset, - self._trigger_completion, - expire_timeout, - ) - etrsync = self._trsyncs[0] - ffi_main, ffi_lib = chelper.get_ffi() - ffi_lib.trdispatch_start(self._trdispatch, etrsync.REASON_HOST_REQUEST) + trigger_completion = self._dispatch.start(print_time) self._home_cmd.send( [ self._oid, @@ -397,26 +432,20 @@ def home_start( sample_count, rest_ticks, triggered ^ self._invert, - etrsync.get_oid(), - etrsync.REASON_ENDSTOP_HIT, + self._dispatch.get_oid(), + MCU_trsync.REASON_ENDSTOP_HIT, ], reqclock=clock, ) - return self._trigger_completion + return trigger_completion def home_wait(self, home_end_time): - etrsync = self._trsyncs[0] - etrsync.set_home_end_time(home_end_time) - if self._mcu.is_fileoutput(): - self._trigger_completion.complete(True) - self._trigger_completion.wait() + self._dispatch.wait_end(home_end_time) self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) - ffi_main, ffi_lib = chelper.get_ffi() - ffi_lib.trdispatch_stop(self._trdispatch) - res = [trsync.stop() for trsync in self._trsyncs] - if any([r == etrsync.REASON_COMMS_TIMEOUT for r in res]): + res = self._dispatch.stop() + if res == MCU_trsync.REASON_COMMS_TIMEOUT: return -1.0 - if res[0] != etrsync.REASON_ENDSTOP_HIT: + if res != MCU_trsync.REASON_ENDSTOP_HIT: return 0.0 if self._mcu.is_fileoutput(): return home_end_time @@ -902,7 +931,6 @@ def _send_config(self, prev_crc): 0, "allocate_oids count=%d" % (self._oid_count,) ) # Resolve pin names - mcu_type = self._serial.get_msgparser().get_constant("MCU") ppins = self._printer.lookup_object("pins") pin_resolver = ppins.get_pin_resolver(self._name) for cmdlist in (self._config_cmds, self._restart_cmds, self._init_cmds): diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 09b496acb..080724c3a 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -721,7 +721,6 @@ def register_step_generator(self, handler): def note_step_generation_scan_time(self, delay, old_delay=0.0): self.flush_step_generation() - cur_delay = self.kin_flush_delay if old_delay: self.kin_flush_times.pop(self.kin_flush_times.index(old_delay)) if delay: diff --git a/scripts/motan/data_logger.py b/scripts/motan/data_logger.py index 087beb23b..6eb54a39a 100755 --- a/scripts/motan/data_logger.py +++ b/scripts/motan/data_logger.py @@ -179,10 +179,12 @@ def handle_subscribe(self, msg, raw_msg): ) # Subscribe to additional sensor data stypes = ["adxl345", "lis2dw", "mpu9250", "angle"] + stypes = {st: st for st in stypes} + stypes["probe_eddy_current"] = "ldc1612" config = status["configfile"]["settings"] for cfgname in config.keys(): - for st in stypes: - if cfgname == st or cfgname.startswith(st + " "): + for capprefix, st in sorted(stypes.items()): + if cfgname == capprefix or cfgname.startswith(capprefix + " "): aname = cfgname.split()[-1] lname = "%s:%s" % (st, aname) qcmd = "%s/dump_%s" % (st, st) diff --git a/scripts/motan/readlog.py b/scripts/motan/readlog.py index ff91df480..a2f0b9e11 100644 --- a/scripts/motan/readlog.py +++ b/scripts/motan/readlog.py @@ -18,6 +18,7 @@ class error(Exception): # Log data handlers: {name: class, ...} LogHandlers = {} + # Extract status fields from log class HandleStatusField: SubscriptionIdParts = 0 @@ -50,6 +51,7 @@ def pull_data(self, req_time): LogHandlers["status"] = HandleStatusField + # Extract requested position, velocity, and accel from a trapq log class HandleTrapQ: SubscriptionIdParts = 2 @@ -163,6 +165,7 @@ def _pull_accel(self, req_time): LogHandlers["trapq"] = HandleTrapQ + # Extract positions from queue_step log class HandleStepQ: SubscriptionIdParts = 2 @@ -267,6 +270,7 @@ def _pull_block(self, req_time): LogHandlers["stepq"] = HandleStepQ + # Extract stepper motor phase position class HandleStepPhase: SubscriptionIdParts = 0 @@ -378,6 +382,7 @@ def _pull_block(self, req_time): LogHandlers["step_phase"] = HandleStepPhase + # Extract accelerometer data class HandleADXL345: SubscriptionIdParts = 2 @@ -427,6 +432,7 @@ def pull_data(self, req_time): LogHandlers["adxl345"] = HandleADXL345 + # Extract positions from magnetic angle sensor class HandleAngle: SubscriptionIdParts = 2 @@ -498,10 +504,84 @@ def pull_data(self, req_time): LogHandlers["angle"] = HandleAngle +def interpolate(next_val, prev_val, next_time, prev_time, req_time): + vdiff = next_val - prev_val + tdiff = next_time - prev_time + rtdiff = req_time - prev_time + return prev_val + rtdiff * vdiff / tdiff + + +# Extract eddy current data +class HandleEddyCurrent: + SubscriptionIdParts = 2 + ParametersMin = 1 + ParametersMax = 2 + DataSets = [ + ("ldc1612()", "Coil resonant frequency"), + ("ldc1612(,period)", "Coil resonant period"), + ("ldc1612(,z)", "Estimated Z height"), + ] + + def __init__(self, lmanager, name, name_parts): + self.name = name + self.sensor_name = name_parts[1] + if len(name_parts) == 3 and name_parts[2] not in ("period", "z"): + raise error("Unknown ldc1612 selection '%s'" % (name_parts[2],)) + self.report_frequency = len(name_parts) == 2 + self.report_z = len(name_parts) == 3 and name_parts[2] == "z" + self.jdispatch = lmanager.get_jdispatch() + self.next_samp = self.prev_samp = [0.0, 0.0, 0.0] + self.cur_data = [] + self.data_pos = 0 + + def get_label(self): + if self.report_frequency: + label = "%s frequency" % (self.sensor_name,) + return {"label": label, "units": "Frequency\n(Hz)"} + if self.report_z: + label = "%s height" % (self.sensor_name,) + return {"label": label, "units": "Position\n(mm)"} + label = "%s period" % (self.sensor_name,) + return {"label": label, "units": "Period\n(s)"} + + def pull_data(self, req_time): + while 1: + next_time, next_freq, next_z = self.next_samp + if req_time <= next_time: + prev_time, prev_freq, prev_z = self.prev_samp + if self.report_frequency: + next_val = next_freq + prev_val = prev_freq + elif self.report_z: + next_val = next_z + prev_val = prev_z + else: + next_val = 1.0 / next_freq + prev_val = 1.0 / prev_freq + return interpolate( + next_val, prev_val, next_time, prev_time, req_time + ) + if self.data_pos >= len(self.cur_data): + # Read next data block + jmsg = self.jdispatch.pull_msg(req_time, self.name) + if jmsg is None: + return 0.0 + self.cur_data = jmsg["data"] + self.data_pos = 0 + continue + self.prev_samp = self.next_samp + self.next_samp = self.cur_data[self.data_pos] + self.data_pos += 1 + + +LogHandlers["ldc1612"] = HandleEddyCurrent + + ###################################################################### # Log reading ###################################################################### + # Read, uncompress, and parse messages in a log built by data_logger.py class JsonLogReader: def __init__(self, filename): @@ -573,6 +653,7 @@ def pull_msg(self, req_time, name): # Dataset and log tracking ###################################################################### + # Tracking of get_status messages class TrackStatus: def __init__(self, lmanager, name, start_status): diff --git a/src/Kconfig b/src/Kconfig index 42c33938a..f77037c78 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -114,6 +114,10 @@ config WANT_LIS2DW bool depends on HAVE_GPIO_SPI default y +config WANT_LDC1612 + bool + depends on HAVE_GPIO_I2C + default y config WANT_SOFTWARE_I2C bool depends on HAVE_GPIO && HAVE_GPIO_I2C @@ -124,7 +128,7 @@ config WANT_SOFTWARE_SPI default y config NEED_SENSOR_BULK bool - depends on WANT_SENSORS || WANT_LIS2DW + depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612 default y menu "Optional features (to reduce code size)" depends on HAVE_LIMITED_CODE_SIZE @@ -140,6 +144,9 @@ config WANT_SENSORS config WANT_LIS2DW bool "Support lis2dw 3-axis accelerometer" depends on HAVE_GPIO_SPI +config WANT_LDC1612 + bool "Support ldc1612 eddy current sensor" + depends on HAVE_GPIO_I2C 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 eddad9783..ed98172e4 100644 --- a/src/Makefile +++ b/src/Makefile @@ -19,4 +19,5 @@ sensors-src-$(CONFIG_HAVE_GPIO_SPI) := thermocouple.c sensor_adxl345.c \ 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_NEED_SENSOR_BULK) += sensor_bulk.c diff --git a/src/linux/gpio.c b/src/linux/gpio.c index bb07f5a07..c7f4c5bf6 100644 --- a/src/linux/gpio.c +++ b/src/linux/gpio.c @@ -10,7 +10,7 @@ #include // memset #include // ioctl #include // close -#include // GPIOHANDLE_REQUEST_OUTPUT +#include // GPIOHANDLE_REQUEST_OUTPUT #include "command.h" // shutdown #include "gpio.h" // gpio_out_write #include "internal.h" // report_errno diff --git a/src/linux/main.c b/src/linux/main.c index f9ea3f6da..b260f162b 100644 --- a/src/linux/main.c +++ b/src/linux/main.c @@ -4,7 +4,7 @@ // // This file may be distributed under the terms of the GNU GPLv3 license. -#include // sched_setscheduler sched_get_priority_max +#include // sched_setscheduler sched_get_priority_max #include // fprintf #include // memset #include // getopt diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c new file mode 100644 index 000000000..9258ce6dc --- /dev/null +++ b/src/sensor_ldc1612.c @@ -0,0 +1,207 @@ +// Support for eddy current sensor data from ldc1612 chip +// +// Copyright (C) 2023 Alan.Ma +// Copyright (C) 2024 Kevin O'Connor +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include // memcpy +#include "basecmd.h" // oid_alloc +#include "board/gpio.h" // i2c_read +#include "board/irq.h" // irq_disable +#include "board/misc.h" // timer_read_time +#include "command.h" // DECL_COMMAND +#include "i2ccmds.h" // i2cdev_oid_lookup +#include "sched.h" // DECL_TASK +#include "sensor_bulk.h" // sensor_bulk_report +#include "trsync.h" // trsync_do_trigger + +enum { + LDC_PENDING = 1<<0, + LH_AWAIT_HOMING = 1<<1, LH_CAN_TRIGGER = 1<<2 +}; + +struct ldc1612 { + struct timer timer; + uint32_t rest_ticks; + struct i2cdev_s *i2c; + uint8_t flags; + struct sensor_bulk sb; + // homing + struct trsync *ts; + uint8_t homing_flags; + uint8_t trigger_reason; + uint32_t trigger_threshold; + uint32_t homing_clock; +}; + +static struct task_wake ldc1612_wake; + +// Query ldc1612 data +static uint_fast8_t +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); + ld->timer.waketime += ld->rest_ticks; + return SF_RESCHEDULE; +} + +void +command_config_ldc1612(uint32_t *args) +{ + struct ldc1612 *ld = oid_alloc(args[0], command_config_ldc1612 + , sizeof(*ld)); + ld->timer.func = ldc1612_event; + ld->i2c = i2cdev_oid_lookup(args[1]); +} +DECL_COMMAND(command_config_ldc1612, "config_ldc1612 oid=%c i2c_oid=%c"); + +void +command_ldc1612_setup_home(uint32_t *args) +{ + struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); + + ld->trigger_threshold = args[2]; + if (!ld->trigger_threshold) { + ld->ts = NULL; + ld->homing_flags = 0; + return; + } + ld->homing_clock = args[1]; + ld->ts = trsync_oid_lookup(args[3]); + ld->trigger_reason = args[4]; + ld->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"); + +void +command_query_ldc1612_home_state(uint32_t *args) +{ + struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); + sendf("ldc1612_home_state oid=%c homing=%c trigger_clock=%u" + , args[0], !!(ld->homing_flags & LH_CAN_TRIGGER), ld->homing_clock); +} +DECL_COMMAND(command_query_ldc1612_home_state, + "query_ldc1612_home_state oid=%c"); + +// Chip registers +#define REG_DATA0_MSB 0x00 +#define REG_DATA0_LSB 0x01 +#define REG_STATUS 0x18 + +// Read a register on the ldc1612 +static void +read_reg(struct ldc1612 *ld, uint8_t reg, uint8_t *res) +{ + i2c_read(ld->i2c->i2c_config, sizeof(reg), ®, 2, res); +} + +// Read the status register on the ldc1612 +static uint16_t +read_reg_status(struct ldc1612 *ld) +{ + uint8_t data_status[2]; + read_reg(ld, REG_STATUS, data_status); + return (data_status[0] << 8) | data_status[1]; +} + +#define BYTES_PER_SAMPLE 4 + +// Query ldc1612 data +static void +ldc1612_query(struct ldc1612 *ld, uint8_t oid) +{ + // Clear pending flag + irq_disable(); + ld->flags &= ~LDC_PENDING; + irq_enable(); + + // Check if data available + uint16_t status = read_reg_status(ld); + if (status != 0x48) + return; + + // Read coil0 frequency + uint8_t *d = &ld->sb.data[ld->sb.data_count]; + read_reg(ld, REG_DATA0_MSB, &d[0]); + read_reg(ld, REG_DATA0_LSB, &d[2]); + ld->sb.data_count += BYTES_PER_SAMPLE; + + // 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; + } + } + + // Flush local buffer if needed + if (ld->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(ld->sb.data)) + sensor_bulk_report(&ld->sb, oid); +} + +void +command_query_ldc1612(uint32_t *args) +{ + struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); + + sched_del_timer(&ld->timer); + ld->flags = 0; + if (!args[1]) + // End measurements + return; + + // Start new measurements query + ld->rest_ticks = args[1]; + sensor_bulk_reset(&ld->sb); + irq_disable(); + ld->timer.waketime = timer_read_time() + ld->rest_ticks; + sched_add_timer(&ld->timer); + irq_enable(); +} +DECL_COMMAND(command_query_ldc1612, "query_ldc1612 oid=%c rest_ticks=%u"); + +void +command_query_ldc1612_status(uint32_t *args) +{ + struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); + + uint32_t time1 = timer_read_time(); + uint16_t status = read_reg_status(ld); + uint32_t time2 = timer_read_time(); + + uint32_t fifo = status == 0x48 ? 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"); + +void +ldc1612_task(void) +{ + if (!sched_check_wake(&ldc1612_wake)) + return; + uint8_t oid; + struct ldc1612 *ld; + foreach_oid(oid, ld, command_config_ldc1612) { + uint_fast8_t flags = ld->flags; + if (!(flags & LDC_PENDING)) + continue; + ldc1612_query(ld, oid); + } +} +DECL_TASK(ldc1612_task); diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index 2ae90bee8..d14622a25 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -288,7 +288,7 @@ choice config STM32_FLASH_START_9000 bool "36KiB bootloader" if MACH_STM32F1 config STM32_FLASH_START_C000 - bool "48KiB bootloader" if MACH_STM32F4x5 + bool "48KiB bootloader" if MACH_STM32F4x5 || MACH_STM32F401 config STM32_FLASH_START_10000 bool "64KiB bootloader" if MACH_STM32F103 || MACH_STM32F4 diff --git a/src/stm32/stm32f0_serial.c b/src/stm32/stm32f0_serial.c index b7f067b63..c987f149e 100644 --- a/src/stm32/stm32f0_serial.c +++ b/src/stm32/stm32f0_serial.c @@ -102,6 +102,13 @@ #define USART5_IRQn USART3_4_5_6_LPUART1_IRQn #define USART6_IRQn USART3_4_5_6_LPUART1_IRQn #endif + #if CONFIG_MACH_STM32G0B0 + #define USART2_IRQn USART2_IRQn + #define USART3_IRQn USART3_4_5_6_IRQn + #define USART4_IRQn USART3_4_5_6_IRQn + #define USART5_IRQn USART3_4_5_6_IRQn + #define USART6_IRQn USART3_4_5_6_IRQn + #endif #define USART_CR1_RXNEIE USART_CR1_RXNEIE_RXFNEIE #define USART_CR1_TXEIE USART_CR1_TXEIE_TXFNFIE #define USART_ISR_RXNE USART_ISR_RXNE_RXFNE diff --git a/test/klippy/printers.test b/test/klippy/printers.test index 7f6753508..622353a65 100644 --- a/test/klippy/printers.test +++ b/test/klippy/printers.test @@ -63,7 +63,11 @@ DICTIONARY stm32f103-serial.dict # Printers using the stm32f401 DICTIONARY stm32f401.dict +CONFIG ../../config/generic-fysetc-cheetah-v2.0.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 +CONFIG ../../config/printer-elegoo-neptune3-pro-2023.cfg # Printers using the stm32f405 DICTIONARY stm32f405.dict From 7884129aad4bf9f187ae3be176e06d0129128ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Beaucamp?= Date: Tue, 23 Apr 2024 22:38:38 +0200 Subject: [PATCH 2/2] Some improvements to dockable_probe (#175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add restore_toolhead, extract_position, insert_position settings * Fix : Black format . Signed-off-by: Frédéric Beaucamp * add parameters in test config --------- Signed-off-by: Frédéric Beaucamp --- docs/Config_Reference.md | 16 +++++++++++ docs/Dockable_Probe.md | 36 ++++++++++++------------ klippy/extras/dockable_probe.py | 49 +++++++++++++++++++++++++++------ test/klippy/dockable_probe.cfg | 3 ++ 4 files changed, 79 insertions(+), 25 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 1a88b0e09..b43bd0676 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2218,12 +2218,28 @@ detach_position: 0,0,0 # If Z is specified the toolhead will move to the Z location before the X, Y # coordinates. # This parameter is required. +#extract_position: 0,0,0 +# Similar to the approach_position, the extract_position is the coordinates +# where the toolhead is moved to extract the probe from the dock. +# If Z is specified the toolhead will move to the Z location before the X, Y +# coordinates. +# The default value is approach_probe value. +#insert_position: 0,0,0 +# Similar to the extract_position, the insert_position is the coordinates +# where the toolhead is moved before inserting the probe into the dock. +# If Z is specified the toolhead will move to the Z location before the X, Y +# coordinates. +# The default value is extract_probe value. #z_hop: 15.0 # Distance (in mm) to lift the Z axis prior to attaching/detaching the probe. # If the Z axis is already homed and the current Z position is less # than `z_hop`, then this will lift the head to a height of `z_hop`. If # the Z axis is not already homed the head is lifted by `z_hop`. # The default is to not implement Z hop. +#restore_toolhead: True +# While True, the position of the toolhead is restored to the position prior +# to the attach/detach movements. +# The default value is True. #dock_retries: # The number of times to attempt to attach/dock the probe before raising # an error and aborting probing. diff --git a/docs/Dockable_Probe.md b/docs/Dockable_Probe.md index cb2955d18..88551bba4 100644 --- a/docs/Dockable_Probe.md +++ b/docs/Dockable_Probe.md @@ -73,6 +73,16 @@ detach_position: away from the probe dock such that the magnets on the probe body are not attracted to the magnets on the toolhead. +- `extract_position: 295, 250, 0`\ + _Default Value: approach\_position_\ + Euclid probe requires the toolhead to move to a different direction to extract + or dock mag_probe. + +- `insert_position: 295, 250, 0`\ + _Default Value: extract\_position_\ + Usually the same as extract position for Euclid probe when the dock is on the + gantry. + - `z_hop: 15.0`\ _Default Value: None_\ Distance (in mm) to lift the Z axis prior to attaching/detaching the probe. @@ -81,6 +91,11 @@ detach_position: the Z axis is not already homed the head is lifted by `z_hop`. The default is to not implement Z hop. +- `restore_toolhead: False|True`\ + _Default Value: True_\ + While True, the position of the toolhead is restored to the position prior to + the attach/detach movements. + ## Position Examples Probe mounted on frame at back of print bed at a fixed Z position. To attach @@ -145,9 +160,7 @@ z_hop: 15 Euclid style probe that requires the attach and detach movements to happen in opposite order. Attach: approach, move to dock, extract. Detach: move to extract position, move to dock, move to approach position. The approach and -detach positions are the same, as are the extract and insert positions. The -movements can be reordered as necessary by overriding the commands for -extract/insert and using the same coordinates for approach and detach. +detach positions are the same, as are the extract and insert positions. ``` Attach: @@ -167,20 +180,11 @@ Detach: ``` approach_position: 50, 150 dock_position: 10, 150 +extract_position: 10, 130 detach_position: 50, 150 z_hop: 15 ``` -``` -[gcode_macro MOVE_TO_EXTRACT_PROBE] -gcode: - G1 X10 Y130 - -[gcode_macro MOVE_TO_INSERT_PROBE] -gcode: - G1 X10 Y130 -``` - ### Homing No configuration specific to the dockable probe is required when using @@ -294,13 +298,11 @@ This command will move the toolhead to the `dock_position`. `MOVE_TO_EXTRACT_PROBE` -This command will move the toolhead away from the dock after attaching the probe. -By default it's an alias for `MOVE_TO_APPROACH_PROBE`. +This command will move the toolhead to the `extract_position`. `MOVE_TO_INSERT_PROBE` -This command will move the toolhead near the dock before detaching the probe. -By default it's an alias for `MOVE_TO_APPROACH_PROBE`. +This command will move the toolhead to the `insert_position`. `MOVE_TO_DETACH_PROBE` diff --git a/klippy/extras/dockable_probe.py b/klippy/extras/dockable_probe.py index cef509b94..2fac13c38 100644 --- a/klippy/extras/dockable_probe.py +++ b/klippy/extras/dockable_probe.py @@ -31,6 +31,7 @@ Please see {0}.md and config_Reference.md. """ + # Helper class to handle polling pins for probe attachment states class PinPollingHelper: def __init__(self, config, endstop): @@ -167,6 +168,7 @@ def __init__(self, config): self.lift_speed = config.getfloat("lift_speed", self.speed, above=0.0) self.dock_retries = config.getint("dock_retries", 0) self.auto_attach_detach = config.getboolean("auto_attach_detach", True) + self.restore_toolhead = config.getboolean("restore_toolhead", True) self.travel_speed = config.getfloat( "travel_speed", self.speed, above=0.0 ) @@ -183,6 +185,12 @@ def __init__(self, config): # Positions (approach, detach, etc) self.approach_position = self._parse_coord(config, "approach_position") self.detach_position = self._parse_coord(config, "detach_position") + self.extract_position = self._parse_coord( + config, "extract_position", self.approach_position + ) + self.insert_position = self._parse_coord( + config, "insert_position", self.extract_position + ) self.dock_position = self._parse_coord(config, "dock_position") self.z_hop = config.getfloat("z_hop", 0.0, above=0.0) @@ -195,7 +203,7 @@ def __init__(self, config): self.dock_position, self.approach_position ) self.detach_angle, self.detach_distance = self._get_vector( - self.dock_position, self.detach_position + self.dock_position, self.insert_position ) # Pins @@ -290,11 +298,14 @@ def __init__(self, config): # and return a list of numbers. # # e.g. "233, 10, 0" -> [233, 10, 0] - def _parse_coord(self, config, name, expected_dims=3): - val = config.get(name) + def _parse_coord(self, config, name, default=None, expected_dims=3): + if default: + val = config.get(name, None) + else: + val = config.get(name) error_msg = "Unable to parse {0} in {1}: {2}" if not val: - return None + return default try: vals = [float(x.strip()) for x in val.split(",")] except Exception as e: @@ -338,6 +349,7 @@ def get_status(self, curtime): # Use last_'status' here to be consistent with QUERY_PROBE_'STATUS'. return { "last_status": self.last_probe_state, + "auto_attach_detach": self.auto_attach_detach, } cmd_MOVE_TO_APPROACH_PROBE_help = ( @@ -382,14 +394,35 @@ def cmd_MOVE_TO_DOCK_PROBE(self, gcmd): ) def cmd_MOVE_TO_EXTRACT_PROBE(self, gcmd): - self.cmd_MOVE_TO_APPROACH_PROBE(gcmd) + if len(self.extract_position) > 2: + self.toolhead.manual_move( + [None, None, self.extract_position[2]], self.attach_speed + ) + + self.toolhead.manual_move( + [self.extract_position[0], self.extract_position[1], None], + self.attach_speed, + ) cmd_MOVE_TO_INSERT_PROBE_help = ( "Move near the dock with the" "probe attached before detaching" ) def cmd_MOVE_TO_INSERT_PROBE(self, gcmd): - self.cmd_MOVE_TO_APPROACH_PROBE(gcmd) + if self._check_distance(dist=self.detach_distance): + self._align_to_vector(self.detach_angle) + else: + self._move_to_vector(self.detach_angle) + + if len(self.insert_position) > 2: + self.toolhead.manual_move( + [None, None, self.insert_position[2]], self.travel_speed + ) + + self.toolhead.manual_move( + [self.insert_position[0], self.insert_position[1], None], + self.travel_speed, + ) cmd_MOVE_TO_DETACH_PROBE_help = ( "Move away from the dock to detach" "the probe" @@ -461,7 +494,7 @@ def attach_probe(self, return_pos=None): if self.get_probe_state() != PROBE_ATTACHED: raise self.printer.command_error("Probe attach failed!") - if return_pos: + if return_pos and self.restore_toolhead: if not self._check_distance(return_pos, self.approach_distance): self.toolhead.manual_move( [return_pos[0], return_pos[1], None], self.travel_speed @@ -492,7 +525,7 @@ def detach_probe(self, return_pos=None): if self.get_probe_state() != PROBE_DOCKED: raise self.printer.command_error("Probe detach failed!") - if return_pos: + if return_pos and self.restore_toolhead: if not self._check_distance(return_pos, self.detach_distance): self.toolhead.manual_move( [return_pos[0], return_pos[1], None], self.travel_speed diff --git a/test/klippy/dockable_probe.cfg b/test/klippy/dockable_probe.cfg index c1b34db0d..d75e8f042 100644 --- a/test/klippy/dockable_probe.cfg +++ b/test/klippy/dockable_probe.cfg @@ -75,6 +75,9 @@ max_z_accel: 100 dock_position: 100, 100 approach_position: 150, 150 detach_position: 120, 120 +extract_position: 140, 140 +insert_position: 130, 130 pin: PH6 z_offset: 1.15 check_open_attach: false +restore_toolhead: true