diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..292578f93 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +_Enter a good description of whats being changed and WHY + +## Checklist + +- [ ] pr title makes sense +- [ ] squashed to 1 commit +- [ ] 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 106157265..735d18633 100644 --- a/Makefile +++ b/Makefile @@ -29,10 +29,11 @@ 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 -fwhole-program -fno-use-linker-plugin -ggdb3 +CFLAGS += -flto=auto -fwhole-program -fno-use-linker-plugin -ggdb3 OBJS_klipper.elf = $(patsubst %.c, $(OUT)src/%.o,$(src-y)) OBJS_klipper.elf += $(OUT)compile_time_request.o diff --git a/README.md b/README.md index 39d205f5f..053992343 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ If I want my printer to light itself on fire, I should be able to make my printe - [gcode: expose math functions to gcode macros](https://github.com/DangerKlippers/danger-klipper/pull/173) ([klipper#4072](https://github.com/Klipper3d/klipper/pull/4072)) +- [gcode: HEATER_INTERRUPT gcode command](https://github.com/DangerKlippers/danger-klipper/pull/94) + - [probe: dockable Probe](https://github.com/DangerKlippers/danger-klipper/pull/43) ([klipper#4328](https://github.com/Klipper3d/klipper/pull/4328)) - [probe: drop the first result](https://github.com/DangerKlippers/danger-klipper/pull/2) ([klipper#3397](https://github.com/Klipper3d/klipper/issues/3397)) @@ -88,6 +90,12 @@ If I want my printer to light itself on fire, I should be able to make my printe - [filament_switch|motion_sensor: runout distance, smart and runout gcode](https://github.com/DangerKlippers/danger-klipper/pull/158) +- [z_tilt|qgl: custom threshold for probe_points_increasing check](https://github.com/DangerKlippers/danger-klipper/pull/189) + +- [save_config: save without restarting the firmware](https://github.com/DangerKlippers/danger-klipper/pull/191) + +- [configfile: recursive globs](https://github.com/DangerKlippers/danger-klipper/pull/200) / ([klipper#6375](https://github.com/Klipper3d/klipper/pull/6375)) + - [temperature_fan: curve control algorithm](https://github.com/DangerKlippers/danger-klipper/pull/193) If you're feeling adventurous, take a peek at the extra features in the bleeding-edge branch [feature documentation](docs/Bleeding_Edge.md) diff --git a/config/printer-anycubic-4maxpro-2.0-2021.cfg b/config/printer-anycubic-4maxpro-2.0-2021.cfg index e0c83b6ae..203e3c8f2 100644 --- a/config/printer-anycubic-4maxpro-2.0-2021.cfg +++ b/config/printer-anycubic-4maxpro-2.0-2021.cfg @@ -126,7 +126,6 @@ restart_method: arduino kinematics: cartesian max_velocity: 150 max_accel: 3000 -max_accel_to_decel: 1500 max_z_velocity: 7 max_z_accel: 50 square_corner_velocity: 5 diff --git a/config/printer-anycubic-kobra-go-2022.cfg b/config/printer-anycubic-kobra-go-2022.cfg index d4fc409f0..bb8a9267f 100644 --- a/config/printer-anycubic-kobra-go-2022.cfg +++ b/config/printer-anycubic-kobra-go-2022.cfg @@ -118,7 +118,7 @@ cycle_time: 0.00005 #20kHz [output_pin enable_pin] pin: PB6 -static_value: 1 +value: 1 #This pin enables the bed, hotend, extruder fan, part fan. [mcu] 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/config/printer-sovol-sv05-2022.cfg b/config/printer-sovol-sv05-2022.cfg index dafc6f3cf..0ec03f84a 100644 --- a/config/printer-sovol-sv05-2022.cfg +++ b/config/printer-sovol-sv05-2022.cfg @@ -19,7 +19,7 @@ restart_method: command kinematics: cartesian max_velocity: 300 max_accel: 1000 -max_accel_to_decel: 1000 +minimum_cruise_ratio: 0.0 max_z_velocity: 5 max_z_accel: 100 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 d68f1f5cc..cdbfe1d8a 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,20 @@ 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 +`printer.toolhead.max_accel_to_decel` status has been removed. Use the +[minimum_cruise_ratio parameter](./Config_Reference.md#printer) +instead. The deprecated features will be removed in the near future, +and using them in the interim may result in subtly different behavior. + 20240215: Several deprecated features have been removed. Using "NTC 100K beta 3950" as a thermistor name has been removed (deprecated on 20211110). The `SYNC_STEPPER_TO_EXTRUDER` and diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index d583c7658..6fb574f5c 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 ``` @@ -148,13 +148,22 @@ max_accel: # will do so at the rate specified here. The value specified here # may be changed at runtime using the SET_VELOCITY_LIMIT command. # This parameter must be specified. -#max_accel_to_decel: -# A pseudo acceleration (in mm/s^2) controlling how fast the -# toolhead may go from acceleration to deceleration. It is used to -# reduce the top speed of short zig-zag moves (and thus reduce -# printer vibration from these moves). The value specified here may -# be changed at runtime using the SET_VELOCITY_LIMIT command. The -# default is half of max_accel. +#minimum_cruise_ratio: 0.5 +# Most moves will accelerate to a cruising speed, travel at that +# cruising speed, and then decelerate. However, some moves that +# travel a short distance could nominally accelerate and then +# immediately decelerate. This option reduces the top speed of these +# moves to ensure there is always a minimum distance traveled at a +# cruising speed. That is, it enforces a minimum distance traveled +# at cruising speed relative to the total distance traveled. It is +# intended to reduce the top speed of short zigzag moves (and thus +# reduce printer vibration from these moves). For example, a +# minimum_cruise_ratio of 0.5 would ensure that a standalone 1.5mm +# move would have a minimum cruising distance of 0.75mm. Specify a +# ratio of 0.0 to disable this feature (there would be no minimum +# cruising distance enforced between acceleration and deceleration). +# The value specified here may be changed at runtime using the +# SET_VELOCITY_LIMIT command. The default is 0.5. #square_corner_velocity: 5.0 # The maximum velocity (in mm/s) that the toolhead may travel a 90 # degree corner at. A non-zero value can reduce changes in extruder @@ -167,6 +176,8 @@ max_accel: # decelerate to zero at each corner. The value specified here may be # changed at runtime using the SET_VELOCITY_LIMIT command. The # default is 5mm/s. +#max_accel_to_decel: +# This parameter is deprecated and should no longer be used. ``` ### [stepper] @@ -1308,6 +1319,9 @@ extended [G-Code command](G-Codes.md#z_tilt) becomes available. # more points than steppers then you will likely have a fixed # minimum value for the range of probed points which you can learn # by observing command output. +#increasing_threshold: 0.0000001 +# Sets the threshold that probe points can increase before z_tilt aborts. +# To disable the validation, set this parameter to a high value. ``` ``` @@ -1332,6 +1346,8 @@ extended [G-Code command](G-Codes.md#z_tilt) becomes available. # See [z_tilt] #retry_tolerance: 0 # See [z_tilt] +#increasing_threshold: 0.0000001 +# See [z_tilt] #extra_points: # A list in the same format as "points" above. This list contains # additional points to be probed during the two calibration commands @@ -1409,6 +1425,9 @@ Where x is the 0, 0 point on the bed #retry_tolerance: 0 # If retries are enabled then retry if largest and smallest probed # points differ more than retry_tolerance. +#increasing_threshold: 0.0000001 +# Sets the threshold that probe points can increase before qgl aborts. +# To disable the validation, set this parameter to a high value. ``` ### [skew_correction] @@ -1669,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] @@ -2008,7 +2029,7 @@ aliases_: Include file support. One may include additional config file from the main printer config file. Wildcards may also be used (eg, -"configs/\*.cfg"). +"configs/\*.cfg", or "configs/\*\*/\*.cfg" if using python version >=3.5). ``` [include my_other_config.cfg] @@ -2197,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. @@ -2297,6 +2334,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 @@ -2925,6 +2996,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 @@ -4821,6 +4911,13 @@ more information. # If set to true the sensor will use the virtual_sd_card module to determine # whether the printer is printing which is more reliable but will not work # when streaming a print over usb or similar. +#always_fire_events: +# If set to true, runout events will always fire no matter whether the sensor +# is enabled or disabled. Usefull for MMUs +#check_on_print_start: +# If set to true, the sensor will be reevaluated when a print starts and if +# no filament is detected the runout_gcode will be run no matter the defined +# runout_distance(immediate_runout_gcode will not be run in this case) ``` ### [filament_motion_sensor] @@ -4848,6 +4945,7 @@ switch_pin: #event_delay: #pause_delay: #smart: +#always_fire_events: # See the "filament_switch_sensor" section for a description of the # above parameters. ``` @@ -5183,6 +5281,237 @@ cs_pin: # above parameters. ``` +### [trad_rack] + +Trad Rack multimaterial system support. See the following documents from the +TradRack repo for additional information: +- [Tuning.md](https://github.com/Annex-Engineering/TradRack/blob/main/docs/Tuning.md): + document referenced by some of the config options below. +- [Trad Rack config reference document](https://github.com/Annex-Engineering/TradRack/blob/main/docs/klipper/Config_Reference.md): contains info on additional config + sections that are expected to be used alongside [trad_rack]. + +``` +[trad_rack] +selector_max_velocity: +# 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. +# This parameter must be specified. +#filament_max_velocity: +# 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 +# be used. +lane_count: +# The number of filament lanes. This parameter must be specified. +lane_spacing: +# 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 +# the lanes (from 0 to lane_count - 1). The option will apply an +# offset (in mm) to the corresponding lane's position. Lane offsets +# do not affect the position of any lanes besides the one specified +# in the option name. This option is intended for fine adjustment +# of each lane's position to ensure that the filament paths in the +# lane module and selector line up with each other. +# The default is 0.0 for each lane. +#lane_spacing_mod_: +# Options with a "lane_spacing_mod_" prefix may be specified for any +# of the lanes (from 0 to lane_count - 1). The option will apply an +# offset (in mm) to the corresponding lane's position, as well as +# any lane with a higher index. For example, if lane_spacing_mod_2 +# is 4.0, any lane with an index of 2 or above will have its +# position increased by 4.0. This option is intended to account for +# variations in a lane module that will affect its position as well +# as the positions of any subsequent modules with a higher index. +# The default is 0.0 for each lane. +servo_down_angle: +# The angle (in degrees) for the servo's down position. +# This parameter must be specified. +servo_up_angle: +# The angle (in degrees) for the servo's up position. +# This parameter must be specified. +#servo_wait_ms: 500 +# Time (in milliseconds) to wait for the servo to complete moves +# between the up and down angles. The default is 500. +selector_unload_length: +# Length (in mm) to retract a piece of filament out of the selector +# and back into the lane module after the selector sensor has been +# triggered or untriggered. This parameter must be specified. +#selector_unload_length_extra: 0.0 +# Extra length (in mm) that is added to selector_unload_length when +# retracting a piece of filament out of the selector and back into +# the lane module. After the retraction, the filament is moved +# forward by this length as well (so this option's value has no +# effect on the final position of the filament). This option may be +# useful when using Trad Rack with a motorized spool rewinder that +# senses tension or compression in the filament between the spool +# and Trad Rack in order to determine when to rotate the spool. The +# extra forward movement of the filament after retracting is +# intended to force the rewinder's sensor to detect tension in the +# filament, causing rewinding to cease immediately so the filament +# tip is not moved out of position by excess spool movement. The +# default is 0.0. +#eject_length: 10.0 +# Length (in mm) to eject the filament into the lane module past the +# length defined by selector_unload_length. The filament is ejected +# whenever unloading a depleted spool after a runout to make sure +# that filament segment is not loaded again until it has been +# replaced. +bowden_length: +# Length (in mm) to quickly move filament through the bowden tube +# between Trad Rack and the toolhead during loads and unloads. +# See Tuning.md for details. This parameter must be specified. +extruder_load_length: +# Length (in mm) to move filament into the extruder when loading the +# toolhead. See Tuning.md for details. +# This parameter must be specified. +hotend_load_length: +# Length (in mm) to move filament into the hotend when loading the +# toolhead. See Tuning.md for details. +# This parameter must be specified. +toolhead_unload_length: +# Length (in mm) to move filament out of the toolhead during an +# unload. See Tuning.md for details. If toolhead_fil_sensor_pin is +# specified, this parameter must be specified. +# If toolhead_fil_sensor_pin is not specified, the default is +# extruder_load_length + hotend_load_length. +#selector_sense_speed: 40.0 +# Speed (in mm/s) when moving filament until the selector +# sensor is triggered or untriggered. See Tuning.md for details +# on when this speed is applied. The default is 40.0. +#selector_unload_speed: 60.0 +# Speed (in mm/s) to move filament when unloading the selector. +# The default is 60.0. +#eject_speed: 80.0 +# Speed (in mm/s) to move the filament when ejecting a filament +# 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. +# The default is 100.0. +#buffer_pull_speed: +# Speed (in mm/s) to move filament through the bowden tube when +# unloading or loading from a buffer. See Tuning.md for details. +# Defaults to spool_pull_speed. +#toolhead_sense_speed: +# Speed (in mm/s) when moving filament until the toolhead +# sensor is triggered or untriggered. See Tuning.md for details on +# when this speed is applied. Defaults to selector_sense_speed. +#extruder_load_speed: +# Speed (in mm/s) to move filament into the extruder when loading +# the toolhead. See Tuning.md for details. The default is 60.0. +#hotend_load_speed: +# Speed (in mm/s) to move filament into the hotend when loading the +# toolhead. See Tuning.md for details. The default is 7.0. +#toolhead_unload_speed: +# Speed (in mm/s) to move filament when unloading the toolhead. +# See Tuning.md for details. Defaults to extruder_load_speed. +#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. +#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 +# toolhead_fil_sensor_pin is not specified. +#fil_homing_retract_dist: 20.0 +# Distance (in mm) to retract filament away from a filament sensor +# before moving on to the next move. This retraction occurs whenever +# a filament sensor is triggered early during a fast move through +# the bowden tube. See Tuning.md for details. The default is 20.0. +#target_toolhead_homing_dist: +# Target filament travel distance (in mm) when homing to the +# toolhead filament sensor during a load. See Tuning.md for details. +# Defaults to either 10.0 or toolhead_unload_length, whichever is +# greater. +#target_selector_homing_dist: +# Target filament travel distance (in mm) when homing to the +# selector filament sensor during an unload. See Tuning.md for +# details. The default is 10.0. +#bowden_length_samples: 10 +# Maximum number of samples that are averaged to set bowden lengths +# for loading and unloading. See Tuning.md for details. The default +# is 10. +#load_lane_time: 15 +# Approximate maximum time (in seconds) to wait for filament to +# reach the selector filament sensor when loading a lane with the +# TR_LOAD_LANE gcode command. This time starts when the user is +# prompted to insert filament and determines when the command will +# be halted early if no filament is detected. The default is 15. +#load_selector_homing_dist: +# Maximum distance to try to move filament when loading from a lane +# module to the selector filament sensor before halting the homing +# move. This value is not used by the TR_LOAD_LANE command but is +# used in similar scenarios that do not involve user interaction. +# Defaults to selector_unload_length * 2. +#bowden_load_homing_dist: +# Maximum distance to try to move filament near the end of a +# toolhead load (during the slow homing move to the toolhead sensor) +# before halting the homing move. Defaults to bowden_length. +#bowden_unload_homing_dist: +# Maximum distance to try to move filament near the end of a +# toolhead unload (during the slow homing move to the selector +# sensor) before halting the homing move. Defaults to bowden_length. +#unload_toolhead_homing_dist: +# Maximum distance to try to move filament near the beginning of a +# toolhead unload (during the homing move to the toolhead sensor) +# before halting the homing move. +# Defaults to (extruder_load_length + hotend_load_length) * 2. +#sync_to_extruder: False +# Syncs Trad Rack's filament driver to the extruder during printing, +# as well as during any extrusion moves within toolhead loading or +# unloading that would normally involve only the extruder. +# The default is False. +#user_wait_time: 15 +# Time (in seconds) to wait for the user to take an action +# before continuing automatically. If set to -1, Trad Rack will wait +# for the user indefinitely. This value is currently used by the +# TR_LOCATE_SELECTOR gcode command. The default is 15. +#register_toolchange_commands: True +# Whether to register gcode commands T0, T1, T2, etc. so that they +# can be used to initiate toolchanges with Trad Rack. If set to +# False, the TR_LOAD_TOOLHEAD command can still be used as a +# substitute to initiate toolchanges. The default is True. +#save_active_lane: True +# Whether to save the active lane to disk whenever it is set using +# save_variables. If set to True, the TR_LOCATE_SELECTOR gcode +# command will infer the active lane if the selector filament sensor +# is triggered and an active lane was saved previously. +# The default is True. +#log_bowden_lengths: False +# Whether to log bowden load length data and bowden unload length +# data (to ~/bowden_load_lengths.csv and ~/bowden_unload_lengths.csv +# respectively). The default is False. +#pre_unload_gcode: +# Gcode command template that is run before the toolhead is +# unloaded. The default is to run no extra commands. +#post_unload_gcode: +# Gcode command template that is run after the toolhead is +# unloaded. The default is to run no extra commands. +#pre_load_gcode: +# Gcode command template that is run before the toolhead is +# loaded. The default is to run no extra commands. +#post_load_gcode: +# Gcode command template that is run after the toolhead is +# loaded. The default is to run no extra commands. +#pause_gcode: +# Gcode command template that is run whenever Trad Rack needs to +# pause the print (usually due to a failed load or unload). The +# default is to run the PAUSE gcode command. +#resume_gcode: +# Gcode command template that is run whenever the TR_RESUME command +# needs to resume the print. The default is to run the RESUME +# gcode command. +``` + ## Common bus parameters ### Common SPI settings 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/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 91d18243a..2f2b02d89 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -146,6 +146,17 @@ Writes raw "value" into register "register". Both "value" and and refer to sensor data sheet for the reference. This is only available for tle5012b chips. +### [axis_twist_compensation] + +The following commands are available when the +[axis_twist_compensation config +section](Config_Reference.md#axis_twist_compensation) is enabled. + +#### AXIS_TWIST_COMPENSATION_CALIBRATE +`AXIS_TWIST_COMPENSATION_CALIBRATE [SAMPLE_COUNT=]`: Initiates the X +twist calibration wizard. `SAMPLE_COUNT` specifies the number of points along +the X axis to calibrate at and defaults to 3. + ### [bed_mesh] The following commands are available when the @@ -280,10 +291,11 @@ EEPROM of a BLTouch V3.1 Available output_modes are: `5V`, `OD` The configfile module is automatically loaded. #### SAVE_CONFIG -`SAVE_CONFIG`: This command will overwrite the main printer config +`SAVE_CONFIG [RESTART=0|1]`: This command will overwrite the main printer config file and restart the host software. This command is used in conjunction with other calibration commands to store the results of calibration tests. +If RESTART is set to 0, no restart will be performed !!USE WITH CAUTION!!. ### [delayed_gcode] @@ -538,29 +550,34 @@ depend on the sensor type defined in the configuration. #### SET_FILAMENT_SENSOR ###### For filament_switch_sensor: `SET_FILAMENT_SENSOR SENSOR= [ENABLE=0|1] [RESET=0|1] -[RUNOUT_DISTANCE=] [SMART=0|1]`: Sets values for the -filament sensor. If all parameters are omitted, the current stats will -be reported.
+[RUNOUT_DISTANCE=] [SMART=0|1] [ALWAYS_FIRE_EVENTS=0|1] [CHECK_ON_PRINT_START=0|1]`: +Sets values for the filament sensor. +If all parameters are omitted, the current stats will be reported.
ENABLE sets the filament sensor on/off. If ENABLE is set to 0, the filament sensor will be disabled, if set to 1 it is enabled. If the state of the sensor changes, a reset will be triggered.
RESET removes all pending runout_gcodes and pauses and force a reevaluation of the sensor state.
RUNOUT_DISTANCE sets the runout_distance.
-SMART sets the smart parameter. +SMART sets the smart parameter.
+ALWAYS_FIRE_EVENTS sets the always_fire_events parameter, if set to true, +a reset of the sensor will be triggered.
+CHECK_ON_PRINT_START sets the check_on_print_start parameter. ###### For filament_motion_sensor: `SET_FILAMENT_SENSOR SENSOR= [ENABLE=0|1] [RESET=0|1] -[DETECTION_LENGTH=] [SMART=0|1]`: Sets values for the -filament sensor. If all parameters are omitted, the current stats will -be reported.
+[DETECTION_LENGTH=] [SMART=0|1] [ALWAYS_FIRE_EVENTS=0|1]`: +Sets values for the filament sensor. +If all parameters are omitted, the current stats will be reported.
ENABLE sets the filament sensor on/off. If ENABLE is set to 0, the filament sensor will be disabled, if set to 1 it is enabled. If the sensor was previously disabled and gets enabled, a reset will be triggered.
RESET resets the state of the sensor and sets it to filament detected.
DETECTION_LENGTH sets the detection_length, if the new detection length is different from the old one, a reset will be triggered.
-SMART sets the smart parameter. +SMART sets the smart parameter.
+ALWAYS_FIRE_EVENTS sets the always_fire_events parameter, no reset will +be triggered. ### [firmware_retraction] @@ -681,6 +698,9 @@ software (see `FIRMWARE_RESTART`: This is similar to a RESTART command, but it also clears any error state from the micro-controller. +#### HEATER_INTERRUPT +`HEATER_INTERRUPT`: Interrupts a TEMPERATURE_WAIT command. + #### STATUS `STATUS`: Report the Klipper host software status. @@ -1150,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 @@ -1468,7 +1510,7 @@ The toolhead module is automatically loaded. #### SET_VELOCITY_LIMIT `SET_VELOCITY_LIMIT [VELOCITY=] [ACCEL=] -[ACCEL_TO_DECEL=] [SQUARE_CORNER_VELOCITY=]`: This +[MINIMUM_CRUISE_RATIO=] [SQUARE_CORNER_VELOCITY=]`: This command can alter the velocity limits that were specified in the printer config file. See the [printer config section](Config_Reference.md#printer) for a @@ -1487,6 +1529,202 @@ The velocity argument is not available on CoreXY. With no arguments, this command responds with the movement direction with the most acceleration or velocity. +### [trad_rack] + +The following commands are available when the +[trad_rack config section](Config_Reference.md#trad_rack) is enabled. + +#### TR_HOME +`TR_HOME`: Homes the selector. + +#### TR_GO_TO_LANE +`TR_GO_TO_LANE LANE=`: Moves the selector to the specified +lane. + +#### TR_LOAD_LANE +`TR_LOAD_LANE LANE= [RESET_SPEED=<0|1>]`: Ensures filament +is loaded into the module for the specified lane by prompting the user +to insert filament, loading filament from the module into the +selector, and retracting the filament back into the module. +If RESET_SPEED is 1, the bowden move speed used for the +specified LANE will be reset to spool_pull_speed from the +[trad_rack config section](Config_Reference.md#trad_rack) +(see [bowden speeds](https://github.com/Annex-Engineering/TradRack/blob/main/docs/Tuning.md#bowden-speeds) +for details on how the bowden speed settings are used). If not +specified, RESET_SPEED defaults to 1. + +#### TR_LOAD_TOOLHEAD +`TR_LOAD_TOOLHEAD LANE=|TOOL= +[MIN_TEMP=] [EXACT_TEMP=] +[BOWDEN_LENGTH=] [EXTRUDER_LOAD_LENGTH=] +[HOTEND_LOAD_LENGTH=]`: Loads filament from the specified lane or +tool into the toolhead*. Either LANE or TOOL must be specified. If +both are specified, then LANE takes precedence. If there is already an +"active lane" because the toolhead has been loaded beforehand, it will +be unloaded before loading the new filament. If `MIN_TEMP` is +specified and it is higher than the extruder's current temperature, +then the extruder will be heated to at least `MIN_TEMP` before +unloading/loading; the current extruder temperature target may be used +instead if it is higher than `MIN_TEMP`, and if not then +[tr_last_heater_target](https://github.com/Annex-Engineering/TradRack/blob/main/docs/klipper/Save_Variables.md) +may be used. If `EXACT_TEMP` is specified, the extruder will be heated +to `EXACT_TEMP` before unloading/loading, regardless of any other +temperature setting. If any of the optional length parameters are +specified, they override the corresponding settings in the +[trad_rack config section](Config_Reference.md#trad_rack). + +\* see the [Tool Mapping document](https://github.com/Annex-Engineering/TradRack/blob/main/docs/Tool_Mapping.md) +for details on the difference between lanes and tools and how they +relate to each other. + +#### T0, T1, T2, etc. +`T`: Equivalent to calling +`TR_LOAD_TOOLHEAD TOOL=`. All of the optional parameters +accepted by the TR_LOAD_TOOLHEAD command can also be used with these +commands. + +#### TR_UNLOAD_TOOLHEAD +`TR_UNLOAD_TOOLHEAD [MIN_TEMP=] +[EXACT_TEMP=]`: Unloads filament from the toolhead and +back into its module. If `MIN_TEMP` is specified and it is higher than +the extruder's current temperature, then the extruder will be heated +to at least `MIN_TEMP` before unloading; the current extruder +temperature target may be used instead if it is higher than +`MIN_TEMP`, and if not then +[tr_last_heater_target](https://github.com/Annex-Engineering/TradRack/blob/main/docs/klipper/Save_Variables.md) +may be used. If `EXACT_TEMP` is specified, the extruder will be heated +to `EXACT_TEMP` before unloading/loading, regardless of any other +temperature setting. + +#### TR_SERVO_DOWN +`TR_SERVO_DOWN [FORCE=<0|1>]`: Moves the servo to bring the drive gear +down. The selector must be moved to a valid lane before using this +command, unless FORCE is 1. If not specified, FORCE defaults to 0. The +FORCE parameter is unsafe for normal use and should only be used when +the servo is not attached to Trad Rack's carriage. + +#### TR_SERVO_UP +`TR_SERVO_UP`: Moves the servo to bring the drive gear up. + +#### TR_SET_ACTIVE_LANE +`TR_SET_ACTIVE_LANE LANE=`: Tells Trad Rack to assume the +toolhead has been loaded with filament from the specified lane. The +selector's position will also be inferred from this lane, and the +selector motor will be enabled if it isn't already. + +#### TR_RESET_ACTIVE_LANE +`TR_RESET_ACTIVE_LANE`: Tells Trad Rack to assume the toolhead has +not been loaded. + +#### TR_RESUME +`TR_RESUME`: Retries loading the last lane, loads the next filament +into the toolhead, and resumes the print. The user will be prompted +to use this command if Trad Rack has paused the print due to a failed +load or unload. + +#### TR_LOCATE_SELECTOR +`TR_LOCATE_SELECTOR`: Ensures the position of Trad Rack's selector is +known so that it is ready for a print. If the user needs to take an +action, they will be prompted to do so and the print will be paused +(for example if the selector sensor is triggered but no active lane is +set). The user_wait_time config option from the +[trad_rack config section](Config_Reference.md#trad_rack) determines +how long Trad Rack will wait for user action before automatically +unloading the toolhead and resuming. In addition, the save_active_lane +config option determines whether this command can infer the "active +lane" from a value saved before the last restart if the selector +filament sensor is triggered but no active lane is currently set. +It is recommended to call this command in the print start gcode. + +#### TR_NEXT +`TR_NEXT`: You will be prompted to use this command if Trad Rack +requires user confirmation before continuing an action. + +#### TR_SYNC_TO_EXTRUDER +`TR_SYNC_TO_EXTRUDER`: Syncs Trad Rack's filament driver to the +extruder during printing, as well as during any extrusion moves within +toolhead loading or unloading that would normally involve only the +extruder. See the +[Extruder syncing document](https://github.com/Annex-Engineering/TradRack/blob/main/docs/Extruder_Syncing.md) +for more details. If you want the filament driver to be synced to the extruder +on every startup without having to call this command, you can set +sync_to_extruder to True in the +[trad_rack config section](Config_Reference.md#trad_rack). + +#### TR_UNSYNC_FROM_EXTRUDER +`TR_UNSYNC_FROM_EXTRUDER`: Unsyncs Trad Rack's filament driver from +the extruder during printing, as well as during any extrusion moves +within toolhead loading or unloading that normally involve only the +extruder. This is the default behavior unless you have set +sync_to_extruder to True in the +[trad_rack config section](Config_Reference.md#trad_rack). + +#### TR_SERVO_TEST +`TR_SERVO_TEST [ANGLE=]`: Moves the servo to the specified +ANGLE relative to the down position. If ANGLE is not specified, the +servo will be moved to the up position defined by servo_up_angle from +the [trad_rack config section](Config_Reference.md#trad_rack). +This command is meant for testing different servo angles in order +to find the correct value for servo_up_angle. + +#### TR_CALIBRATE_SELECTOR +`TR_CALIBRATE_SELECTOR`: Initiates the process of calibrating +lane_spacing, as well as the min, endstop, and max positions of the +selector motor. You will be guided through the selector calibration +process via messages in the console. + +#### TR_SET_HOTEND_LOAD_LENGTH +`TR_SET_HOTEND_LOAD_LENGTH VALUE=|ADJUST=`: Sets the +value of hotend_load_length, overriding its value from the +[trad_rack config section](Config_Reference.md#trad_rack). Does not +persist across restarts. If the VALUE parameter is used, +hotend_load_length will be set to the value passed in. If the ADJUST +parameter is used, the adjustment will be added to the current value +of hotend_load_length. + +### TR_DISCARD_BOWDEN_LENGTHS +`TR_DISCARD_BOWDEN_LENGTHS [MODE=[ALL|LOAD|UNLOAD]]`: Discards saved +values for "bowden_load_length" and/or "bowden_unload_length" (see +[bowden lengths](https://github.com/Annex-Engineering/TradRack/blob/main/docs/Tuning.md#bowden-lengths) +for details on how these settings are used). These settings will each +be reset to the value of `bowden_length` from the +[trad_rack config section](Config_Reference.md#trad_rack), and empty +dictionaries will be saved for +[tr_calib_bowden_load_length and tr_calib_bowden_unload_length](https://github.com/Annex-Engineering/TradRack/blob/main/docs/klipper/Save_Variables.md). +"bowden_load_length" and tr_calib_bowden_load_length will be +affected if MODE=LOAD is specified, "bowden_unload_length" and +tr_calib_bowden_unload_length will be affected if MODE=UNLOAD is +specified, and all 4 will be affected if MODE=ALL is specified. If not +specified, MODE defaults to ALL. + +#### TR_ASSIGN_LANE +`TR_ASSIGN_LANE LANE= TOOL= +[SET_DEFAULT=<0|1>]`: +Assigns the specified LANE to the specified TOOL. If SET_DEFAULT is 1, +LANE will become the default lane for the tool. If not specified, +SET_DEFAULT defaults to 0. + +#### TR_SET_DEFAULT_LANE +`TR_SET_DEFAULT_LANE LANE= [TOOL=]`: If TOOL +is specified, LANE will be set as the default lane for the tool. If +TOOL is not specified, LANE will be set as the default lane for its +currently-assigned tool. + +#### TR_RESET_TOOL_MAP +`TR_RESET_TOOL_MAP`: Resets lane/tool mapping. Each tool will be +mapped to a lane group consisting of a single lane with the same index +as the tool. + +#### TR_PRINT_TOOL_MAP +`TR_PRINT_TOOL_MAP`: Prints a table of the lane/tool mapping to the +console, with rows corresponding to tools and columns corresponding to +lanes. + +#### TR_PRINT_TOOL_GROUPS +`TR_PRINT_TOOL_GROUPS`: Prints a list of lanes assigned to each tool +to the console. If a tool has multiple lanes assigned to it, the +default lane will be indicated. + ### [tuning_tower] The tuning_tower module is automatically loaded. @@ -1543,17 +1781,6 @@ print. #### SDCARD_RESET_FILE `SDCARD_RESET_FILE`: Unload file and clear SD state. -### [axis_twist_compensation] - -The following commands are available when the -[axis_twist_compensation config -section](Config_Reference.md#axis_twist_compensation) is enabled. - -#### AXIS_TWIST_COMPENSATION_CALIBRATE -`AXIS_TWIST_COMPENSATION_CALIBRATE [SAMPLE_COUNT=]`: Initiates the X -twist calibration wizard. `SAMPLE_COUNT` specifies the number of points along -the X axis to calibrate at and defaults to 3. - ### [z_thermal_adjust] The following commands are available when the @@ -1594,11 +1821,13 @@ The following commands are available when the [z_tilt config section](Config_Reference.md#z_tilt) is enabled. #### Z_TILT_ADJUST -`Z_TILT_ADJUST [HORIZONTAL_MOVE_Z=] [=]`: This +`Z_TILT_ADJUST [HORIZONTAL_MOVE_Z=] [=] +[INCREASING_THRESHOLD=]`: This command will probe the points specified in the config and then make independent adjustments to each Z stepper to compensate for tilt. See the PROBE command for details on the optional probe parameters. The optional `HORIZONTAL_MOVE_Z` value overrides the `horizontal_move_z` option specified in the config file. +INCREASING_THRESHOLD sets the increasing_threshold parameter of z_tilt. The follwing commands are availabe when the parameter "extra_points" is configured in the z_tilt_ng section: - `Z_TILT_CALIBRATE [AVGLEN=]`: This command does multiple probe diff --git a/docs/Kinematics.md b/docs/Kinematics.md index 313d5dce0..1996579d2 100644 --- a/docs/Kinematics.md +++ b/docs/Kinematics.md @@ -96,7 +96,7 @@ Key formula for look-ahead: end_velocity^2 = start_velocity^2 + 2*accel*move_distance ``` -### Smoothed look-ahead +### Minimum cruise ratio Klipper also implements a mechanism for smoothing out the motions of short "zigzag" moves. Consider the following moves: @@ -105,21 +105,27 @@ short "zigzag" moves. Consider the following moves: In the above, the frequent changes from acceleration to deceleration can cause the machine to vibrate which causes stress on the machine -and increases the noise. To reduce this, Klipper tracks both regular -move acceleration as well as a virtual "acceleration to deceleration" -rate. Using this system, the top speed of these short "zigzag" moves -are limited to smooth out the printer motion: +and increases the noise. Klipper implements a mechanism to ensure +there is always some movement at a cruising speed between acceleration +and deceleration. This is done by reducing the top speed of some moves +(or sequence of moves) to ensure there is a minimum distance traveled +at cruising speed relative to the distance traveled during +acceleration and deceleration. + +Klipper implements this feature by tracking both a regular move +acceleration as well as a virtual "acceleration to deceleration" rate: ![smoothed](img/smoothed.svg.png) Specifically, the code calculates what the velocity of each move would be if it were limited to this virtual "acceleration to deceleration" -rate (half the normal acceleration rate by default). In the above -picture the dashed gray lines represent this virtual acceleration rate -for the first move. If a move can not reach its full cruising speed -using this virtual acceleration rate then its top speed is reduced to -the maximum speed it could obtain at this virtual acceleration -rate. For most moves the limit will be at or above the move's existing +rate. In the above picture the dashed gray lines represent this +virtual acceleration rate for the first move. If a move can not reach +its full cruising speed using this virtual acceleration rate then its +top speed is reduced to the maximum speed it could obtain at this +virtual acceleration rate. + +For most moves the limit will be at or above the move's existing limits and no change in behavior is induced. For short zigzag moves, however, this limit reduces the top speed. Note that it does not change the actual acceleration within the move - the move continues to diff --git a/docs/Measuring_Resonances.md b/docs/Measuring_Resonances.md index d1d998941..c06f17a58 100644 --- a/docs/Measuring_Resonances.md +++ b/docs/Measuring_Resonances.md @@ -207,7 +207,7 @@ software dependencies not installed by default. First, run on your Raspberry Pi the following commands: ``` sudo apt update -sudo apt install python3-numpy python3-matplotlib libatlas-base-dev libopenblas-base +sudo apt install python3-numpy python3-matplotlib libatlas-base-dev libopenblas-dev ``` Next, in order to install NumPy in the Klipper environment, run the command: 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 aaeaf7abf..8f2d2b643 100644 --- a/docs/Resonance_Compensation.md +++ b/docs/Resonance_Compensation.md @@ -48,8 +48,8 @@ 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. Increase `max_accel_to_decel` by issuing the following command: - `SET_VELOCITY_LIMIT ACCEL_TO_DECEL=7000` +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, execute `SET_INPUT_SHAPER SHAPER_FREQ_X=0 SHAPER_FREQ_Y=0` command. If you @@ -149,7 +149,7 @@ a few other related parameters. Print the ringing test model as follows: 1. Restart the firmware: `RESTART` -2. Prepare for test: `SET_VELOCITY_LIMIT ACCEL_TO_DECEL=7000` +2. Prepare for test: `SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0` 3. Disable Pressure Advance: `SET_PRESSURE_ADVANCE ADVANCE=0` 4. Execute: `SET_INPUT_SHAPER SHAPER_TYPE=MZV` 5. Execute the command: @@ -270,7 +270,7 @@ frequencies after enabling [input_shaper], this section will not help with that. Assuming that you have sliced the ringing model with suggested parameters, complete the following steps for each of the axes X and Y: -1. Prepare for test: `SET_VELOCITY_LIMIT ACCEL_TO_DECEL=7000` +1. Prepare for test: `SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0` 2. Make sure Pressure Advance is disabled: `SET_PRESSURE_ADVANCE ADVANCE=0` 3. Execute: `SET_INPUT_SHAPER SHAPER_TYPE=ZV` 4. From the existing ringing test model with your chosen input shaper select @@ -331,7 +331,7 @@ with suggested parameters, print the test model 3 times as follows. First time, prior to printing, run 1. `RESTART` -2. `SET_VELOCITY_LIMIT ACCEL_TO_DECEL=7000` +2. `SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0` 3. `SET_PRESSURE_ADVANCE ADVANCE=0` 4. `SET_INPUT_SHAPER SHAPER_TYPE=2HUMP_EI SHAPER_FREQ_X=60 SHAPER_FREQ_Y=60` 5. `TUNING_TOWER COMMAND=SET_VELOCITY_LIMIT PARAMETER=ACCEL START=1500 STEP_DELTA=500 STEP_HEIGHT=5` diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index 013e89f9d..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 @@ -540,7 +541,7 @@ The following information is available in the `toolhead` object limit value (eg, `axis_minimum.x`, `axis_maximum.z`). - For Delta printers the `cone_start_z` is the max z height at maximum radius (`printer.toolhead.cone_start_z`). -- `max_velocity`, `max_accel`, `max_accel_to_decel`, +- `max_velocity`, `max_accel`, `minimum_cruise_ratio`, `square_corner_velocity`: The current printing limits that are in effect. This may differ from the config file settings if a `SET_VELOCITY_LIMIT` (or `M204`) command alters them at run-time. @@ -558,6 +559,14 @@ on a cartesian, hybrid_corexy or hybrid_corexz robot - `carriage_1`: The mode of the carriage 1. Possible values are: "INACTIVE", "PRIMARY", "COPY", and "MIRROR". +## trad_rack + +The following informatin is available in the +[trad_rack](Config_Reference.md#trad_rack) object: +- `curr_lane`: The lane the selector is currently positioned at. +- `active_lane`: The lane currently loaded in the toolhead. +- `selector_homed`: Whether or not the selector axis is homed. + ## virtual_sdcard The following information is available in the 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 7f82e6987..71bc4c8c8 100644 --- a/klippy/configfile.py +++ b/klippy/configfile.py @@ -4,6 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import sys, os, glob, re, time, logging, configparser, io +from extras.danger_options import get_danger_options error = configparser.Error @@ -262,6 +263,8 @@ def __init__(self, printer): self.printer = printer self.autosave = None self.deprecated = {} + self.runtime_warnings = [] + self.deprecate_warnings = [] self.status_raw_config = {} self.status_save_pending = {} self.status_settings = {} @@ -321,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 @@ -361,7 +363,10 @@ def _resolve_include( dirname = os.path.dirname(source_filename) include_spec = include_spec.strip() include_glob = os.path.join(dirname, include_spec) - include_filenames = glob.glob(include_glob) + if sys.version_info >= (3, 5): + include_filenames = glob.glob(include_glob, recursive=True) + else: + include_filenames = glob.glob(include_glob) if not include_filenames and not glob.has_magic(include_glob): # Empty set is OK if wildcard but not for direct file reference raise error("Include file '%s' does not exist" % (include_glob,)) @@ -473,6 +478,12 @@ def log_config(self, config): self.printer.set_rollover_info("config", "\n".join(lines)) # Status reporting + def runtime_warning(self, msg): + logging.warn(msg) + res = {"type": "runtime_warning", "message": msg} + self.runtime_warnings.append(res) + self.status_warnings = self.runtime_warnings + self.deprecate_warnings + def deprecate(self, section, option, value=None, msg=None): self.deprecated[(section, option, value)] = msg @@ -676,8 +687,7 @@ def cmd_SAVE_CONFIG(self, gcmd): raise gcode.error(msg) regular_data = self._strip_duplicates(regular_data, self.autosave) - self.danger_options = self.printer.lookup_object("danger_options") - if self.danger_options.autosave_includes: + if get_danger_options().autosave_includes: self._save_includes(cfgname, data, set(), gcode) # NOW we're safe to check for conflicts @@ -685,5 +695,12 @@ def cmd_SAVE_CONFIG(self, gcmd): data = regular_data.rstrip() + autosave_data self._write_backup(cfgname, data, gcode) - # Request a restart - gcode.request_restart("restart") + # If requested restart or no restart just flag config saved + require_restart = gcmd.get_int("RESTART", 1, minval=0, maxval=1) + if require_restart: + # Request a restart + gcode.request_restart("restart") + else: + # flag config updated to false since config saved with no restart + self.save_config_pending = False + gcode.respond_info("Config update without restart successful") diff --git a/klippy/extras/adc_temperature.py b/klippy/extras/adc_temperature.py index 9164f78bd..ef50415e8 100644 --- a/klippy/extras/adc_temperature.py +++ b/klippy/extras/adc_temperature.py @@ -5,6 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging, bisect +from extras.danger_options import get_danger_options ###################################################################### # Interface between MCU adc and heater temperature callbacks @@ -25,9 +26,6 @@ def __init__(self, config, adc_convert): self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback) query_adc = config.get_printer().load_object(config, "query_adc") query_adc.register_adc(config.get_name(), self.mcu_adc) - self.danger_options = config.get_printer().lookup_object( - "danger_options" - ) def setup_callback(self, temperature_callback): self.temperature_callback = temperature_callback @@ -40,7 +38,7 @@ def adc_callback(self, read_time, read_value): self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) def setup_minmax(self, min_temp, max_temp): - if self.danger_options.adc_ignore_limits: + if get_danger_options().adc_ignore_limits: danger_check_count = 0 else: danger_check_count = RANGE_CHECK_COUNT 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/bed_mesh.py b/klippy/extras/bed_mesh.py index 509e0ff79..c03bb5a8a 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -5,6 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging, math, json, collections from . import probe +from extras.danger_options import get_danger_options PROFILE_VERSION = 1 PROFILE_OPTIONS = { @@ -140,7 +141,7 @@ def __init__(self, config): else: config_file.warn( "config", - f"Selected default bed mesh profile '{self.default_mesh_name}' not found in available profiles.", + f"Selected default bed mesh profile '{self.default_mesh_name}' not in available profiles.", "Invalid profile name", ) # register gcodes @@ -172,8 +173,7 @@ def __init__(self, config): def handle_connect(self): self.toolhead = self.printer.lookup_object("toolhead") - self.danger_options = self.printer.lookup_object("danger_options") - if self.danger_options.log_bed_mesh_at_startup: + if get_danger_options().log_bed_mesh_at_startup: self.bmc.print_generated_points(logging.info) def set_mesh(self, mesh): @@ -1313,9 +1313,11 @@ def _sample_lagrange(self, z_matrix): y_mult = self.y_mult self.mesh_matrix = [ [ - 0.0 - if ((i % x_mult) or (j % y_mult)) - else z_matrix[j // y_mult][i // x_mult] + ( + 0.0 + if ((i % x_mult) or (j % y_mult)) + else z_matrix[j // y_mult][i // x_mult] + ) for i in range(self.mesh_x_count) ] for j in range(self.mesh_y_count) @@ -1375,9 +1377,11 @@ def _sample_bicubic(self, z_matrix): c = self.mesh_params["tension"] self.mesh_matrix = [ [ - 0.0 - if ((i % x_mult) or (j % y_mult)) - else z_matrix[j // y_mult][i // x_mult] + ( + 0.0 + if ((i % x_mult) or (j % y_mult)) + else z_matrix[j // y_mult][i // x_mult] + ) for i in range(self.mesh_x_count) ] for j in range(self.mesh_y_count) 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/danger_options.py b/klippy/extras/danger_options.py index 72ea4e487..8d6983b56 100644 --- a/klippy/extras/danger_options.py +++ b/klippy/extras/danger_options.py @@ -1,5 +1,6 @@ class DangerOptions: def __init__(self, config): + self.minimal_logging = config.getboolean("minimal_logging", False) self.log_statistics = config.getboolean("log_statistics", True) self.log_config_file_at_startup = config.getboolean( "log_config_file_at_startup", True @@ -8,6 +9,13 @@ def __init__(self, config): "log_bed_mesh_at_startup", True ) self.log_shutdown_info = config.getboolean("log_shutdown_info", True) + self.log_serial_reader_warnings = config.getboolean( + "log_serial_reader_warnings", True + ) + self.log_startup_info = config.getboolean("log_startup_info", True) + self.log_webhook_method_register_messages = config.getboolean( + "log_webhook_method_register_messages", False + ) self.error_on_unused_config_options = config.getboolean( "error_on_unused_config_options", True ) @@ -29,6 +37,27 @@ def __init__(self, config): "bgflush_extra_time", 0.250, minval=0.0 ) + if self.minimal_logging: + self.log_statistics = False + self.log_config_file_at_startup = False + self.log_bed_mesh_at_startup = False + self.log_shutdown_info = False + self.log_serial_reader_warnings = False + self.log_startup_info = False + self.log_webhook_method_register_messages = False + + +DANGER_OPTIONS: DangerOptions = None + + +def get_danger_options(): + global DANGER_OPTIONS + if DANGER_OPTIONS is None: + raise Exception("DangerOptions has not been loaded yet!") + return DANGER_OPTIONS + def load_config(config): - return DangerOptions(config) + global DANGER_OPTIONS + DANGER_OPTIONS = DangerOptions(config) + return DANGER_OPTIONS 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/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/filament_motion_sensor.py b/klippy/extras/filament_motion_sensor.py index c245f35e4..226e0cf48 100644 --- a/klippy/extras/filament_motion_sensor.py +++ b/klippy/extras/filament_motion_sensor.py @@ -45,7 +45,7 @@ def _update_filament_runout_pos(self, eventtime=None): if eventtime is None: eventtime = self.reactor.monotonic() self.filament_runout_pos = ( - self._get_extruder_pos(eventtime) + self.detection_length + self.get_extruder_pos(eventtime) + self.detection_length ) def _handle_ready(self): @@ -68,14 +68,14 @@ def _handle_not_printing(self, print_time): self._extruder_pos_update_timer, self.reactor.NEVER ) - def _get_extruder_pos(self, eventtime=None): + def get_extruder_pos(self, eventtime=None): if eventtime is None: eventtime = self.reactor.monotonic() print_time = self.estimated_print_time(eventtime) return self.extruder.find_past_position(print_time) def _extruder_pos_update_event(self, eventtime): - extruder_pos = self._get_extruder_pos(eventtime) + extruder_pos = self.get_extruder_pos(eventtime) # Check for filament runout self.runout_helper.note_filament_present( extruder_pos < self.filament_runout_pos @@ -94,7 +94,8 @@ def get_sensor_status(self): "Filament Sensor %s: %s\n" "Filament Detected: %s\n" "Detection Length: %.2f\n" - "Smart: %s" + "Smart: %s\n" + "Always Fire Events: %s" % ( self.runout_helper.name, ( @@ -105,6 +106,7 @@ def get_sensor_status(self): "true" if self.runout_helper.filament_present else "false", self.detection_length, "true" if self.runout_helper.smart else "false", + "true" if self.runout_helper.always_fire_events else "false", ) ) @@ -118,7 +120,7 @@ def get_info(self, gcmd): return True return False - def reset_needed(self, enable): + def reset_needed(self, enable=None, always_fire_events=None): if enable and not self.runout_helper.sensor_enabled: return True return False diff --git a/klippy/extras/filament_switch_sensor.py b/klippy/extras/filament_switch_sensor.py index 58951ba1d..b8d08f1cb 100644 --- a/klippy/extras/filament_switch_sensor.py +++ b/klippy/extras/filament_switch_sensor.py @@ -5,7 +5,6 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging - CHECK_RUNOUT_TIMEOUT = 0.250 @@ -40,15 +39,12 @@ def __init__(self, config, defined_sensor, runout_distance=0): self.filament_present = False self.sensor_enabled = True self.smart = config.getboolean("smart", False) + self.always_fire_events = config.getboolean("always_fire_events", False) self.runout_position = 0.0 self.runout_elapsed = 0.0 self.runout_distance_timer = None - self.force_trigger = False # Register commands and event handlers self.printer.register_event_handler("klippy:ready", self._handle_ready) - self.printer.register_event_handler( - "idle_timeout:printing", self._handle_printing - ) self.gcode.register_mux_command( "QUERY_FILAMENT_SENSOR", "SENSOR", @@ -67,9 +63,6 @@ def __init__(self, config, defined_sensor, runout_distance=0): def _handle_ready(self): self.min_event_systime = self.reactor.monotonic() + 2.0 - def _handle_printing(self, print_time): - self.note_filament_present(self.filament_present, True) - def _runout_event_handler(self, eventtime): if self.immediate_runout_gcode is not None: self._exec_gcode("", self.immediate_runout_gcode) @@ -125,12 +118,18 @@ def _exec_gcode(self, prefix, template): logging.exception("Script running error") self.min_event_systime = self.reactor.monotonic() + self.event_delay - def note_filament_present(self, is_filament_present, force=False): + def note_filament_present( + self, is_filament_present=None, force=False, immediate=False + ): + if is_filament_present is None: + is_filament_present = self.filament_present if is_filament_present == self.filament_present and not force: return self.filament_present = is_filament_present eventtime = self.reactor.monotonic() - if eventtime < self.min_event_systime: + if eventtime < self.min_event_systime or ( + not self.always_fire_events and not self.sensor_enabled + ): # do not process during the initialization time, duplicates, # during the event delay time, while an event is running, or # when the sensor is disabled @@ -166,13 +165,18 @@ def note_filament_present(self, is_filament_present, force=False): "Filament Sensor %s: runout event detected, Time %.2f" % (self.name, eventtime) ) - self.reactor.register_callback(self._runout_event_handler) + self.reactor.register_callback( + self._execute_runout + if immediate + else self._runout_event_handler + ) def get_status(self, eventtime): status = { "filament_detected": bool(self.filament_present), "enabled": bool(self.sensor_enabled), "smart": bool(self.smart), + "always_fire_events": bool(self.always_fire_events), } status.update(self.defined_sensor.sensor_get_status(eventtime)) return status @@ -192,24 +196,32 @@ def cmd_SET_FILAMENT_SENSOR(self, gcmd): enable = gcmd.get_int("ENABLE", None, minval=0, maxval=1) reset = gcmd.get_int("RESET", None, minval=0, maxval=1) smart = gcmd.get_int("SMART", None, minval=0, maxval=1) + always_fire_events = gcmd.get_int( + "ALWAYS_FIRE_EVENTS", None, minval=0, maxval=1 + ) reset_needed = False if ( enable is None and reset is None and smart is None + and always_fire_events is None and self.defined_sensor.get_info(gcmd) ): return if enable is not None: - if self.defined_sensor.reset_needed(enable): - reset_needed = True self.sensor_enabled = enable if reset is not None and reset: reset_needed = True if smart is not None: self.smart = smart + if always_fire_events is not None: + self.always_fire_events = always_fire_events if self.defined_sensor.set_filament_sensor(gcmd): reset_needed = True + if self.defined_sensor.reset_needed( + enable=enable, always_fire_events=always_fire_events + ): + reset_needed = True if reset_needed: self.defined_sensor.reset() @@ -222,6 +234,9 @@ def __init__(self, config): switch_pin = config.get("switch_pin") runout_distance = config.getfloat("runout_distance", 0.0, minval=0.0) buttons.register_buttons([switch_pin], self._button_handler) + self.check_on_print_start = config.getboolean( + "check_on_print_start", False + ) self.reactor = self.printer.get_reactor() self.estimated_print_time = None self.runout_helper = RunoutHelper(config, self, runout_distance) @@ -231,12 +246,19 @@ def __init__(self, config): ) self.get_status = self.runout_helper.get_status self.printer.register_event_handler("klippy:ready", self._handle_ready) + self.printer.register_event_handler( + "idle_timeout:printing", self._handle_printing + ) def _handle_ready(self): self.estimated_print_time = self.printer.lookup_object( "mcu" ).estimated_print_time + def _handle_printing(self, print_time): + if self.check_on_print_start: + self.runout_helper.note_filament_present(None, True, True) + def _button_handler(self, eventtime, state): self.runout_helper.note_filament_present(state) @@ -253,7 +275,9 @@ def get_sensor_status(self): "Filament Detected: %s\n" "Runout Distance: %.2f\n" "Runout Elapsed: %.2f\n" - "Smart: %s" + "Smart: %s\n" + "Always Fire Events: %s\n" + "Check on Print Start: %s" % ( self.runout_helper.name, "enabled" if self.runout_helper.sensor_enabled else "disabled", @@ -261,6 +285,8 @@ def get_sensor_status(self): self.runout_helper.runout_distance, self.runout_helper.runout_elapsed, "true" if self.runout_helper.smart else "false", + "true" if self.runout_helper.always_fire_events else "false", + "true" if self.check_on_print_start else "false", ) ) @@ -268,24 +294,35 @@ def sensor_get_status(self, eventtime): return { "runout_distance": float(self.runout_helper.runout_distance), "runout_elapsed": float(self.runout_helper.runout_elapsed), + "check_on_print_start": bool(self.check_on_print_start), } def get_info(self, gcmd): runout_distance = gcmd.get_float("RUNOUT_DISTANCE", None, minval=0.0) - if runout_distance is None: + check_on_print_start = gcmd.get_int( + "CHECK_ON_PRINT_START", None, minval=0, maxval=1 + ) + if runout_distance is None and check_on_print_start is None: gcmd.respond_info(self.get_sensor_status()) return True return False - def reset_needed(self, enable): - if enable != self.runout_helper.sensor_enabled: + def reset_needed(self, enable=None, always_fire_events=None): + if enable is not None and enable != self.runout_helper.sensor_enabled: + return True + if always_fire_events is not None and always_fire_events: return True return False def set_filament_sensor(self, gcmd): runout_distance = gcmd.get_float("RUNOUT_DISTANCE", None, minval=0.0) + check_on_print_start = gcmd.get_int( + "CHECK_ON_PRINT_START", None, minval=0, maxval=1 + ) if runout_distance is not None: self.runout_helper.runout_distance = runout_distance + if check_on_print_start is not None: + self.check_on_print_start = check_on_print_start # No reset is needed when changing the runout_distance, so we always # return False return False diff --git a/klippy/extras/heaters.py b/klippy/extras/heaters.py index b23d1b7e6..6e59253f9 100644 --- a/klippy/extras/heaters.py +++ b/klippy/extras/heaters.py @@ -32,7 +32,9 @@ class Heater: def __init__(self, config, sensor): self.printer = config.get_printer() - self.name = config.get_name().split()[-1] + self.name = config.get_name() + self.short_name = short_name = self.name.split()[-1] + self.reactor = self.printer.get_reactor() self.config = config self.configfile = self.printer.lookup_object("configfile") # Setup sensor @@ -59,6 +61,7 @@ def __init__(self, config, sensor): self.config_smooth_time = config.getfloat("smooth_time", 1.0, above=0.0) self.smooth_time = self.config_smooth_time self.inv_smooth_time = 1.0 / self.smooth_time + self.is_shutdown = False self.lock = threading.Lock() self.last_temp = self.smoothed_temp = self.target_temp = 0.0 self.last_temp_time = 0.0 @@ -81,7 +84,7 @@ def __init__(self, config, sensor): self.mcu_pwm.setup_cycle_time(pwm_cycle_time) self.mcu_pwm.setup_max_duration(MAX_HEAT_TIME) # Load additional modules - self.printer.load_object(config, "verify_heater %s" % (self.name,)) + self.printer.load_object(config, "verify_heater %s" % (short_name,)) self.printer.load_object(config, "pid_calibrate") self.gcode = self.printer.lookup_object("gcode") self.pmgr = self.ProfileManager(self) @@ -91,32 +94,36 @@ def __init__(self, config, sensor): self.gcode.register_mux_command( "SET_HEATER_TEMPERATURE", "HEATER", - self.name, + short_name, self.cmd_SET_HEATER_TEMPERATURE, desc=self.cmd_SET_HEATER_TEMPERATURE_help, ) self.gcode.register_mux_command( "SET_SMOOTH_TIME", "HEATER", - self.name, + short_name, self.cmd_SET_SMOOTH_TIME, desc=self.cmd_SET_SMOOTH_TIME_help, ) self.gcode.register_mux_command( "PID_PROFILE", "HEATER", - self.name, + short_name, self.pmgr.cmd_PID_PROFILE, desc=self.pmgr.cmd_PID_PROFILE_help, ) self.gcode.register_mux_command( "SET_HEATER_PID", "HEATER", - self.name, + short_name, self.cmd_SET_HEATER_PID, desc=self.cmd_SET_HEATER_PID_help, ) + self.printer.register_event_handler( + "klippy:shutdown", self._handle_shutdown + ) + def lookup_control(self, profile, load_clean=False): algos = collections.OrderedDict( { @@ -128,7 +135,7 @@ def lookup_control(self, profile, load_clean=False): return algos[profile["control"]](profile, self, load_clean) def set_pwm(self, read_time, value): - if self.target_temp <= 0.0: + if self.target_temp <= 0.0 or self.is_shutdown: value = 0.0 if (read_time < self.next_pwm_time or not self.last_pwm_value) and abs( value - self.last_pwm_value @@ -155,7 +162,13 @@ def temperature_callback(self, read_time, temp): self.can_extrude = self.smoothed_temp >= self.min_extrude_temp # logging.debug("temp: %.3f %f = %f", read_time, temp) + def _handle_shutdown(self): + self.is_shutdown = True + # External commands + def get_name(self): + return self.name + def get_pwm_delay(self): return self.pwm_delay @@ -215,7 +228,7 @@ def stats(self, eventtime): last_pwm_value = self.last_pwm_value is_active = target_temp or last_temp > 50.0 return is_active, "%s: target=%.0f temp=%.1f pwm=%.3f" % ( - self.name, + self.short_name, target_temp, last_temp, last_pwm_value, @@ -830,14 +843,14 @@ def __init__(self, config): "gcode:request_restart", self.turn_off_all_heaters ) # Register commands - gcode = self.printer.lookup_object("gcode") - gcode.register_command( + self.gcode = self.printer.lookup_object("gcode") + self.gcode.register_command( "TURN_OFF_HEATERS", self.cmd_TURN_OFF_HEATERS, desc=self.cmd_TURN_OFF_HEATERS_help, ) - gcode.register_command("M105", self.cmd_M105, when_not_ready=True) - gcode.register_command( + self.gcode.register_command("M105", self.cmd_M105, when_not_ready=True) + self.gcode.register_command( "TEMPERATURE_WAIT", self.cmd_TEMPERATURE_WAIT, desc=self.cmd_TEMPERATURE_WAIT_help, @@ -947,16 +960,15 @@ def cmd_M105(self, gcmd): def _wait_for_temperature(self, heater): # Helper to wait on heater.check_busy() and report M105 temperatures + if self.printer.get_start_args().get("debugoutput") is not None: return - toolhead = self.printer.lookup_object("toolhead") - gcode = self.printer.lookup_object("gcode") - reactor = self.printer.get_reactor() - eventtime = reactor.monotonic() - while not self.printer.is_shutdown() and heater.check_busy(eventtime): - print_time = toolhead.get_last_move_time() - gcode.respond_raw(self._get_temp(eventtime)) - eventtime = reactor.pause(eventtime + 1.0) + + def check(eventtime): + self.gcode.respond_raw(self._get_temp(eventtime)) + return heater.check_busy(eventtime) + + self.printer.wait_while(check) def set_temperature(self, heater, temp, wait=False): toolhead = self.printer.lookup_object("toolhead") @@ -983,16 +995,15 @@ def cmd_TEMPERATURE_WAIT(self, gcmd): sensor = self.heaters[sensor_name] else: sensor = self.printer.lookup_object(sensor_name) - toolhead = self.printer.lookup_object("toolhead") - reactor = self.printer.get_reactor() - eventtime = reactor.monotonic() - while not self.printer.is_shutdown(): - temp, target = sensor.get_temp(eventtime) + + def check(eventtime): + temp, _ = sensor.get_temp(eventtime) if temp >= min_temp and temp <= max_temp: - return - print_time = toolhead.get_last_move_time() + return False gcmd.respond_raw(self._get_temp(eventtime)) - eventtime = reactor.pause(eventtime + 1.0) + return True + + self.printer.wait_while(check) def load_config(config): diff --git a/klippy/extras/homing.py b/klippy/extras/homing.py index 1f633c2c6..2dff044c6 100644 --- a/klippy/extras/homing.py +++ b/klippy/extras/homing.py @@ -5,6 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import math import logging +from extras.danger_options import get_danger_options HOMING_START_DELAY = 0.001 ENDSTOP_SAMPLE_TIME = 0.000015 @@ -50,7 +51,6 @@ def __init__(self, printer, endstops, toolhead=None): self.toolhead = toolhead self.stepper_positions = [] self.distance_elapsed = [] - self.danger_options = printer.lookup_object("danger_options") def get_mcu_endstops(self): return [es for es, name in self.endstops] @@ -205,7 +205,7 @@ def moved_less_than_dist(self, min_dist, homing_axes): if i in homing_axes ] distance_tolerance = ( - self.danger_options.homing_elapsed_distance_tolerance + get_danger_options().homing_elapsed_distance_tolerance ) if any( [ 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/resonance_tester.py b/klippy/extras/resonance_tester.py index e66ff4fb5..b5a609f15 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -55,8 +55,8 @@ def _parse_axis(gcmd, raw_axis): @contextmanager def suspend_limits(printer, max_accel, max_velocity, input_shaping): - # Override maximum acceleration and acceleration to - # deceleration based on the maximum test frequency + # Override maximum acceleration and cruise ratio + # based on the maximum test frequency gcode = printer.lookup_object("gcode") input_shaper = printer.lookup_object("input_shaper", None) if input_shaper is not None and not input_shaping: @@ -68,11 +68,11 @@ def suspend_limits(printer, max_accel, max_velocity, input_shaping): systime = printer.get_reactor().monotonic() toolhead_info = toolhead.get_status(systime) old_max_accel = toolhead_info["max_accel"] - old_max_accel_to_decel = toolhead_info["max_accel_to_decel"] + old_minimum_cruise_ratio = toolhead_info["minimum_cruise_ratio"] old_max_velocity = toolhead_info["max_velocity"] gcode.run_script_from_command( - "SET_VELOCITY_LIMIT ACCEL=%.3f ACCEL_TO_DECEL=%.3f VELOCITY=%.3f" - % (max_accel, max_accel, max_velocity) + "SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=0 VELOCITY=%.3f" + % (max_accel, max_velocity) ) kin = toolhead.get_kinematics() old_max_velocities = getattr(kin, "max_velocities", None) @@ -105,8 +105,8 @@ def suspend_limits(printer, max_accel, max_velocity, input_shaping): gcode.respond_info("Re-enabled [input_shaper]") # Restore the original acceleration values gcode.run_script_from_command( - "SET_VELOCITY_LIMIT ACCEL=%.3f ACCEL_TO_DECEL=%.3f VELOCITY=%.3f" - % (old_max_accel, old_max_accel_to_decel, old_max_velocity) + "SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=%.3f VELOCITY=%.3f" + % (old_max_accel, old_minimum_cruise_ratio, old_max_velocity) ) if old_max_velocities is not None: kin.max_velocities = old_max_velocities 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/statistics.py b/klippy/extras/statistics.py index e3ca9acab..4d7bb9d69 100644 --- a/klippy/extras/statistics.py +++ b/klippy/extras/statistics.py @@ -4,6 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import os, time, logging +from extras.danger_options import get_danger_options class PrinterSysStats: @@ -61,7 +62,6 @@ def get_status(self, eventtime): class PrinterStats: def __init__(self, config): self.printer = config.get_printer() - self.danger_options = self.printer.lookup_object("danger_options") reactor = self.printer.get_reactor() self.stats_timer = reactor.register_timer(self.generate_stats) self.stats_cb = [] @@ -79,7 +79,7 @@ def handle_ready(self): def generate_stats(self, eventtime): stats = [cb(eventtime) for cb in self.stats_cb] - if max([s[0] for s in stats]) and self.danger_options.log_statistics: + if max([s[0] for s in stats]) and get_danger_options().log_statistics: logging.info( "Stats %.1f: %s", eventtime, " ".join([s[1] for s in stats]) ) diff --git a/klippy/extras/temperature_fan.py b/klippy/extras/temperature_fan.py index 93d8d33a6..5c292ba4a 100644 --- a/klippy/extras/temperature_fan.py +++ b/klippy/extras/temperature_fan.py @@ -98,6 +98,11 @@ def get_status(self, eventtime): status["control"] = self.control.get_type() return status + def is_adc_faulty(self): + if self.last_temp > self.max_temp or self.last_temp < self.min_temp: + return True + return False + cmd_SET_TEMPERATURE_FAN_TARGET_help = ( "Sets a temperature fan target and fan speed limits" ) diff --git a/klippy/extras/temperature_sensor.py b/klippy/extras/temperature_sensor.py index 873ba3644..0e99e4f7a 100644 --- a/klippy/extras/temperature_sensor.py +++ b/klippy/extras/temperature_sensor.py @@ -45,6 +45,11 @@ def get_status(self, eventtime): "measured_max_temp": round(self.measured_max, 2), } + def is_adc_faulty(self): + if self.last_temp > self.max_temp or self.last_temp < self.min_temp: + return True + return False + def load_config_prefix(config): return PrinterSensorGeneric(config) 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/tmc2240.py b/klippy/extras/tmc2240.py index de8ca986a..59db629ac 100644 --- a/klippy/extras/tmc2240.py +++ b/klippy/extras/tmc2240.py @@ -253,6 +253,8 @@ "s2vsa": (lambda v: "1(ShortToSupply_A!)" if v else ""), "s2vsb": (lambda v: "1(ShortToSupply_B!)" if v else ""), "adc_temp": (lambda v: "0x%04x(%.1fC)" % (v, ((v - 2038) / 7.7))), + "adc_vsupply": (lambda v: "0x%04x(%.3fV)" % (v, v * 0.009732)), + "adc_ain": (lambda v: "0x%04x(%.3fmV)" % (v, v * 0.3052)), } ) diff --git a/klippy/extras/trad_rack.py b/klippy/extras/trad_rack.py new file mode 100644 index 000000000..87eef2691 --- /dev/null +++ b/klippy/extras/trad_rack.py @@ -0,0 +1,2817 @@ +# Trad Rack multimaterial system support +# +# Copyright (C) 2022-2024 Ryan Ghosh +# based on code by Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging, math, os, time +from collections import deque +from extras.homing import Homing, HomingMove +from gcode import CommandError +from stepper import LookupMultiRail +import chelper, toolhead, kinematics.extruder + +SERVO_NAME = "servo tr_servo" +SELECTOR_STEPPER_NAME = "stepper_tr_selector" +FIL_DRIVER_STEPPER_NAME = "stepper_tr_fil_driver" + + +class TradRack: + + VARS_CALIB_BOWDEN_LOAD_LENGTH = "tr_calib_bowden_load_length" + VARS_CALIB_BOWDEN_UNLOAD_LENGTH = "tr_calib_bowden_unload_length" + VARS_CONFIG_BOWDEN_LENGTH = "tr_config_bowden_length" + VARS_TOOL_STATUS = "tr_state_tool_status" + VARS_HEATER_TARGET = "tr_last_heater_target" + VARS_ACTIVE_LANE = "tr_active_lane" + + def __init__(self, config): + self.printer = config.get_printer() + self.reactor = self.printer.get_reactor() + self.printer.register_event_handler( + "klippy:connect", self.handle_connect + ) + self.printer.register_event_handler("klippy:ready", self.handle_ready) + + # read spool and buffer pull speeds + self.spool_pull_speed = config.getfloat( + "spool_pull_speed", default=100.0, above=0.0 + ) + self.buffer_pull_speed = config.getfloat( + "buffer_pull_speed", default=self.spool_pull_speed, above=0.0 + ) + + # create toolhead + self.tr_toolhead = TradRackToolHead( + config, + self.buffer_pull_speed, + lambda: self.extruder_sync_manager.is_extruder_synced(), + ) + self.sel_max_velocity, _ = self.tr_toolhead.get_sel_max_velocity() + + # get servo + self.servo = TradRackServo( + self.printer.load_object(config, SERVO_NAME), self.tr_toolhead + ) + + # get kinematics and filament driver endstops + self.tr_kinematics = self.tr_toolhead.get_kinematics() + self.fil_driver_endstops = ( + self.tr_kinematics.get_fil_driver_rail().get_endstops() + ) + + # set up toolhead filament sensor + toolhead_fil_sensor_pin = config.get("toolhead_fil_sensor_pin", None) + self.toolhead_fil_endstops = [] + if toolhead_fil_sensor_pin is not None: + # register endstop + ppins = self.printer.lookup_object("pins") + mcu_endstop = ppins.setup_pin("endstop", toolhead_fil_sensor_pin) + name = "toolhead_fil_sensor" + self.toolhead_fil_endstops.append((mcu_endstop, name)) + query_endstops = self.printer.load_object(config, "query_endstops") + query_endstops.register_endstop(mcu_endstop, name) + + # add filament driver stepper to endstop + for ( + stepper + ) in self.tr_kinematics.get_fil_driver_rail().get_steppers(): + mcu_endstop.add_stepper(stepper) + + # set up selector sensor as a runout sensor + pin = config.getsection(FIL_DRIVER_STEPPER_NAME).get("endstop_pin") + self.selector_sensor = TradRackRunoutSensor( + config, self.handle_runout, pin + ) + + # read lane count and get lane positions + self.lane_count = config.getint("lane_count", minval=2) + self.lane_position_manager = TradRackLanePositionManager( + self.lane_count, config + ) + self.lane_positions = self.lane_position_manager.get_lane_positions() + + # create bowden length filters + bowden_samples = config.getint( + "bowden_length_samples", default=10, minval=1 + ) + self.bowden_load_length_filter = MovingAverageFilter(bowden_samples) + self.bowden_unload_length_filter = MovingAverageFilter(bowden_samples) + + # create extruder sync manager + self.extruder_sync_manager = TradRackExtruderSyncManager( + self.printer, + self.tr_toolhead, + self.tr_kinematics.get_fil_driver_rail(), + ) + + # read other values + self.servo_down_angle = config.getfloat("servo_down_angle") + self.servo_up_angle = config.getfloat("servo_up_angle") + self.servo_wait = ( + config.getfloat("servo_wait_ms", default=500.0, above=0.0) / 1000.0 + ) + self.selector_unload_length = config.getfloat( + "selector_unload_length", above=0.0 + ) + self.selector_unload_length_extra = config.getfloat( + "selector_unload_length_extra", default=0.0, minval=0.0 + ) + self.eject_length = config.getfloat( + "eject_length", default=30.0, above=0.0 + ) + self.config_bowden_length = ( + self.bowden_load_length + ) = self.bowden_unload_length = config.getfloat( + "bowden_length", above=0.0 + ) + self.extruder_load_length = config.getfloat( + "extruder_load_length", above=0.0 + ) + self.hotend_load_length = config.getfloat( + "hotend_load_length", above=0.0 + ) + if self.toolhead_fil_endstops: + self.toolhead_unload_length = config.getfloat( + "toolhead_unload_length", minval=0.0 + ) + else: + self.toolhead_unload_length = config.getfloat( + "toolhead_unload_length", + default=self.extruder_load_length + self.hotend_load_length, + above=0.0, + ) + self.selector_sense_speed = config.getfloat( + "selector_sense_speed", default=40.0, above=0.0 + ) + self.selector_unload_speed = config.getfloat( + "selector_unload_speed", default=60.0, above=0.0 + ) + self.eject_speed = config.getfloat( + "eject_speed", default=80.0, above=0.0 + ) + self.toolhead_sense_speed = config.getfloat( + "toolhead_sense_speed", default=self.selector_sense_speed, above=0.0 + ) + self.extruder_load_speed = config.getfloat( + "extruder_load_speed", default=60.0, above=0.0 + ) + self.hotend_load_speed = config.getfloat( + "hotend_load_speed", default=7.0, above=0.0 + ) + self.toolhead_unload_speed = config.getfloat( + "toolhead_unload_speed", default=self.extruder_load_speed, above=0.0 + ) + self.load_with_toolhead_sensor = config.getboolean( + "load_with_toolhead_sensor", True + ) + self.unload_with_toolhead_sensor = config.getboolean( + "unload_with_toolhead_sensor", True + ) + self.fil_homing_retract_dist = config.getfloat( + "fil_homing_retract_dist", 20.0, minval=0.0 + ) + self.target_toolhead_homing_dist = config.getfloat( + "target_toolhead_homing_dist", + max(10.0, self.toolhead_unload_length), + above=0.0, + ) + self.target_selector_homing_dist = config.getfloat( + "target_selector_homing_dist", 10.0, above=0.0 + ) + self.fil_homing_lengths = { + "user load lane": ( + config.getint( + "load_lane_time", + default=15, + minval=self.selector_unload_length + / self.selector_sense_speed, + ) + * self.selector_sense_speed + ), + "load selector": config.getfloat( + "load_selector_homing_dist", + default=self.selector_unload_length * 2, + above=self.selector_unload_length, + ), + "bowden load": config.getfloat( + "bowden_load_homing_dist", + default=self.config_bowden_length, + above=self.target_toolhead_homing_dist, + ), + "bowden unload": config.getfloat( + "bowden_unload_homing_dist", + default=self.config_bowden_length, + above=self.target_selector_homing_dist, + ), + "unload toolhead": config.getfloat( + "unload_toolhead_homing_dist", + default=(self.extruder_load_length + self.hotend_load_length) + * 2, + above=0.0, + ), + } + self.sync_to_extruder = config.getboolean("sync_to_extruder", False) + self.user_wait_time = config.getint( + "user_wait_time", default=15, minval=-1 + ) + register_toolchange_commands = config.getboolean( + "register_toolchange_commands", default=True + ) + self.save_active_lane = config.getboolean("save_active_lane", True) + self.log_bowden_lengths = config.getboolean("log_bowden_lengths", False) + + # other variables + self.toolhead = None + self.curr_lane = None # which lane the selector is positioned at + self.active_lane = None # lane currently loaded in the toolhead + self.retry_lane = None # lane to reload before resuming + self.retry_tool = None # tool to load a lane from before resuming + self.next_lane = None # next lane to load to toolhead if resuming + self.servo_raised = None + self.lanes_unloaded = [False] * self.lane_count + self.bowden_load_calibrated = False + self.bowden_unload_calibrated = False + self.bowden_load_lengths_filename = os.path.expanduser( + "~/bowden_load_lengths.csv" + ) + self.bowden_unload_lengths_filename = os.path.expanduser( + "~/bowden_unload_lengths.csv" + ) + self.ignore_next_unload_length = False + self.last_heater_target = 0.0 + self.tr_next_generator = None + + # resume variables + self.resume_callbacks = { + "load toolhead": self._resume_load_toolhead, + "check condition": self._resume_check_condition, + "runout": self._resume_runout, + } + self.resume_stack = deque() + + # tool mapping + self.lanes_dead = [False] * self.lane_count + self.tool_map = [] + self.default_lanes = [] + self._reset_tool_map() + + # runout variables + self.runout_lane = None + self.runout_steps_done = 0 + self.replacement_lane = None + + # custom user macros + gcode_macro = self.printer.load_object(config, "gcode_macro") + self.pre_unload_macro = gcode_macro.load_template( + config, "pre_unload_gcode", "" + ) + self.post_unload_macro = gcode_macro.load_template( + config, "post_unload_gcode", "" + ) + self.pre_load_macro = gcode_macro.load_template( + config, "pre_load_gcode", "" + ) + self.post_load_macro = gcode_macro.load_template( + config, "post_load_gcode", "" + ) + self.pause_macro = gcode_macro.load_template( + config, "pause_gcode", "PAUSE" + ) + self.resume_macro = gcode_macro.load_template( + config, "resume_gcode", "RESUME" + ) + + # register gcode commands + self.gcode = self.printer.lookup_object("gcode") + self.gcode.register_command( + "TR_HOME", self.cmd_TR_HOME, desc=self.cmd_TR_HOME_help + ) + self.gcode.register_command( + "TR_GO_TO_LANE", + self.cmd_TR_GO_TO_LANE, + desc=self.cmd_TR_GO_TO_LANE_help, + ) + self.gcode.register_command( + "TR_LOAD_LANE", + self.cmd_TR_LOAD_LANE, + desc=self.cmd_TR_LOAD_LANE_help, + ) + self.gcode.register_command( + "TR_LOAD_TOOLHEAD", + self.cmd_TR_LOAD_TOOLHEAD, + desc=self.cmd_TR_LOAD_TOOLHEAD_help, + ) + self.gcode.register_command( + "TR_UNLOAD_TOOLHEAD", + self.cmd_TR_UNLOAD_TOOLHEAD, + desc=self.cmd_TR_UNLOAD_TOOLHEAD_help, + ) + self.gcode.register_command( + "TR_SERVO_DOWN", + self.cmd_TR_SERVO_DOWN, + desc=self.cmd_TR_SERVO_DOWN_help, + ) + self.gcode.register_command( + "TR_SERVO_UP", self.cmd_TR_SERVO_UP, desc=self.cmd_TR_SERVO_UP_help + ) + self.gcode.register_command( + "TR_SERVO_TEST", + self.cmd_TR_SERVO_TEST, + desc=self.cmd_TR_SERVO_TEST_help, + ) + self.gcode.register_command( + "TR_SET_ACTIVE_LANE", + self.cmd_TR_SET_ACTIVE_LANE, + desc=self.cmd_TR_SET_ACTIVE_LANE_help, + ) + self.gcode.register_command( + "TR_RESET_ACTIVE_LANE", + self.cmd_TR_RESET_ACTIVE_LANE, + desc=self.cmd_TR_RESET_ACTIVE_LANE_help, + ) + self.gcode.register_command( + "TR_RESUME", self.cmd_TR_RESUME, desc=self.cmd_TR_RESUME_help + ) + self.gcode.register_command( + "TR_LOCATE_SELECTOR", + self.cmd_TR_LOCATE_SELECTOR, + desc=self.cmd_TR_LOCATE_SELECTOR_help, + ) + self.gcode.register_command( + "TR_CALIBRATE_SELECTOR", + self.cmd_TR_CALIBRATE_SELECTOR, + desc=self.cmd_TR_CALIBRATE_SELECTOR_help, + ) + self.gcode.register_command( + "TR_NEXT", self.cmd_TR_NEXT, desc=self.cmd_TR_NEXT_help + ) + self.gcode.register_command( + "TR_SET_HOTEND_LOAD_LENGTH", + self.cmd_TR_SET_HOTEND_LOAD_LENGTH, + desc=self.cmd_TR_SET_HOTEND_LOAD_LENGTH_help, + ) + self.gcode.register_command( + "TR_DISCARD_BOWDEN_LENGTHS", + self.cmd_TR_DISCARD_BOWDEN_LENGTHS, + desc=self.cmd_TR_DISCARD_BOWDEN_LENGTHS_help, + ) + self.gcode.register_command( + "TR_SYNC_TO_EXTRUDER", + self.cmd_TR_SYNC_TO_EXTRUDER, + desc=self.cmd_TR_SYNC_TO_EXTRUDER_help, + ) + self.gcode.register_command( + "TR_UNSYNC_FROM_EXTRUDER", + self.cmd_TR_UNSYNC_FROM_EXTRUDER, + desc=self.cmd_TR_UNSYNC_FROM_EXTRUDER_help, + ) + self.gcode.register_command( + "TR_ASSIGN_LANE", + self.cmd_TR_ASSIGN_LANE, + desc=self.cmd_TR_ASSIGN_LANE_help, + ) + self.gcode.register_command( + "TR_SET_DEFAULT_LANE", + self.cmd_TR_SET_DEFAULT_LANE, + desc=self.cmd_TR_SET_DEFAULT_LANE_help, + ) + self.gcode.register_command( + "TR_RESET_TOOL_MAP", + self.cmd_TR_RESET_TOOL_MAP, + desc=self.cmd_TR_RESET_TOOL_MAP_help, + ) + self.gcode.register_command( + "TR_PRINT_TOOL_MAP", + self.cmd_TR_PRINT_TOOL_MAP, + desc=self.cmd_TR_PRINT_TOOL_MAP_help, + ) + self.gcode.register_command( + "TR_PRINT_TOOL_GROUPS", + self.cmd_TR_PRINT_TOOL_GROUPS, + desc=self.cmd_TR_PRINT_TOOL_GROUPS_help, + ) + if register_toolchange_commands: + for i in range(self.lane_count): + self.gcode.register_command( + "T{}".format(i), + lambda params: self.cmd_SELECT_TOOL( + self.gcode._get_extended_params(params) + ), + desc=self.cmd_SELECT_TOOL_help, + ) + + def handle_connect(self): + self.toolhead = self.printer.lookup_object("toolhead") + save_variables = self.printer.lookup_object("save_variables", None) + if save_variables is None: + raise self.printer.config_error( + "[save_variables] is required for trad_rack" + ) + self.variables = save_variables.allVariables + + def handle_ready(self): + self._load_saved_state() + + def _load_saved_state(self): + # load bowden lengths if the user has not changed the config value + prev_config_bowden_length = self.variables.get( + self.VARS_CONFIG_BOWDEN_LENGTH + ) + if ( + prev_config_bowden_length + and self.config_bowden_length == prev_config_bowden_length + ): + # update load length + load_length_stats = self.variables.get( + self.VARS_CALIB_BOWDEN_LOAD_LENGTH + ) + if load_length_stats: + self.bowden_load_length = load_length_stats["new_set_length"] + for _ in range(load_length_stats["sample_count"]): + self.bowden_load_length_filter.update( + self.bowden_load_length + ) + + # update unload length + unload_length_stats = self.variables.get( + self.VARS_CALIB_BOWDEN_UNLOAD_LENGTH + ) + if unload_length_stats: + self.bowden_unload_length = unload_length_stats[ + "new_set_length" + ] + for _ in range(unload_length_stats["sample_count"]): + self.bowden_unload_length_filter.update( + self.bowden_unload_length + ) + else: + # save bowden_length config value + self.gcode.run_script_from_command( + 'SAVE_VARIABLE VARIABLE=%s VALUE="%s"' + % (self.VARS_CONFIG_BOWDEN_LENGTH, self.config_bowden_length) + ) + + # load last heater target + self.last_heater_target = self.variables.get( + self.VARS_HEATER_TARGET, 0.0 + ) + + def handle_runout(self, eventtime): + # pause + pause_resume = self.printer.lookup_object("pause_resume") + pause_resume.send_pause_command() + # self.printer.get_reactor().pause(eventtime + self.pause_delay) + self._send_pause() + + # set up resume callback + self._set_up_resume("runout", {}) + + # note runout + self.runout_lane = self.active_lane + self._set_active_lane(None) + self.lanes_unloaded[self.runout_lane] = False + self.lanes_dead[self.runout_lane] = True + self.gcode.respond_info( + "Runout detected at selector on lane {} (tool {})".format( + self.runout_lane, self.tool_map[self.runout_lane] + ) + ) + + # unload filament and reload from a new lane + self.runout_steps_done = 0 + self.cmd_TR_RESUME( + self.gcode.create_gcode_command("TR_RESUME", "TR_RESUME", {}) + ) + + # gcode commands + cmd_TR_HOME_help = "Home Trad Rack's selector" + + def cmd_TR_HOME(self, gcmd): + # check for filament in the selector + if self._query_selector_sensor(): + raise self.gcode.error("Cannot home with filament in selector") + + # reset current lane + self.curr_lane = None + + # raise servo + self._raise_servo() + + # home selector + homing_state = TradRackHoming(self.printer, self.tr_toolhead) + homing_state.set_axes([0]) + try: + self.tr_kinematics.home(homing_state) + except self.printer.command_error: + if self.printer.is_shutdown(): + raise self.printer.command_error( + "Homing failed due to printer shutdown" + ) + self.printer.lookup_object("stepper_enable").motor_off() + raise + + cmd_TR_GO_TO_LANE_help = "Move Trad Rack's selector to a filament lane" + + def cmd_TR_GO_TO_LANE(self, gcmd): + self._go_to_lane(gcmd.get_int("LANE", None)) + + cmd_TR_LOAD_LANE_help = ( + "Load filament from the spool into Trad Rack in the specified lane" + ) + + def cmd_TR_LOAD_LANE(self, gcmd): + lane = gcmd.get_int("LANE", None) + self._load_lane(lane, gcmd, gcmd.get_int("RESET_SPEED", 1), True) + self.lanes_dead[lane] = False + + cmd_TR_LOAD_TOOLHEAD_help = "Load filament from Trad Rack into the toolhead" + + def cmd_TR_LOAD_TOOLHEAD(self, gcmd): + start_lane = self.active_lane + lane = gcmd.get_int("LANE", None) + tool = gcmd.get_int("TOOL", None) + + # select lane + if lane is None: + # check tool + self._check_tool_valid(tool) + + # get default lane for the selected tool + lane = self.default_lanes[tool] + if lane is None: + gcmd.respond_info( + "Tool {tool} has no lanes assigned to it. Use" + " TR_ASSIGN_LANE LANE=<lane index> TOOL={tool} to" + " assign a lane to tool {tool}, then use TR_RESUME to" + " continue.".format(tool=str(tool)) + ) + + # set up resume callback + resume_kwargs = { + "condition": ( + lambda t=tool: self.default_lanes[t] is not None + ), + "action": lambda g=gcmd: self.cmd_TR_LOAD_TOOLHEAD(g), + "fail_msg": ( + "Cannot resume. Please use TR_ASSIGN_LANE to assign a" + " lane to tool %d, then use TR_RESUME." % tool + ), + } + self._set_up_resume("check condition", resume_kwargs) + + # pause and wait for user to resume + self._send_pause() + return + + # load toolhead + try: + self._load_toolhead( + lane, + gcmd, + tool, + gcmd.get_float("MIN_TEMP", 0.0, minval=0.0), + gcmd.get_float("EXACT_TEMP", 0.0, minval=0.0), + gcmd.get_float("BOWDEN_LENGTH", None, minval=0.0), + gcmd.get_float("EXTRUDER_LOAD_LENGTH", None, minval=0.0), + gcmd.get_float("HOTEND_LOAD_LENGTH", None, minval=0.0), + ) + except TradRackLoadError: + logging.warning( + "trad_rack: Toolchange from lane {} to {} failed".format( + start_lane, lane + ), + exc_info=True, + ) + # set up resume callback + self._set_up_resume("load toolhead", {}) + + # pause and wait for user to resume + self._send_pause() + except SelectorNotHomedError: + gcmd.respond_info( + "Selector not homed. Use TR_LOCATE_SELECTOR (or TR_HOME to home" + " the selector directly), then use TR_RESUME to continue." + ) + + # set up resume callback + resume_kwargs = { + "condition": self._is_selector_homed, + "action": lambda g=gcmd: self.cmd_TR_LOAD_TOOLHEAD(g), + "fail_msg": ( + "Cannot resume. Please use either TR_LOCATE_SELECTOR or" + " TR_HOME to home the selector, then use TR_RESUME." + ), + } + self._set_up_resume("check condition", resume_kwargs) + + # pause and wait for user to resume + self._send_pause() + + cmd_TR_UNLOAD_TOOLHEAD_help = "Unload filament from the toolhead" + + def cmd_TR_UNLOAD_TOOLHEAD(self, gcmd): + self._unload_toolhead( + gcmd, + gcmd.get_float("MIN_TEMP", 0.0, minval=0.0), + gcmd.get_float("EXACT_TEMP", 0.0, minval=0.0), + ) + + cmd_TR_SERVO_DOWN_help = "Lower the servo" + + def cmd_TR_SERVO_DOWN(self, gcmd): + if not gcmd.get_int("FORCE", 0): + # check that the selector is at a lane + if not self._can_lower_servo(): + raise self.gcode.error( + "Selector must be moved to a lane before lowering the servo" + ) + + # lower servo + self._lower_servo() + + cmd_TR_SERVO_UP_help = "Raise the servo" + + def cmd_TR_SERVO_UP(self, gcmd): + # raise servo + self._raise_servo() + + cmd_TR_SERVO_TEST_help = ( + "Test an angle for Trad Rack's servo relative to servo_down_angle" + ) + + def cmd_TR_SERVO_TEST(self, gcmd): + # get commanded and raw angles + cmd_angle = gcmd.get_float( + "ANGLE", abs(self.servo_up_angle - self.servo_down_angle) + ) + if self.servo_up_angle > self.servo_down_angle: + raw_angle = self.servo_down_angle + cmd_angle + + def raw_to_cmd(raw): + return raw - self.servo_down_angle + + else: + raw_angle = self.servo_down_angle - cmd_angle + + def raw_to_cmd(raw): + return self.servo_down_angle - raw + + # display angles + gcmd.respond_info( + "commanded angle: %.3f\nraw angle: %.3f" % (cmd_angle, raw_angle) + ) + + # check raw angle + max_angle = self.servo.get_max_angle() + if raw_angle > max_angle: + raise self.gcode.error( + "Raw angle is above the maximum of %.3f (corresponding to a" + " commanded angle of %.3f). If the servo is not rotating far" + " enough, try increasing maximum_pulse_width in the [%s]" + " section in the config file." + % (max_angle, raw_to_cmd(max_angle), SERVO_NAME) + ) + elif raw_angle < 0.0: + raise self.gcode.error( + "Raw angle is below the minimum of 0.0 (corresponding to a" + " commanded angle of %.3f). If the servo is not rotating far" + " enough, try decreasing minimum_pulse_width in the [%s]" + " section in the config file." % (raw_to_cmd(0.0), SERVO_NAME) + ) + + # set servo + self.servo.set_servo(angle=raw_angle) + + cmd_TR_SET_ACTIVE_LANE_help = ( + "Set lane number that is currently loaded in the toolhead" + ) + + def cmd_TR_SET_ACTIVE_LANE(self, gcmd): + # get lane + lane = gcmd.get_int("LANE", None) + + # check lane + self._check_lane_valid(lane) + + # check for filament in the selector + if not self._query_selector_sensor(): + raise self.gcode.error( + "Cannot set active lane without filament in selector" + ) + + # set selector position + print_time = self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + pos[0] = self.lane_positions[lane] + self.tr_toolhead.set_position(pos, homing_axes=(0,)) + stepper_enable = self.printer.lookup_object("stepper_enable") + enable = stepper_enable.lookup_enable(SELECTOR_STEPPER_NAME) + enable.motor_enable(print_time) + + # set current lane and active lane + self.curr_lane = lane + self._set_active_lane(lane) + + # restore extruder sync + self._restore_extruder_sync() + + # make lane the new default for its assigned tool + self._make_lane_default(lane) + + # enable runout detection + self.selector_sensor.set_active(True) + + # notify active lane was set without loading the toolhead + self.printer.send_event("trad_rack:forced_active_lane") + + cmd_TR_RESET_ACTIVE_LANE_help = ( + "Resets active lane to None to indicate the toolhead is not loaded" + ) + + def cmd_TR_RESET_ACTIVE_LANE(self, gcmd): + self._set_active_lane(None) + self._raise_servo() + self.extruder_sync_manager.unsync() + self.selector_sensor.set_active(False) + self.printer.send_event("trad_rack:reset_active_lane") + + cmd_TR_RESUME_help = "Resume after a failed load or unload" + + def cmd_TR_RESUME(self, gcmd): + resume_msg = None + + # loop through all resumes in the stack + while True: + # pop the last resume + try: + resume_callback, resume_kwargs = self.resume_stack.pop() + except IndexError: + break + curr_stack_size = len(self.resume_stack) + + # run the resume callback + try: + retry_resume, resume_msg = resume_callback( + gcmd, **resume_kwargs + ) + except: + # if the resume callback raised an error, add the resume back to + # the stack + self.resume_stack.append((resume_callback, resume_kwargs)) + raise + + # if a new resume was set up by the callback, return since the user + # needs to handle that and call TR_RESUME again + if len(self.resume_stack) > curr_stack_size: + return + + # if the resume needs to be retried, add it back to the stack and + # wait for the user to call TR_RESUME again + if retry_resume: + self.resume_stack.append((resume_callback, resume_kwargs)) + return + + # resume the print + self._send_resume(resume_msg) + + cmd_TR_LOCATE_SELECTOR_help = ( + "Ensures the position of Trad Rack's selector is known so that it is" + " ready for a print" + ) + + def cmd_TR_LOCATE_SELECTOR(self, gcmd): + if self._query_selector_sensor(): + if self.active_lane is None and self.save_active_lane: + # set active lane if a valid lane was saved + saved_active_lane = self.variables.get(self.VARS_ACTIVE_LANE) + try: + self._check_lane_valid(saved_active_lane) + self.active_lane = saved_active_lane + except self.gcode.error: + pass + + if self.active_lane is None: + # ask user to set the active lane or unload the lane + gcmd.respond_info( + "Selector sensor is triggered but no active lane is set." + " Please use TR_SET_ACTIVE_LANE LANE=<lane index> if" + " the toolhead is already loaded, else use" + " TR_UNLOAD_TOOLHEAD to unload the filament. Then use" + " TR_RESUME to resume the print." + ) + self.ignore_next_unload_length = True + + # set up resume callback + resume_kwargs = { + "condition": ( + lambda: self.active_lane is not None + or not self._query_selector_sensor() + ), + "action": self._resume_act_locate_selector, + "fail_msg": ( + "Cannot resume. Please use either TR_SET_ACTIVE_LANE or" + " TR_UNLOAD_TOOLHEAD, then use TR_RESUME." + ), + } + self._set_up_resume("check condition", resume_kwargs) + + # set up callback to run if user takes no action + if self.user_wait_time != -1: + gcmd.respond_info( + "If no action is taken within %d seconds, Trad Rack" + " will unload the toolhead and resume automatically." + % (self.user_wait_time) + ) + RunIfNoActivity( + self.tr_toolhead, + self.reactor, + self._unload_toolhead_and_resume, + self.user_wait_time, + ) + + # pause and wait for user to resume + self._send_pause() + else: + # (if the selector is homed, nothing needs to be done) + if not self._is_selector_homed(): + # set selector position and enable motor + # (this allows printing again without reloading the + # toolhead if the motor was disabled after the last print) + self.cmd_TR_SET_ACTIVE_LANE( + self.gcode.create_gcode_command( + "TR_SET_ACTIVE_LANE", + "TR_SET_ACTIVE_LANE", + {"LANE": self.active_lane}, + ) + ) + gcmd.respond_info( + "Set lane %d as the active lane" % (self.active_lane) + ) + else: + self._set_active_lane(None) + self.selector_sensor.set_active(False) + if not self._is_selector_homed(): + self.cmd_TR_HOME( + self.gcode.create_gcode_command("TR_HOME", "TR_HOME", {}) + ) + + cmd_TR_CALIBRATE_SELECTOR_help = ( + "Calibrate lane_spacing and the selector's min, endstop, and max" + " positions" + ) + + def cmd_TR_CALIBRATE_SELECTOR(self, gcmd): + self.tr_next_generator = self._calibrate_selector(gcmd) + next(self.tr_next_generator) + + cmd_TR_NEXT_help = ( + "You will be prompted to use this command if Trad Rack requires user" + " confirmation" + ) + + def cmd_TR_NEXT(self, gcmd): + if self.tr_next_generator: + try: + next(self.tr_next_generator) + except Exception as e: + self.tr_next_generator = None + if not isinstance(e, StopIteration): + raise + else: + raise self.gcode.error("TR_NEXT command is inactive") + + cmd_TR_SET_HOTEND_LOAD_LENGTH_help = ( + "Sets hotend_load_length. Does not persist across restarts." + ) + + def cmd_TR_SET_HOTEND_LOAD_LENGTH(self, gcmd): + value = gcmd.get_float("VALUE", None, minval=0.0) + if value is None: + adjust = gcmd.get_float("ADJUST", None) + if adjust is None: + raise self.gcode.error("VALUE or ADJUST must be specified") + value = max(0.0, self.hotend_load_length + adjust) + self.hotend_load_length = value + gcmd.respond_info("hotend_load_length: %f" % (self.hotend_load_length)) + + cmd_TR_DISCARD_BOWDEN_LENGTHS_help = ( + "Discards saved bowden lengths and reverts them to the bowden_length" + " config value" + ) + + def cmd_TR_DISCARD_BOWDEN_LENGTHS(self, gcmd): + mode = gcmd.get("MODE", "ALL").upper() + if mode not in ["ALL", "LOAD", "UNLOAD"]: + raise gcmd.error("Invalid MODE: %s" % mode) + + # discard load length + if mode in ["ALL", "LOAD"]: + self.bowden_load_length = self.config_bowden_length + self.bowden_load_length_filter.reset() + self.gcode.run_script_from_command( + 'SAVE_VARIABLE VARIABLE=%s VALUE="%s"' + % (self.VARS_CALIB_BOWDEN_LOAD_LENGTH, {}) + ) + + # discard unload length + if mode in ["ALL", "UNLOAD"]: + self.bowden_unload_length = self.config_bowden_length + self.bowden_unload_length_filter.reset() + self.gcode.run_script_from_command( + 'SAVE_VARIABLE VARIABLE=%s VALUE="%s"' + % (self.VARS_CALIB_BOWDEN_UNLOAD_LENGTH, {}) + ) + + cmd_TR_SYNC_TO_EXTRUDER_help = ( + "Sync Trad Rack's filament driver to the extruder" + ) + + def cmd_TR_SYNC_TO_EXTRUDER(self, gcmd): + self.toolhead.wait_moves() + self.sync_to_extruder = True + self._restore_extruder_sync() + + cmd_TR_UNSYNC_FROM_EXTRUDER_help = ( + "Unsync Trad Rack's filament driver from the extruder" + ) + + def cmd_TR_UNSYNC_FROM_EXTRUDER(self, gcmd): + self.toolhead.wait_moves() + self.sync_to_extruder = False + self._restore_extruder_sync() + + cmd_TR_ASSIGN_LANE_help = "Assign a lane to a tool" + + def cmd_TR_ASSIGN_LANE(self, gcmd): + lane = gcmd.get_int("LANE", None) + tool = gcmd.get_int("TOOL", None) + + # check lane and tool + self._check_lane_valid(lane) + self._check_tool_valid(tool) + + # assign lane + self._assign_lane(lane, tool) + + # mark lane as not dead + self.lanes_dead[lane] = False + + # make lane the new default for the tool + if gcmd.get_int("SET_DEFAULT", 0): + self.default_lanes[tool] = lane + + cmd_TR_SET_DEFAULT_LANE_help = "Set the default lane for a tool" + + def cmd_TR_SET_DEFAULT_LANE(self, gcmd): + lane = gcmd.get_int("LANE", None) + tool = gcmd.get_int("TOOL", None) + + # check lane + self._check_lane_valid(lane) + + if tool is None: + # set lane as default for its currently-assigned tool + self._make_lane_default(lane) + else: + # check tool + self._check_tool_valid(tool) + + # set lane as default for the tool + self._set_default_lane(tool, lane) + + cmd_TR_RESET_TOOL_MAP_help = "Reset tools assigned to each lane" + + def cmd_TR_RESET_TOOL_MAP(self, gcmd): + self._reset_tool_map() + + cmd_TR_PRINT_TOOL_MAP_help = "Print tool assignment for each lane" + + def cmd_TR_PRINT_TOOL_MAP(self, gcmd): + num_chars = len(str(self.lane_count - 1)) + lane_msg = "|Lane: |" + tool_msg = "|Tool: |" + for lane in range(self.lane_count): + lane_str = str(lane) + lane_msg += " " * (num_chars - len(lane_str)) + lane_str + "|" + tool_str = str(self.tool_map[lane]) + tool_msg += " " * (num_chars - len(tool_str)) + tool_str + "|" + gcmd.respond_info(lane_msg + "\n" + tool_msg) + + cmd_TR_PRINT_TOOL_GROUPS_help = "Print lanes assigned to each tool" + + def cmd_TR_PRINT_TOOL_GROUPS(self, gcmd): + tool_groups = [] + for _ in range(self.lane_count): + tool_groups.append([]) + for lane in range(self.lane_count): + tool_groups[self.tool_map[lane]].append(lane) + msg = "" + for tool in range(len(tool_groups)): + msg += "Tool {}: {}".format(tool, tool_groups[tool]) + if len(tool_groups[tool]) > 1: + msg += " (default: {})".format(self.default_lanes[tool]) + msg += "\n" + gcmd.respond_info(msg) + + cmd_SELECT_TOOL_help = ( + "Load filament from Trad Rack into the toolhead with T commands" + ) + + def cmd_SELECT_TOOL(self, gcmd): + tool = int(gcmd.get_command().partition("T")[2]) + params = gcmd.get_command_parameters() + params["TOOL"] = tool + self.cmd_TR_LOAD_TOOLHEAD( + self.gcode.create_gcode_command( + "TR_LOAD_TOOLHEAD", "TR_LOAD_TOOLHEAD", params + ) + ) + + # helper functions + def _lower_servo(self, toolhead_dwell=False): + self.tr_toolhead.wait_moves() + self.servo.set_servo(angle=self.servo_down_angle) + if self.servo_raised or self.servo_raised is None: + self.tr_toolhead.dwell(self.servo_wait) + if toolhead_dwell: + self.toolhead.dwell(self.servo_wait) + self.servo_raised = False + + def _raise_servo( + self, + toolhead_dwell=False, + tr_toolhead_dwell=True, + wait_moves=True, + print_time=None, + ): + if wait_moves: + self.tr_toolhead.wait_moves() + self.servo.set_servo(angle=self.servo_up_angle, print_time=print_time) + if not self.servo_raised: + if tr_toolhead_dwell: + self.tr_toolhead.dwell(self.servo_wait) + if toolhead_dwell: + self.toolhead.dwell(self.servo_wait) + self.servo_raised = True + + def _is_selector_homed(self): + return ( + "x" + in self.tr_toolhead.get_kinematics().get_status( + self.reactor.monotonic() + )["homed_axes"] + ) + + def _query_selector_sensor(self): + move_time = self.tr_toolhead.get_last_move_time() + return not not self.fil_driver_endstops[0][0].query_endstop(move_time) + + def _query_toolhead_sensor(self): + if not self.toolhead_fil_endstops: + return None + move_time = self.tr_toolhead.get_last_move_time() + return not not self.toolhead_fil_endstops[0][0].query_endstop(move_time) + + def _check_lane_valid(self, lane): + if lane is None or lane > self.lane_count - 1 or lane < 0: + raise self.gcode.error("Invalid LANE") + + def _check_tool_valid(self, tool): + try: + self._check_lane_valid(tool) + except: + raise self.gcode.error("Invalid TOOL") + + def _check_selector_homed(self): + if not self._is_selector_homed(): + raise SelectorNotHomedError("Must home selector first") + + def _can_lower_servo(self): + return ( + self._is_selector_homed() and self.curr_lane is not None + ) or self._query_selector_sensor() + + def _reset_fil_driver(self): + self.extruder_sync_manager.reset_fil_driver() + + def _restore_extruder_sync(self): + if self.sync_to_extruder and self.active_lane is not None: + self.extruder_sync_manager.sync_fil_driver_to_extruder() + self._lower_servo(True) + else: + self._raise_servo() + self.tr_toolhead.wait_moves() + self.extruder_sync_manager.unsync() + + def _go_to_lane(self, lane): + # check if homed + self._check_selector_homed() + + # check lane + self._check_lane_valid(lane) + if lane == self.curr_lane: + return + + # check for filament in the selector + if self._query_selector_sensor(): + raise self.gcode.error( + "Cannot change lane with filament in selector" + ) + + # raise servo + self._raise_servo() + + # move to lane + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + pos[0] = self.lane_positions[lane] + self.tr_toolhead.move(pos, self.sel_max_velocity) + + # set current lane + self.curr_lane = lane + + def _load_lane(self, lane, gcmd, reset_speed=False, user_load=False): + # check lane + self._check_lane_valid(lane) + + # reset lane speed + if reset_speed: + self.lanes_unloaded[lane] = False + + # move selector + self._go_to_lane(lane) + + # lower servo and turn the drive gear until filament is detected + self._lower_servo() + self.tr_toolhead.wait_moves() + if user_load: + gcmd.respond_info("Please insert filament in lane %d" % (lane)) + + # load filament into the selector + self._load_selector(lane, user_load=user_load) + + # retract filament into the module + self._reset_fil_driver() + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + pos[1] -= ( + self.selector_unload_length + self.selector_unload_length_extra + ) + self.tr_toolhead.move(pos, self.selector_unload_speed) + + # undo extra unload length offset + pos[1] += self.selector_unload_length_extra + self.tr_toolhead.move(pos, self.selector_unload_speed) + + # reset filament driver position + self._reset_fil_driver() + + # raise servo + self._raise_servo() + + if user_load: + gcmd.respond_info("Load complete") + + def _wait_for_heater_temp(self, min_temp=0.0, exact_temp=0.0): + # get current and target temps + heater = self.toolhead.get_extruder().get_heater() + smoothed_temp, target_temp = heater.get_temp(self.reactor.monotonic()) + min_extrude_temp = heater.min_extrude_temp + + # raise an error if no valid temp has been set + if ( + max(min_temp, exact_temp, target_temp, self.last_heater_target) + < min_extrude_temp + ): + raise self.gcode.error( + "Extruder temperature must be set above min_extrude_temp" + ) + + # set temp and wait if below acceptable temp + min_temp = max(min_temp, min_extrude_temp) + if exact_temp or smoothed_temp < min_temp: + if exact_temp: + temp = save_temp = exact_temp + elif target_temp > min_temp: + temp = save_temp = target_temp + else: + temp = max(min_temp, self.last_heater_target) + if min_temp >= min_extrude_temp: + save_temp = min_temp + else: + save_temp = None + pheaters = self.printer.lookup_object("heaters") + pheaters.set_temperature(heater, temp, True) + return save_temp + return target_temp + + def _save_heater_target(self, target_temp=None): + if target_temp is None: + heater = self.toolhead.get_extruder().get_heater() + _, target_temp = heater.get_temp(self.reactor.monotonic()) + self.gcode.run_script_from_command( + 'SAVE_VARIABLE VARIABLE=%s VALUE="%s"' + % (self.VARS_HEATER_TARGET, target_temp) + ) + self.last_heater_target = target_temp + + def _load_toolhead( + self, + lane, + gcmd, + tool=None, + min_temp=0.0, + exact_temp=0.0, + selector_already_loaded=False, + bowden_length=None, + extruder_load_length=None, + hotend_load_length=None, + ): + # keep track of lane in case of an error + self.next_lane = lane + + # check lane + self._check_lane_valid(lane) + if lane == self.active_lane: + return + + # check if homed + self._check_selector_homed() + + # check and set lengths + if bowden_length is None: + bowden_length = self.bowden_load_length + if extruder_load_length is None: + extruder_load_length = self.extruder_load_length + if hotend_load_length is None: + hotend_load_length = self.hotend_load_length + + # save gcode state + self.gcode.run_script_from_command( + "SAVE_GCODE_STATE NAME=TR_TOOLCHANGE_STATE" + ) + + # wait for heater temp if needed + save_temp = self._wait_for_heater_temp(min_temp, exact_temp) + + # disable runout detection + self.selector_sensor.set_active(False) + + # unload current lane (if filament is detected) + if not (selector_already_loaded and self.curr_lane == lane): + try: + self._unload_toolhead(gcmd) + except: + self._raise_servo() + if self.curr_lane is None: + gcmd.respond_info( + "Failed to unload. Please either pull the filament out" + " of the toolhead and selector or retry with" + " TR_UNLOAD_TOOLHEAD, then use TR_RESUME to continue." + ) + else: + gcmd.respond_info( + "Failed to unload. Please either pull the filament in" + " lane {lane} out of the toolhead and selector or retry" + " with TR_UNLOAD_TOOLHEAD, then use TR_RESUME to reload" + " lane {lane} and continue.".format( + lane=str(self.curr_lane) + ) + ) + self.lanes_unloaded[self.curr_lane] = False + self.retry_lane = self.curr_lane + logging.warning( + "trad_rack: Failed to unload toolhead", exc_info=True + ) + raise TradRackLoadError( + "Failed to load toolhead. Could not unload toolhead before" + " load" + ) + + # notify toolhead load started + self.printer.send_event("trad_rack:load_started") + + # run pre-load custom gcode + self.pre_load_macro.run_gcode_from_command() + self.toolhead.wait_moves() + self.tr_toolhead.wait_moves() + + # load filament into the selector + try: + self._load_selector(lane, tool=tool) + except: + self._raise_servo() + if tool is None: + gcmd.respond_info( + "Failed to load selector from lane {lane}. Use TR_RESUME to" + " reload lane {lane} and retry.".format(lane=str(lane)) + ) + else: + assigned_lanes = self._get_assigned_lanes(tool) + gcmd.respond_info( + "Failed to load selector from any of the lanes assigned to" + " tool {tool}: {lanes}. Use TR_LOAD_LANE LANE=<lane" + " index> to reload one of these lanes, then use TR_RESUME" + " to retry. (If you want to use a different lane, use" + " TR_ASSIGN_LANE LANE=<lane index> TOOL={tool}" + " beforehand.)".format(tool=tool, lanes=assigned_lanes) + ) + self.retry_lane = lane + self.retry_tool = tool + logging.warning("trad_rack: Failed to load selector", exc_info=True) + raise TradRackLoadError( + "Failed to load toolhead. Could not load selector from lane %d" + % lane + ) + self.retry_tool = None + + # move filament through the bowden tube + self._reset_fil_driver() + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + move_start = pos[1] + pos[1] += bowden_length + if self.lanes_unloaded[self.curr_lane]: + speed = self.buffer_pull_speed + else: + speed = self.spool_pull_speed + reached_sensor_early = True + if self.load_with_toolhead_sensor and self.toolhead_fil_endstops: + hmove = HomingMove( + self.printer, self.toolhead_fil_endstops, self.tr_toolhead + ) + try: + # move and check for early sensor trigger + trigpos = hmove.homing_move(pos, speed, probe_pos=True) + + # if sensor triggered early, retract before next homing move + pos[1] = trigpos[1] - self.fil_homing_retract_dist + except self.printer.command_error: + reached_sensor_early = False + self.tr_toolhead.move(pos, speed) + base_length = pos[1] - move_start + + # sync extruder to filament driver + self.tr_toolhead.wait_moves() + self.extruder_sync_manager.sync_extruder_to_fil_driver() + + # move filament until toolhead sensor is triggered + if self.load_with_toolhead_sensor and self.toolhead_fil_endstops: + pos = self.tr_toolhead.get_position() + move_start = pos[1] + pos[1] += self.fil_homing_lengths["bowden load"] + hmove = HomingMove( + self.printer, self.toolhead_fil_endstops, self.tr_toolhead + ) + try: + trigpos = hmove.homing_move( + pos, self.toolhead_sense_speed, probe_pos=True + ) + except: + self._raise_servo() + self.extruder_sync_manager.unsync() + gcmd.respond_info( + "Failed to load toolhead from lane {lane} (no trigger on" + " toolhead sensor). Please either pull the filament in lane" + " {lane} out of the toolhead and selector or use" + " TR_UNLOAD_TOOLHEAD. Then use TR_RESUME to reload lane" + " {lane} and retry.".format(lane=str(lane)) + ) + self.retry_lane = lane + logging.warning( + "trad_rack: Toolhead sensor homing move failed", + exc_info=True, + ) + raise TradRackLoadError( + "Failed to load toolhead. No trigger on toolhead sensor" + " after full movement" + ) + + # update bowden_load_length + length = ( + trigpos[1] + - move_start + + base_length + - self.target_toolhead_homing_dist + ) + old_set_length = self.bowden_load_length + self.bowden_load_length = self.bowden_load_length_filter.update( + length + ) + samples = self.bowden_load_length_filter.get_entry_count() + if self.log_bowden_lengths: + self._write_bowden_length_data( + self.bowden_load_lengths_filename, + length, + old_set_length, + self.bowden_load_length, + samples, + ) + self._save_bowden_length("load", self.bowden_load_length, samples) + if not (self.bowden_load_calibrated or reached_sensor_early): + self.bowden_load_calibrated = True + gcmd.respond_info( + "Calibrated bowden_load_length: {}".format( + self.bowden_load_length + ) + ) + + # finish loading filament into extruder + self._reset_fil_driver() + pos = self.tr_toolhead.get_position() + pos[1] += extruder_load_length + self.tr_toolhead.move(pos, self.extruder_load_speed) + + # load filament into hotend + pos[1] += hotend_load_length + self.tr_toolhead.move(pos, self.hotend_load_speed) + + # check whether servo move might overlap extruder loading move + if hotend_load_length: + if hasattr(toolhead, "LookAheadQueue"): + hotend_load_time = ( + self.tr_toolhead.lookahead.get_last().min_move_t + ) + else: + hotend_load_time = ( + self.tr_toolhead.move_queue.get_last().min_move_t + ) + else: + hotend_load_time = 0.0 + servo_delay = max(0.0, self.servo_wait - hotend_load_time) + + # flush lookahead and raise servo before move ends + print_time = ( + self.tr_toolhead.get_last_move_time() + - self.servo_wait + + servo_delay + ) + if not self.sync_to_extruder: + self._raise_servo( + tr_toolhead_dwell=False, wait_moves=False, print_time=print_time + ) + + # wait for servo move if necessary + if servo_delay: + self.tr_toolhead.dwell(servo_delay) + + # set active lane + self._set_active_lane(lane) + + # unsync extruder from filament driver + self.tr_toolhead.wait_moves() + self._restore_extruder_sync() + + # make lane the new default for its assigned tool + self._make_lane_default(lane) + + # enable runout detection + self.selector_sensor.set_active(True) + + # save heater target + if save_temp is not None: + self._save_heater_target(target_temp=save_temp) + + # run post-load custom gcode + self.post_load_macro.run_gcode_from_command() + self.toolhead.wait_moves() + self.tr_toolhead.wait_moves() + + # restore gcode state + self.gcode.run_script_from_command( + "RESTORE_GCODE_STATE NAME=TR_TOOLCHANGE_STATE MOVE=1" + ) + + # notify toolhead load complete + self.printer.send_event("trad_rack:load_complete") + + def _load_selector(self, lane, tool=None, user_load=False): + try: + self._do_load_selector(lane, user_load=user_load) + except self.gcode.error: + if tool is None: + raise + elif self._find_replacement_lane(lane) is None: + raise self.gcode.error( + "Failed to load filament into selector from any of the" + " lanes assigned to tool {}".format(tool) + ) + + def _do_load_selector(self, lane, user_load=False): + # move selector + self._go_to_lane(lane) + + # lower servo and turn the drive gear until filament is detected + self._lower_servo() + self._reset_fil_driver() + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + hmove = HomingMove( + self.printer, self.fil_driver_endstops, self.tr_toolhead + ) + if user_load: + length_key = "user load lane" + else: + length_key = "load selector" + pos[1] += self.fil_homing_lengths[length_key] + try: + hmove.homing_move(pos, self.selector_sense_speed) + except: + self._raise_servo() + logging.warning( + "trad_rack: Selector homing move failed", exc_info=True + ) + raise self.gcode.error( + "Failed to load filament into selector. No trigger on selector" + " sensor after full movement" + ) + + def _unload_selector( + self, gcmd, base_length=None, mark_calibrated=False, eject=False + ): + # check for filament in selector + if not self._query_selector_sensor(): + gcmd.respond_info( + "No filament detected. Attempting to load selector" + ) + self._load_selector(self.curr_lane) + gcmd.respond_info( + "Loaded selector. Retracting filament into module" + ) + else: + # lower servo and turn the drive gear until filament + # is no longer detected + self._lower_servo() + self._reset_fil_driver() + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + move_start = pos[1] + hmove = HomingMove( + self.printer, self.fil_driver_endstops, self.tr_toolhead + ) + pos[1] -= self.fil_homing_lengths["bowden unload"] + try: + trigpos = hmove.homing_move( + pos, + self.selector_sense_speed, + probe_pos=True, + triggered=False, + ) + except: + self._raise_servo() + logging.warning( + "trad_rack: Selector homing move failed", exc_info=True + ) + raise self.gcode.error( + "Failed to unload filament from selector. Selector sensor" + " still triggered after full movement" + ) + + # update bowden_unload_length + if base_length is not None and not self.ignore_next_unload_length: + length = ( + move_start + - trigpos[1] + + base_length + - self.target_selector_homing_dist + ) + old_set_length = self.bowden_unload_length + self.bowden_unload_length = ( + self.bowden_unload_length_filter.update(length) + ) + samples = self.bowden_unload_length_filter.get_entry_count() + if self.log_bowden_lengths: + self._write_bowden_length_data( + self.bowden_unload_lengths_filename, + length, + old_set_length, + self.bowden_unload_length, + samples, + ) + self._save_bowden_length( + "unload", self.bowden_unload_length, samples + ) + if mark_calibrated: + self.bowden_unload_calibrated = True + gcmd.respond_info( + "Calibrated bowden_unload_length: {}".format( + self.bowden_unload_length + ) + ) + + # retract filament into the module + self._reset_fil_driver() + pos = self.tr_toolhead.get_position() + if eject: + pos[1] -= self.selector_unload_length + self.eject_length + speed = self.eject_speed + else: + pos[1] -= ( + self.selector_unload_length + self.selector_unload_length_extra + ) + speed = self.selector_unload_speed + self.tr_toolhead.move(pos, speed) + + if not eject: + # undo extra unload length offset + pos[1] += self.selector_unload_length_extra + self.tr_toolhead.move(pos, self.selector_unload_speed) + + # reset filament driver position + self._reset_fil_driver() + + # raise servo + self._raise_servo() + + def _unload_toolhead( + self, + gcmd, + min_temp=0.0, + exact_temp=0.0, + force_unload=False, + sync=False, + eject=False, + ): + # reset active lane + self._set_active_lane(None) + + # disable runout detection + self.selector_sensor.set_active(False) + + selector_sensor_state = self._query_selector_sensor() + toolhead_sensor_state = self._query_toolhead_sensor() + + # check for filament + self.toolhead.wait_moves() + if not (force_unload or selector_sensor_state or toolhead_sensor_state): + # reset ignore_next_unload_length + self.ignore_next_unload_length = False + + return + + # check for faulty toolhead or selector sensor + if not force_unload: + if toolhead_sensor_state and not selector_sensor_state: + gcmd.respond_info( + "WARNING: The toolhead filament sensor is triggered but the" + " selector sensor is not. This may indicate that one of the" + " sensors is faulty or that there is a short piece of" + " filament in the bowden tube that does not reach the" + " selector." + ) + + # check that the selector is at a lane + if not self._can_lower_servo(): + raise self.gcode.error( + "Selector must be moved to a lane before unloading" + ) + + # notify toolhead unload started + self.printer.send_event("trad_rack:unload_started") + + # wait for heater temp if needed + self._wait_for_heater_temp(min_temp, exact_temp) + + # sync filament driver to extruder for pre-unload custom gcode + if sync: + self.extruder_sync_manager.sync_fil_driver_to_extruder() + self._lower_servo(True) + + # run pre-unload custom gcode + try: + self.pre_unload_macro.run_gcode_from_command() + self.toolhead.wait_moves() + self.tr_toolhead.wait_moves() + finally: + # unsync filament driver from extruder + self.extruder_sync_manager.unsync() + + # lower servo + self._lower_servo(True) + + # sync extruder to filament driver + self.tr_toolhead.wait_moves() + self.extruder_sync_manager.sync_extruder_to_fil_driver() + + # move filament until toolhead sensor is untriggered + if self.unload_with_toolhead_sensor and self.toolhead_fil_endstops: + pos = self.tr_toolhead.get_position() + pos[1] -= self.fil_homing_lengths["unload toolhead"] + hmove = HomingMove( + self.printer, self.toolhead_fil_endstops, self.tr_toolhead + ) + try: + hmove.homing_move( + pos, self.toolhead_sense_speed, triggered=False + ) + except: + self._raise_servo() + self.extruder_sync_manager.unsync() + logging.warning( + "trad_rack: Toolhead sensor homing move failed", + exc_info=True, + ) + raise self.gcode.error( + "Failed to unload toolhead. Toolhead sensor still triggered" + " after full movement" + ) + + # get filament out of the extruder + self._reset_fil_driver() + pos = self.tr_toolhead.get_position() + pos[1] -= self.toolhead_unload_length + self.tr_toolhead.move(pos, self.toolhead_unload_speed) + + # unsync extruder from filament driver + self.tr_toolhead.wait_moves() + self.extruder_sync_manager.unsync() + + # move filament through the bowden tube + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + move_start = pos[1] + pos[1] -= self.bowden_unload_length + hmove = HomingMove( + self.printer, self.fil_driver_endstops, self.tr_toolhead + ) + reached_sensor_early = True + try: + # move and check for early sensor trigger + trigpos = hmove.homing_move( + pos, self.buffer_pull_speed, probe_pos=True, triggered=False + ) + + # if sensor triggered early, retract before next homing move + pos[1] = trigpos[1] + self.fil_homing_retract_dist + except self.printer.command_error: + reached_sensor_early = False + self.tr_toolhead.move(pos, self.buffer_pull_speed) + + # unload selector + mark_calibrated = not ( + self.bowden_unload_calibrated or reached_sensor_early + ) + self._unload_selector(gcmd, move_start - pos[1], mark_calibrated, eject) + + # note that the current lane's buffer has been filled + if self.curr_lane is not None: + self.lanes_unloaded[self.curr_lane] = True + + # reset ignore_next_unload_length + self.ignore_next_unload_length = False + + # run post-unload custom gcode + self.post_unload_macro.run_gcode_from_command() + self.toolhead.wait_moves() + self.tr_toolhead.wait_moves() + + # notify toolhead unload complete + self.printer.send_event("trad_rack:unload_complete") + + def _send_pause(self): + pause_resume = self.printer.lookup_object("pause_resume") + if not pause_resume.get_status(self.reactor.monotonic())["is_paused"]: + self.pause_macro.run_gcode_from_command() + + def _send_resume(self, resume_msg=None): + pause_resume = self.printer.lookup_object("pause_resume") + if not pause_resume.get_status(self.reactor.monotonic())["is_paused"]: + return + if resume_msg: + self.gcode.respond_info(resume_msg) + self.resume_macro.run_gcode_from_command() + + def _set_active_lane(self, lane): + self.active_lane = lane + if self.save_active_lane: + self.gcode.run_script_from_command( + 'SAVE_VARIABLE VARIABLE=%s VALUE="%s"' + % (self.VARS_ACTIVE_LANE, lane) + ) + + def _reset_tool_map(self): + self.tool_map = list(range(self.lane_count)) + self.default_lanes = list(range(self.lane_count)) + + def _find_replacement_lane(self, runout_lane): + tool = self.tool_map[runout_lane] + pre_dead_lanes = [] + + # 1st pass - check lanes not marked as dead + lane = (runout_lane + 1) % self.lane_count + while True: + if self.tool_map[lane] == tool: + if self.lanes_dead[lane]: + pre_dead_lanes.append(lane) + else: + try: + self._load_selector(lane) + self.default_lanes[tool] = lane + return lane + except: + self.lanes_dead[lane] = True + if lane == runout_lane: + break + lane = (lane + 1) % self.lane_count + + # 2nd pass - check lanes previously marked as dead + for lane in pre_dead_lanes: + try: + self._load_selector(lane) + self.lanes_dead[lane] = False + self.default_lanes[tool] = lane + return lane + except: + pass + return None + + def _set_default_lane(self, tool, lane=None): + # set lane that was passed in + if lane is not None: + self._assign_lane(lane, tool) + self.default_lanes[tool] = lane + return + + # find a lane that is already assigned to the tool + for lane in range(self.lane_count): + if self.tool_map[lane] == tool: + self.default_lanes[tool] = lane + return + self.default_lanes[tool] = None + + def _make_lane_default(self, lane): + self.default_lanes[self.tool_map[lane]] = lane + + def _assign_lane(self, lane, tool): + prev_tool = self.tool_map[lane] + + # assign lane to tool + self.tool_map[lane] = tool + + # reassign default lane for previous tool if needed + if self.default_lanes[prev_tool] == lane: + self._set_default_lane(prev_tool) + + # ensure new tool has a default lane assigned + if self.default_lanes[tool] is None: + self.default_lanes[tool] = lane + + def _get_assigned_lanes(self, tool): + lanes = [] + for lane in range(self.lane_count): + if self.tool_map[lane] == tool: + lanes.append(lane) + return lanes + + def _runout_replace_filament(self, gcmd): + # unload + if self.runout_steps_done < 1: + try: + self._unload_toolhead( + gcmd, force_unload=True, sync=True, eject=True + ) + except: + self._raise_servo() + gcmd.respond_info( + "Failed to unload. Please pull filament {} out of the" + " toolhead and selector, then use TR_RESUME to continue.".format( + self.runout_lane + ) + ) + logging.warning( + "trad_rack: Failed to unload toolhead", exc_info=True + ) + return False + self.runout_steps_done = 1 + + # find a new lane to use + selector_already_loaded = False + if self.runout_steps_done < 2: + lane = self._find_replacement_lane(self.runout_lane) + if lane is None: + runout_tool = self.tool_map[self.runout_lane] + assigned_lanes = self._get_assigned_lanes(runout_tool) + gcmd.respond_info( + "No replacement lane found for tool {tool}. The following" + " lanes are assigned to tool {tool}: {lanes}. Use" + " TR_LOAD_LANE LANE=<lane index> to load one of these" + " lanes, or use TR_ASSIGN_LANE LANE=<lane index>" + " TOOL={tool} to assign another lane. Then use TR_RESUME to" + " continue.".format( + tool=str(runout_tool), lanes=str(assigned_lanes) + ) + ) + return False + self.replacement_lane = lane + selector_already_loaded = True + self.runout_lane = None + self.runout_steps_done = 2 + + # load toolhead + self._load_toolhead( + self.replacement_lane, + gcmd, + selector_already_loaded=selector_already_loaded, + ) + return True + + def _write_bowden_length_data( + self, filename, length, old_set_length, new_set_length, samples + ): + try: + with open(filename, "a+") as f: + if os.stat(filename).st_size == 0: + f.write( + "time,length,diff_from_set_length,new_set_length," + "new_sample_count\n" + ) + f.write( + "{},{:.3f},{:.3f},{:.3f},{}\n".format( + time.strftime("%Y%m%d_%H%M%S"), + length, + length - old_set_length, + new_set_length, + samples, + ) + ) + except IOError as e: + raise self.printer.command_error( + "Error writing to file '%s': %s", filename, str(e) + ) + + def _save_bowden_length(self, mode, new_set_length, samples): + length_stats = { + "new_set_length": new_set_length, + "sample_count": samples, + } + if mode == "load": + self.gcode.run_script_from_command( + 'SAVE_VARIABLE VARIABLE=%s VALUE="%s"' + % (self.VARS_CALIB_BOWDEN_LOAD_LENGTH, length_stats) + ) + else: + self.gcode.run_script_from_command( + 'SAVE_VARIABLE VARIABLE=%s VALUE="%s"' + % (self.VARS_CALIB_BOWDEN_UNLOAD_LENGTH, length_stats) + ) + + def _calibrate_selector(self, gcmd): + extra_travel = 1.0 + + # prompt user to set the selector at lane 0 + self._prompt_selector_calibration(0, gcmd) + yield + + # measure position of lane 0 relative to endstop + pos_endstop = ( + self.tr_kinematics.get_selector_rail() + .get_homing_info() + .position_endstop + ) + max_travel = self.lane_positions[0] - pos_endstop + extra_travel + endstop_to_lane0 = self._measure_selector_to_endstop(max_travel, gcmd) + + # prompt user to set the selector at the last lane + self._prompt_selector_calibration(self.lane_count - 1, gcmd) + yield + + # measure position of last lane relative to endstop + max_travel = ( + self.lane_positions[self.lane_count - 1] + - self.lane_positions[0] + + endstop_to_lane0 + + extra_travel + ) + endstop_to_last_lane = self._measure_selector_to_endstop( + max_travel, gcmd + ) + + # process calibration and set new lane positions + ( + pos_endstop, + lane_spacing, + self.lane_positions, + ) = self.lane_position_manager.process_selector_calibration( + endstop_to_lane0, endstop_to_last_lane, 6 + ) + + # round new config values + pos_endstop = round(pos_endstop, 3) + pos_min = ( + math.floor(min(pos_endstop, self.lane_positions[0]) * 1000) / 1000 + ) + pos_max = ( + math.ceil(self.lane_positions[self.lane_count - 1] * 1000) / 1000 + ) + + # set new selector values + rail = self.tr_kinematics.get_selector_rail() + rail.position_min = pos_min + rail.position_endstop = pos_endstop + rail.position_max = pos_max + + # set current selector position + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + pos[0] = pos_endstop + self.tr_toolhead.set_position(pos, homing_axes=(0,)) + + # show results and prompt user to save config + gcmd.respond_info( + "trad_rack: lane_spacing: {lane_spacing:.6f}\n{stepper}:" + " position_min: {pos_min:.3f}\n{stepper}: position_endstop:" + " {pos_endstop:.3f}\n{stepper}: position_max: {pos_max:.3f}\nMake" + " sure to update the printer config file with these parameters so" + " they will be kept across restarts.".format( + lane_spacing=lane_spacing, + stepper=SELECTOR_STEPPER_NAME, + pos_min=pos_min, + pos_endstop=pos_endstop, + pos_max=pos_max, + ) + ) + + def _prompt_selector_calibration(self, lane, gcmd): + # go to lane + if not self._is_selector_homed(): + self.cmd_TR_HOME( + self.gcode.create_gcode_command("TR_HOME", "TR_HOME", {}) + ) + self._go_to_lane(lane) + + # lower servo and turn the drive gear until filament is detected + self._lower_servo() + self.tr_toolhead.wait_moves() + gcmd.respond_info("Please insert filament in lane %d" % (lane)) + + # disable selector motor + print_time = self.tr_toolhead.get_last_move_time() + stepper_enable = self.printer.lookup_object("stepper_enable") + enable = stepper_enable.lookup_enable(SELECTOR_STEPPER_NAME) + enable.motor_disable(print_time) + + # load filament into the selector + self._load_selector(lane) + + # extend filament past the sensor + self._reset_fil_driver() + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + pos[1] += 15.0 + self.tr_toolhead.move(pos, self.selector_unload_speed) + + # reset filament driver position + self._reset_fil_driver() + + # raise servo + self._raise_servo() + + # prompt user to position selector + self.tr_toolhead.wait_moves() + gcmd.respond_info( + "To ensure that the filament paths of the lane module and selector" + " are aligned, adjust the selector's position by hand until the" + " filament can slide smoothly with very little resistance. Then use" + " TR_NEXT to continue selector calibration." + ) + + def _measure_selector_to_endstop(self, max_travel, gcmd): + # set selector position + print_time = self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + pos[0] = max_travel + self.tr_toolhead.set_position(pos, homing_axes=(0,)) + stepper_enable = self.printer.lookup_object("stepper_enable") + enable = stepper_enable.lookup_enable(SELECTOR_STEPPER_NAME) + enable.motor_enable(print_time) + + # unload selector into current lane + self._unload_selector(gcmd) + + # clear current lane + self.curr_lane = None + + # prepare for homing move + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + pos[0] = 0.0 + rail = self.tr_kinematics.get_selector_rail() + endstops = rail.get_endstops() + hi = rail.get_homing_info() + if hi.retract_dist: + speed = hi.second_homing_speed + else: + speed = hi.speed + + # retract first if endstop is already triggered + self.selector_endstops = ( + self.tr_kinematics.get_selector_rail().get_endstops() + ) + move_time = self.tr_toolhead.get_last_move_time() + if not not self.selector_endstops[0][0].query_endstop(move_time): + curr_pos = self.tr_toolhead.get_position() + curr_pos[0] += 5.0 + self.tr_toolhead.move(curr_pos, hi.speed) + self.tr_toolhead.wait_moves() + + # homing move + hmove = HomingMove(self.printer, endstops, self.tr_toolhead) + trigpos = hmove.homing_move(pos, speed, probe_pos=True) + + # set selector position + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + pos[0] = hi.position_endstop + self.tr_toolhead.set_position(pos, homing_axes=(0,)) + + # return distance traveled + return max_travel - trigpos[0] + + # resume callbacks + def _resume_load_toolhead(self, gcmd): + # load any of the tool's assigned lanes to selector + selector_already_loaded = False + if self.retry_tool is not None: + replacement_lane = self._find_replacement_lane(self.retry_lane) + if replacement_lane is None: + raise self.gcode.error( + "Failed to load filament into selector from any of the" + " lanes assigned to tool {}".format(self.retry_tool) + ) + self.next_lane = replacement_lane + selector_already_loaded = True + + # retry loading lane + elif self.retry_lane is not None: + self._load_lane(self.retry_lane, gcmd, user_load=True) + + # load next filament into toolhead + self._load_toolhead( + self.next_lane, + gcmd, + selector_already_loaded=selector_already_loaded, + ) + + return False, "Toolhead loaded successfully. Resuming print" + + def _resume_check_condition( + self, + gcmd, + condition, + action=None, + resume_msg="Resuming print", + fail_msg="Condition not met to resume", + ): + if condition(): + if action is not None: + action() + return False, resume_msg + gcmd.respond_info(fail_msg) + return True, None + + def _resume_runout(self, gcmd): + if self._runout_replace_filament(gcmd): + return False, "Toolhead loaded successfully. Resuming print" + return True, None + + # other resume helper functions + def _set_up_resume(self, resume_type, resume_kwargs): + self.resume_stack.append( + (self.resume_callbacks[resume_type], resume_kwargs) + ) + + def _unload_toolhead_and_resume(self): + pause_resume = self.printer.lookup_object("pause_resume") + if pause_resume.get_status(self.reactor.monotonic())["is_paused"]: + self.gcode.respond_info("Unloading toolhead") + self.cmd_TR_UNLOAD_TOOLHEAD( + self.gcode.create_gcode_command( + "TR_UNLOAD_TOOLHEAD", "TR_UNLOAD_TOOLHEAD", {} + ) + ) + self.cmd_TR_RESUME( + self.gcode.create_gcode_command("TR_RESUME", "TR_RESUME", {}) + ) + + def _resume_act_locate_selector(self): + if not self._is_selector_homed(): + self.cmd_TR_HOME( + self.gcode.create_gcode_command("TR_HOME", "TR_HOME", {}) + ) + self.ignore_next_unload_length = False + + # other functions + def set_fil_driver_multiplier(self, multiplier): + self.extruder_sync_manager.set_fil_driver_multiplier(multiplier) + + def is_fil_driver_synced(self): + return self.extruder_sync_manager.is_fil_driver_synced() + + def get_status(self, eventtime): + return { + "curr_lane": self.curr_lane, + "active_lane": self.active_lane, + "selector_homed": self._is_selector_homed(), + } + + +class TradRackToolHead(toolhead.ToolHead, object): + def __init__(self, config, buffer_pull_speed, is_extruder_synced): + self.printer = config.get_printer() + self.danger_options = self.printer.lookup_object("danger_options") + self.reactor = self.printer.get_reactor() + self.all_mcus = [ + m for n, m in self.printer.lookup_objects(module="mcu") + ] + self.mcu = self.all_mcus[0] + if hasattr(toolhead, "LookAheadQueue"): + self.lookahead = toolhead.LookAheadQueue(self) + self.lookahead.set_flush_time(toolhead.BUFFER_TIME_HIGH) + else: + self.move_queue = toolhead.MoveQueue(self) + self.move_queue.set_flush_time(toolhead.BUFFER_TIME_HIGH) + self.commanded_pos = [0.0, 0.0, 0.0, 0.0] + # Velocity and acceleration control + tr_config = config.getsection("trad_rack") + self.sel_max_velocity = tr_config.getfloat( + "selector_max_velocity", above=0.0 + ) + self.fil_max_velocity = tr_config.getfloat( + "filament_max_velocity", default=buffer_pull_speed, above=0.0 + ) + self.max_velocity = max(self.sel_max_velocity, self.fil_max_velocity) + self.sel_max_accel = tr_config.getfloat("selector_max_accel", above=0.0) + self.fil_max_accel = tr_config.getfloat( + "filament_max_accel", default=1500.0, above=0.0 + ) + self.max_accel = max(self.sel_max_accel, self.fil_max_accel) + self.min_cruise_ratio = config.getfloat( + "minimum_cruise_ratio", None, below=1.0, minval=0.0 + ) + if self.min_cruise_ratio is None: + self.min_cruise_ratio = 0.5 + req_accel_to_decel = config.getfloat( + "max_accel_to_decel", None, above=0.0 + ) + if req_accel_to_decel is not None: + config.deprecate("max_accel_to_decel") + self.min_cruise_ratio = 1.0 - min( + 1.0, (req_accel_to_decel / self.max_accel) + ) + self.requested_accel_to_decel = self.min_cruise_ratio * self.max_accel + self.square_corner_velocity = config.getfloat( + "square_corner_velocity", 5.0, minval=0.0 + ) + self.junction_deviation = self.max_accel_to_decel = 0.0 + self._calc_junction_deviation() + # Input stall detection + self.check_stall_time = 0.0 + self.print_stall = 0 + # Input pause tracking + self.can_pause = True + if self.mcu.is_fileoutput(): + self.can_pause = False + self.need_check_pause = -1.0 + # Print time tracking + self.print_time = 0.0 + self.special_queuing_state = "NeedPrime" + self.priming_timer = None + self.drip_completion = None + # Flush tracking + self.flush_timer = self.reactor.register_timer(self._flush_handler) + self.do_kick_flush_timer = True + self.last_flush_time = ( + self.last_sg_flush_time + ) = self.min_restart_time = 0.0 + self.need_flush_time = ( + self.step_gen_time + ) = self.clear_history_time = 0.0 + # Kinematic step generation scan window time tracking + self.kin_flush_delay = toolhead.SDS_CHECK_TIME + self.kin_flush_times = [] + # Setup iterative solver + ffi_main, ffi_lib = chelper.get_ffi() + self.trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free) + self.trapq_append = ffi_lib.trapq_append + self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves + self.step_generators = [] + # Create kinematic class + gcode = self.printer.lookup_object("gcode") + self.Coord = gcode.Coord + self.extruder = kinematics.extruder.DummyExtruder(self.printer) + try: + self.kin = TradRackKinematics(self, config, is_extruder_synced) + except config.error as e: + raise + except self.printer.lookup_object("pins").error as e: + raise + except: + msg = "Error loading kinematics 'trad_rack'" + logging.exception(msg) + raise config.error(msg) + self.printer.register_event_handler( + "klippy:shutdown", self._handle_shutdown + ) + + def set_position(self, newpos, homing_axes=()): + for _ in range(4 - len(newpos)): + newpos.append(0.0) + super(TradRackToolHead, self).set_position(newpos, homing_axes) + + def get_sel_max_velocity(self): + return self.sel_max_velocity, self.sel_max_accel + + def get_fil_max_velocity(self): + return self.fil_max_velocity, self.fil_max_accel + + +class TradRackKinematics: + def __init__(self, toolhead, config, is_extruder_synced): + self.printer = config.get_printer() + # Setup axis rails + selector_stepper_section = config.getsection(SELECTOR_STEPPER_NAME) + fil_driver_stepper_section = config.getsection(FIL_DRIVER_STEPPER_NAME) + selector_rail = LookupMultiRail(selector_stepper_section) + fil_driver_rail = LookupMultiRail(fil_driver_stepper_section) + self.rails = [selector_rail, fil_driver_rail] + for rail, axis in zip(self.rails, "xy"): + rail.setup_itersolve("cartesian_stepper_alloc", axis.encode()) + for s in self.get_steppers(): + s.set_trapq(toolhead.get_trapq()) + toolhead.register_step_generator(s.generate_steps) + self.printer.register_event_handler( + "stepper_enable:motor_off", self._motor_off + ) + + # Setup boundary checks + ( + self.sel_max_velocity, + self.sel_max_accel, + ) = toolhead.get_sel_max_velocity() + ( + self.fil_max_velocity, + self.fil_max_accel, + ) = toolhead.get_fil_max_velocity() + self.stepper_count = len(self.rails) + self.limits = [(1.0, -1.0)] * self.stepper_count + self.selector_min = selector_stepper_section.getfloat( + "position_min", note_valid=False + ) + self.selector_max = selector_stepper_section.getfloat( + "position_max", note_valid=False + ) + + self.is_extruder_synced = is_extruder_synced + + def get_steppers(self): + rails = self.rails + return [s for rail in rails for s in rail.get_steppers()] + + def calc_position(self, stepper_positions): + return [stepper_positions[rail.get_name()] for rail in self.rails] + + def set_position(self, newpos, homing_axes): + for i, rail in enumerate(self.rails): + rail.set_position(newpos) + if i in homing_axes: + self.limits[i] = rail.get_range() + + def note_z_not_homed(self): + # Helper for Safe Z Home + pass + + def _home_axis(self, homing_state, axis, rail): + # Determine movement + position_min, position_max = rail.get_range() + hi = rail.get_homing_info() + homepos = [None, None, None, None] + homepos[axis] = hi.position_endstop + forcepos = list(homepos) + if hi.positive_dir: + forcepos[axis] -= 1.5 * (hi.position_endstop - position_min) + else: + forcepos[axis] += 1.5 * (position_max - hi.position_endstop) + # Perform homing + homing_state.home_rails([rail], forcepos, homepos) + + def home(self, homing_state): + # Each axis is homed independently and in order + for axis in homing_state.get_axes(): + self._home_axis(homing_state, axis, self.rails[axis]) + + def _motor_off(self, print_time): + self.limits = [(1.0, -1.0)] * self.stepper_count + + def _check_endstops(self, move): + end_pos = move.end_pos + for i in range(self.stepper_count): + if move.axes_d[i] and ( + end_pos[i] < self.limits[i][0] or end_pos[i] > self.limits[i][1] + ): + if self.limits[i][0] > self.limits[i][1]: + raise move.move_error("Must home axis first") + raise move.move_error() + + def check_move(self, move): + limits = self.limits + xpos, ypos = move.end_pos[:2] + if ( + xpos < limits[0][0] + or xpos > limits[0][1] + or ypos < limits[1][0] + or ypos > limits[1][1] + ): + self._check_endstops(move) + + # Get filament driver speed and accel limits + if self.is_extruder_synced(): + extruder = self.printer.lookup_object("toolhead").get_extruder() + fil_max_velocity = min( + self.fil_max_velocity, extruder.max_e_velocity + ) + fil_max_accel = min(self.fil_max_accel, extruder.max_e_accel) + else: + fil_max_velocity = self.fil_max_velocity + fil_max_accel = self.fil_max_accel + + # Move with both axes - update velocity and accel + if move.axes_d[0] and move.axes_d[1]: + vel = move.move_d * min( + self.sel_max_velocity / abs(move.axes_d[0]), + fil_max_velocity / abs(move.axes_d[1]), + ) + accel = move.move_d * min( + self.sel_max_accel / abs(move.axes_d[0]), + fil_max_accel / abs(move.axes_d[1]), + ) + move.limit_speed(vel, accel) + + # Move with selector - update velocity and accel + elif move.axes_d[0]: + move.limit_speed(self.sel_max_velocity, self.sel_max_accel) + + # Move with filament driver - update velocity and accel + elif move.axes_d[1]: + move.limit_speed(fil_max_velocity, fil_max_accel) + + def get_status(self, eventtime): + axes = [a for a, (l, h) in zip("xy", self.limits) if l <= h] + return { + "homed_axes": "".join(axes), + "selector_min": self.selector_min, + "selector_max": self.selector_max, + } + + def get_selector_rail(self): + return self.rails[0] + + def get_fil_driver_rail(self): + return self.rails[1] + + +class TradRackHoming(Homing, object): + def __init__(self, printer, toolhead): + super(TradRackHoming, self).__init__(printer) + self.toolhead = toolhead + + def home_rails(self, rails, forcepos, movepos): + # Notify of upcoming homing operation + self.printer.send_event("homing:home_rails_begin", self, rails) + # Alter kinematics class to think printer is at forcepos + homing_axes = [axis for axis in range(3) if forcepos[axis] is not None] + startpos = self._fill_coord(forcepos) + homepos = self._fill_coord(movepos) + self.toolhead.set_position(startpos, homing_axes=homing_axes) + # Perform first home + endstops = [es for rail in rails for es in rail.get_endstops()] + hi = rails[0].get_homing_info() + hmove = HomingMove(self.printer, endstops, self.toolhead) + hmove.homing_move(homepos, hi.speed) + # Perform second home + if hi.retract_dist: + # Retract + startpos = self._fill_coord(forcepos) + homepos = self._fill_coord(movepos) + axes_d = [hp - sp for hp, sp in zip(homepos, startpos)] + move_d = math.sqrt(sum([d * d for d in axes_d[:3]])) + retract_r = min(1.0, hi.retract_dist / move_d) + retractpos = [ + hp - ad * retract_r for hp, ad in zip(homepos, axes_d) + ] + self.toolhead.move(retractpos, hi.retract_speed) + # Home again + startpos = [ + rp - ad * retract_r for rp, ad in zip(retractpos, axes_d) + ] + self.toolhead.set_position(startpos) + hmove = HomingMove(self.printer, endstops, self.toolhead) + hmove.homing_move(homepos, hi.second_homing_speed) + if hmove.check_no_movement() is not None: + raise self.printer.command_error( + "Endstop %s still triggered after retract" + % (hmove.check_no_movement(),) + ) + # Signal home operation complete + self.toolhead.flush_step_generation() + self.trigger_mcu_pos = { + sp.stepper_name: sp.trig_pos for sp in hmove.stepper_positions + } + self.adjust_pos = {} + self.printer.send_event("homing:home_rails_end", self, rails) + if any(self.adjust_pos.values()): + # Apply any homing offsets + kin = self.toolhead.get_kinematics() + homepos = self.toolhead.get_position() + kin_spos = { + s.get_name(): s.get_commanded_position() + + self.adjust_pos.get(s.get_name(), 0.0) + for s in kin.get_steppers() + } + newpos = kin.calc_position(kin_spos) + for axis in homing_axes: + homepos[axis] = newpos[axis] + self.toolhead.set_position(homepos) + + +class TradRackServo: + def __init__(self, servo, toolhead): + self.servo = servo + self.toolhead = toolhead + + def set_servo(self, width=None, angle=None, print_time=None): + if print_time is None: + print_time = self.toolhead.get_last_move_time() + if width is not None: + self.servo._set_pwm( + print_time, self.servo._get_pwm_from_pulse_width(width) + ) + else: + self.servo._set_pwm( + print_time, self.servo._get_pwm_from_angle(angle) + ) + + def get_max_angle(self): + return self.servo.max_angle + + +class TradRackLanePositionManager: + def __init__(self, lane_count, config): + self.lane_count = lane_count + self.lane_spacing = config.getfloat("lane_spacing", above=0.0) + self.lane_spacing_mods = [] + self.lane_spacing_mod_internal = 0.0 + self.lane_offsets = [] + for i in range(lane_count): + self.lane_spacing_mods.append( + config.getfloat("lane_spacing_mod_" + str(i), default=0.0) + ) + self.lane_spacing_mod_internal += self.lane_spacing_mods[i] + self.lane_offsets.append( + config.getfloat("lane_offset_" + str(i), default=0.0) + ) + self.lane_spacing_mod_internal -= self.lane_spacing_mods[0] + + def get_lane_positions(self): + lane_positions = [] + curr_pos = 0.0 + for i in range(self.lane_count): + curr_pos += self.lane_spacing_mods[i] + lane_positions.append(curr_pos + self.lane_offsets[i]) + curr_pos += self.lane_spacing + return lane_positions + + def process_selector_calibration( + self, endstop_to_lane0, endstop_to_last_lane, lane_spacing_ndigits=6 + ): + # account for lane offsets + endstop_to_lane0 -= self.lane_offsets[0] + endstop_to_last_lane -= self.lane_offsets[self.lane_count - 1] + + # calculate endstop position and lane settings + pos_endstop = self.lane_spacing_mods[0] - endstop_to_lane0 + lane_span = endstop_to_last_lane - endstop_to_lane0 + lane_spacing = (lane_span - self.lane_spacing_mod_internal) / ( + self.lane_count - 1 + ) + self.lane_spacing = round(lane_spacing, lane_spacing_ndigits) + lane_positions = self.get_lane_positions() + + return pos_endstop, self.lane_spacing, lane_positions + + +EXTRUDER_TO_FIL_DRIVER = 0 +FIL_DRIVER_TO_EXTRUDER = 1 + + +class TradRackExtruderSyncManager: + def __init__(self, printer, tr_toolhead, fil_driver_rail): + self.printer = printer + self.toolhead = None + self.tr_toolhead = tr_toolhead + self.fil_driver_rail = fil_driver_rail + + self.printer.register_event_handler( + "klippy:connect", self.handle_connect + ) + self.sync_state = None + self._prev_sks = None + self._prev_trapq = None + self._prev_rotation_dists = None + + def handle_connect(self): + self.toolhead = self.printer.lookup_object("toolhead") + + def _get_extruder_mcu_steppers(self): + extruder = self.toolhead.get_extruder() + if hasattr(extruder, "get_extruder_steppers"): + steppers = [] + for extruder_stepper in extruder.get_extruder_steppers(): + steppers.append(extruder_stepper.stepper) + return steppers + else: + return [extruder.extruder_stepper.stepper] + + def reset_fil_driver(self): + self.tr_toolhead.get_last_move_time() + pos = self.tr_toolhead.get_position() + pos[1] = 0.0 + self.tr_toolhead.set_position(pos, homing_axes=(1,)) + if self.sync_state == EXTRUDER_TO_FIL_DRIVER: + steppers = self._get_extruder_mcu_steppers() + for stepper in steppers: + stepper.set_position((0.0, 0.0, 0.0)) + + def _sync(self, sync_type): + self.unsync() + self.toolhead.flush_step_generation() + self.tr_toolhead.flush_step_generation() + + ffi_main, ffi_lib = chelper.get_ffi() + if sync_type == EXTRUDER_TO_FIL_DRIVER: + steppers = self._get_extruder_mcu_steppers() + self._prev_trapq = steppers[0].get_trapq() + external_trapq = self.tr_toolhead.get_trapq() + stepper_alloc = ffi_lib.cartesian_stepper_alloc(b"y") + prev_toolhead = self.toolhead + external_toolhead = self.tr_toolhead + self.reset_fil_driver() + new_pos = 0.0 + elif sync_type == FIL_DRIVER_TO_EXTRUDER: + steppers = self.fil_driver_rail.get_steppers() + self._prev_trapq = self.tr_toolhead.get_trapq() + extruder = self.toolhead.get_extruder() + external_trapq = extruder.get_trapq() + stepper_alloc = ffi_lib.extruder_stepper_alloc() + prev_toolhead = self.tr_toolhead + external_toolhead = self.toolhead + new_pos = extruder.last_position + else: + raise Exception("Invalid sync_type: %d" % sync_type) + + self._prev_sks = [] + self._prev_rotation_dists = [] + for stepper in steppers: + stepper_kinematics = ffi_main.gc(stepper_alloc, ffi_lib.free) + self._prev_rotation_dists.append(stepper.get_rotation_distance()[0]) + self._prev_sks.append( + stepper.set_stepper_kinematics(stepper_kinematics) + ) + stepper.set_trapq(external_trapq) + stepper.set_position((new_pos, 0.0, 0.0)) + prev_toolhead.step_generators.remove(stepper.generate_steps) + external_toolhead.register_step_generator(stepper.generate_steps) + self.sync_state = sync_type + + def sync_extruder_to_fil_driver(self): + self._sync(EXTRUDER_TO_FIL_DRIVER) + + def sync_fil_driver_to_extruder(self): + self._sync(FIL_DRIVER_TO_EXTRUDER) + self.printer.send_event("trad_rack:synced_to_extruder") + + def unsync(self): + if self.sync_state is None: + return + + self.toolhead.flush_step_generation() + self.tr_toolhead.flush_step_generation() + + if self.sync_state == EXTRUDER_TO_FIL_DRIVER: + steppers = self._get_extruder_mcu_steppers() + prev_toolhead = self.toolhead + external_toolhead = self.tr_toolhead + elif self.sync_state == FIL_DRIVER_TO_EXTRUDER: + self.printer.send_event("trad_rack:unsyncing_from_extruder") + steppers = self.fil_driver_rail.get_steppers() + prev_toolhead = self.tr_toolhead + external_toolhead = self.toolhead + else: + raise Exception("Invalid sync_state: %d" % self.sync_state) + + for i in range(len(steppers)): + stepper = steppers[i] + external_toolhead.step_generators.remove(stepper.generate_steps) + prev_toolhead.register_step_generator(stepper.generate_steps) + stepper.set_trapq(self._prev_trapq) + stepper.set_stepper_kinematics(self._prev_sks[i]) + stepper.set_rotation_distance(self._prev_rotation_dists[i]) + self.sync_state = None + + def is_extruder_synced(self): + return self.sync_state == EXTRUDER_TO_FIL_DRIVER + + def is_fil_driver_synced(self): + return self.sync_state == FIL_DRIVER_TO_EXTRUDER + + def set_fil_driver_multiplier(self, multiplier): + if not self.is_fil_driver_synced(): + raise Exception( + "Cannot set stepper multiplier when filament driver is not" + " synced to extruder" + ) + steppers = self.fil_driver_rail.get_steppers() + for i in range(len(steppers)): + steppers[i].set_rotation_distance( + self._prev_rotation_dists[i] / multiplier + ) + + +class RunIfNoActivity: + def __init__(self, toolhead, reactor, callback, delay): + self.toolhead = toolhead + self.initial_print_time = self.toolhead.get_last_move_time() + self.callback = callback + reactor.register_callback( + self._run_if_no_activity, reactor.monotonic() + delay + ) + + def _run_if_no_activity(self, eventtime): + print_time, _, lookahead_empty = self.toolhead.check_busy(eventtime) + if lookahead_empty and print_time == self.initial_print_time: + self.callback() + + +class MovingAverageFilter: + def __init__(self, max_entries): + self.max_entries = max_entries + self.queue = deque() + self.total = 0.0 + + def update(self, value): + if len(self.queue) >= self.max_entries: + self.total -= self.queue.popleft() + self.total += value + self.queue.append(value) + return self.total / len(self.queue) + + def reset(self): + self.queue.clear() + self.total = 0.0 + + def get_entry_count(self): + return len(self.queue) + + +class TradRackLoadError(CommandError): + pass + + +class SelectorNotHomedError(CommandError): + pass + + +class TradRackRunoutSensor: + def __init__(self, config, runout_callback, pin): + self.printer = config.get_printer() + self.reactor = self.printer.get_reactor() + self.runout_callback = runout_callback + + # disable config checks for duplicate pins + pin_desc = pin + if pin_desc.startswith("^") or pin_desc.startswith("~"): + pin_desc = pin_desc[1:].strip() + if pin_desc.startswith("!"): + pin_desc = pin_desc[1:].strip() + ppins = self.printer.lookup_object("pins") + ppins.allow_multi_use_pin(pin_desc) + + # register button + buttons = config.get_printer().load_object(config, "buttons") + buttons.register_buttons([pin], self.sensor_callback) + + self.active = False + + def sensor_callback(self, eventtime, state): + if self.active and not state: + idle_timeout = self.printer.lookup_object("idle_timeout") + if idle_timeout.get_status(eventtime)["state"] == "Printing": + self.active = False + self.reactor.register_callback(self.runout_callback) + + def set_active(self, active): + self.active = active + + +def load_config(config): + return TradRack(config) diff --git a/klippy/extras/virtual_sdcard.py b/klippy/extras/virtual_sdcard.py index f075219cd..f9617e8b2 100644 --- a/klippy/extras/virtual_sdcard.py +++ b/klippy/extras/virtual_sdcard.py @@ -1,13 +1,20 @@ # Virtual sdcard support (print files directly from a host g-code file) # -# Copyright (C) 2018 Kevin O'Connor +# Copyright (C) 2018-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import os, logging, io +import os, sys, logging, io 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") @@ -305,7 +312,10 @@ def work_handler(self, eventtime): # Dispatch command self.cmd_from_sd = True line = lines.pop() - next_file_position = self.file_position + len(line.encode()) + 1 + if sys.version_info.major >= 3: + next_file_position = self.file_position + len(line.encode()) + 1 + else: + next_file_position = self.file_position + len(line) + 1 self.next_file_position = next_file_position try: self.gcode.run_script(line) diff --git a/klippy/extras/z_tilt.py b/klippy/extras/z_tilt.py index 948551f05..faf43f615 100644 --- a/klippy/extras/z_tilt.py +++ b/klippy/extras/z_tilt.py @@ -104,6 +104,9 @@ def __init__(self, config, error_msg_extra=""): self.default_retry_tolerance = config.getfloat( "retry_tolerance", 0.0, above=0.0 ) + self.default_increasing_threshold = config.getfloat( + "increasing_threshold", 0.0000001, above=0.0 + ) self.value_label = "Probed points range" self.error_msg_extra = error_msg_extra @@ -117,12 +120,15 @@ def start(self, gcmd): minval=0.0, maxval=1.0, ) + self.increasing_threshold = gcmd.get_float( + "INCREASING_THRESHOLD", self.default_increasing_threshold, above=0.0 + ) self.current_retry = 0 self.previous = None self.increasing = 0 def check_increase(self, error): - if self.previous and error > self.previous + 0.0000001: + if self.previous and error > self.previous + self.increasing_threshold: self.increasing += 1 elif self.increasing > 0: self.increasing -= 1 diff --git a/klippy/extras/z_tilt_ng.py b/klippy/extras/z_tilt_ng.py index f632aa856..0ab8aecdd 100644 --- a/klippy/extras/z_tilt_ng.py +++ b/klippy/extras/z_tilt_ng.py @@ -126,6 +126,9 @@ def __init__(self, config, error_msg_extra=""): self.default_retry_tolerance = config.getfloat( "retry_tolerance", 0.0, above=0.0 ) + self.default_increasing_threshold = config.getfloat( + "increasing_threshold", 0.0000001, above=0.0 + ) self.value_label = "Probed points range" self.error_msg_extra = error_msg_extra @@ -139,12 +142,15 @@ def start(self, gcmd): minval=0.0, maxval=1.0, ) + self.increasing_threshold = gcmd.get_float( + "INCREASING_THRESHOLD", self.default_increasing_threshold, above=0.0 + ) self.current_retry = 0 self.previous = None self.increasing = 0 def check_increase(self, error): - if self.previous and error > self.previous + 0.0000001: + if self.previous and error > self.previous + self.increasing_threshold: self.increasing += 1 elif self.increasing > 0: self.increasing -= 1 diff --git a/klippy/gcode.py b/klippy/gcode.py index e326cec94..0422755f3 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -154,6 +154,7 @@ def __init__(self, printer): self.mux_commands = {} self.gcode_help = {} self.status_commands = {} + self._interrupt_counter = 0 # Register commands needed before config file is loaded handlers = [ "M110", @@ -164,12 +165,19 @@ def __init__(self, printer): "ECHO", "STATUS", "HELP", + "HEATER_INTERRUPT", ] for cmd in handlers: func = getattr(self, "cmd_" + cmd) desc = getattr(self, "cmd_" + cmd + "_help", None) self.register_command(cmd, func, True, desc) + def get_interrupt_counter(self): + return self._interrupt_counter + + def increment_interrupt_counter(self): + self._interrupt_counter += 1 + def is_traditional_gcode(self, cmd): # A "traditional" g-code command is a letter and followed by a number try: @@ -306,8 +314,11 @@ def run_script_from_command(self, script): self._process_commands(script.split("\n"), need_ack=False) def run_script(self, script): - with self.mutex: + if "INTERRUPT" in script: self._process_commands(script.split("\n"), need_ack=False) + else: + with self.mutex: + self._process_commands(script.split("\n"), need_ack=False) def get_mutex(self): return self.mutex @@ -475,6 +486,9 @@ def cmd_HELP(self, gcmd): cmdhelp.append("%-10s: %s" % (cmd, self.gcode_help[cmd])) gcmd.respond_info("\n".join(cmdhelp), log=False) + def cmd_HEATER_INTERRUPT(self, gcmd): + self.increment_interrupt_counter() + # Support reading gcode from a pseudo-tty interface class GCodeIO: 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/klippy.py b/klippy/klippy.py index 1eb8ba47a..9bba5066e 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -7,6 +7,7 @@ import sys, os, gc, optparse, logging, time, collections, importlib, importlib.util import util, reactor, queuelogger, msgproto import gcode, configfile, pins, mcu, toolhead, webhooks +from extras.danger_options import get_danger_options message_ready = "Printer is ready" @@ -68,7 +69,6 @@ def __init__(self, main_reactor, bglogger, start_args): self.run_result = None self.event_handlers = {} self.objects = collections.OrderedDict() - self.danger_options = None # Init printer components that must be setup prior to config for m in [gcode, webhooks]: m.add_early_printer_objects(self) @@ -158,7 +158,7 @@ def load_object(self, config, section, default=configfile.sentinel): if ( found_in_extras and found_in_plugins - and not self.danger_options.allow_plugin_override + and not get_danger_options().allow_plugin_override ): raise self.config_error( "Module '%s' found in both extras and plugins!" % (section,) @@ -187,10 +187,10 @@ def load_object(self, config, section, default=configfile.sentinel): def _read_config(self): self.objects["configfile"] = pconfig = configfile.PrinterConfig(self) config = pconfig.read_main_config() - self.danger_options = self.load_object(config, "danger_options") + self.load_object(config, "danger_options", None) if ( self.bglogger is not None - and self.danger_options.log_config_file_at_startup + and get_danger_options().log_config_file_at_startup ): pconfig.log_config(config) # Create printer components @@ -204,7 +204,7 @@ def _read_config(self): for m in [toolhead]: m.add_printer_objects(config) # Validate that there are no undefined parameters in the config file - error_on_unused = self.danger_options.error_on_unused_config_options + error_on_unused = get_danger_options().error_on_unused_config_options pconfig.check_unused_options(config, error_on_unused) def _build_protocol_error_message(self, e): @@ -376,6 +376,20 @@ def request_exit(self, result): self.run_result = result self.reactor.end() + def wait_while(self, condition_cb): + """ + receives a callback + waits until callback returns False + (or is interrupted, or printer shuts down) + """ + gcode = self.lookup_object("gcode") + counter = gcode.get_interrupt_counter() + eventtime = self.reactor.monotonic() + while condition_cb(eventtime): + if self.is_shutdown() or counter != gcode.get_interrupt_counter(): + return + eventtime = self.reactor.pause(eventtime + 1.0) + ###################################################################### # Startup diff --git a/klippy/mcu.py b/klippy/mcu.py index 3c96b24c4..f0464d81b 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -8,6 +8,7 @@ import os import zlib import serialhdl, msgproto, pins, chelper, clocksync +from extras.danger_options import get_danger_options class error(Exception): @@ -296,28 +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)] - self.danger_options = self._mcu.get_printer().lookup_object( - "danger_options" - ) - 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} @@ -341,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( @@ -354,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", @@ -375,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 = self.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, @@ -399,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 @@ -707,7 +734,7 @@ class MCU: def __init__(self, config, clocksync): self._printer = printer = config.get_printer() - self.danger_options = printer.lookup_object("danger_options") + self.gcode = printer.lookup_object("gcode") self._clocksync = clocksync self._reactor = printer.get_reactor() self._name = config.get_name() @@ -778,6 +805,7 @@ def __init__(self, config, clocksync): printer.register_event_handler("klippy:connect", self._connect) printer.register_event_handler("klippy:shutdown", self._shutdown) printer.register_event_handler("klippy:disconnect", self._disconnect) + printer.register_event_handler("klippy:ready", self._ready) # Serial callbacks def _handle_mcu_stats(self, params): @@ -798,7 +826,7 @@ def _handle_shutdown(self, params): if clock is not None: self._shutdown_clock = self.clock32_to_clock64(clock) self._shutdown_msg = msg = params["static_string_id"] - if self.danger_options.log_shutdown_info: + if get_danger_options().log_shutdown_info: logging.info( "MCU '%s' %s: %s\n%s\n%s", self._name, @@ -814,8 +842,8 @@ def _handle_shutdown(self, params): append_msgs = [] if ( msg.startswith("ADC out of range") - and not self.danger_options.adc_ignore_limits - ): + or msg.startswith("Thermocouple reader fault") + ) and not get_danger_options.adc_ignore_limits: pheaters = self._printer.lookup_object("heaters") heaters = [ pheaters.lookup_heater(n) for n in pheaters.available_heaters @@ -830,6 +858,25 @@ def _handle_shutdown(self, params): "max_temp": heater.max_temp, } ) + sensor_names = [ + sensor + for sensor in self._printer.objects + if ( + sensor.startswith("temperature_sensor") + or sensor.startswith("temperature_fan") + ) + ] + for sensor_name in sensor_names: + sensor = self._printer.lookup_object(sensor_name) + if sensor.is_adc_faulty(): + append_msgs.append( + { + sensor_name.split(" ")[0]: sensor.name, + "last_temp": "{:.2f}".format(sensor.last_temp), + "min_temp": sensor.min_temp, + "max_temp": sensor.max_temp, + } + ) self._printer.invoke_async_shutdown( prefix + msg + error_help(msg=msg, append_msgs=append_msgs) @@ -884,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): @@ -1024,7 +1070,8 @@ def _mcu_identify(self): self._clocksync.connect(self._serial) except serialhdl.error as e: raise error(str(e)) - logging.info(self._log_info()) + if get_danger_options().log_startup_info: + logging.info(self._log_info()) ppins = self._printer.lookup_object("pins") pin_resolver = ppins.get_pin_resolver(self._name) for cname, value in self.get_constants().items(): @@ -1054,6 +1101,25 @@ def _mcu_identify(self): self.register_response(self._handle_shutdown, "is_shutdown") self.register_response(self._handle_mcu_stats, "stats") + def _ready(self): + if self.is_fileoutput(): + return + # Check that reported mcu frequency is in range + mcu_freq = self._mcu_freq + systime = self._reactor.monotonic() + get_clock = self._clocksync.get_clock + calc_freq = get_clock(systime + 1) - get_clock(systime) + mcu_freq_mhz = int(mcu_freq / 1000000.0 + 0.5) + calc_freq_mhz = int(calc_freq / 1000000.0 + 0.5) + if mcu_freq_mhz != calc_freq_mhz: + pconfig = self._printer.lookup_object("configfile") + msg = "MCU '%s' configured for %dMhz but running at %dMhz!" % ( + self._name, + mcu_freq_mhz, + calc_freq_mhz, + ) + pconfig.runtime_warning(msg) + # Config creation helpers def setup_pin(self, pin_type, pin_params): pcs = { @@ -1309,6 +1375,7 @@ def stats(self, eventtime): communication failure between micro-controller and host.""", ( "ADC out of range", + "Thermocouple reader fault", ): """ This generally occurs when a heater temperature exceeds its configured min_temp or max_temp.""", diff --git a/klippy/serialhdl.py b/klippy/serialhdl.py index d16624487..287c49388 100644 --- a/klippy/serialhdl.py +++ b/klippy/serialhdl.py @@ -7,6 +7,7 @@ import serial import msgproto, chelper, util +from extras.danger_options import get_danger_options class error(Exception): @@ -367,7 +368,8 @@ def handle_output(self, params): ) def handle_default(self, params): - logging.warn("%sgot %s", self.warn_prefix, params) + if get_danger_options().log_serial_reader_warnings: + logging.warn("%sgot %s", self.warn_prefix, params) # Class to send a query command and return the received response diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 98e043e5a..080724c3a 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -6,6 +6,7 @@ import math, logging, importlib import chelper import kinematics.extruder +from extras.danger_options import get_danger_options # Common suffixes: _d is distance (in mm), _v is velocity (in # mm/second), _v2 is velocity squared (mm^2/s^2), _t is time (in @@ -248,7 +249,6 @@ class DripModeEndSignal(Exception): class ToolHead: def __init__(self, config): self.printer = config.get_printer() - self.danger_options = self.printer.lookup_object("danger_options") self.reactor = self.printer.get_reactor() self.all_mcus = [ m for n, m in self.printer.lookup_objects(module="mcu") @@ -260,14 +260,24 @@ def __init__(self, config): # Velocity and acceleration control self.max_velocity = config.getfloat("max_velocity", above=0.0) self.max_accel = config.getfloat("max_accel", above=0.0) - self.requested_accel_to_decel = config.getfloat( - "max_accel_to_decel", self.max_accel * 0.5, above=0.0 + min_cruise_ratio = 0.5 + if config.getfloat("minimum_cruise_ratio", None) is None: + req_accel_to_decel = config.getfloat( + "max_accel_to_decel", None, above=0.0 + ) + if req_accel_to_decel is not None: + config.deprecate("max_accel_to_decel") + min_cruise_ratio = 1.0 - min( + 1.0, (req_accel_to_decel / self.max_accel) + ) + self.min_cruise_ratio = config.getfloat( + "minimum_cruise_ratio", min_cruise_ratio, below=1.0, minval=0.0 ) - self.max_accel_to_decel = self.requested_accel_to_decel self.square_corner_velocity = config.getfloat( "square_corner_velocity", 5.0, minval=0.0 ) self.equilateral_corner_v2 = 0.0 + self.max_accel_to_decel = 0.0 self._calc_junction_deviation() # Input stall detection self.check_stall_time = 0.0 @@ -535,7 +545,7 @@ def _flush_handler(self, eventtime): while 1: end_flush = ( self.need_flush_time - + self.danger_options.bgflush_extra_time + + get_danger_options().bgflush_extra_time ) if self.last_flush_time >= end_flush: self.do_kick_flush_timer = True @@ -690,7 +700,7 @@ def get_status(self, eventtime): "position": self.Coord(*self.commanded_pos), "max_velocity": self.max_velocity, "max_accel": self.max_accel, - "max_accel_to_decel": self.requested_accel_to_decel, + "minimum_cruise_ratio": self.min_cruise_ratio, "square_corner_velocity": self.square_corner_velocity, } ) @@ -711,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: @@ -740,9 +749,7 @@ def get_max_velocity(self): def _calc_junction_deviation(self): scv2 = self.square_corner_velocity**2 self.equilateral_corner_v2 = scv2 * (math.sqrt(2.0) - 1.0) - self.max_accel_to_decel = min( - self.requested_accel_to_decel, self.max_accel - ) + self.max_accel_to_decel = self.max_accel * (1.0 - self.min_cruise_ratio) def cmd_G4(self, gcmd): # Dwell @@ -761,27 +768,39 @@ def cmd_SET_VELOCITY_LIMIT(self, gcmd): square_corner_velocity = gcmd.get_float( "SQUARE_CORNER_VELOCITY", None, minval=0.0 ) - requested_accel_to_decel = gcmd.get_float( - "ACCEL_TO_DECEL", None, above=0.0 + min_cruise_ratio = gcmd.get_float( + "MINIMUM_CRUISE_RATIO", None, minval=0.0, below=1.0 ) + if min_cruise_ratio is None: + req_accel_to_decel = gcmd.get_float( + "ACCEL_TO_DECEL", None, above=0.0 + ) + if req_accel_to_decel is not None and max_accel is not None: + min_cruise_ratio = 1.0 - min( + 1.0, req_accel_to_decel / max_accel + ) + elif req_accel_to_decel is not None and max_accel is None: + min_cruise_ratio = 1.0 - min( + 1.0, (req_accel_to_decel / self.max_accel) + ) if max_velocity is not None: self.max_velocity = max_velocity if max_accel is not None: self.max_accel = max_accel if square_corner_velocity is not None: self.square_corner_velocity = square_corner_velocity - if requested_accel_to_decel is not None: - self.requested_accel_to_decel = requested_accel_to_decel + if min_cruise_ratio is not None: + self.min_cruise_ratio = min_cruise_ratio self._calc_junction_deviation() msg = ( "max_velocity: %.6f\n" "max_accel: %.6f\n" - "max_accel_to_decel: %.6f\n" + "minimum_cruise_ratio: %.6f\n" "square_corner_velocity: %.6f" % ( self.max_velocity, self.max_accel, - self.requested_accel_to_decel, + self.min_cruise_ratio, self.square_corner_velocity, ) ) @@ -790,7 +809,7 @@ def cmd_SET_VELOCITY_LIMIT(self, gcmd): max_velocity is None and max_accel is None and square_corner_velocity is None - and requested_accel_to_decel is None + and min_cruise_ratio is None ): gcmd.respond_info(msg, log=False) diff --git a/klippy/webhooks.py b/klippy/webhooks.py index ed5cba36d..e28bcaaa2 100644 --- a/klippy/webhooks.py +++ b/klippy/webhooks.py @@ -5,6 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license import logging, socket, os, sys, errno, json, collections import gcode +from extras.danger_options import get_danger_options REQUEST_LOG_SIZE = 20 @@ -407,10 +408,11 @@ def _handle_rpc_registration(self, web_request): template = web_request.get_dict("response_template") method = web_request.get_str("remote_method") new_conn = web_request.get_client_connection() - logging.info( - "webhooks: registering remote method '%s' " - "for connection id: %d" % (method, id(new_conn)) - ) + if get_danger_options().log_webhook_method_register_messages: + logging.info( + "webhooks: registering remote method '%s' " + "for connection id: %d" % (method, id(new_conn)) + ) self._remote_methods.setdefault(method, {})[new_conn] = template def get_connection(self): diff --git a/scripts/buildcommands.py b/scripts/buildcommands.py index b8b64f26d..d43878d9c 100644 --- a/scripts/buildcommands.py +++ b/scripts/buildcommands.py @@ -1,7 +1,7 @@ #!/usr/bin/env python2 # Script to handle build time requests embedded in C code. # -# Copyright (C) 2016-2021 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import sys, os, subprocess, optparse, logging, shlex, socket, time, traceback @@ -33,6 +33,7 @@ def error(msg): # C call list generation ###################################################################### + # Create dynamic C functions that call a list of other C functions class HandleCallList: def __init__(self): @@ -76,6 +77,7 @@ def generate_code(self, options): STATIC_STRING_MIN = 2 + # Generate a dynamic string to integer mapping class HandleEnumerations: def __init__(self): @@ -139,6 +141,7 @@ def generate_code(self, options): # Constants ###################################################################### + # Allow adding build time constants to the data dictionary class HandleConstants: def __init__(self): @@ -232,6 +235,7 @@ def generate_code(self, options): # ARM IRQ vector table generation ###################################################################### + # Create ARM IRQ vector table from interrupt handler declarations class Handle_arm_irq: def __init__(self): @@ -282,6 +286,7 @@ def generate_code(self, options): # Wire protocol commands and responses ###################################################################### + # Dynamic command and response registration class HandleCommandGeneration: def __init__(self): @@ -529,6 +534,7 @@ def generate_code(self, options): # Version generation ###################################################################### + # Run program and return the specified output def check_output(prog): logging.debug("Running %s" % (repr(prog),)) @@ -615,6 +621,8 @@ def __init__(self): def update_data_dictionary(self, data): data["version"] = self.version data["build_versions"] = self.toolstr + data["app"] = "Klipper" + data["license"] = "GNU GPLv3" def generate_code(self, options): cleanbuild, self.toolstr = tool_versions(options.tools) @@ -633,6 +641,7 @@ def generate_code(self, options): # Identify data dictionary generation ###################################################################### + # Automatically generate the wire protocol data dictionary class HandleIdentify: def __init__(self): diff --git a/scripts/install-ubuntu-22.04.sh b/scripts/install-ubuntu-22.04.sh old mode 100644 new mode 100755 diff --git a/scripts/klippy-requirements.txt b/scripts/klippy-requirements.txt index 55cff5069..f9befeb61 100644 --- a/scripts/klippy-requirements.txt +++ b/scripts/klippy-requirements.txt @@ -4,7 +4,8 @@ # pip install -r klippy-requirements.txt cffi==1.15.1 pyserial==3.4 -greenlet==2.0.2 +greenlet==2.0.2 ; python_version < '3.12' +greenlet==3.0.3 ; python_version >= '3.12' Jinja2==2.11.3 python-can==3.3.4 markupsafe==1.1.1 # https://github.com/Klipper3d/klipper/pull/5286 diff --git a/scripts/motan/data_logger.py b/scripts/motan/data_logger.py index 80eb1bb10..6eb54a39a 100755 --- a/scripts/motan/data_logger.py +++ b/scripts/motan/data_logger.py @@ -178,20 +178,17 @@ def handle_subscribe(self, msg, raw_msg): {"name": stepper}, ) # 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(): - if cfgname == "adxl345" or cfgname.startswith("adxl345 "): - aname = cfgname.split()[-1] - self.send_subscribe( - "adxl345:" + aname, - "adxl345/dump_adxl345", - {"sensor": aname}, - ) - if cfgname.startswith("angle "): - aname = cfgname.split()[1] - self.send_subscribe( - "angle:" + aname, "angle/dump_angle", {"sensor": aname} - ) + 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) + self.send_subscribe(lname, qcmd, {"sensor": aname}) def handle_dump(self, msg, raw_msg): msg_id = msg["id"] 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/hc32f460/gpio.c b/src/hc32f460/gpio.c index e3b98df44..a11449270 100644 --- a/src/hc32f460/gpio.c +++ b/src/hc32f460/gpio.c @@ -18,7 +18,8 @@ DECL_ENUMERATION_RANGE("pin", "PA0", GPIO('A', 0), 16); DECL_ENUMERATION_RANGE("pin", "PB0", GPIO('B', 0), 16); DECL_ENUMERATION_RANGE("pin", "PC0", GPIO('C', 0), 16); -DECL_ENUMERATION_RANGE("pin", "PD2", GPIO('D', 2), 1); +DECL_ENUMERATION_RANGE("pin", "PD0", GPIO('D', 0), 16); +DECL_ENUMERATION_RANGE("pin", "PE0", GPIO('E', 0), 16); DECL_ENUMERATION_RANGE("pin", "PH2", PortH * 16 + 2, 1); // H: special case diff --git a/src/hc32f460/hard_pwm.c b/src/hc32f460/hard_pwm.c index 9d3387e60..8cf57acf7 100644 --- a/src/hc32f460/hard_pwm.c +++ b/src/hc32f460/hard_pwm.c @@ -75,6 +75,22 @@ static const struct gpio_pwm_info pwm_mapping[] = { {GPIO('C',14), 4, TimeraCh5}, {GPIO('C',15), 4, TimeraCh6}, {GPIO('D', 2), 2, TimeraCh4}, + {GPIO('D',12), 4, TimeraCh1}, + {GPIO('D',13), 4, TimeraCh2}, + {GPIO('D',14), 4, TimeraCh3}, + {GPIO('D',15), 4, TimeraCh4}, + {GPIO('E', 2), 3, TimeraCh5}, + {GPIO('E', 3), 3, TimeraCh6}, + {GPIO('E', 4), 3, TimeraCh7}, + {GPIO('E', 5), 3, TimeraCh8}, + {GPIO('E', 8), 1, TimeraCh5}, + {GPIO('E', 9), 1, TimeraCh1}, + {GPIO('E',10), 1, TimeraCh6}, + {GPIO('E',11), 1, TimeraCh2}, + {GPIO('E',12), 1, TimeraCh7}, + {GPIO('E',13), 1, TimeraCh3}, + {GPIO('E',14), 1, TimeraCh4}, + {GPIO('E',15), 1, TimeraCh8}, }; 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/i2c.c b/src/linux/i2c.c index 2994403f2..b328dc56e 100644 --- a/src/linux/i2c.c +++ b/src/linux/i2c.c @@ -14,7 +14,7 @@ #include "internal.h" // report_errno #include "sched.h" // sched_shutdown -DECL_ENUMERATION_RANGE("i2c_bus", "i2c.0", 0, 7); +DECL_ENUMERATION_RANGE("i2c_bus", "i2c.0", 0, 15); struct i2c_s { uint32_t bus; 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_i2c.c b/src/stm32/stm32f0_i2c.c index e9cadccbd..1441079f7 100644 --- a/src/stm32/stm32f0_i2c.c +++ b/src/stm32/stm32f0_i2c.c @@ -44,6 +44,8 @@ struct i2c_info { #ifdef I2C3 DECL_ENUMERATION("i2c_bus", "i2c3_PB3_PB4", 5); DECL_CONSTANT_STR("BUS_PINS_i2c3_PB3_PB4", "PB3,PB4"); + DECL_ENUMERATION("i2c_bus", "i2c3_PC0_PC1", 6); + DECL_CONSTANT_STR("BUS_PINS_i2c3_PC0_PC1", "PC0,PC1"); #endif #elif CONFIG_MACH_STM32L4 DECL_ENUMERATION("i2c_bus", "i2c1_PB6_PB7", 0); @@ -82,6 +84,8 @@ struct i2c_info { DECL_CONSTANT_STR("BUS_PINS_i2c1_PB8_PB9", "PB8,PB9"); DECL_ENUMERATION("i2c_bus", "i2c2_PB10_PB11", 2); DECL_CONSTANT_STR("BUS_PINS_i2c2_PB10_PB11", "PB10,PB11"); + DECL_ENUMERATION("i2c_bus", "i2c3_PA8_PC9", 3); + DECL_CONSTANT_STR("BUS_PINS_i2c3_PA8_PC9", "PA8,PC9"); #endif static const struct i2c_info i2c_bus[] = { @@ -99,6 +103,7 @@ static const struct i2c_info i2c_bus[] = { { I2C2, GPIO('B', 13), GPIO('B', 14), GPIO_FUNCTION(6) }, #ifdef I2C3 { I2C3, GPIO('B', 3), GPIO('B', 4), GPIO_FUNCTION(6) }, + { I2C3, GPIO('C', 0), GPIO('C', 1), GPIO_FUNCTION(6) }, #endif #elif CONFIG_MACH_STM32L4 { I2C1, GPIO('B', 6), GPIO('B', 7), GPIO_FUNCTION(4) }, @@ -120,6 +125,7 @@ static const struct i2c_info i2c_bus[] = { { I2C1, GPIO('B', 6), GPIO('B', 7), GPIO_FUNCTION(4) }, { I2C1, GPIO('B', 8), GPIO('B', 9), GPIO_FUNCTION(4) }, { I2C2, GPIO('B', 10), GPIO('B', 11), GPIO_FUNCTION(4) }, + { I2C3, GPIO('A', 8), GPIO('C', 9), GPIO_FUNCTION(4) }, #endif }; diff --git a/src/stm32/stm32f0_serial.c b/src/stm32/stm32f0_serial.c index e48960f11..c987f149e 100644 --- a/src/stm32/stm32f0_serial.c +++ b/src/stm32/stm32f0_serial.c @@ -68,7 +68,7 @@ DECL_CONSTANT_STR("RESERVE_PINS_serial", "PD9,PD8"); #define GPIO_Rx GPIO('D', 9) #define GPIO_Tx GPIO('D', 8) - #define USARTx_FUNCTION GPIO_FUNCTION(7) + #define USARTx_FUNCTION GPIO_FUNCTION(CONFIG_MACH_STM32G0 ? 0 : 7) #define USARTx USART3 #define USARTx_IRQn USART3_IRQn #elif CONFIG_STM32_SERIAL_UART4 @@ -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/configs/stm32f031.config b/test/configs/stm32f031.config-disabled similarity index 100% rename from test/configs/stm32f031.config rename to test/configs/stm32f031.config-disabled diff --git a/test/klippy/danger_options.test b/test/klippy/danger_options.test index c886479e9..e674e35e8 100644 --- a/test/klippy/danger_options.test +++ b/test/klippy/danger_options.test @@ -12,3 +12,7 @@ G1 X1 G1 Y1 SAVE_CONFIG + +SAVE_CONFIG RESTART=0 + +SAVE_CONFIG RESTART=1 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 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 diff --git a/test/klippy/recursive_globs.cfg b/test/klippy/recursive_globs.cfg new file mode 100644 index 000000000..1ebfdc5b0 --- /dev/null +++ b/test/klippy/recursive_globs.cfg @@ -0,0 +1 @@ +[include **/recursive_globs.include] diff --git a/test/klippy/recursive_globs.test b/test/klippy/recursive_globs.test new file mode 100644 index 000000000..f4dd9f826 --- /dev/null +++ b/test/klippy/recursive_globs.test @@ -0,0 +1,4 @@ +CONFIG recursive_globs.cfg +DICTIONARY atmega2560.dict + +G28 diff --git a/test/klippy/recursive_globs_include/recursive_globs.include b/test/klippy/recursive_globs_include/recursive_globs.include new file mode 100644 index 000000000..8632530db --- /dev/null +++ b/test/klippy/recursive_globs_include/recursive_globs.include @@ -0,0 +1 @@ +[include ./**/*.cfg] diff --git a/test/klippy/recursive_globs_include/recursive_globs_include.cfg b/test/klippy/recursive_globs_include/recursive_globs_include.cfg new file mode 100644 index 000000000..c7c253db9 --- /dev/null +++ b/test/klippy/recursive_globs_include/recursive_globs_include.cfg @@ -0,0 +1,71 @@ +[stepper_x] +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PE5 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PJ1 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: probe:z_virtual_endstop +position_max: 200 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[heater_bed] +heater_pin: PH5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +control: watermark +min_temp: 0 +max_temp: 130 + +[probe] +pin: PH6 +z_offset: 1.15 +drop_first_result: true + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 diff --git a/test/klippy/z_tilt.cfg b/test/klippy/z_tilt.cfg index 444b43c4b..405538522 100644 --- a/test/klippy/z_tilt.cfg +++ b/test/klippy/z_tilt.cfg @@ -73,6 +73,7 @@ points: 50,195 195,195 195,50 +increasing_threshold: 0.001 [bed_tilt] points: diff --git a/test/klippy/z_tilt.test b/test/klippy/z_tilt.test index ca6a8e10a..1324b328e 100644 --- a/test/klippy/z_tilt.test +++ b/test/klippy/z_tilt.test @@ -9,7 +9,7 @@ M400 GET_POSITION # Run Z_TILT_ADJUST in manual mode -Z_TILT_ADJUST METHOD=MANUAL +Z_TILT_ADJUST METHOD=MANUAL INCREASING_THRESHOLD=0.0000001 G1 Z2.909972 ACCEPT G1 Z2.924972 @@ -25,4 +25,4 @@ M400 GET_POSITION # Run again in automatic mode -Z_TILT_ADJUST +Z_TILT_ADJUST INCREASING_THRESHOLD=0.0000001 diff --git a/test/klippy/z_tilt_ng.cfg b/test/klippy/z_tilt_ng.cfg index 300d3089f..e20ed11c5 100644 --- a/test/klippy/z_tilt_ng.cfg +++ b/test/klippy/z_tilt_ng.cfg @@ -59,6 +59,7 @@ extra_points: 50,50 50,195 195,195 +increasing_threshold: 0.001 [extruder] step_pin: PA4 diff --git a/test/klippy/z_tilt_ng.test b/test/klippy/z_tilt_ng.test index 2649097ad..999ae0011 100644 --- a/test/klippy/z_tilt_ng.test +++ b/test/klippy/z_tilt_ng.test @@ -9,7 +9,7 @@ M400 GET_POSITION # Run Z_TILT_ADJUST in manual mode -Z_TILT_ADJUST METHOD=MANUAL +Z_TILT_ADJUST METHOD=MANUAL INCREASING_THRESHOLD=0.0000001 G1 Z2.909972 ACCEPT G1 Z2.924972 @@ -25,4 +25,4 @@ M400 GET_POSITION # Run again in automatic mode -Z_TILT_ADJUST +Z_TILT_ADJUST INCREASING_THRESHOLD=0.0000001