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