diff --git a/config/generic-bigtreetech-skr-mini-e3-v3.0.cfg b/config/generic-bigtreetech-skr-mini-e3-v3.0.cfg index 2ea064b09..b6a98bb02 100644 --- a/config/generic-bigtreetech-skr-mini-e3-v3.0.cfg +++ b/config/generic-bigtreetech-skr-mini-e3-v3.0.cfg @@ -85,11 +85,10 @@ uart_pin: PC11 tx_pin: PC10 uart_address: 3 run_current: 0.650 -stealthchop_threshold: 999999 [heater_bed] heater_pin: PC9 -sensor_type: ATC Semitec 104GT-2 +sensor_type: EPCOS 100K B57560G104F sensor_pin: PC4 control: pid pid_Kp: 54.027 diff --git a/config/printer-artillery-genius-pro-2022.cfg b/config/printer-artillery-genius-pro-2022.cfg new file mode 100644 index 000000000..aec7841c5 --- /dev/null +++ b/config/printer-artillery-genius-pro-2022.cfg @@ -0,0 +1,126 @@ +# This file contains pin mappings for the Artillery Genius Pro (2022) +# with a Artillery_Ruby-v1.2 board. To use this config, during "make menuconfig" +# select the STM32F401 with "No bootloader" and USB (on PA11/PA12) +# communication. + +# To flash this firmware, set the physical bridge between +3.3V and Boot0 PIN +# on Artillery_Ruby mainboard. Then run the command: +# make flash FLASH_DEVICE=/dev/serial/by-id/usb-Klipper_stm32f401xc_*-if00 + +# See docs/Config_Reference.md for a description of parameters. + +[extruder] +max_extrude_only_distance: 700.0 +step_pin: PA7 +dir_pin: PA6 +enable_pin: !PC4 +microsteps: 16 +rotation_distance: 7.1910 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PC9 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC0 +min_temp: 0 +max_temp: 250 +control: pid +pid_Kp: 23.223 +pid_Ki: 1.518 +pid_Kd: 88.826 + +[stepper_x] +step_pin: !PB14 +dir_pin: PB13 +enable_pin: !PB15 +microsteps: 16 +rotation_distance: 40 +endstop_pin: !PA2 +position_endstop: 0 +position_max: 220 +homing_speed: 60 + +[stepper_y] +step_pin: PB10 +dir_pin: PB2 +enable_pin: !PB12 +microsteps: 16 +rotation_distance: 40 +endstop_pin: !PA1 +position_endstop: 0 +position_max: 220 +homing_speed: 60 + +[stepper_z] +step_pin: PB0 +dir_pin: !PC5 +enable_pin: !PB1 +microsteps: 16 +rotation_distance: 8 +endstop_pin: probe:z_virtual_endstop +position_max: 250 +position_min: -5 + +[heater_bed] +heater_pin: PA8 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC1 +min_temp: 0 +max_temp: 130 +control: pid +pid_Kp: 23.223 +pid_Ki: 1.518 +pid_Kd: 88.826 + +[bed_screws] +screw1: 38,45 +screw2: 180,45 +screw3: 180,180 +screw4: 38,180 + +[fan] +pin: PC8 +off_below: 0.1 + +[heater_fan hotend_fan] +pin: PC7 +heater: extruder +heater_temp: 50.0 + +[controller_fan stepper_fan] +pin: PC6 +idle_timeout: 300 + +[mcu] +serial: /dev/serial/by-id/usb-Klipper_stm32f401xc_ + +[printer] +kinematics: cartesian +max_velocity: 500 +max_accel: 4000 +max_z_velocity: 50 +square_corner_velocity: 5.0 +max_z_accel: 100 + +[bltouch] +sensor_pin: PC2 +control_pin: PC3 +x_offset:27.25 +y_offset:-12.8 +z_offset: 0.25 +speed:10 +samples:1 +samples_result:average + +[bed_mesh] +speed: 800 +mesh_min: 30, 20 +mesh_max: 210, 200 +probe_count: 5,5 +algorithm: bicubic +move_check_distance: 3.0 + +[safe_z_home] +home_xy_position: 110,110 +speed: 100 +z_hop: 10 +z_hop_speed: 5 diff --git a/config/printer-creality-cr6se-2020.cfg b/config/printer-creality-cr6se-2020.cfg index 11d827eaf..2929a9ac4 100644 --- a/config/printer-creality-cr6se-2020.cfg +++ b/config/printer-creality-cr6se-2020.cfg @@ -98,6 +98,10 @@ z_offset: 0.0 speed: 2.0 samples: 5 +[safe_z_home] +home_xy_position: 117, 117 +z_hop: 10 + [filament_switch_sensor filament_sensor] pause_on_runout: true switch_pin: ^!PA7 diff --git a/config/printer-creality-cr6se-2021.cfg b/config/printer-creality-cr6se-2021.cfg index 12c17120f..932a9263d 100644 --- a/config/printer-creality-cr6se-2021.cfg +++ b/config/printer-creality-cr6se-2021.cfg @@ -98,6 +98,10 @@ z_offset: 0.0 speed: 2.0 samples: 5 +[safe_z_home] +home_xy_position: 117, 117 +z_hop: 10 + [filament_switch_sensor filament_sensor] pause_on_runout: true switch_pin: ^!PA7 diff --git a/config/printer-tronxy-crux1-2022.cfg b/config/printer-tronxy-crux1-2022.cfg new file mode 100644 index 000000000..e3254d85b --- /dev/null +++ b/config/printer-tronxy-crux1-2022.cfg @@ -0,0 +1,138 @@ +# Klipper configuration for the TronXY Crux1 printer +# CXY-V10.1-220921 mainboard, GD32F4XX or STM32F446 MCU +# +# ======================= +# BUILD AND FLASH OPTIONS +# ======================= +# +# MCU-architecture: STMicroelectronics +# Processor model: STM32F446 +# Bootloader offset: 64KiB +# Comms interface: Serial on USART1 PA10/PA9 +# +# Build the firmware with these options +# Rename the resulting klipper.bin into fmw_tronxy.bin +# Put the file into a directory called "update" on a FAT32 formatted SD card. +# Turn off the printer, plug in the SD card and turn the printer back on +# Flashing will start automatically and progress will be indicated on the LCD +# Once the flashing is completed the display will get stuck on the white Tronxy logo bootscreen +# The LCD display will NOT work anymore after flashing Klipper onto this printer + +[mcu] +serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 +restart_method: command + +[printer] +kinematics: cartesian +max_velocity: 250 +max_accel: 1500 +square_corner_velocity: 5 +max_z_velocity: 15 +max_z_accel: 100 + +[controller_fan drivers_fan] +pin: PD7 + +[pwm_cycle_time BEEPER_pin] +pin: PA8 +value: 0 +shutdown_value: 0 +cycle_time: 0.001 + +[safe_z_home] +home_xy_position: 0, 0 +speed: 100 +z_hop: 10 +z_hop_speed: 5 + +[stepper_x] +step_pin: PE5 +dir_pin: PF1 +enable_pin: !PF0 +microsteps: 16 +rotation_distance: 20 +endstop_pin: ^!PC15 +position_endstop: -1 +position_min: -1 +position_max: 180 +homing_speed: 100 +homing_retract_dist: 10 +second_homing_speed: 25 + +[stepper_y] +step_pin: PF9 +dir_pin: !PF3 +enable_pin: !PF5 +microsteps: 16 +rotation_distance: 20 +endstop_pin: ^!PC14 +position_endstop: -3 +position_min: -3 +position_max: 180 +homing_retract_dist: 10 +homing_speed: 100 +second_homing_speed: 25 + +[stepper_z] +step_pin: PA6 +dir_pin: !PF15 +enable_pin: !PA5 +microsteps: 16 +rotation_distance: 4 +endstop_pin: ^!PC13 +position_endstop: 0 +position_max: 180 +position_min: 0 + +[extruder] +step_pin: PB1 +dir_pin: PF13 +enable_pin: !PF14 +microsteps: 16 +rotation_distance: 16.75 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PG7 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC3 +control: pid +pid_kp: 22.2 +pid_ki: 1.08 +pid_kd: 114.00 +min_temp: 0 +max_temp: 250 +min_extrude_temp: 170 +max_extrude_only_distance: 450 + +[heater_fan hotend_fan] +heater: extruder +heater_temp: 50.0 +pin: PG9 + +[fan] +pin: PG0 + +[filament_switch_sensor filament_sensor] +pause_on_runout: True +switch_pin: ^!PE6 + +[heater_bed] +heater_pin: PE2 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC2 +min_temp: 0 +max_temp: 130 +control: pid +pid_kp: 10.00 +pid_ki: 0.023 +pid_kd: 305.4 + +[bed_screws] +screw1: 17.5, 11 +screw1_name: front_left +screw2: 162.5, 11 +screw2_name: front_right +screw3: 162.5, 162.5 +screw3_name: back_right +screw4: 17.5, 162.5 +screw4_name: back_left diff --git a/docs/API_Server.md b/docs/API_Server.md index 4af1812a3..3837f737e 100644 --- a/docs/API_Server.md +++ b/docs/API_Server.md @@ -364,6 +364,38 @@ and might later produce asynchronous messages such as: The "header" field in the initial query response is used to describe the fields found in later "data" responses. +### hx71x/dump_hx71x + +This endpoint is used to subscribe to raw HX711 and HX717 ADC data. +Obtaining these low-level ADC updates may be useful for diagnostic +and debugging purposes. Using this endpoint may increase Klipper's +system load. + +A request may look like: +`{"id": 123, "method":"hx71x/dump_hx71x", +"params": {"sensor": "load_cell", "response_template": {}}}` +and might return: +`{"id": 123,"result":{"header":["time","counts","value"]}}` +and might later produce asynchronous messages such as: +`{"params":{"data":[[3292.432935, 562534, 0.067059278], +[3292.4394937, 5625322, 0.670590639]]}}` + +### ads1220/dump_ads1220 + +This endpoint is used to subscribe to raw ADS1220 ADC data. +Obtaining these low-level ADC updates may be useful for diagnostic +and debugging purposes. Using this endpoint may increase Klipper's +system load. + +A request may look like: +`{"id": 123, "method":"ads1220/dump_ads1220", +"params": {"sensor": "load_cell", "response_template": {}}}` +and might return: +`{"id": 123,"result":{"header":["time","counts","value"]}}` +and might later produce asynchronous messages such as: +`{"params":{"data":[[3292.432935, 562534, 0.067059278], +[3292.4394937, 5625322, 0.670590639]]}}` + ### pause_resume/cancel This endpoint is similar to running the "PRINT_CANCEL" G-Code command. @@ -401,3 +433,130 @@ might return: As with the "gcode/script" endpoint, this endpoint only completes after any pending G-Code commands complete. + +### bed_mesh/dump_mesh + +Dumps the configuration and state for the current mesh and all +saved profiles. + +For example: +`{"id": 123, "method": "bed_mesh/dump_mesh"}` + +might return: + +``` +{ + "current_mesh": { + "name": "eddy-scan-test", + "probed_matrix": [...], + "mesh_matrix": [...], + "mesh_params": { + "x_count": 9, + "y_count": 9, + "mesh_x_pps": 2, + "mesh_y_pps": 2, + "algo": "bicubic", + "tension": 0.5, + "min_x": 20, + "max_x": 330, + "min_y": 30, + "max_y": 320 + } + }, + "profiles": { + "default": { + "points": [...], + "mesh_params": { + "min_x": 20, + "max_x": 330, + "min_y": 30, + "max_y": 320, + "x_count": 9, + "y_count": 9, + "mesh_x_pps": 2, + "mesh_y_pps": 2, + "algo": "bicubic", + "tension": 0.5 + } + }, + "eddy-scan-test": { + "points": [...], + "mesh_params": { + "x_count": 9, + "y_count": 9, + "mesh_x_pps": 2, + "mesh_y_pps": 2, + "algo": "bicubic", + "tension": 0.5, + "min_x": 20, + "max_x": 330, + "min_y": 30, + "max_y": 320 + } + }, + "eddy-rapid-test": { + "points": [...], + "mesh_params": { + "x_count": 9, + "y_count": 9, + "mesh_x_pps": 2, + "mesh_y_pps": 2, + "algo": "bicubic", + "tension": 0.5, + "min_x": 20, + "max_x": 330, + "min_y": 30, + "max_y": 320 + } + } + }, + "calibration": { + "points": [...], + "config": { + "x_count": 9, + "y_count": 9, + "mesh_x_pps": 2, + "mesh_y_pps": 2, + "algo": "bicubic", + "tension": 0.5, + "mesh_min": [ + 20, + 30 + ], + "mesh_max": [ + 330, + 320 + ], + "origin": null, + "radius": null + }, + "probe_path": [...], + "rapid_path": [...] + }, + "probe_offsets": [ + 0, + 25, + 0.5 + ], + "axis_minimum": [ + 0, + 0, + -5, + 0 + ], + "axis_maximum": [ + 351, + 358, + 330, + 0 + ] +} +``` + +The `dump_mesh` endpoint takes one optional parameter, `mesh_args`. +This parameter must be an object, where the keys and values are +parameters available to [BED_MESH_CALIBRATE](#bed_mesh_calibrate). +This will update the mesh configuration and probe points using the +supplied parameters prior to returning the result. It is recommended +to omit mesh parameters unless it is desired to visualize the probe points +and/or travel path before performing `BED_MESH_CALIBRATE`. diff --git a/docs/Bed_Mesh.md b/docs/Bed_Mesh.md index 1538f6257..62f1dee84 100644 --- a/docs/Bed_Mesh.md +++ b/docs/Bed_Mesh.md @@ -421,12 +421,75 @@ have undesirable results when attempting print moves **outside** of the probed a full bed mesh has a variance greater than 1 layer height, caution must be taken when using adaptive bed meshes and attempting print moves outside of the meshed area. +## Surface Scans + +Some probes, such as the [Eddy Current Probe](./Eddy_Probe.md), are capable of +"scanning" the surface of the bed. That is, these probes can sample a mesh +without lifting the tool between samples. To activate scanning mode, the +`METHOD=scan` or `METHOD=rapid_scan` probe parameter should be passed in the +`BED_MESH_CALIBRATE` gcode command. + +### Scan Height + +The scan height is set by the `horizontal_move_z` option in `[bed_mesh]`. In +addition it can be supplied with the `BED_MESH_CALIBRATE` gcode command via the +`HORIZONTAL_MOVE_Z` parameter. + +The scan height must be sufficiently low to avoid scanning errors. Typically +a height of 2mm (ie: `HORIZONTAL_MOVE_Z=2`) should work well, presuming that the +probe is mounted correctly. + +It should be noted that if the probe is more than 4mm above the surface then the +results will be invalid. Thus, scanning is not possible on beds with severe +surface deviation or beds with extreme tilt that hasn't been corrected. + +### Rapid (Continuous) Scanning + +When performing a `rapid_scan` one should keep in mind that the results will +have some amount of error. This error should be low enough to be useful on +large print areas with reasonably thick layer heights. Some probes may be +more prone to error than others. + +It is not recommended that rapid mode be used to scan a "dense" mesh. Some of +the error introduced during a rapid scan may be gaussian noise from the sensor, +and a dense mesh will reflect this noise (ie: there will be peaks and valleys). + +Bed Mesh will attempt to optimize the travel path to provide the best possible +result based on the configuration. This includes avoiding faulty regions +when collecting samples and "overshooting" the mesh when changing direction. +This overshoot improves sampling at the edges of a mesh, however it requires +that the mesh be configured in a way that allows the tool to travel outside +of the mesh. + +``` +[bed_mesh] +speed: 120 +horizontal_move_z: 5 +mesh_min: 35, 6 +mesh_max: 240, 198 +probe_count: 5 +scan_overshoot: 8 +``` + +- `scan_overshoot` + _Default Value: 0 (disabled)_\ + The maximum amount of travel (in mm) available outside of the mesh. + For rectangular beds this applies to travel on the X axis, and for round beds + it applies to the entire radius. The tool must be able to travel the amount + specified outside of the mesh. This value is used to optimize the travel + path when performing a "rapid scan". The minimum value that may be specified + is 1. The default is no overshoot. + +If no scan overshoot is configured then travel path optimization will not +be applied to changes in direction. + ## Bed Mesh Gcodes ### Calibration -`BED_MESH_CALIBRATE PROFILE= METHOD=[manual | automatic] [=] - [=] [ADAPTIVE=[0|1] [ADAPTIVE_MARGIN=]`\ +`BED_MESH_CALIBRATE PROFILE= METHOD=[manual | automatic | scan | rapid_scan] \ +[=] [=] [ADAPTIVE=[0|1] \ +[ADAPTIVE_MARGIN=]`\ _Default Profile: default_\ _Default Method: automatic if a probe is detected, otherwise manual_ \ _Default Adaptive: 0_ \ @@ -435,9 +498,17 @@ _Default Adaptive Margin: 0_ Initiates the probing procedure for Bed Mesh Calibration. The mesh will be saved into a profile specified by the `PROFILE` parameter, -or `default` if unspecified. If `METHOD=manual` is selected then manual probing -will occur. When switching between automatic and manual probing the generated -mesh points will automatically be adjusted. +or `default` if unspecified. The `METHOD` parameter takes one of the following +values: + +- `METHOD=manual`: enables manual probing using the nozzle and the paper test +- `METHOD=automatic`: Automatic (standard) probing. This is the default. +- `METHOD=scan`: Enables surface scanning. The tool will pause over each position + to collect a sample. +- `METHOD=rapid_scan`: Enables continuous surface scanning. + +XY positions are automatically adjusted to include the X and/or Y offsets +when a probing method other than `manual` is selected. It is possible to specify mesh parameters to modify the probed area. The following parameters are available: @@ -451,6 +522,7 @@ following parameters are available: - `MESH_ORIGIN` - `ROUND_PROBE_COUNT` - All beds: + - `MESH_PPS` - `ALGORITHM` - `ADAPTIVE` - `ADAPTIVE_MARGIN` @@ -557,3 +629,191 @@ is intended to compensate for a `gcode offset` when [mesh fade](#mesh-fade) is enabled. For example, if a secondary extruder is higher than the primary and needs a negative gcode offset, ie: `SET_GCODE_OFFSET Z=-.2`, it can be accounted for in `bed_mesh` with `BED_MESH_OFFSET ZFADE=.2`. + +## Bed Mesh Webhooks APIs + +### Dumping mesh data + +`{"id": 123, "method": "bed_mesh/dump_mesh"}` + +Dumps the configuration and state for the current mesh and all +saved profiles. + +The `dump_mesh` endpoint takes one optional parameter, `mesh_args`. +This parameter must be an object, where the keys and values are +parameters available to [BED_MESH_CALIBRATE](#bed_mesh_calibrate). +This will update the mesh configuration and probe points using the +supplied parameters prior to returning the result. It is recommended +to omit mesh parameters unless it is desired to visualize the probe points +and/or travel path before performing `BED_MESH_CALIBRATE`. + +## Visualization and analysis + +Most users will likely find that the visualizers included with +applications such as Mainsail, Fluidd, and Octoprint are sufficient +for basic analysis. However, Klipper's `scripts` folder contains the +`graph_mesh.py` script that may be used to perform additional +visualizations and more detailed analysis, particularly useful +for debugging hardware or the results produced by `bed_mesh`: + +``` +usage: graph_mesh.py [-h] {list,plot,analyze,dump} ... + +Graph Bed Mesh Data + +positional arguments: + {list,plot,analyze,dump} + list List available plot types + plot Plot a specified type + analyze Perform analysis on mesh data + dump Dump API response to json file + +options: + -h, --help show this help message and exit +``` + +### Pre-requisites + +Like most graphing tools provided by Klipper, `graph_mesh.py` requires +the `matplotlib` and `numpy` python dependencies. In addition, connecting +to Klipper via Moonraker's websocket requires the `websockets` python +dependency. While all visualizations can be output to an `svg` file, most of +the visualizations offered by `graph_mesh.py` are better viewed in live +preview mode on a desktop class PC. For example, the 3D visualizations may be +rotated and zoomed in preview mode, and the path visualizations can optionally +be animated in preview mode. + +### Plotting Mesh data + +The `graph_mesh.py` tool can plot several types of visualizations. +Available types can be shown by running `graph_mesh.py list`: + +``` +graph_mesh.py list +points Plot original generated points +path Plot probe travel path +rapid Plot rapid scan travel path +probedz Plot probed Z values +meshz Plot mesh Z values +overlay Plots the current probed mesh overlaid with a profile +delta Plots the delta between current probed mesh and a profile +``` + +Several options are available when plotting visualizations: + +``` +usage: graph_mesh.py plot [-h] [-a] [-s] [-p PROFILE_NAME] [-o OUTPUT] + +positional arguments: + Type of data to graph + Path/url to Klipper Socket or path to json file + +options: + -h, --help show this help message and exit + -a, --animate Animate paths in live preview + -s, --scale-plot Use axis limits reported by Klipper to scale plot X/Y + -p PROFILE_NAME, --profile-name PROFILE_NAME + Optional name of a profile to plot for 'probedz' + -o OUTPUT, --output OUTPUT + Output file path +``` + +Below is a description of each argument: + +- `plot type`: A required positional argument designating the type of + visualization to generate. Must be one of the types output by the + `graph_mesh.py list` command. +- `input`: A required positional argument containing a path or url + to the input source. This must be one of the following: + - A path to Klipper's Unix Domain Socket + - A url to an instance of Moonraker + - A path to a json file produced by `graph_mesh.py dump ` +- `-a`: Optional animation for the `path` and `rapid` visualization types. + Animations only apply to a live preview. +- `-s`: Optionally scales a plot using the `axis_minimum` and `axis_maximum` + values reported by Klipper's `toolhead` object when the dump file was + generated. +- `-p`: A profile name that may be specified when generating the + `probedz` 3D mesh visualization. When generating an `overlay` or + `delta` visualization this argument must be provided. +- `-o`: An optional file path indicating that the script should save the + visualization to this location rather than run in preview mode. Images + are saved in `svg` format. + +For example, to plot an animated rapid path, connecting via Klipper's unix +socket: + +``` +graph_mesh.py plot -a rapid ~/printer_data/comms/klippy.sock +``` + +Or to plot a 3d visualization of the mesh, connecting via Moonraker: + +``` +graph_mesh.py plot meshz http://my-printer.local +``` + +### Bed Mesh Analysis + +The `graph_mesh.py` tool may also be used to perform an analysis on the +data provided by the [bed_mesh/dump_mesh](#dumping-mesh-data) API: + +``` +graph_mesh.py analyze +``` + +As with the `plot` command, the `` must be a path to Klipper's +unix socket, a URL to an instance of Moonraker, or a path to a json file +generated by the dump command. + +To begin, the analysis will perform various checks on the points and +probe paths generated by `bed_mesh` at the time of the dump. This +includes the following: + +- The number of probe points generated, without any additions +- The number of probe points generated including any points generated + as the result faulty regions and/or a configured zero reference position. +- The number of probe points generated when performing a rapid scan. +- The total number of moves generated for a rapid scan. +- A validation that the probe points generated for a rapid scan are + identical to the probe points generated for a standard probing procedure. +- A "backtracking" check for both the standard probe path and a rapid scan + path. Backtracking can be defined as moving to the same position more than + once during the probing procedure. Backtracking should never occur during a + standard probe. Faulty regions *can* result in backtracking during a rapid + scan in an attempt to avoid entering a faulty region when approaching or + leaving a probe location, however should never occur otherwise. + +Next each probed mesh present in the dump will by analyzed, beginning with +the mesh loaded at the time of the dump (if present) and followed by any +saved profiles. The following data is extracted: + +- Mesh shape (Min X,Y, Max X,Y Probe Count) +- Mesh Z range, (Minimum Z, Maximum Z) +- Mean Z value in the mesh +- Standard Deviation of the Z values in the Mesh + +In addition to the above, a delta analysis is performed between meshes +with the same shape, reporting the following: +- The range of the delta between to meshes (Minimum and Maximum) +- The mean delta +- Standard Deviation of the delta +- The absolute maximum difference +- The absolute mean + +### Save mesh data to a file + +The `dump` command may be used to save the response to a file which +can be shared for analysis when troubleshooting: + +``` +graph_mesh.py dump -o +``` + +The `` should be a path to Klipper's unix socket or +a URL to an instance of Moonraker. The `-o` option may be used to +specify the path to the output file. If omitted, the file will be +saved in the working directory, with a file name in the following +format: + +`klipper-bedmesh-{year}{month}{day}{hour}{minute}{second}.json` diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 18a6226e1..74fee44cd 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -998,6 +998,13 @@ Visual Examples: #adaptive_margin: # An optional margin (in mm) to be added around the bed area used by # the defined print objects when generating an adaptive mesh. +#scan_overshoot: +# The maximum amount of travel (in mm) available outside of the mesh. +# For rectangular beds this applies to travel on the X axis, and for round beds +# it applies to the entire radius. The tool must be able to travel the amount +# specified outside of the mesh. This value is used to optimize the travel +# path when performing a "rapid scan". The minimum value that may be specified +# is 1. The default is no overshoot. ``` ### [bed_tilt] @@ -2007,6 +2014,9 @@ Support for eddy current inductive probes. One may define this section sensor_type: ldc1612 # The sensor chip used to perform eddy current measurements. This # parameter must be provided and must be set to ldc1612. +#intb_pin: +# MCU gpio pin connected to the ldc1612 sensor's INTB pin (if +# available). The default is to not use the INTB pin. #z_offset: # The nominal distance (in mm) between the nozzle and bed that a # probing attempt should stop at. This parameter must be provided. @@ -2391,6 +2401,69 @@ temperature sensors that are reported via the M105 command. # parameter. ``` +### [temperature_probe] + +Reports probe coil temperature. Includes optional thermal drift +calibration for eddy current based probes. A `[temperature_probe]` +section may be linked to a `[probe_eddy_current]` by using the same +postfix for both sections. + +``` +[temperature_probe my_probe] +#sensor_type: +#sensor_pin: +#min_temp: +#max_temp: +# Temperature sensor configuration. +# See the "extruder" section for the definition of the above +# parameters. +#smooth_time: +# A time value (in seconds) over which temperature measurements will +# be smoothed to reduce the impact of measurement noise. The default +# is 2.0 seconds. +#gcode_id: +# See the "heater_generic" section for the definition of this +# parameter. +#speed: +# The travel speed [mm/s] for xy moves during calibration. Default +# is the speed defined by the probe. +#horizontal_move_z: +# The z distance [mm] from the bed at which xy moves will occur +# during calibration. Default is 2mm. +#resting_z: +# The z distance [mm] from the bed at which the tool will rest +# to heat the probe coil during calibration. Default is .4mm +#calibration_position: +# The X, Y, Z position where the tool should be moved when +# probe drift calibration initializes. This is the location +# where the first manual probe will occur. If omitted, the +# default behavior is not to move the tool prior to the first +# manual probe. +#calibration_bed_temp: +# The maximum safe bed temperature (in C) used to heat the probe +# during probe drift calibration. When set, the calibration +# procedure will turn on the bed after the first sample is +# taken. When the calibration procedure is complete the bed +# temperature will be set to zero. When omitted the default +# behavior is not to set the bed temperature. +#calibration_extruder_temp: +# The extruder temperature (in C) set probe during drift calibration. +# When this option is supplied the procedure will wait for until the +# specified temperature is reached before requesting the first manual +# probe. When the calibration procedure is complete the extruder +# temperature will be set to 0. When omitted the default behavior is +# not to set the extruder temperature. +#extruder_heating_z: 50. +# The Z location where extruder heating will occur if the +# "calibration_extruder_temp" option is set. Its recommended to heat +# the extruder some distance from the bed to minimize its impact on +# the probe coil temperature. The default is 50. +#max_validation_temp: 60. +# The maximum temperature used to validate the calibration. It is +# recommended to set this to a value between 100 and 120 for enclosed +# printers. The default is 60. +``` + ## Temperature sensors Klipper includes definitions for many types of temperature sensors. @@ -3317,6 +3390,18 @@ run_current: # set, "stealthChop" mode will be enabled if the stepper motor # velocity is below this value. The default is 0, which disables # "stealthChop" mode. +#coolstep_threshold: +# The velocity (in mm/s) to set the TMC driver internal "CoolStep" +# threshold to. If set, the coolstep feature will be enabled when +# the stepper motor velocity is near or above this value. Important +# - if coolstep_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is above the coolstep +# threshold! The default is to not enable the coolstep feature. +#high_velocity_threshold: +# The velocity (in mm/s) to set the TMC driver internal "high +# velocity" threshold (THIGH) to. This is typically used to disable +# the "CoolStep" feature at high speeds. The default is to not set a +# TMC "high velocity" threshold. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 @@ -3347,11 +3432,19 @@ run_current: #driver_TOFF: 4 #driver_HEND: 7 #driver_HSTRT: 0 +#driver_VHIGHFS: 0 +#driver_VHIGHCHM: 0 #driver_PWM_AUTOSCALE: True #driver_PWM_FREQ: 1 #driver_PWM_GRAD: 4 #driver_PWM_AMPL: 128 #driver_SGT: 0 +#driver_SEMIN: 0 +#driver_SEUP: 0 +#driver_SEMAX: 0 +#driver_SEDN: 0 +#driver_SEIMIN: 0 +#driver_SFILT: 0 # Set the given register during the configuration of the TMC2130 # chip. This may be used to set custom motor parameters. The # defaults for each parameter are next to the parameter name in the @@ -3448,6 +3541,13 @@ run_current: #sense_resistor: 0.110 #stealthchop_threshold: 0 # See the "tmc2208" section for the definition of these parameters. +#coolstep_threshold: +# The velocity (in mm/s) to set the TMC driver internal "CoolStep" +# threshold to. If set, the coolstep feature will be enabled when +# the stepper motor velocity is near or above this value. Important +# - if coolstep_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is above the coolstep +# threshold! The default is to not enable the coolstep feature. #uart_address: # The address of the TMC2209 chip for UART messages (an integer # between 0 and 3). This is typically used when multiple TMC2209 @@ -3467,6 +3567,11 @@ run_current: #driver_PWM_GRAD: 14 #driver_PWM_OFS: 36 #driver_SGTHRS: 0 +#driver_SEMIN: 0 +#driver_SEUP: 0 +#driver_SEMAX: 0 +#driver_SEDN: 0 +#driver_SEIMIN: 0 # Set the given register during the configuration of the TMC2209 # chip. This may be used to set custom motor parameters. The # defaults for each parameter are next to the parameter name in the @@ -3601,6 +3706,18 @@ run_current: # set, "stealthChop" mode will be enabled if the stepper motor # velocity is below this value. The default is 0, which disables # "stealthChop" mode. +#coolstep_threshold: +# The velocity (in mm/s) to set the TMC driver internal "CoolStep" +# threshold to. If set, the coolstep feature will be enabled when +# the stepper motor velocity is near or above this value. Important +# - if coolstep_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is above the coolstep +# threshold! The default is to not enable the coolstep feature. +#high_velocity_threshold: +# The velocity (in mm/s) to set the TMC driver internal "high +# velocity" threshold (THIGH) to. This is typically used to disable +# the "CoolStep" feature at high speeds. The default is to not set a +# TMC "high velocity" threshold. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 @@ -3722,6 +3839,18 @@ run_current: # set, "stealthChop" mode will be enabled if the stepper motor # velocity is below this value. The default is 0, which disables # "stealthChop" mode. +#coolstep_threshold: +# The velocity (in mm/s) to set the TMC driver internal "CoolStep" +# threshold to. If set, the coolstep feature will be enabled when +# the stepper motor velocity is near or above this value. Important +# - if coolstep_threshold is set and "sensorless homing" is used, +# then one must ensure that the homing speed is above the coolstep +# threshold! The default is to not enable the coolstep feature. +#high_velocity_threshold: +# The velocity (in mm/s) to set the TMC driver internal "high +# velocity" threshold (THIGH) to. This is typically used to disable +# the "CoolStep" feature at high speeds. The default is to not set a +# TMC "high velocity" threshold. #driver_MSLUT0: 2863314260 #driver_MSLUT1: 1251300522 #driver_MSLUT2: 608774441 @@ -4535,6 +4664,95 @@ adc2: # above parameters. ``` +## Load Cells + +### [load_cell] +Load Cell. Uses an ADC sensor attached to a load cell to create a digital +scale. + +``` +[load_cell] +sensor_type: +# This must be one of the supported sensor types, see below. +``` + +#### XH711 +This is a 24 bit low sample rate chip using "bit-bang" communications. It is +suitable for filament scales. +``` +[load_cell] +sensor_type: hx711 +sclk_pin: +# The pin connected to the HX711 clock line. This parameter must be provided. +dout_pin: +# The pin connected to the HX711 data output line. This parameter must be +# provided. +#gain: A-128 +# Valid values for gain are: A-128, A-64, B-32. The default is A-128. +# 'A' denotes the input channel and the number denotes the gain. Only the 3 +# listed combinations are supported by the chip. Note that changing the gain +# setting also selects the channel being read. +#sample_rate: 80 +# Valid values for sample_rate are 80 or 10. The default value is 80. +# This must match the wiring of the chip. The sample rate cannot be changed +# in software. +``` + +#### HX717 +This is the 4x higher sample rate version of the HX711, suitable for probing. +``` +[load_cell] +sensor_type: hx717 +sclk_pin: +# The pin connected to the HX717 clock line. This parameter must be provided. +dout_pin: +# The pin connected to the HX717 data output line. This parameter must be +# provided. +#gain: A-128 +# Valid values for gain are A-128, B-64, A-64, B-8. +# 'A' denotes the input channel and the number denotes the gain setting. +# Only the 4 listed combinations are supported by the chip. Note that +# changing the gain setting also selects the channel being read. +#sample_rate: 320 +# Valid values for sample_rate are: 10, 20, 80, 320. The default is 320. +# This must match the wiring of the chip. The sample rate cannot be changed +# in software. +``` + +#### ADS1220 +The ADS1220 is a 24 bit ADC supporting up to a 2Khz sample rate configurable in +software. +``` +[load_cell] +sensor_type: ads1220 +cs_pin: +# The pin connected to the ADS1220 chip select line. This parameter must +# be provided. +#spi_speed: 512000 +# This chip supports 2 speeds: 256000 or 512000. The faster speed is only +# enabled when one of the Turbo sample rates is used. The correct spi_speed +# is selected based on the sample rate. +#spi_bus: +#spi_software_sclk_pin: +#spi_software_mosi_pin: +#spi_software_miso_pin: +# See the "common SPI settings" section for a description of the +# above parameters. +data_ready_pin: +# Pin connected to the ADS1220 data ready line. This parameter must be +# provided. +#gain: 128 +# Valid gain values are 128, 64, 32, 16, 8, 4, 2, 1 +# The default is 128 +#sample_rate: 660 +# This chip supports two ranges of sample rates, Normal and Turbo. In turbo +# mode the chips c internal clock runs twice as fast and the SPI communication +# speed is also doubled. +# Normal sample rates: 20, 45, 90, 175, 330, 600, 1000 +# Turbo sample rates: 40, 90, 180, 350, 660, 1200, 2000 +# The default is 660 +``` + ## Board specific hardware support ### [sx1509] diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 221c855b6..3c36a613c 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -54,3 +54,93 @@ result in changes in reported Z height. Changes in either the bed surface temperature or sensor hardware temperature can skew the results. It is important that calibration and probing is only done when the printer is at a stable temperature. + +## Thermal Drift Calibration + +As with all inductive probes, eddy current probes are subject to +significant thermal drift. If the eddy probe has a temperature +sensor on the coil it is possible to configure a `[temperature_probe]` +to report coil temperature and enable software drift compensation. To +link a temperature probe to an eddy current probe the +`[temperature_probe]` section must share a name with the +`[probe_eddy_current]` section. For example: + +``` +[probe_eddy_current my_probe] +# eddy probe configuration... + +[temperature_probe my_probe] +# temperature probe configuration... +``` + +See the [configuration reference](Config_Reference.md#temperature_probe) +for further details on how to configure a `temperature_probe`. It is +advised to configure the `calibration_position`, +`calibration_extruder_temp`, `extruder_heating_z`, and +`calibration_bed_temp` options, as doing so will automate some of the +steps outlined below. If the printer to be calibrated is enclosed, it +is strongly recommended to set the `max_validation_temp` option to a value +between 100 and 120. + +Eddy probe manufacturers may offer a stock drift calibration that can be +manually added to `drift_calibration` option of the `[probe_eddy_current]` +section. If they do not, or if the stock calibration does not perform well on +your system, the `temperature_probe` module offers a manual calibration +procedure via the `TEMPERATURE_PROBE_CALIBRATE` gcode command. + +Prior to performing calibration the user should have an idea of what the +maximum attainable temperature probe coil temperature is. This temperature +should be used to set the `TARGET` parameter of the +`TEMPERATURE_PROBE_CALIBRATE` command. The goal is to calibrate across the +widest temperature range possible, thus its desirable to start with the printer +cold and finish with the coil at the maximum temperature it can reach. + +Once a `[temperature_probe]` is configured, the following steps may be taken +to perform thermal drift calibration: + +- The probe must be calibrated using `PROBE_EDDY_CURRENT_CALIBRATE` + when a `[temperature_probe]` is configured and linked. This captures + the temperature during calibration which is necessary to perform + thermal drift compensation. +- Make sure the nozzle is free of debris and filament. +- The bed, nozzle, and probe coil should be cold prior to calibration. +- The following steps are required if the `calibration_position`, + `calibration_extruder_temp`, and `extruder_heating_z` options in + `[temperature_probe]` are **NOT** configured: + - Move the tool to the center of the bed. Z should be 30mm+ above the bed. + - Heat the extruder to a temperature above the maximum safe bed temperature. + 150-170C should be sufficient for most configurations. The purpose of + heating the extruder is to avoid nozzle expansion during calibration. + - When the extruder temperature has settled, move the Z axis down to about 1mm + above the bed. +- Start drift calibration. If the probe's name is `my_probe` and the maximum + probe temperature we can achieve is 80C, the appropriate gcode command is + `TEMPERATURE_PROBE_CALIBRATE PROBE=my_probe TARGET=80`. If configured, the + tool will move to the X,Y coordinate specified by the `calibration_position` + and the Z value specified by `extruder_heating_z`. After heating the extruder + to the specified temperature the tool will move to the Z value specified + by the`calibration_position`. +- The procedure will request a manual probe. Perform the manual probe with + the paper test and `ACCEPT`. The calibration procedure will take the first + set of samples with the probe then park the probe in the heating position. +- If the `calibration_bed_temp` is **NOT** configured turn on the bed heat + to the maximum safe temperature. Otherwise this step will be performed + automatically. +- By default the calibration procedure will request a manual probe every + 2C between samples until the `TARGET` is reached. The temperature delta + between samples can be customized by setting the `STEP` parameter in + `TEMPERATURE_PROBE_CALIBRATE`. Care should be taken when setting a custom + `STEP` value, a value too high may request too few samples resulting in + a poor calibration. +- The following additional gcode commands are available during drift + calibration: + - `TEMPERATURE_PROBE_NEXT` may be used to force a new sample before the step + delta has been reached. + - `TEMPERATURE_PROBE_COMPLETE` may be used to complete calibration before the + `TARGET` has been reached. + - `ABORT` may be used to end calibration and discard results. +- When calibration is finished use `SAVE_CONFIG` to store the drift + calibration. + +As one may conclude, the calibration process outlined above is more challenging +and time consuming than most other procedures. It may require practice and several attempts to achieve an optimal calibration. diff --git a/docs/G-Codes.md b/docs/G-Codes.md index e55fba35d..2444c53b1 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -1415,3 +1415,39 @@ command will probe the points specified in the config and then make independent adjustments to each Z stepper to compensate for tilt. See the PROBE command for details on the optional probe parameters. The optional `HORIZONTAL_MOVE_Z` value overrides the `horizontal_move_z` option specified in the config file. + +### [temperature_probe] + +The following commands are available when a +[temperature_probe config section](Config_Reference.md#temperature_probe) +is enabled. + +#### TEMPERATURE_PROBE_CALIBRATE +`TEMPERATURE_PROBE_CALIBRATE [PROBE=] [TARGET=] [STEP=]`: +Initiates probe drift calibration for eddy current based probes. The `TARGET` +is a target temperature for the last sample. When the temperature recorded +during a sample exceeds the `TARGET` calibration will complete. The `STEP` +parameter sets temperature delta (in C) between samples. After a sample has +been taken, this delta is used to schedule a call to `TEMPERATURE_PROBE_NEXT`. +The default `STEP` is 2. + +#### TEMPERATURE_PROBE_NEXT +`TEMPERATURE_PROBE_NEXT`: After calibration has started this command is run to +take the next sample. It is automatically scheduled to run when the delta +specified by `STEP` has been reached, however its also possible to manually run +this command to force a new sample. This command is only available during +calibration. + +#### TEMPERATURE_PROBE_COMPLETE: +`TEMPERATURE_PROBE_COMPLETE`: Can be used to end calibration and save the +current result before the `TARGET` temperature is reached. This command +is only available during calibration. + +#### ABORT +`ABORT`: Aborts the calibration process, discarding the current results. +This command is only available during drift calibration. + +### TEMPERATURE_PROBE_ENABLE +`TEMPERATURE_PROBE_ENABLE ENABLE=[0|1]`: Sets temperature drift +compensation on or off. If ENABLE is set to 0, drift compensation +will be disabled, if set to 1 it is enabled. diff --git a/docs/Installation.md b/docs/Installation.md index 004d963a0..ca64259aa 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -1,15 +1,20 @@ # Installation -These instructions assume the software will run on a Raspberry Pi -computer in conjunction with OctoPrint. It is recommended that a -Raspberry Pi 2 (or later) be used as the host machine (see the +These instructions assume the software will run on a linux based host +running a Klipper compatible front end. It is recommended that a +SBC(Small Board Computer) such as a Raspberry Pi or Debian based Linux +device be used as the host machine (see the [FAQ](FAQ.md#can-i-run-klipper-on-something-other-than-a-raspberry-pi-3) -for other machines). +for other options). + +For the purposes of these instructions host relates to the Linux device and +mcu relates to the printboard. SBC relates to the term Small Board Computer +such as the Raspberry Pi. ## Obtain a Klipper Configuration File Most Klipper settings are determined by a "printer configuration file" -that will be stored on the Raspberry Pi. An appropriate configuration +printer.cfg, that will be stored on the host. An appropriate configuration file can often be found by looking in the Klipper [config directory](../config/) for a file starting with a "printer-" prefix that corresponds to the target printer. The Klipper @@ -35,38 +40,51 @@ printer configuration file, then start with the closest example [config file](../config/) and use the Klipper [config reference](Config_Reference.md) for further information. -## Prepping an OS image +## Interacting with Klipper -Start by installing [OctoPi](https://github.com/guysoft/OctoPi) on the -Raspberry Pi computer. Use OctoPi v0.17.0 or later - see the -[OctoPi releases](https://github.com/guysoft/OctoPi/releases) for -release information. One should verify that OctoPi boots and that the -OctoPrint web server works. After connecting to the OctoPrint web -page, follow the prompt to upgrade OctoPrint to v1.4.2 or later. +Klipper is a 3d printer firmware, so it needs some way for the user to +interact with it. -After installing OctoPi and upgrading OctoPrint, it will be necessary -to ssh into the target machine to run a handful of system commands. If -using a Linux or MacOS desktop, then the "ssh" software should already -be installed on the desktop. There are free ssh clients available for -other desktops (eg, -[PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/)). Use the -ssh utility to connect to the Raspberry Pi (`ssh pi@octopi` -- password -is "raspberry") and run the following commands: +Currently the best choices are front ends that retrieve information through +the [Moonraker web API](https://moonraker.readthedocs.io/) and there is also +the option to use [Octoprint](https://octoprint.org/) to control Klipper. -``` -git clone https://github.com/Klipper3d/klipper -./klipper/scripts/install-octopi.sh -``` +The choice is up to the user on what to use, but the underlying Klipper is the +same in all cases. We encourage users to research the options available and +make an informed decision. -The above will download Klipper, install some system dependencies, -setup Klipper to run at system startup, and start the Klipper host -software. It will require an internet connection and it may take a few -minutes to complete. +## Obtaining an OS image for SBC's + +There are many ways to obtain an OS image for Klipper for SBC use, most depend on +what front end you wish to use. Some manafactures of these SBC boards also provide +their own Klipper-centric images. + +The two main Moonraker based front ends are [Fluidd](https://docs.fluidd.xyz/) +and [Mainsail](https://docs.mainsail.xyz/), the latter of which has a premade install +image ["MainsailOS"](http://docs.mainsailOS.xyz), this has the option for Raspberry Pi +and some OrangePi varianta. + +Fluidd can be installed via KIAUH(Klipper Install And Update Helper), which +is explained below and is a 3rd party installer for all things Klipper. + +OctoPrint can be installed via the popular OctoPi image or via KIAUH, this +process is explained in [OctoPrint.md](OctoPrint.md) + +## Installing via KIAUH + +Normally you would start with a base image for your SBC, RPiOS Lite for example, +or in the case of a x86 Linux device, Ubuntu Server. Please note that Desktop +variants are not recommended due to certain helper programs that can stop some +Klipper functions working and even mask access to some print boards. + +KIAUH can be used to install Klipper and its associated programs on a variety +of Linux based systems that run a form of Debian. More information can be found +at https://github.com/dw-0/kiauh ## Building and flashing the micro-controller To compile the micro-controller code, start by running these commands -on the Raspberry Pi: +on your host device: ``` cd ~/klipper/ @@ -108,10 +126,21 @@ It should report something similar to the following: It's common for each printer to have its own unique serial port name. This unique name will be used when flashing the micro-controller. It's possible there may be multiple lines in the above output - if so, -choose the line corresponding to the micro-controller (see the +choose the line corresponding to the micro-controller. If many +items are listed and the choice is ambiguous, unplug the board and +run the command again, the missing item will be your print board(see the [FAQ](FAQ.md#wheres-my-serial-port) for more information). -For common micro-controllers, the code can be flashed with something +For common micro-controllers with STM32 or clone chips, LPC chips and +others it is usual that these need an initial Klipper flash via SD card. + +When flashing with this method, it is important to make sure that the +print board is not connected with USB to the host, due to some boards +being able to feed power back to the board and stopping a flash from +occuring. + +For common micro-controllers using Atmega chips, for example the 2560, +the code can be flashed with something similar to: ``` @@ -123,53 +152,38 @@ sudo service klipper start Be sure to update the FLASH_DEVICE with the printer's unique serial port name. -When flashing for the first time, make sure that OctoPrint is not -connected directly to the printer (from the OctoPrint web page, under -the "Connection" section, click "Disconnect"). +For common micro-controllers using RP2040 chips, the code can be flashed +with something similar to: -## Configuring OctoPrint to use Klipper +``` +sudo service klipper stop +make flash FLASH_DEVICE=first +sudo service klipper start +``` -The OctoPrint web server needs to be configured to communicate with -the Klipper host software. Using a web browser, login to the OctoPrint -web page and then configure the following items: +It is important to note that RP2040 chips may need to be put into Boot mode +before this operation. -Navigate to the Settings tab (the wrench icon at the top of the -page). Under "Serial Connection" in "Additional serial ports" add -`/tmp/printer`. Then click "Save". - -Enter the Settings tab again and under "Serial Connection" change the -"Serial Port" setting to `/tmp/printer`. - -In the Settings tab, navigate to the "Behavior" sub-tab and select the -"Cancel any ongoing prints but stay connected to the printer" -option. Click "Save". - -From the main page, under the "Connection" section (at the top left of -the page) make sure the "Serial Port" is set to `/tmp/printer` and -click "Connect". (If `/tmp/printer` is not an available selection then -try reloading the page.) - -Once connected, navigate to the "Terminal" tab and type "status" -(without the quotes) into the command entry box and click "Send". The -terminal window will likely report there is an error opening the -config file - that means OctoPrint is successfully communicating with -Klipper. Proceed to the next section. ## Configuring Klipper The next step is to copy the [printer configuration file](#obtain-a-klipper-configuration-file) to -the Raspberry Pi. +the host. -Arguably the easiest way to set the Klipper configuration file is to -use a desktop editor that supports editing files over the "scp" and/or -"sftp" protocols. There are freely available tools that support this -(eg, Notepad++, WinSCP, and Cyberduck). Load the printer config file -in the editor and then save it as a file named `printer.cfg` in the -home directory of the pi user (ie, `/home/pi/printer.cfg`). +Arguably the easiest way to set the Klipper configuration file is using the +built in editors in Mainsail or Fluidd. These will allow the user to open +the configuration examples and save them to be printer.cfg. + +Another option is to use a desktop editor that supports editing files +over the "scp" and/or "sftp" protocols. There are freely available tools +that support this (eg, Notepad++, WinSCP, and Cyberduck). +Load the printer config file in the editor and then save it as a file +named "printer.cfg" in the home directory of the pi user +(ie, /home/pi/printer.cfg). Alternatively, one can also copy and edit the file directly on the -Raspberry Pi via ssh. That may look something like the following (be +host via ssh. That may look something like the following (be sure to update the command to use the appropriate printer config filename): @@ -201,7 +215,7 @@ serial: /dev/serial/by-id/usb-1a86_USB2.0-Serial-if00-port0 ``` After creating and editing the file it will be necessary to issue a -"restart" command in the OctoPrint web terminal to load the config. A +"restart" command in the command console to load the config. A "status" command will report the printer is ready if the Klipper config file is successfully read and the micro-controller is successfully found and configured. @@ -211,10 +225,10 @@ Klipper to report a configuration error. If an error occurs, make any necessary corrections to the printer config file and issue "restart" until "status" reports the printer is ready. -Klipper reports error messages via the OctoPrint terminal tab. The -"status" command can be used to re-report error messages. The default -Klipper startup script also places a log in **/tmp/klippy.log** which -provides more detailed information. +Klipper reports error messages via the command console and via pop up in +Fluidd and Mainsail. The "status" command can be used to re-report error +messages. A log is available and usually located in ~/printer_data/logs +this is named klippy.log After Klipper reports that the printer is ready, proceed to the [config check document](Config_checks.md) to perform some basic checks diff --git a/docs/OctoPrint.md b/docs/OctoPrint.md new file mode 100644 index 000000000..4fa1b0217 --- /dev/null +++ b/docs/OctoPrint.md @@ -0,0 +1,79 @@ +# OctoPrint for Klipper + +Klipper has a few options for its front ends, Octoprint was the first +and original front end for Klipper. This document will give +a brief overview of installing with this option. + +## Install with OctoPi + +Start by installing [OctoPi](https://github.com/guysoft/OctoPi) on the +Raspberry Pi computer. Use OctoPi v0.17.0 or later - see the +[OctoPi releases](https://github.com/guysoft/OctoPi/releases) for +release information. + +One should verify that OctoPi boots and that the +OctoPrint web server works. After connecting to the OctoPrint web +page, follow the prompt to upgrade OctoPrint if needed. + +After installing OctoPi and upgrading OctoPrint, it will be necessary +to ssh into the target machine to run a handful of system commands. + +Start by running these commands on your host device: + +__If you do not have git installed, please do so with:__ +``` +sudo apt install git +``` +then proceed: +``` +cd ~ +git clone https://github.com/Klipper3d/klipper +./klipper/scripts/install-octopi.sh +``` + +The above will download Klipper, install the needed system dependencies, +setup Klipper to run at system startup, and start the Klipper host +software. It will require an internet connection and it may take a few +minutes to complete. + +## Installing with KIAUH + +KIAUH can be used to install OctoPrint on a variety of Linux based systems +that run a form of Debian. More information can be found +at https://github.com/dw-0/kiauh + +## Configuring OctoPrint to use Klipper + +The OctoPrint web server needs to be configured to communicate with the Klipper +host software. Using a web browser, login to the OctoPrint web page and then +configure the following items: + +Navigate to the Settings tab (the wrench icon at the top of the page). +Under "Serial Connection" in "Additional serial ports" add: + +``` +~/printer_data/comms/klippy.sock +``` +Then click "Save". + +_In some older setups this address may be `/tmp/printer`_ + + +Enter the Settings tab again and under "Serial Connection" change the "Serial Port" +setting to the one added above. + +In the Settings tab, navigate to the "Behavior" sub-tab and select the +"Cancel any ongoing prints but stay connected to the printer" option. Click "Save". + +From the main page, under the "Connection" section (at the top left of the page) +make sure the "Serial Port" is set to the new additional one added +and click "Connect". (If it is not in the available selection then +try reloading the page.) + +Once connected, navigate to the "Terminal" tab and type "status" (without the quotes) +into the command entry box and click "Send". The terminal window will likely report +there is an error opening the config file - that means OctoPrint is successfully +communicating with Klipper. + +Please proceed to [Installation.md](Installation.md) and the +_Building and flashing the micro-controller_ section diff --git a/docs/Overview.md b/docs/Overview.md index 2b9253c26..1ab948910 100644 --- a/docs/Overview.md +++ b/docs/Overview.md @@ -17,6 +17,7 @@ communication with the Klipper developers. ## Installation and Configuration - [Installation](Installation.md): Guide to installing Klipper. + - [Octoprint](OctoPrint.md): Guide to installing Octoprint with Klipper. - [Config Reference](Config_Reference.md): Description of config parameters. - [Rotation Distance](Rotation_Distance.md): Calculating the diff --git a/docs/_klipper3d/mkdocs-requirements.txt b/docs/_klipper3d/mkdocs-requirements.txt index 739288959..96bf60051 100644 --- a/docs/_klipper3d/mkdocs-requirements.txt +++ b/docs/_klipper3d/mkdocs-requirements.txt @@ -1,5 +1,5 @@ # Python virtualenv module requirements for mkdocs -jinja2==3.1.3 +jinja2==3.1.4 mkdocs==1.2.4 mkdocs-material==8.1.3 mkdocs-simple-hooks==0.1.3 diff --git a/docs/_klipper3d/mkdocs.yml b/docs/_klipper3d/mkdocs.yml index c5da747b5..02d32fadc 100644 --- a/docs/_klipper3d/mkdocs.yml +++ b/docs/_klipper3d/mkdocs.yml @@ -71,7 +71,7 @@ extra: # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-analytics/#site-search-tracking analytics: provider: google - property: UA-138371409-1 + property: G-VEN1PGNQL4 # Language Selection alternate: - name: English @@ -88,7 +88,9 @@ nav: - Config_Changes.md - Contact.md - Installation and Configuration: - - Installation.md + - Installation: + - Installation.md + - OctoPrint.md - Configuration Reference: - Config_Reference.md - Rotation_Distance.md diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index e4199561d..fa1261be9 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -142,8 +142,9 @@ defs_kin_winch = """ defs_kin_extruder = """ struct stepper_kinematics *extruder_stepper_alloc(void); + void extruder_stepper_free(struct stepper_kinematics *sk); void extruder_set_pressure_advance(struct stepper_kinematics *sk - , double pressure_advance, double smooth_time); + , double print_time, double pressure_advance, double smooth_time); """ defs_kin_shaper = """ diff --git a/klippy/chelper/kin_extruder.c b/klippy/chelper/kin_extruder.c index b8d1cc221..0cd6523c3 100644 --- a/klippy/chelper/kin_extruder.c +++ b/klippy/chelper/kin_extruder.c @@ -9,9 +9,15 @@ #include // memset #include "compiler.h" // __visible #include "itersolve.h" // struct stepper_kinematics +#include "list.h" // list_node #include "pyhelper.h" // errorf #include "trapq.h" // move_get_distance +struct pa_params { + double pressure_advance, active_print_time; + struct list_node node; +}; + // Without pressure advance, the extruder stepper position is: // extruder_position(t) = nominal_position(t) // When pressure advance is enabled, additional filament is pushed @@ -52,17 +58,25 @@ extruder_integrate_time(double base, double start_v, double half_accel // Calculate the definitive integral of extruder for a given move static double -pa_move_integrate(struct move *m, double pressure_advance +pa_move_integrate(struct move *m, struct list_head *pa_list , double base, double start, double end, double time_offset) { if (start < 0.) start = 0.; if (end > m->move_t) end = m->move_t; - // Calculate base position and velocity with pressure advance + // Determine pressure_advance value int can_pressure_advance = m->axes_r.y != 0.; - if (!can_pressure_advance) - pressure_advance = 0.; + double pressure_advance = 0.; + if (can_pressure_advance) { + struct pa_params *pa = list_last_entry(pa_list, struct pa_params, node); + while (unlikely(pa->active_print_time > m->print_time) && + !list_is_first(&pa->node, pa_list)) { + pa = list_prev_entry(pa, node); + } + pressure_advance = pa->pressure_advance; + } + // Calculate base position and velocity with pressure advance base += pressure_advance * m->start_v; double start_v = m->start_v + pressure_advance * 2. * m->half_accel; // Calculate definitive integral @@ -75,20 +89,20 @@ pa_move_integrate(struct move *m, double pressure_advance // Calculate the definitive integral of the extruder over a range of moves static double pa_range_integrate(struct move *m, double move_time - , double pressure_advance, double hst) + , struct list_head *pa_list, double hst) { // Calculate integral for the current move double res = 0., start = move_time - hst, end = move_time + hst; double start_base = m->start_pos.x; - res += pa_move_integrate(m, pressure_advance, 0., start, move_time, start); - res -= pa_move_integrate(m, pressure_advance, 0., move_time, end, end); + res += pa_move_integrate(m, pa_list, 0., start, move_time, start); + res -= pa_move_integrate(m, pa_list, 0., move_time, end, end); // Integrate over previous moves struct move *prev = m; while (unlikely(start < 0.)) { prev = list_prev_entry(prev, node); start += prev->move_t; double base = prev->start_pos.x - start_base; - res += pa_move_integrate(prev, pressure_advance, base, start + res += pa_move_integrate(prev, pa_list, base, start , prev->move_t, start); } // Integrate over future moves @@ -96,14 +110,15 @@ pa_range_integrate(struct move *m, double move_time end -= m->move_t; m = list_next_entry(m, node); double base = m->start_pos.x - start_base; - res -= pa_move_integrate(m, pressure_advance, base, 0., end, end); + res -= pa_move_integrate(m, pa_list, base, 0., end, end); } return res; } struct extruder_stepper { struct stepper_kinematics sk; - double pressure_advance, half_smooth_time, inv_half_smooth_time2; + struct list_head pa_list; + double half_smooth_time, inv_half_smooth_time2; }; static double @@ -116,22 +131,45 @@ extruder_calc_position(struct stepper_kinematics *sk, struct move *m // Pressure advance not enabled return m->start_pos.x + move_get_distance(m, move_time); // Apply pressure advance and average over smooth_time - double area = pa_range_integrate(m, move_time, es->pressure_advance, hst); + double area = pa_range_integrate(m, move_time, &es->pa_list, hst); return m->start_pos.x + area * es->inv_half_smooth_time2; } void __visible -extruder_set_pressure_advance(struct stepper_kinematics *sk +extruder_set_pressure_advance(struct stepper_kinematics *sk, double print_time , double pressure_advance, double smooth_time) { struct extruder_stepper *es = container_of(sk, struct extruder_stepper, sk); - double hst = smooth_time * .5; + double hst = smooth_time * .5, old_hst = es->half_smooth_time; es->half_smooth_time = hst; es->sk.gen_steps_pre_active = es->sk.gen_steps_post_active = hst; + + // Cleanup old pressure advance parameters + double cleanup_time = sk->last_flush_time - (old_hst > hst ? old_hst : hst); + struct pa_params *first_pa = list_first_entry( + &es->pa_list, struct pa_params, node); + while (!list_is_last(&first_pa->node, &es->pa_list)) { + struct pa_params *next_pa = list_next_entry(first_pa, node); + if (next_pa->active_print_time >= cleanup_time) break; + list_del(&first_pa->node); + first_pa = next_pa; + } + if (! hst) return; es->inv_half_smooth_time2 = 1. / (hst * hst); - es->pressure_advance = pressure_advance; + + if (list_last_entry(&es->pa_list, struct pa_params, node)->pressure_advance + == pressure_advance) { + // Retain old pa_params + return; + } + // Add new pressure advance parameters + struct pa_params *pa = malloc(sizeof(*pa)); + memset(pa, 0, sizeof(*pa)); + pa->pressure_advance = pressure_advance; + pa->active_print_time = print_time; + list_add_tail(&pa->node, &es->pa_list); } struct stepper_kinematics * __visible @@ -141,5 +179,22 @@ extruder_stepper_alloc(void) memset(es, 0, sizeof(*es)); es->sk.calc_position_cb = extruder_calc_position; es->sk.active_flags = AF_X; + list_init(&es->pa_list); + struct pa_params *pa = malloc(sizeof(*pa)); + memset(pa, 0, sizeof(*pa)); + list_add_tail(&pa->node, &es->pa_list); return &es->sk; } + +void __visible +extruder_stepper_free(struct stepper_kinematics *sk) +{ + struct extruder_stepper *es = container_of(sk, struct extruder_stepper, sk); + while (!list_empty(&es->pa_list)) { + struct pa_params *pa = list_first_entry( + &es->pa_list, struct pa_params, node); + list_del(&pa->node); + free(pa); + } + free(sk); +} diff --git a/klippy/configfile.py b/klippy/configfile.py index 91b555cde..a8a4a4ff7 100644 --- a/klippy/configfile.py +++ b/klippy/configfile.py @@ -69,6 +69,8 @@ class ConfigWrapper: return self._get_wrapper(self.fileconfig.getboolean, option, default, note_valid=note_valid) def getchoice(self, option, choices, default=sentinel, note_valid=True): + if type(choices) == type([]): + choices = {i: i for i in choices} if choices and type(list(choices.keys())[0]) == int: c = self.getint(option, default, note_valid=note_valid) else: diff --git a/klippy/extras/adc_scaled.py b/klippy/extras/adc_scaled.py index c2d2cb877..80ea452f3 100644 --- a/klippy/extras/adc_scaled.py +++ b/klippy/extras/adc_scaled.py @@ -7,7 +7,6 @@ SAMPLE_TIME = 0.001 SAMPLE_COUNT = 8 REPORT_TIME = 0.300 -RANGE_CHECK_COUNT = 4 class MCU_scaled_adc: def __init__(self, main, pin_params): @@ -18,7 +17,7 @@ class MCU_scaled_adc: qname = main.name + ":" + pin_params['pin'] query_adc.register_adc(qname, self._mcu_adc) self._callback = None - self.setup_minmax = self._mcu_adc.setup_minmax + self.setup_adc_sample = self._mcu_adc.setup_adc_sample self.get_mcu = self._mcu_adc.get_mcu def _handle_callback(self, read_time, read_value): max_adc = self._main.last_vref[1] @@ -54,8 +53,7 @@ class PrinterADCScaled: ppins = self.printer.lookup_object('pins') mcu_adc = ppins.setup_pin('adc', pin_name) mcu_adc.setup_adc_callback(REPORT_TIME, callback) - mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, minval=0., maxval=1., - range_check_count=RANGE_CHECK_COUNT) + mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT) query_adc = config.get_printer().load_object(config, 'query_adc') query_adc.register_adc(self.name + ":" + name, mcu_adc) return mcu_adc diff --git a/klippy/extras/adc_temperature.py b/klippy/extras/adc_temperature.py index b76e8c66f..c53ae7056 100644 --- a/klippy/extras/adc_temperature.py +++ b/klippy/extras/adc_temperature.py @@ -1,6 +1,6 @@ # Obtain temperature using linear interpolation of ADC values # -# Copyright (C) 2016-2018 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import logging, bisect @@ -22,8 +22,8 @@ class PrinterADCtoTemperature: ppins = config.get_printer().lookup_object('pins') self.mcu_adc = ppins.setup_pin('adc', config.get('sensor_pin')) self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback) - query_adc = config.get_printer().load_object(config, 'query_adc') - query_adc.register_adc(config.get_name(), self.mcu_adc) + self.diag_helper = HelperTemperatureDiagnostics( + config, self.mcu_adc, adc_convert.calc_temp) def setup_callback(self, temperature_callback): self.temperature_callback = temperature_callback def get_report_time_delta(self): @@ -32,10 +32,44 @@ class PrinterADCtoTemperature: temp = self.adc_convert.calc_temp(read_value) self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) def setup_minmax(self, min_temp, max_temp): - adc_range = [self.adc_convert.calc_adc(t) for t in [min_temp, max_temp]] - self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, - minval=min(adc_range), maxval=max(adc_range), - range_check_count=RANGE_CHECK_COUNT) + arange = [self.adc_convert.calc_adc(t) for t in [min_temp, max_temp]] + min_adc, max_adc = sorted(arange) + self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT, + minval=min_adc, maxval=max_adc, + range_check_count=RANGE_CHECK_COUNT) + self.diag_helper.setup_diag_minmax(min_temp, max_temp, min_adc, max_adc) + +# Tool to register with query_adc and report extra info on ADC range errors +class HelperTemperatureDiagnostics: + def __init__(self, config, mcu_adc, calc_temp_cb): + self.printer = config.get_printer() + self.name = config.get_name() + self.mcu_adc = mcu_adc + self.calc_temp_cb = calc_temp_cb + self.min_temp = self.max_temp = self.min_adc = self.max_adc = None + query_adc = self.printer.load_object(config, 'query_adc') + query_adc.register_adc(self.name, self.mcu_adc) + error_mcu = self.printer.load_object(config, 'error_mcu') + error_mcu.add_clarify("ADC out of range", self._clarify_adc_range) + def setup_diag_minmax(self, min_temp, max_temp, min_adc, max_adc): + self.min_temp, self.max_temp = min_temp, max_temp + self.min_adc, self.max_adc = min_adc, max_adc + def _clarify_adc_range(self, msg, details): + if self.min_temp is None: + return None + last_value, last_read_time = self.mcu_adc.get_last_value() + if not last_read_time: + return None + if last_value >= self.min_adc and last_value <= self.max_adc: + return None + tempstr = "?" + try: + last_temp = self.calc_temp_cb(last_value) + tempstr = "%.3f" % (last_temp,) + except e: + logging.exception("Error in calc_temp callback") + return ("Sensor '%s' temperature %s not in range %.3f:%.3f" + % (self.name, tempstr, self.min_temp, self.max_temp)) ###################################################################### diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py new file mode 100644 index 000000000..14d47581e --- /dev/null +++ b/klippy/extras/ads1220.py @@ -0,0 +1,187 @@ +# ADS1220 Support +# +# Copyright (C) 2024 Gareth Farrington +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from . import bulk_sensor, bus + +# +# Constants +# +BYTES_PER_SAMPLE = 4 # samples are 4 byte wide unsigned integers +MAX_SAMPLES_PER_MESSAGE = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE +UPDATE_INTERVAL = 0.10 +RESET_CMD = 0x06 +START_SYNC_CMD = 0x08 +RREG_CMD = 0x20 +WREG_CMD = 0x40 +NOOP_CMD = 0x0 +RESET_STATE = bytearray([0x0, 0x0, 0x0, 0x0]) + +# turn bytearrays into pretty hex strings: [0xff, 0x1] +def hexify(byte_array): + return "[%s]" % (", ".join([hex(b) for b in byte_array])) + + +class ADS1220(): + def __init__(self, config): + self.printer = printer = config.get_printer() + self.name = config.get_name().split()[-1] + self.last_error_count = 0 + self.consecutive_fails = 0 + # Chip options + # Gain + self.gain_options = {'1': 0x0, '2': 0x1, '4': 0x2, '8': 0x3, '16': 0x4, + '32': 0x5, '64': 0x6, '128': 0x7} + self.gain = config.getchoice('gain', self.gain_options, default='128') + # Sample rate + self.sps_normal = {'20': 20, '45': 45, '90': 90, '175': 175, + '330': 330, '600': 600, '1000': 1000} + self.sps_turbo = {'40': 40, '90': 90, '180': 180, '350': 350, + '660': 660, '1200': 1200, '2000': 2000} + self.sps_options = self.sps_normal.copy() + self.sps_options.update(self.sps_turbo) + self.sps = config.getchoice('sps', self.sps_options, default='660') + self.is_turbo = str(self.sps) in self.sps_turbo + # SPI Setup + spi_speed = 512000 if self.is_turbo else 256000 + self.spi = bus.MCU_SPI_from_config(config, 1, default_speed=spi_speed) + self.mcu = mcu = self.spi.get_mcu() + self.oid = mcu.create_oid() + # Data Ready (DRDY) Pin + drdy_pin = config.get('data_ready_pin') + ppins = printer.lookup_object('pins') + drdy_ppin = ppins.lookup_pin(drdy_pin) + self.data_ready_pin = drdy_ppin['pin'] + drdy_pin_mcu = drdy_ppin['chip'] + if drdy_pin_mcu != self.mcu: + raise config.error("ADS1220 config error: SPI communication and" + " data_ready_pin must be on the same MCU") + # Bulk Sensor Setup + self.bulk_queue = bulk_sensor.BulkDataQueue(self.mcu, oid=self.oid) + # Clock tracking + chip_smooth = self.sps * UPDATE_INTERVAL * 2 + # Measurement conversion + self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, " .5mm: orig pt = (%.2f, %.2f)" + ", probed pt = (%.2f, %.2f)" + % (offset_pos[0], offset_pos[1], result[0], result[1]) + ) + z_pos = result[2] - z_offset if not isclose(pos[1], prev_pos[1], abs_tol=.1): # y has changed, append row and start new probed_matrix.append(row) row = [] if pos[0] > prev_pos[0]: # probed in the positive direction - row.append(pos[2] - z_offset) + row.append(z_pos) else: # probed in the negative direction - row.insert(0, pos[2] - z_offset) + row.insert(0, z_pos) prev_pos = pos # append last row probed_matrix.append(row) @@ -863,11 +773,12 @@ class BedMeshCalibrate: z_mesh.build_mesh(probed_matrix) except BedMeshError as e: raise self.gcode.error(str(e)) - if self.zero_reference_mode == ZrefMode.IN_MESH: + if self.probe_mgr.get_zero_ref_mode() == ZrefMode.IN_MESH: # The reference can be anywhere in the mesh, therefore # it is necessary to set the reference after the initial mesh # is generated to lookup the correct z value. - z_mesh.set_zero_reference(*self.zero_ref_pos) + zero_ref_pos = self.probe_mgr.get_zero_ref_pos() + z_mesh.set_zero_reference(*zero_ref_pos) self.bedmesh.set_mesh(z_mesh) self.gcode.respond_info("Mesh Bed Leveling Complete") if self._profile_name is not None: @@ -875,14 +786,15 @@ class BedMeshCalibrate: def _dump_points(self, probed_pts, corrected_pts, offsets): # logs generated points with offset applied, points received # from the finalize callback, and the list of corrected points - max_len = max([len(self.points), len(probed_pts), len(corrected_pts)]) + points = self.probe_mgr.get_base_points() + max_len = max([len(points), len(probed_pts), len(corrected_pts)]) logging.info( "bed_mesh: calibration point dump\nIndex | %-17s| %-25s|" " Corrected Point" % ("Generated Point", "Probed Point")) for i in list(range(max_len)): gen_pt = probed_pt = corr_pt = "" - if i < len(self.points): - off_pt = [p - o for p, o in zip(self.points[i], offsets[:2])] + if i < len(points): + off_pt = [p - o for p, o in zip(points[i], offsets[:2])] gen_pt = "(%.2f, %.2f)" % tuple(off_pt) if i < len(probed_pts): probed_pt = "(%.2f, %.2f, %.4f)" % tuple(probed_pts[i]) @@ -891,6 +803,453 @@ class BedMeshCalibrate: logging.info( " %-4d| %-17s| %-25s| %s" % (i, gen_pt, probed_pt, corr_pt)) +class ProbeManager: + def __init__(self, config, orig_config, finalize_cb): + self.printer = config.get_printer() + self.cfg_overshoot = config.getfloat("scan_overshoot", 0, minval=1.) + self.orig_config = orig_config + self.faulty_regions = [] + self.overshoot = self.cfg_overshoot + self.zero_ref_pos = config.getfloatlist( + "zero_reference_position", None, count=2 + ) + self.zref_mode = ZrefMode.DISABLED + self.base_points = [] + self.substitutes = collections.OrderedDict() + self.is_round = orig_config["radius"] is not None + self.probe_helper = probe.ProbePointsHelper(config, finalize_cb, []) + self.probe_helper.use_xy_offsets(True) + self.rapid_scan_helper = RapidScanHelper(config, self, finalize_cb) + self._init_faulty_regions(config) + + def _init_faulty_regions(self, config): + for i in list(range(1, 100, 1)): + start = config.getfloatlist("faulty_region_%d_min" % (i,), None, + count=2) + if start is None: + break + end = config.getfloatlist("faulty_region_%d_max" % (i,), count=2) + # Validate the corners. If necessary reorganize them. + # c1 = min point, c3 = max point + # c4 ---- c3 + # | | + # c1 ---- c2 + c1 = [min([s, e]) for s, e in zip(start, end)] + c3 = [max([s, e]) for s, e in zip(start, end)] + c2 = [c1[0], c3[1]] + c4 = [c3[0], c1[1]] + # Check for overlapping regions + for j, (prev_c1, prev_c3) in enumerate(self.faulty_regions): + prev_c2 = [prev_c1[0], prev_c3[1]] + prev_c4 = [prev_c3[0], prev_c1[1]] + # Validate that no existing corner is within the new region + for coord in [prev_c1, prev_c2, prev_c3, prev_c4]: + if within(coord, c1, c3): + raise config.error( + "bed_mesh: Existing faulty_region_%d %s overlaps " + "added faulty_region_%d %s" + % (j+1, repr([prev_c1, prev_c3]), + i, repr([c1, c3]))) + # Validate that no new corner is within an existing region + for coord in [c1, c2, c3, c4]: + if within(coord, prev_c1, prev_c3): + raise config.error( + "bed_mesh: Added faulty_region_%d %s overlaps " + "existing faulty_region_%d %s" + % (i, repr([c1, c3]), + j+1, repr([prev_c1, prev_c3]))) + self.faulty_regions.append((c1, c3)) + + def start_probe(self, gcmd): + method = gcmd.get("METHOD", "automatic").lower() + can_scan = False + pprobe = self.printer.lookup_object("probe", None) + if pprobe is not None: + probe_name = pprobe.get_status(None).get("name", "") + can_scan = probe_name.startswith("probe_eddy_current") + if method == "rapid_scan" and can_scan: + self.rapid_scan_helper.perform_rapid_scan(gcmd) + else: + self.probe_helper.start_probe(gcmd) + + def get_zero_ref_pos(self): + return self.zero_ref_pos + + def get_zero_ref_mode(self): + return self.zref_mode + + def get_substitutes(self): + return self.substitutes + + def generate_points( + self, mesh_config, mesh_min, mesh_max, radius, origin, + probe_method="automatic" + ): + x_cnt = mesh_config['x_count'] + y_cnt = mesh_config['y_count'] + min_x, min_y = mesh_min + max_x, max_y = mesh_max + x_dist = (max_x - min_x) / (x_cnt - 1) + y_dist = (max_y - min_y) / (y_cnt - 1) + # floor distances down to next hundredth + x_dist = math.floor(x_dist * 100) / 100 + y_dist = math.floor(y_dist * 100) / 100 + if x_dist < 1. or y_dist < 1.: + raise BedMeshError("bed_mesh: min/max points too close together") + + if radius is not None: + # round bed, min/max needs to be recalculated + y_dist = x_dist + new_r = (x_cnt // 2) * x_dist + min_x = min_y = -new_r + max_x = max_y = new_r + else: + # rectangular bed, only re-calc max_x + max_x = min_x + x_dist * (x_cnt - 1) + pos_y = min_y + points = [] + for i in range(y_cnt): + for j in range(x_cnt): + if not i % 2: + # move in positive directon + pos_x = min_x + j * x_dist + else: + # move in negative direction + pos_x = max_x - j * x_dist + if radius is None: + # rectangular bed, append + points.append((pos_x, pos_y)) + else: + # round bed, check distance from origin + dist_from_origin = math.sqrt(pos_x*pos_x + pos_y*pos_y) + if dist_from_origin <= radius: + points.append( + (origin[0] + pos_x, origin[1] + pos_y)) + pos_y += y_dist + if self.zero_ref_pos is None or probe_method == "manual": + # Zero Reference Disabled + self.zref_mode = ZrefMode.DISABLED + elif within(self.zero_ref_pos, mesh_min, mesh_max): + # Zero Reference position within mesh + self.zref_mode = ZrefMode.IN_MESH + else: + # Zero Reference position outside of mesh + self.zref_mode = ZrefMode.PROBE + self.base_points = points + self.substitutes.clear() + # adjust overshoot + og_min_x = self.orig_config["mesh_min"][0] + og_max_x = self.orig_config["mesh_max"][0] + add_ovs = min(max(0, min_x - og_min_x), max(0, og_max_x - max_x)) + self.overshoot = self.cfg_overshoot + math.floor(add_ovs) + min_pt, max_pt = (min_x, min_y), (max_x, max_y) + self._process_faulty_regions(min_pt, max_pt, radius) + self.probe_helper.update_probe_points(self.get_std_path(), 3) + + def _process_faulty_regions(self, min_pt, max_pt, radius): + if not self.faulty_regions: + return + # Cannot probe a reference within a faulty region + if self.zref_mode == ZrefMode.PROBE: + for min_c, max_c in self.faulty_regions: + if within(self.zero_ref_pos, min_c, max_c): + opt = "zero_reference_position" + raise BedMeshError( + "bed_mesh: Cannot probe zero reference position at " + "(%.2f, %.2f) as it is located within a faulty region." + " Check the value for option '%s'" + % (self.zero_ref_pos[0], self.zero_ref_pos[1], opt,) + ) + # Check to see if any points fall within faulty regions + last_y = self.base_points[0][1] + is_reversed = False + for i, coord in enumerate(self.base_points): + if not isclose(coord[1], last_y): + is_reversed = not is_reversed + last_y = coord[1] + adj_coords = [] + for min_c, max_c in self.faulty_regions: + if within(coord, min_c, max_c, tol=.00001): + # Point lies within a faulty region + adj_coords = [ + (min_c[0], coord[1]), (coord[0], min_c[1]), + (coord[0], max_c[1]), (max_c[0], coord[1])] + if is_reversed: + # Swap first and last points for zig-zag pattern + first = adj_coords[0] + adj_coords[0] = adj_coords[-1] + adj_coords[-1] = first + break + if not adj_coords: + # coord is not located within a faulty region + continue + valid_coords = [] + for ac in adj_coords: + # make sure that coordinates are within the mesh boundary + if radius is None: + if within(ac, min_pt, max_pt, .000001): + valid_coords.append(ac) + else: + dist_from_origin = math.sqrt(ac[0]*ac[0] + ac[1]*ac[1]) + if dist_from_origin <= radius: + valid_coords.append(ac) + if not valid_coords: + raise BedMeshError( + "bed_mesh: Unable to generate coordinates" + " for faulty region at index: %d" % (i) + ) + self.substitutes[i] = valid_coords + + def get_base_points(self): + return self.base_points + + def get_std_path(self): + path = [] + for idx, pt in enumerate(self.base_points): + if idx in self.substitutes: + for sub_pt in self.substitutes[idx]: + path.append(sub_pt) + else: + path.append(pt) + if self.zref_mode == ZrefMode.PROBE: + path.append(self.zero_ref_pos) + return path + + def iter_rapid_path(self): + ascnd_x = True + last_base_pt = last_mv_pt = self.base_points[0] + # Generate initial move point + if self.overshoot: + overshoot = min(8, self.overshoot) + last_mv_pt = (last_base_pt[0] - overshoot, last_base_pt[1]) + yield last_mv_pt, False + for idx, pt in enumerate(self.base_points): + # increasing Y indicates direction change + dir_change = not isclose(pt[1], last_base_pt[1], abs_tol=1e-6) + if idx in self.substitutes: + fp_gen = self._gen_faulty_path( + last_mv_pt, idx, ascnd_x, dir_change + ) + for sub_pt, is_smp in fp_gen: + yield sub_pt, is_smp + last_mv_pt = sub_pt + else: + if dir_change: + for dpt in self._gen_dir_change(last_mv_pt, pt, ascnd_x): + yield dpt, False + yield pt, True + last_mv_pt = pt + last_base_pt = pt + ascnd_x ^= dir_change + if self.zref_mode == ZrefMode.PROBE: + if self.overshoot: + ovs = min(4, self.overshoot) + ovs = ovs if ascnd_x else -ovs + yield (last_mv_pt[0] + ovs, last_mv_pt[1]), False + yield self.zero_ref_pos, True + + def _gen_faulty_path(self, last_pt, idx, ascnd_x, dir_change): + subs = self.substitutes[idx] + sub_cnt = len(subs) + if dir_change: + for dpt in self._gen_dir_change(last_pt, subs[0], ascnd_x): + yield dpt, False + if self.is_round: + # No faulty region path handling for round beds + for pt in subs: + yield pt, True + return + # Check to see if this is the first corner + first_corner = False + sorted_sub_idx = sorted(self.substitutes.keys()) + if sub_cnt == 2 and idx < len(sorted_sub_idx): + first_corner = sorted_sub_idx[idx] == idx + yield subs[0], True + if sub_cnt == 1: + return + last_pt, next_pt = subs[:2] + if sub_cnt == 2: + if first_corner or dir_change: + # horizontal move first + yield (next_pt[0], last_pt[1]), False + else: + yield (last_pt[0], next_pt[1]), False + yield next_pt, True + elif sub_cnt >= 3: + if dir_change: + # first move should be a vertical switch up. If overshoot + # is available, simulate another direction change. Otherwise + # move inward 2 mm, then up through the faulty region. + if self.overshoot: + for dpt in self._gen_dir_change(last_pt, next_pt, ascnd_x): + yield dpt, False + else: + shift = -2 if ascnd_x else 2 + yield (last_pt[0] + shift, last_pt[1]), False + yield (last_pt[0] + shift, next_pt[1]), False + yield next_pt, True + last_pt, next_pt = subs[1:3] + else: + # vertical move + yield (last_pt[0], next_pt[1]), False + yield next_pt, True + last_pt, next_pt = subs[1:3] + if sub_cnt == 4: + # Vertical switch up within faulty region + shift = 2 if ascnd_x else -2 + yield (last_pt[0] + shift, last_pt[1]), False + yield (next_pt[0] - shift, next_pt[1]), False + yield next_pt, True + last_pt, next_pt = subs[2:4] + # horizontal move before final point + yield (next_pt[0], last_pt[1]), False + yield next_pt, True + + def _gen_dir_change(self, last_pt, next_pt, ascnd_x): + if not self.overshoot: + return + # overshoot X beyond the outer point + xdir = 1 if ascnd_x else -1 + overshoot = 2. if self.overshoot >= 3. else self.overshoot + ovr_pt = (last_pt[0] + overshoot * xdir, last_pt[1]) + yield ovr_pt + if self.overshoot < 3.: + # No room to generate an arc, move up to next y + yield (next_pt[0] + overshoot * xdir, next_pt[1]) + else: + # generate arc + STEP_ANGLE = 3 + START_ANGLE = 270 + ydiff = abs(next_pt[1] - last_pt[1]) + xdiff = abs(next_pt[0] - last_pt[0]) + max_radius = min(self.overshoot - 2, 8) + radius = min(ydiff / 2, max_radius) + origin = [ovr_pt[0], last_pt[1] + radius] + next_origin_y = next_pt[1] - radius + # determine angle + if xdiff < .01: + # Move is aligned on the x-axis + angle = 90 + if next_origin_y - origin[1] < .05: + # The move can be completed in a single arc + angle = 180 + else: + angle = int(math.degrees(math.atan(ydiff / xdiff))) + if ( + (ascnd_x and next_pt[0] < last_pt[0]) or + (not ascnd_x and next_pt[0] > last_pt[0]) + ): + angle = 180 - angle + count = int(angle // STEP_ANGLE) + # Gen first arc + step = STEP_ANGLE * xdir + start = START_ANGLE + step + for arc_pt in self._gen_arc(origin, radius, start, step, count): + yield arc_pt + if angle == 180: + # arc complete + return + # generate next arc + origin = [next_pt[0] + overshoot * xdir, next_origin_y] + # start at the angle where the last arc finished + start = START_ANGLE + count * step + # recalculate the count to make sure we generate a full 180 + # degrees. Add a step for the repeated connecting angle + count = 61 - count + for arc_pt in self._gen_arc(origin, radius, start, step, count): + yield arc_pt + + def _gen_arc(self, origin, radius, start, step, count): + end = start + step * count + # create a segent for every 3 degress of travel + for angle in range(start, end, step): + rad = math.radians(angle % 360) + opp = math.sin(rad) * radius + adj = math.cos(rad) * radius + yield (origin[0] + adj, origin[1] + opp) + + +MAX_HIT_DIST = 2. +MM_WIN_SPEED = 125 + +class RapidScanHelper: + def __init__(self, config, probe_mgr, finalize_cb): + self.printer = config.get_printer() + self.probe_manager = probe_mgr + self.speed = config.getfloat("speed", 50., above=0.) + self.scan_height = config.getfloat("horizontal_move_z", 5.) + self.finalize_callback = finalize_cb + + def perform_rapid_scan(self, gcmd): + speed = gcmd.get_float("SCAN_SPEED", self.speed) + scan_height = gcmd.get_float("HORIZONTAL_MOVE_Z", self.scan_height) + gcmd.respond_info( + "Beginning rapid surface scan at height %.2f..." % (scan_height) + ) + pprobe = self.printer.lookup_object("probe") + toolhead = self.printer.lookup_object("toolhead") + # Calculate time window around which a sample is valid. Current + # assumption is anything within 2mm is usable, so: + # window = 2 / max_speed + # + # TODO: validate maximum speed allowed based on sample rate of probe + # Scale the hit distance window for speeds lower than 125mm/s. The + # lower the speed the less the window shrinks. + scale = max(0, 1 - speed / MM_WIN_SPEED) + 1 + hit_dist = min(MAX_HIT_DIST, scale * speed / MM_WIN_SPEED) + half_window = hit_dist / speed + gcmd.respond_info( + "Sample hit distance +/- %.4fmm, time window +/- ms %.4f" + % (hit_dist, half_window * 1000) + ) + gcmd_params = gcmd.get_command_parameters() + gcmd_params["SAMPLE_TIME"] = half_window * 2 + self._raise_tool(gcmd, scan_height) + probe_session = pprobe.start_probe_session(gcmd) + offsets = pprobe.get_offsets() + initial_move = True + for pos, is_probe_pt in self.probe_manager.iter_rapid_path(): + pos = self._apply_offsets(pos[:2], offsets) + toolhead.manual_move(pos, speed) + if initial_move: + initial_move = False + self._move_to_scan_height(gcmd, scan_height) + if is_probe_pt: + probe_session.run_probe(gcmd) + results = probe_session.pull_probed_results() + toolhead.get_last_move_time() + self.finalize_callback(offsets, results) + probe_session.end_probe_session() + + def _raise_tool(self, gcmd, scan_height): + # If the nozzle is below scan height raise the tool + toolhead = self.printer.lookup_object("toolhead") + pprobe = self.printer.lookup_object("probe") + cur_pos = toolhead.get_position() + if cur_pos[2] >= scan_height: + return + pparams = pprobe.get_probe_params(gcmd) + lift_speed = pparams["lift_speed"] + cur_pos[2] = self.scan_height + .5 + toolhead.manual_move(cur_pos, lift_speed) + + def _move_to_scan_height(self, gcmd, scan_height): + time_window = gcmd.get_float("SAMPLE_TIME") + toolhead = self.printer.lookup_object("toolhead") + pprobe = self.printer.lookup_object("probe") + cur_pos = toolhead.get_position() + pparams = pprobe.get_probe_params(gcmd) + lift_speed = pparams["lift_speed"] + probe_speed = pparams["probe_speed"] + cur_pos[2] = scan_height + .5 + toolhead.manual_move(cur_pos, lift_speed) + cur_pos[2] = scan_height + toolhead.manual_move(cur_pos, probe_speed) + toolhead.dwell(time_window / 2 + .01) + + def _apply_offsets(self, point, offsets): + return [(pos - ofs) for pos, ofs in zip(point, offsets)] + class MoveSplitter: def __init__(self, config, gcode): diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 49385428a..ae461f4b8 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -1,6 +1,6 @@ # BLTouch support # -# Copyright (C) 2018-2021 Kevin O'Connor +# Copyright (C) 2018-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import logging @@ -23,13 +23,9 @@ Commands = { } # BLTouch "endstop" wrapper -class BLTouchEndstopWrapper: +class BLTouchProbe: def __init__(self, config): self.printer = config.get_printer() - self.printer.register_event_handler("klippy:connect", - self.handle_connect) - self.printer.register_event_handler('klippy:mcu_identify', - self.handle_mcu_identify) self.position_endstop = config.getfloat('z_offset', minval=0.) self.stow_on_each_sample = config.getboolean('stow_on_each_sample', True) @@ -44,12 +40,9 @@ class BLTouchEndstopWrapper: self.next_cmd_time = self.action_end_time = 0. self.finish_home_complete = self.wait_trigger_complete = None # Create an "endstop" object to handle the sensor pin - pin = config.get('sensor_pin') - pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True) - mcu = pin_params['chip'] - self.mcu_endstop = mcu.setup_pin('endstop', pin_params) + self.mcu_endstop = ppins.setup_pin('endstop', config.get('sensor_pin')) # output mode - omodes = {'5V': '5V', 'OD': 'OD', None: None} + omodes = ['5V', 'OD', None] self.output_mode = config.getchoice('set_output_mode', omodes, None) # Setup for sensor test self.next_test_time = 0. @@ -65,19 +58,30 @@ class BLTouchEndstopWrapper: self.get_steppers = self.mcu_endstop.get_steppers self.home_wait = self.mcu_endstop.home_wait self.query_endstop = self.mcu_endstop.query_endstop + # multi probes state + self.multi = 'OFF' + # Common probe implementation helpers + self.cmd_helper = probe.ProbeCommandHelper( + config, self, self.mcu_endstop.query_endstop) + self.probe_offsets = probe.ProbeOffsetsHelper(config) + self.probe_session = probe.ProbeSessionHelper(config, self) # Register BLTOUCH_DEBUG command self.gcode = self.printer.lookup_object('gcode') self.gcode.register_command("BLTOUCH_DEBUG", self.cmd_BLTOUCH_DEBUG, desc=self.cmd_BLTOUCH_DEBUG_help) self.gcode.register_command("BLTOUCH_STORE", self.cmd_BLTOUCH_STORE, desc=self.cmd_BLTOUCH_STORE_help) - # multi probes state - self.multi = 'OFF' - 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) + # Register events + self.printer.register_event_handler("klippy:connect", + self.handle_connect) + def get_probe_params(self, gcmd=None): + return self.probe_session.get_probe_params(gcmd) + def get_offsets(self): + return self.probe_offsets.get_offsets() + def get_status(self, eventtime): + return self.cmd_helper.get_status(eventtime) + def start_probe_session(self, gcmd): + return self.probe_session.start_probe_session(gcmd) def handle_connect(self): self.sync_mcu_print_time() self.next_cmd_time += 0.200 @@ -116,7 +120,11 @@ class BLTouchEndstopWrapper: self.mcu_endstop.home_start(self.action_end_time, ENDSTOP_SAMPLE_TIME, ENDSTOP_SAMPLE_COUNT, ENDSTOP_REST_TIME, triggered=triggered) - trigger_time = self.mcu_endstop.home_wait(self.action_end_time + 0.100) + try: + trigger_time = self.mcu_endstop.home_wait( + self.action_end_time + 0.100) + except self.printer.command_error as e: + return False return trigger_time > 0. def raise_probe(self): self.sync_mcu_print_time() @@ -274,6 +282,6 @@ class BLTouchEndstopWrapper: self.sync_print_time() def load_config(config): - blt = BLTouchEndstopWrapper(config) - config.get_printer().add_object('probe', probe.PrinterProbe(config, blt)) + blt = BLTouchProbe(config) + config.get_printer().add_object('probe', blt) return blt diff --git a/klippy/extras/bme280.py b/klippy/extras/bme280.py index 262dc130f..1c26bbee7 100644 --- a/klippy/extras/bme280.py +++ b/klippy/extras/bme280.py @@ -83,6 +83,7 @@ BMP180_REGS = { STATUS_MEASURING = 1 << 3 STATUS_IM_UPDATE = 1 MODE = 1 +MODE_PERIODIC = 3 RUN_GAS = 1 << 4 NB_CONV_0 = 0 EAS_NEW_DATA = 1 << 7 @@ -143,6 +144,7 @@ class BME280: pow(2, self.os_temp - 1), pow(2, self.os_hum - 1), pow(2, self.os_pres - 1))) logging.info("BMxx80: IIR: %dx" % (pow(2, self.iir_filter) - 1)) + self.iir_filter = self.iir_filter & 0x07 self.temp = self.pressure = self.humidity = self.gas = self.t_fine = 0. self.min_temp = self.max_temp = self.range_switching_error = 0. @@ -155,6 +157,7 @@ class BME280: return self.printer.register_event_handler("klippy:connect", self.handle_connect) + self.last_gas_time = 0 def handle_connect(self): self._init_bmxx80() @@ -281,7 +284,7 @@ class BME280: self.chip_type, self.i2c.i2c_address)) # Reset chip - self.write_register('RESET', [RESET_CHIP_VALUE]) + self.write_register('RESET', [RESET_CHIP_VALUE], wait=True) self.reactor.pause(self.reactor.monotonic() + .5) # Make sure non-volatile memory has been copied to registers @@ -293,15 +296,15 @@ class BME280: status = self.read_register('STATUS', 1)[0] if self.chip_type == 'BME680': - self.max_sample_time = 0.5 + self.max_sample_time = \ + (1.25 + (2.3 * self.os_temp) + ((2.3 * self.os_pres) + .575) + + ((2.3 * self.os_hum) + .575)) / 1000 self.sample_timer = self.reactor.register_timer(self._sample_bme680) self.chip_registers = BME680_REGS elif self.chip_type == 'BMP180': - self.max_sample_time = (1.25 + ((2.3 * self.os_pres) + .575)) / 1000 self.sample_timer = self.reactor.register_timer(self._sample_bmp180) self.chip_registers = BMP180_REGS elif self.chip_type == 'BMP388': - self.max_sample_time = 0.5 self.chip_registers = BMP388_REGS self.write_register( "PWR_CTRL", @@ -318,15 +321,18 @@ class BME280: self.write_register("INT_CTRL", [BMP388_REG_VAL_DRDY_EN]) self.sample_timer = self.reactor.register_timer(self._sample_bmp388) - else: + elif self.chip_type == 'BME280': self.max_sample_time = \ (1.25 + (2.3 * self.os_temp) + ((2.3 * self.os_pres) + .575) + ((2.3 * self.os_hum) + .575)) / 1000 self.sample_timer = self.reactor.register_timer(self._sample_bme280) self.chip_registers = BME280_REGS - - if self.chip_type in ('BME680', 'BME280'): - self.write_register('CONFIG', (self.iir_filter & 0x07) << 2) + else: + self.max_sample_time = \ + (1.25 + (2.3 * self.os_temp) + + ((2.3 * self.os_pres) + .575)) / 1000 + self.sample_timer = self.reactor.register_timer(self._sample_bme280) + self.chip_registers = BME280_REGS # Read out and calculate the trimming parameters if self.chip_type == 'BMP180': @@ -347,21 +353,64 @@ class BME280: elif self.chip_type == 'BMP388': self.dig = read_calibration_data_bmp388(cal_1) + if self.chip_type in ('BME280', 'BMP280'): + max_standby_time = REPORT_TIME - self.max_sample_time + # 0.5 ms + t_sb = 0 + if self.chip_type == 'BME280': + if max_standby_time > 1: + t_sb = 5 + elif max_standby_time > 0.5: + t_sb = 4 + elif max_standby_time > 0.25: + t_sb = 3 + elif max_standby_time > 0.125: + t_sb = 2 + elif max_standby_time > 0.0625: + t_sb = 1 + elif max_standby_time > 0.020: + t_sb = 7 + elif max_standby_time > 0.010: + t_sb = 6 + else: + if max_standby_time > 4: + t_sb = 7 + elif max_standby_time > 2: + t_sb = 6 + elif max_standby_time > 1: + t_sb = 5 + elif max_standby_time > 0.5: + t_sb = 4 + elif max_standby_time > 0.25: + t_sb = 3 + elif max_standby_time > 0.125: + t_sb = 2 + elif max_standby_time > 0.0625: + t_sb = 1 + + cfg = t_sb << 5 | self.iir_filter << 2 + self.write_register('CONFIG', cfg) + if self.chip_type == 'BME280': + self.write_register('CTRL_HUM', self.os_hum) + # Enter normal (periodic) mode + meas = self.os_temp << 5 | self.os_pres << 2 | MODE_PERIODIC + self.write_register('CTRL_MEAS', meas, wait=True) + + if self.chip_type == 'BME680': + self.write_register('CONFIG', self.iir_filter << 2) + # Should be set once and reused on every mode register write + self.write_register('CTRL_HUM', self.os_hum & 0x07) + gas_wait_0 = self._calc_gas_heater_duration(self.gas_heat_duration) + self.write_register('GAS_WAIT_0', [gas_wait_0]) + res_heat_0 = self._calc_gas_heater_resistance(self.gas_heat_temp) + self.write_register('RES_HEAT_0', [res_heat_0]) + # Set initial heater current to reach Gas heater target on start + self.write_register('IDAC_HEAT_0', 96) + def _sample_bme280(self, eventtime): - # Enter forced mode - if self.chip_type == 'BME280': - self.write_register('CTRL_HUM', self.os_hum) - meas = self.os_temp << 5 | self.os_pres << 2 | MODE - self.write_register('CTRL_MEAS', meas) - + # In normal mode data shadowing is performed + # So reading can be done while measurements are in process try: - # wait until results are ready - status = self.read_register('STATUS', 1)[0] - while status & STATUS_MEASURING: - self.reactor.pause( - self.reactor.monotonic() + self.max_sample_time) - status = self.read_register('STATUS', 1)[0] - if self.chip_type == 'BME280': data = self.read_register('PRESSURE_MSB', 8) elif self.chip_type == 'BMP280': @@ -462,36 +511,40 @@ class BME280: return comp_press def _sample_bme680(self, eventtime): - self.write_register('CTRL_HUM', self.os_hum & 0x07) - meas = self.os_temp << 5 | self.os_pres << 2 - self.write_register('CTRL_MEAS', [meas]) - - gas_wait_0 = self._calculate_gas_heater_duration(self.gas_heat_duration) - self.write_register('GAS_WAIT_0', [gas_wait_0]) - res_heat_0 = self._calculate_gas_heater_resistance(self.gas_heat_temp) - self.write_register('RES_HEAT_0', [res_heat_0]) - gas_config = RUN_GAS | NB_CONV_0 - self.write_register('CTRL_GAS_1', [gas_config]) - - def data_ready(stat): + def data_ready(stat, run_gas): new_data = (stat & EAS_NEW_DATA) gas_done = not (stat & GAS_DONE) meas_done = not (stat & MEASURE_DONE) + if not run_gas: + gas_done = True return new_data and gas_done and meas_done + run_gas = False + # Check VOC once a while + if self.reactor.monotonic() - self.last_gas_time > 3: + gas_config = RUN_GAS | NB_CONV_0 + self.write_register('CTRL_GAS_1', [gas_config]) + run_gas = True + # Enter forced mode - meas = meas | MODE - self.write_register('CTRL_MEAS', meas) + meas = self.os_temp << 5 | self.os_pres << 2 | MODE + self.write_register('CTRL_MEAS', meas, wait=True) + max_sample_time = self.max_sample_time + if run_gas: + max_sample_time += self.gas_heat_duration / 1000 + self.reactor.pause(self.reactor.monotonic() + max_sample_time) try: # wait until results are ready status = self.read_register('EAS_STATUS_0', 1)[0] - while not data_ready(status): + while not data_ready(status, run_gas): self.reactor.pause( self.reactor.monotonic() + self.max_sample_time) status = self.read_register('EAS_STATUS_0', 1)[0] data = self.read_register('PRESSURE_MSB', 8) - gas_data = self.read_register('GAS_R_MSB', 2) + gas_data = [0, 0] + if run_gas: + gas_data = self.read_register('GAS_R_MSB', 2) except Exception: logging.exception("BME680: Error reading data") self.temp = self.pressure = self.humidity = self.gas = .0 @@ -515,6 +568,10 @@ class BME280: gas_raw = (gas_data[0] << 2) | ((gas_data[1] & 0xC0) >> 6) gas_range = (gas_data[1] & 0x0F) self.gas = self._compensate_gas(gas_raw, gas_range) + # Disable gas measurement on success + gas_config = NB_CONV_0 + self.write_register('CTRL_GAS_1', [gas_config]) + self.last_gas_time = self.reactor.monotonic() if self.temp < self.min_temp or self.temp > self.max_temp: self.printer.invoke_shutdown( @@ -643,7 +700,7 @@ class BME280: gas_raw - 512. + var1) return gas - def _calculate_gas_heater_resistance(self, target_temp): + def _calc_gas_heater_resistance(self, target_temp): amb_temp = self.temp heater_data = self.read_register('RES_HEAT_VAL', 3) res_heat_val = get_signed_byte(heater_data[0]) @@ -658,7 +715,7 @@ class BME280: * (1. / (1. + (res_heat_val * 0.002)))) - 25)) return int(res_heat) - def _calculate_gas_heater_duration(self, duration_ms): + def _calc_gas_heater_duration(self, duration_ms): if duration_ms >= 4032: duration_reg = 0xff else: @@ -719,12 +776,15 @@ class BME280: params = self.i2c.i2c_read(regs, read_len) return bytearray(params['response']) - def write_register(self, reg_name, data): + def write_register(self, reg_name, data, wait = False): if type(data) is not list: data = [data] reg = self.chip_registers[reg_name] data.insert(0, reg) - self.i2c.i2c_write(data) + if not wait: + self.i2c.i2c_write(data) + else: + self.i2c.i2c_write_wait_ack(data) def get_status(self, eventtime): data = { diff --git a/klippy/extras/bulk_sensor.py b/klippy/extras/bulk_sensor.py index 1720c0522..b0aa320d0 100644 --- a/klippy/extras/bulk_sensor.py +++ b/klippy/extras/bulk_sensor.py @@ -198,7 +198,7 @@ class ClockSyncRegression: inv_freq = clock_to_print_time(base_mcu + inv_cfreq) - base_time return base_time, base_chip, inv_freq -MAX_BULK_MSG_SIZE = 52 +MAX_BULK_MSG_SIZE = 51 # Read sensor_bulk_data and calculate timestamps for devices that take # samples at a fixed frequency (and produce fixed data size samples). diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index 9b2ec371f..28bfcdf0d 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -192,6 +192,9 @@ class MCU_I2C: return self.i2c_write_cmd.send([self.oid, data], minclock=minclock, reqclock=reqclock) + def i2c_write_wait_ack(self, data, minclock=0, reqclock=0): + self.i2c_write_cmd.send_wait_ack([self.oid, data], + minclock=minclock, reqclock=reqclock) def i2c_read(self, write, read_len): return self.i2c_read_cmd.send([self.oid, write, read_len]) def i2c_modify_bits(self, reg, clear_bits, set_bits, diff --git a/klippy/extras/buttons.py b/klippy/extras/buttons.py index 70d76a60e..daa998a93 100644 --- a/klippy/extras/buttons.py +++ b/klippy/extras/buttons.py @@ -104,7 +104,7 @@ class MCU_ADC_buttons: self.max_value = 0. ppins = printer.lookup_object('pins') self.mcu_adc = ppins.setup_pin('adc', self.pin) - self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback) query_adc = printer.lookup_object('query_adc') query_adc.register_adc('adc_button:' + pin.strip(), self.mcu_adc) diff --git a/klippy/extras/display/hd44780.py b/klippy/extras/display/hd44780.py index 9adfa20f7..2da49c51e 100644 --- a/klippy/extras/display/hd44780.py +++ b/klippy/extras/display/hd44780.py @@ -8,7 +8,7 @@ import logging BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000 LINE_LENGTH_DEFAULT=20 -LINE_LENGTH_OPTIONS={16:16, 20:20} +LINE_LENGTH_OPTIONS=[16, 20] TextGlyphs = { 'right_arrow': b'\x7e' } diff --git a/klippy/extras/display/hd44780_spi.py b/klippy/extras/display/hd44780_spi.py index cd1d9e3ea..f21accbb4 100644 --- a/klippy/extras/display/hd44780_spi.py +++ b/klippy/extras/display/hd44780_spi.py @@ -9,7 +9,7 @@ import logging from .. import bus LINE_LENGTH_DEFAULT=20 -LINE_LENGTH_OPTIONS={16:16, 20:20} +LINE_LENGTH_OPTIONS=[16, 20] TextGlyphs = { 'right_arrow': b'\x7e' } diff --git a/klippy/extras/display/menu_keys.py b/klippy/extras/display/menu_keys.py index 91a96e19f..8094c9964 100644 --- a/klippy/extras/display/menu_keys.py +++ b/klippy/extras/display/menu_keys.py @@ -18,7 +18,7 @@ class MenuKeys: # Register rotary encoder encoder_pins = config.get('encoder_pins', None) encoder_steps_per_detent = config.getchoice('encoder_steps_per_detent', - {2: 2, 4: 4}, 4) + [2, 4], 4) if encoder_pins is not None: try: pin1, pin2 = encoder_pins.split(',') diff --git a/klippy/extras/error_mcu.py b/klippy/extras/error_mcu.py new file mode 100644 index 000000000..dc91c33a9 --- /dev/null +++ b/klippy/extras/error_mcu.py @@ -0,0 +1,133 @@ +# More verbose information on micro-controller errors +# +# Copyright (C) 2024 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging + +message_shutdown = """ +Once the underlying issue is corrected, use the +"FIRMWARE_RESTART" command to reset the firmware, reload the +config, and restart the host software. +Printer is shutdown +""" + +message_protocol_error1 = """ +This is frequently caused by running an older version of the +firmware on the MCU(s). Fix by recompiling and flashing the +firmware. +""" + +message_protocol_error2 = """ +Once the underlying issue is corrected, use the "RESTART" +command to reload the config and restart the host software. +""" + +message_mcu_connect_error = """ +Once the underlying issue is corrected, use the +"FIRMWARE_RESTART" command to reset the firmware, reload the +config, and restart the host software. +Error configuring printer +""" + +Common_MCU_errors = { + ("Timer too close",): """ +This often indicates the host computer is overloaded. Check +for other processes consuming excessive CPU time, high swap +usage, disk errors, overheating, unstable voltage, or +similar system problems on the host computer.""", + ("Missed scheduling of next ",): """ +This is generally indicative of an intermittent +communication failure between micro-controller and host.""", + ("ADC out of range",): """ +This generally occurs when a heater temperature exceeds +its configured min_temp or max_temp.""", + ("Rescheduled timer in the past", "Stepper too far in past"): """ +This generally occurs when the micro-controller has been +requested to step at a rate higher than it is capable of +obtaining.""", + ("Command request",): """ +This generally occurs in response to an M112 G-Code command +or in response to an internal error in the host software.""", +} + +def error_hint(msg): + for prefixes, help_msg in Common_MCU_errors.items(): + for prefix in prefixes: + if msg.startswith(prefix): + return help_msg + return "" + +class PrinterMCUError: + def __init__(self, config): + self.printer = config.get_printer() + self.clarify_callbacks = {} + self.printer.register_event_handler("klippy:notify_mcu_shutdown", + self._handle_notify_mcu_shutdown) + self.printer.register_event_handler("klippy:notify_mcu_error", + self._handle_notify_mcu_error) + def add_clarify(self, msg, callback): + self.clarify_callbacks.setdefault(msg, []).append(callback) + def _check_mcu_shutdown(self, msg, details): + mcu_name = details['mcu'] + mcu_msg = details['reason'] + event_type = details['event_type'] + prefix = "MCU '%s' shutdown: " % (mcu_name,) + if event_type == 'is_shutdown': + prefix = "Previous MCU '%s' shutdown: " % (mcu_name,) + # Lookup generic hint + hint = error_hint(mcu_msg) + # Add per instance help + clarify = [cb(msg, details) + for cb in self.clarify_callbacks.get(mcu_msg, [])] + clarify = [cm for cm in clarify if cm is not None] + clarify_msg = "" + if clarify: + clarify_msg = "\n".join(["", ""] + clarify + [""]) + # Update error message + newmsg = "%s%s%s%s%s" % (prefix, mcu_msg, clarify_msg, + hint, message_shutdown) + self.printer.update_error_msg(msg, newmsg) + def _handle_notify_mcu_shutdown(self, msg, details): + if msg == "MCU shutdown": + self._check_mcu_shutdown(msg, details) + else: + self.printer.update_error_msg(msg, "%s%s" % (msg, message_shutdown)) + def _check_protocol_error(self, msg, details): + host_version = self.printer.start_args['software_version'] + msg_update = [] + msg_updated = [] + for mcu_name, mcu in self.printer.lookup_objects('mcu'): + try: + mcu_version = mcu.get_status()['mcu_version'] + except: + logging.exception("Unable to retrieve mcu_version from mcu") + continue + if mcu_version != host_version: + msg_update.append("%s: Current version %s" + % (mcu_name.split()[-1], mcu_version)) + else: + msg_updated.append("%s: Current version %s" + % (mcu_name.split()[-1], mcu_version)) + if not msg_update: + msg_update.append("") + if not msg_updated: + msg_updated.append("") + newmsg = ["MCU Protocol error", + message_protocol_error1, + "Your Klipper version is: %s" % (host_version,), + "MCU(s) which should be updated:"] + newmsg += msg_update + ["Up-to-date MCU(s):"] + msg_updated + newmsg += [message_protocol_error2, details['error']] + self.printer.update_error_msg(msg, "\n".join(newmsg)) + def _check_mcu_connect_error(self, msg, details): + newmsg = "%s%s" % (details['error'], message_mcu_connect_error) + self.printer.update_error_msg(msg, newmsg) + def _handle_notify_mcu_error(self, msg, details): + if msg == "Protocol error": + self._check_protocol_error(msg, details) + elif msg == "MCU error during connect": + self._check_mcu_connect_error(msg, details) + +def load_config(config): + return PrinterMCUError(config) diff --git a/klippy/extras/gcode_arcs.py b/klippy/extras/gcode_arcs.py index 76c165dd5..3917dac30 100644 --- a/klippy/extras/gcode_arcs.py +++ b/klippy/extras/gcode_arcs.py @@ -39,8 +39,6 @@ class ArcSupport: self.gcode.register_command("G18", self.cmd_G18) self.gcode.register_command("G19", self.cmd_G19) - self.Coord = self.gcode.Coord - # backwards compatibility, prior implementation only supported XY self.plane = ARC_PLANE_X_Y @@ -64,52 +62,36 @@ class ArcSupport: if not gcodestatus['absolute_coordinates']: raise gcmd.error("G2/G3 does not support relative move mode") currentPos = gcodestatus['gcode_position'] + absolut_extrude = gcodestatus['absolute_extrude'] # Parse parameters - asTarget = self.Coord(x=gcmd.get_float("X", currentPos[0]), - y=gcmd.get_float("Y", currentPos[1]), - z=gcmd.get_float("Z", currentPos[2]), - e=None) + asTarget = [gcmd.get_float("X", currentPos[0]), + gcmd.get_float("Y", currentPos[1]), + gcmd.get_float("Z", currentPos[2])] if gcmd.get_float("R", None) is not None: raise gcmd.error("G2/G3 does not support R moves") # determine the plane coordinates and the helical axis - asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('IJ') ] + I = gcmd.get_float('I', 0.) + J = gcmd.get_float('J', 0.) + asPlanar = (I, J) axes = (X_AXIS, Y_AXIS, Z_AXIS) if self.plane == ARC_PLANE_X_Z: - asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('IK') ] + K = gcmd.get_float('K', 0.) + asPlanar = (I, K) axes = (X_AXIS, Z_AXIS, Y_AXIS) elif self.plane == ARC_PLANE_Y_Z: - asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('JK') ] + K = gcmd.get_float('K', 0.) + asPlanar = (J, K) axes = (Y_AXIS, Z_AXIS, X_AXIS) if not (asPlanar[0] or asPlanar[1]): raise gcmd.error("G2/G3 requires IJ, IK or JK parameters") - asE = gcmd.get_float("E", None) - asF = gcmd.get_float("F", None) - - # Build list of linear coordinates to move - coords = self.planArc(currentPos, asTarget, asPlanar, - clockwise, *axes) - e_per_move = e_base = 0. - if asE is not None: - if gcodestatus['absolute_extrude']: - e_base = currentPos[3] - e_per_move = (asE - e_base) / len(coords) - - # Convert coords into G1 commands - for coord in coords: - g1_params = {'X': coord[0], 'Y': coord[1], 'Z': coord[2]} - if e_per_move: - g1_params['E'] = e_base + e_per_move - if gcodestatus['absolute_extrude']: - e_base += e_per_move - if asF is not None: - g1_params['F'] = asF - g1_gcmd = self.gcode.create_gcode_command("G1", "G1", g1_params) - self.gcode_move.cmd_G1(g1_gcmd) + # Build linear coordinates to move + self.planArc(currentPos, asTarget, asPlanar, clockwise, + gcmd, absolut_extrude, *axes) # function planArc() originates from marlin plan_arc() # https://github.com/MarlinFirmware/Marlin @@ -120,6 +102,7 @@ class ArcSupport: # # alpha and beta axes are the current plane, helical axis is linear travel def planArc(self, currentPos, targetPos, offset, clockwise, + gcmd, absolut_extrude, alpha_axis, beta_axis, helical_axis): # todo: sometimes produces full circles @@ -159,23 +142,42 @@ class ArcSupport: # Generate coordinates theta_per_segment = angular_travel / segments linear_per_segment = linear_travel / segments - coords = [] - for i in range(1, int(segments)): + + asE = gcmd.get_float("E", None) + asF = gcmd.get_float("F", None) + + e_per_move = e_base = 0. + if asE is not None: + if absolut_extrude: + e_base = currentPos[3] + e_per_move = (asE - e_base) / segments + + for i in range(1, int(segments) + 1): dist_Helical = i * linear_per_segment - cos_Ti = math.cos(i * theta_per_segment) - sin_Ti = math.sin(i * theta_per_segment) + c_theta = i * theta_per_segment + cos_Ti = math.cos(c_theta) + sin_Ti = math.sin(c_theta) r_P = -offset[0] * cos_Ti + offset[1] * sin_Ti r_Q = -offset[0] * sin_Ti - offset[1] * cos_Ti - # Coord doesn't support index assignment, create list - c = [None, None, None, None] + c = [None, None, None] c[alpha_axis] = center_P + r_P c[beta_axis] = center_Q + r_Q c[helical_axis] = currentPos[helical_axis] + dist_Helical - coords.append(self.Coord(*c)) - coords.append(targetPos) - return coords + + if i == segments: + c = targetPos + # Convert coords into G1 commands + g1_params = {'X': c[0], 'Y': c[1], 'Z': c[2]} + if e_per_move: + g1_params['E'] = e_base + e_per_move + if absolut_extrude: + e_base += e_per_move + if asF is not None: + g1_params['F'] = asF + g1_gcmd = self.gcode.create_gcode_command("G1", "G1", g1_params) + self.gcode_move.cmd_G1(g1_gcmd) def load_config(config): return ArcSupport(config) diff --git a/klippy/extras/hall_filament_width_sensor.py b/klippy/extras/hall_filament_width_sensor.py index e08028874..8dab35226 100644 --- a/klippy/extras/hall_filament_width_sensor.py +++ b/klippy/extras/hall_filament_width_sensor.py @@ -49,10 +49,10 @@ class HallFilamentWidthSensor: # Start adc self.ppins = self.printer.lookup_object('pins') self.mcu_adc = self.ppins.setup_pin('adc', self.pin1) - self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback) self.mcu_adc2 = self.ppins.setup_pin('adc', self.pin2) - self.mcu_adc2.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc2.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) self.mcu_adc2.setup_adc_callback(ADC_REPORT_TIME, self.adc2_callback) # extrude factor updating self.extrude_factor_update_timer = self.reactor.register_timer( diff --git a/klippy/extras/homing.py b/klippy/extras/homing.py index 634ad81b7..06b52f1ec 100644 --- a/klippy/extras/homing.py +++ b/klippy/extras/homing.py @@ -98,11 +98,14 @@ class HomingMove: trigger_times = {} move_end_print_time = self.toolhead.get_last_move_time() for mcu_endstop, name in self.endstops: - trigger_time = mcu_endstop.home_wait(move_end_print_time) + try: + trigger_time = mcu_endstop.home_wait(move_end_print_time) + except self.printer.command_error as e: + if error is None: + error = "Error during homing %s: %s" % (name, str(e)) + continue if trigger_time > 0.: trigger_times[name] = trigger_time - elif trigger_time < 0. and error is None: - error = "Communication timeout during homing %s" % (name,) elif check_triggered and error is None: error = "No trigger on %s after full movement" % (name,) # Determine stepper halt positions diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py new file mode 100644 index 000000000..85eff85f9 --- /dev/null +++ b/klippy/extras/hx71x.py @@ -0,0 +1,170 @@ +# HX711/HX717 Support +# +# Copyright (C) 2024 Gareth Farrington +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from . import bulk_sensor + +# +# Constants +# +UPDATE_INTERVAL = 0.10 +SAMPLE_ERROR_DESYNC = -0x80000000 +SAMPLE_ERROR_LONG_READ = 0x40000000 + +# Implementation of HX711 and HX717 +class HX71xBase(): + def __init__(self, config, sensor_type, + sample_rate_options, default_sample_rate, + gain_options, default_gain): + self.printer = printer = config.get_printer() + self.name = config.get_name().split()[-1] + self.last_error_count = 0 + self.consecutive_fails = 0 + self.sensor_type = sensor_type + # Chip options + dout_pin_name = config.get('dout_pin') + sclk_pin_name = config.get('sclk_pin') + ppins = printer.lookup_object('pins') + dout_ppin = ppins.lookup_pin(dout_pin_name) + sclk_ppin = ppins.lookup_pin(sclk_pin_name) + self.mcu = mcu = dout_ppin['chip'] + self.oid = mcu.create_oid() + if sclk_ppin['chip'] is not mcu: + raise config.error("%s config error: All pins must be " + "connected to the same MCU" % (self.name,)) + self.dout_pin = dout_ppin['pin'] + self.sclk_pin = sclk_ppin['pin'] + # Samples per second choices + self.sps = config.getchoice('sample_rate', sample_rate_options, + default=default_sample_rate) + # gain/channel choices + self.gain_channel = int(config.getchoice('gain', gain_options, + default=default_gain)) + ## Bulk Sensor Setup + self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, oid=self.oid) + # Clock tracking + chip_smooth = self.sps * UPDATE_INTERVAL * 2 + self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, " 0: + logging.error("%s: Forced sensor restart due to error", self.name) + self._finish_measurements() + self._start_measurements() + elif overflows > 0: + self.consecutive_fails += 1 + if self.consecutive_fails > 4: + logging.error("%s: Forced sensor restart due to overflows", + self.name) + self._finish_measurements() + self._start_measurements() + else: + self.consecutive_fails = 0 + return {'data': samples, 'errors': self.last_error_count, + 'overflows': self.ffreader.get_last_overflows()} + + +class HX711(HX71xBase): + def __init__(self, config): + super(HX711, self).__init__(config, "hx711", + # HX711 sps options + {80: 80, 10: 10}, 80, + # HX711 gain/channel options + {'A-128': 1, 'B-32': 2, 'A-64': 3}, 'A-128') + + +class HX717(HX71xBase): + def __init__(self, config): + super(HX717, self).__init__(config, "hx717", + # HX717 sps options + {320: 320, 80: 80, 20: 20, 10: 10}, 320, + # HX717 gain/channel options + {'A-128': 1, 'B-64': 2, 'A-64': 3, + 'B-8': 4}, 'A-128') + + +HX71X_SENSOR_TYPES = { + "hx711": HX711, + "hx717": HX717 +} diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index 2ae4dd7d7..281d34226 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -87,8 +87,17 @@ class LDC1612: self.oid = oid = mcu.create_oid() self.query_ldc1612_cmd = None self.ldc1612_setup_home_cmd = self.query_ldc1612_home_state_cmd = None - mcu.add_config_cmd("config_ldc1612 oid=%d i2c_oid=%d" - % (oid, self.i2c.get_oid())) + if config.get('intb_pin', None) is not None: + ppins = config.get_printer().lookup_object("pins") + pin_params = ppins.lookup_pin(config.get('intb_pin')) + if pin_params['chip'] != mcu: + raise config.error("ldc1612 intb_pin must be on same mcu") + mcu.add_config_cmd( + "config_ldc1612_with_intb oid=%d i2c_oid=%d intb_pin=%s" + % (oid, self.i2c.get_oid(), pin_params['pin'])) + else: + mcu.add_config_cmd("config_ldc1612 oid=%d i2c_oid=%d" + % (oid, self.i2c.get_oid())) mcu.add_config_cmd("query_ldc1612 oid=%d rest_ticks=0" % (oid,), on_restart=True) mcu.register_config_callback(self._build_config) @@ -108,11 +117,11 @@ class LDC1612: cmdqueue = self.i2c.get_command_queue() self.query_ldc1612_cmd = self.mcu.lookup_command( "query_ldc1612 oid=%c rest_ticks=%u", cq=cmdqueue) - self.ffreader.setup_query_command("query_ldc1612_status oid=%c", + self.ffreader.setup_query_command("query_status_ldc1612 oid=%c", oid=self.oid, cq=cmdqueue) self.ldc1612_setup_home_cmd = self.mcu.lookup_command( "ldc1612_setup_home oid=%c clock=%u threshold=%u" - " trsync_oid=%c trigger_reason=%c", cq=cmdqueue) + " trsync_oid=%c trigger_reason=%c error_reason=%c", cq=cmdqueue) self.query_ldc1612_home_state_cmd = self.mcu.lookup_query_command( "query_ldc1612_home_state oid=%c", "ldc1612_home_state oid=%c homing=%c trigger_clock=%u", @@ -129,13 +138,14 @@ class LDC1612: def add_client(self, cb): self.batch_bulk.add_client(cb) # Homing - def setup_home(self, print_time, trigger_freq, trsync_oid, reason): + def setup_home(self, print_time, trigger_freq, + trsync_oid, hit_reason, err_reason): clock = self.mcu.print_time_to_clock(print_time) tfreq = int(trigger_freq * (1<<28) / float(LDC1612_FREQ) + 0.5) self.ldc1612_setup_home_cmd.send( - [self.oid, clock, tfreq, trsync_oid, reason]) + [self.oid, clock, tfreq, trsync_oid, hit_reason, err_reason]) def clear_home(self): - self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0]) + self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0, 0]) if self.mcu.is_fileoutput(): return 0. params = self.query_ldc1612_home_state_cmd.send([self.oid]) diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py new file mode 100644 index 000000000..14f3c2983 --- /dev/null +++ b/klippy/extras/load_cell.py @@ -0,0 +1,30 @@ +# Load Cell Implementation +# +# Copyright (C) 2024 Gareth Farrington +# +# This file may be distributed under the terms of the GNU GPLv3 license. +from . import hx71x +from . import ads1220 + +# Printer class that controls a load cell +class LoadCell: + def __init__(self, config, sensor): + self.printer = printer = config.get_printer() + self.sensor = sensor # must implement BulkAdcSensor + + def _on_sample(self, msg): + return True + + def get_sensor(self): + return self.sensor + +def load_config(config): + # Sensor types + sensors = {} + sensors.update(hx71x.HX71X_SENSOR_TYPES) + sensors.update(ads1220.ADS1220_SENSOR_TYPE) + sensor_class = config.getchoice('sensor_type', sensors) + return LoadCell(config, sensor_class(config)) + +def load_config_prefix(config): + return load_config(config) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 073c875cc..c467e181e 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -1,6 +1,6 @@ # Z-Probe support # -# Copyright (C) 2017-2021 Kevin O'Connor +# Copyright (C) 2017-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import logging @@ -13,44 +13,176 @@ consider reducing the Z axis minimum position so the probe can travel further (the Z minimum position can be negative). """ -class PrinterProbe: +# Calculate the average Z from a set of positions +def calc_probe_z_average(positions, method='average'): + if method != 'median': + # Use mean average + count = float(len(positions)) + return [sum([pos[i] for pos in positions]) / count + for i in range(3)] + # Use median + z_sorted = sorted(positions, key=(lambda p: p[2])) + middle = len(positions) // 2 + if (len(positions) & 1) == 1: + # odd number of samples + return z_sorted[middle] + # even number of samples + return calc_probe_z_average(z_sorted[middle-1:middle+1], 'average') + + +###################################################################### +# Probe device implementation helpers +###################################################################### + +# Helper to implement common probing commands +class ProbeCommandHelper: + def __init__(self, config, probe, query_endstop=None): + self.printer = config.get_printer() + self.probe = probe + self.query_endstop = query_endstop + self.name = config.get_name() + gcode = self.printer.lookup_object('gcode') + # QUERY_PROBE command + self.last_state = False + gcode.register_command('QUERY_PROBE', self.cmd_QUERY_PROBE, + desc=self.cmd_QUERY_PROBE_help) + # PROBE command + self.last_z_result = 0. + gcode.register_command('PROBE', self.cmd_PROBE, + desc=self.cmd_PROBE_help) + # PROBE_CALIBRATE command + self.probe_calibrate_z = 0. + gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, + desc=self.cmd_PROBE_CALIBRATE_help) + # Other commands + gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, + desc=self.cmd_PROBE_ACCURACY_help) + gcode.register_command('Z_OFFSET_APPLY_PROBE', + self.cmd_Z_OFFSET_APPLY_PROBE, + desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) + def _move(self, coord, speed): + self.printer.lookup_object('toolhead').manual_move(coord, speed) + def get_status(self, eventtime): + return {'name': self.name, + 'last_query': self.last_state, + 'last_z_result': self.last_z_result} + cmd_QUERY_PROBE_help = "Return the status of the z-probe" + def cmd_QUERY_PROBE(self, gcmd): + if self.query_endstop is None: + raise gcmd.error("Probe does not support QUERY_PROBE") + toolhead = self.printer.lookup_object('toolhead') + print_time = toolhead.get_last_move_time() + res = self.query_endstop(print_time) + self.last_state = res + gcmd.respond_info("probe: %s" % (["open", "TRIGGERED"][not not res],)) + cmd_PROBE_help = "Probe Z-height at current XY position" + def cmd_PROBE(self, gcmd): + pos = run_single_probe(self.probe, gcmd) + gcmd.respond_info("Result is z=%.6f" % (pos[2],)) + self.last_z_result = pos[2] + def probe_calibrate_finalize(self, kin_pos): + if kin_pos is None: + return + z_offset = self.probe_calibrate_z - kin_pos[2] + gcode = self.printer.lookup_object('gcode') + gcode.respond_info( + "%s: z_offset: %.3f\n" + "The SAVE_CONFIG command will update the printer config file\n" + "with the above and restart the printer." % (self.name, z_offset)) + configfile = self.printer.lookup_object('configfile') + configfile.set(self.name, 'z_offset', "%.3f" % (z_offset,)) + cmd_PROBE_CALIBRATE_help = "Calibrate the probe's z_offset" + def cmd_PROBE_CALIBRATE(self, gcmd): + manual_probe.verify_no_manual_probe(self.printer) + params = self.probe.get_probe_params(gcmd) + # Perform initial probe + curpos = run_single_probe(self.probe, gcmd) + # Move away from the bed + self.probe_calibrate_z = curpos[2] + curpos[2] += 5. + self._move(curpos, params['lift_speed']) + # Move the nozzle over the probe point + x_offset, y_offset, z_offset = self.probe.get_offsets() + curpos[0] += x_offset + curpos[1] += y_offset + self._move(curpos, params['probe_speed']) + # Start manual probe + manual_probe.ManualProbeHelper(self.printer, gcmd, + self.probe_calibrate_finalize) + cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position" + def cmd_PROBE_ACCURACY(self, gcmd): + params = self.probe.get_probe_params(gcmd) + sample_count = gcmd.get_int("SAMPLES", 10, minval=1) + toolhead = self.printer.lookup_object('toolhead') + pos = toolhead.get_position() + gcmd.respond_info("PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f" + " (samples=%d retract=%.3f" + " speed=%.1f lift_speed=%.1f)\n" + % (pos[0], pos[1], pos[2], + sample_count, params['sample_retract_dist'], + params['probe_speed'], params['lift_speed'])) + # Create dummy gcmd with SAMPLES=1 + fo_params = dict(gcmd.get_command_parameters()) + fo_params['SAMPLES'] = '1' + gcode = self.printer.lookup_object('gcode') + fo_gcmd = gcode.create_gcode_command("", "", fo_params) + # Probe bed sample_count times + probe_session = self.probe.start_probe_session(fo_gcmd) + probe_num = 0 + while probe_num < sample_count: + # Probe position + probe_session.run_probe(fo_gcmd) + probe_num += 1 + # Retract + pos = toolhead.get_position() + liftpos = [None, None, pos[2] + params['sample_retract_dist']] + self._move(liftpos, params['lift_speed']) + positions = probe_session.pull_probed_results() + probe_session.end_probe_session() + # Calculate maximum, minimum and average values + max_value = max([p[2] for p in positions]) + min_value = min([p[2] for p in positions]) + range_value = max_value - min_value + avg_value = calc_probe_z_average(positions, 'average')[2] + median = calc_probe_z_average(positions, 'median')[2] + # calculate the standard deviation + deviation_sum = 0 + for i in range(len(positions)): + deviation_sum += pow(positions[i][2] - avg_value, 2.) + sigma = (deviation_sum / len(positions)) ** 0.5 + # Show information + gcmd.respond_info( + "probe accuracy results: maximum %.6f, minimum %.6f, range %.6f, " + "average %.6f, median %.6f, standard deviation %.6f" % ( + max_value, min_value, range_value, avg_value, median, sigma)) + cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset" + def cmd_Z_OFFSET_APPLY_PROBE(self, gcmd): + gcode_move = self.printer.lookup_object("gcode_move") + offset = gcode_move.get_status()['homing_origin'].z + if offset == 0: + gcmd.respond_info("Nothing to do: Z Offset is 0") + return + z_offset = self.probe.get_offsets()[2] + new_calibrate = z_offset - offset + gcmd.respond_info( + "%s: z_offset: %.3f\n" + "The SAVE_CONFIG command will update the printer config file\n" + "with the above and restart the printer." + % (self.name, new_calibrate)) + configfile = self.printer.lookup_object('configfile') + configfile.set(self.name, 'z_offset', "%.3f" % (new_calibrate,)) + +# Homing via probe:z_virtual_endstop +class HomingViaProbeHelper: def __init__(self, config, mcu_probe): self.printer = config.get_printer() - self.name = config.get_name() self.mcu_probe = mcu_probe - self.speed = config.getfloat('speed', 5.0, above=0.) - self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.) - self.x_offset = config.getfloat('x_offset', 0.) - self.y_offset = config.getfloat('y_offset', 0.) - self.z_offset = config.getfloat('z_offset') - self.probe_calibrate_z = 0. self.multi_probe_pending = False - self.last_state = False - self.last_z_result = 0. - self.gcode_move = self.printer.load_object(config, "gcode_move") - # Infer Z position to move to during a probe - if config.has_section('stepper_z'): - zconfig = config.getsection('stepper_z') - self.z_position = zconfig.getfloat('position_min', 0., - note_valid=False) - else: - pconfig = config.getsection('printer') - self.z_position = pconfig.getfloat('minimum_z_position', 0., - note_valid=False) - # Multi-sample support (for improved accuracy) - self.sample_count = config.getint('samples', 1, minval=1) - self.sample_retract_dist = config.getfloat('sample_retract_dist', 2., - above=0.) - atypes = {'median': 'median', 'average': 'average'} - self.samples_result = config.getchoice('samples_result', atypes, - 'average') - self.samples_tolerance = config.getfloat('samples_tolerance', 0.100, - minval=0.) - self.samples_retries = config.getint('samples_tolerance_retries', 0, - minval=0) # Register z_virtual_endstop pin self.printer.lookup_object('pins').register_chip('probe', self) - # Register homing event handlers + # Register event handlers + self.printer.register_event_handler('klippy:mcu_identify', + self._handle_mcu_identify) self.printer.register_event_handler("homing:homing_move_begin", self._handle_homing_move_begin) self.printer.register_event_handler("homing:homing_move_end", @@ -61,19 +193,11 @@ class PrinterProbe: self._handle_home_rails_end) self.printer.register_event_handler("gcode:command_error", self._handle_command_error) - # Register PROBE/QUERY_PROBE commands - self.gcode = self.printer.lookup_object('gcode') - self.gcode.register_command('PROBE', self.cmd_PROBE, - desc=self.cmd_PROBE_help) - self.gcode.register_command('QUERY_PROBE', self.cmd_QUERY_PROBE, - desc=self.cmd_QUERY_PROBE_help) - self.gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, - desc=self.cmd_PROBE_CALIBRATE_help) - self.gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, - desc=self.cmd_PROBE_ACCURACY_help) - self.gcode.register_command('Z_OFFSET_APPLY_PROBE', - self.cmd_Z_OFFSET_APPLY_PROBE, - desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) + def _handle_mcu_identify(self): + kin = self.printer.lookup_object('toolhead').get_kinematics() + for stepper in kin.get_steppers(): + if stepper.is_active_axis('z'): + self.mcu_probe.add_stepper(stepper) def _handle_homing_move_begin(self, hmove): if self.mcu_probe in hmove.get_mcu_endstops(): self.mcu_probe.probe_prepare(hmove) @@ -83,35 +207,106 @@ class PrinterProbe: def _handle_home_rails_begin(self, homing_state, rails): endstops = [es for rail in rails for es, name in rail.get_endstops()] if self.mcu_probe in endstops: - self.multi_probe_begin() + self.mcu_probe.multi_probe_begin() + self.multi_probe_pending = True def _handle_home_rails_end(self, homing_state, rails): endstops = [es for rail in rails for es, name in rail.get_endstops()] - if self.mcu_probe in endstops: - self.multi_probe_end() - def _handle_command_error(self): - try: - self.multi_probe_end() - except: - logging.exception("Multi-probe end") - def multi_probe_begin(self): - self.mcu_probe.multi_probe_begin() - self.multi_probe_pending = True - def multi_probe_end(self): - if self.multi_probe_pending: + if self.multi_probe_pending and self.mcu_probe in endstops: self.multi_probe_pending = False self.mcu_probe.multi_probe_end() + def _handle_command_error(self): + if self.multi_probe_pending: + self.multi_probe_pending = False + try: + self.mcu_probe.multi_probe_end() + except: + logging.exception("Homing multi-probe end") def setup_pin(self, pin_type, pin_params): if pin_type != 'endstop' or pin_params['pin'] != 'z_virtual_endstop': raise pins.error("Probe virtual endstop only useful as endstop pin") if pin_params['invert'] or pin_params['pullup']: raise pins.error("Can not pullup/invert probe virtual endstop") return self.mcu_probe - def get_lift_speed(self, gcmd=None): - if gcmd is not None: - return gcmd.get_float("LIFT_SPEED", self.lift_speed, above=0.) - return self.lift_speed - def get_offsets(self): - return self.x_offset, self.y_offset, self.z_offset + +# Helper to track multiple probe attempts in a single command +class ProbeSessionHelper: + def __init__(self, config, mcu_probe): + self.printer = config.get_printer() + self.mcu_probe = mcu_probe + gcode = self.printer.lookup_object('gcode') + self.dummy_gcode_cmd = gcode.create_gcode_command("", "", {}) + # Infer Z position to move to during a probe + if config.has_section('stepper_z'): + zconfig = config.getsection('stepper_z') + self.z_position = zconfig.getfloat('position_min', 0., + note_valid=False) + else: + pconfig = config.getsection('printer') + self.z_position = pconfig.getfloat('minimum_z_position', 0., + note_valid=False) + self.homing_helper = HomingViaProbeHelper(config, mcu_probe) + # Configurable probing speeds + self.speed = config.getfloat('speed', 5.0, above=0.) + self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.) + # Multi-sample support (for improved accuracy) + self.sample_count = config.getint('samples', 1, minval=1) + self.sample_retract_dist = config.getfloat('sample_retract_dist', 2., + above=0.) + atypes = ['median', 'average'] + self.samples_result = config.getchoice('samples_result', atypes, + 'average') + self.samples_tolerance = config.getfloat('samples_tolerance', 0.100, + minval=0.) + self.samples_retries = config.getint('samples_tolerance_retries', 0, + minval=0) + # Session state + self.multi_probe_pending = False + self.results = [] + # Register event handlers + self.printer.register_event_handler("gcode:command_error", + self._handle_command_error) + def _handle_command_error(self): + if self.multi_probe_pending: + try: + self.end_probe_session() + except: + logging.exception("Multi-probe end") + def _probe_state_error(self): + raise self.printer.command_error( + "Internal probe error - start/end probe session mismatch") + def start_probe_session(self, gcmd): + if self.multi_probe_pending: + self._probe_state_error() + self.mcu_probe.multi_probe_begin() + self.multi_probe_pending = True + self.results = [] + return self + def end_probe_session(self): + if not self.multi_probe_pending: + self._probe_state_error() + self.results = [] + self.multi_probe_pending = False + self.mcu_probe.multi_probe_end() + def get_probe_params(self, gcmd=None): + if gcmd is None: + gcmd = self.dummy_gcode_cmd + probe_speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.) + lift_speed = gcmd.get_float("LIFT_SPEED", self.lift_speed, above=0.) + samples = gcmd.get_int("SAMPLES", self.sample_count, minval=1) + sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST", + self.sample_retract_dist, above=0.) + samples_tolerance = gcmd.get_float("SAMPLES_TOLERANCE", + self.samples_tolerance, minval=0.) + samples_retries = gcmd.get_int("SAMPLES_TOLERANCE_RETRIES", + self.samples_retries, minval=0) + samples_result = gcmd.get("SAMPLES_RESULT", self.samples_result) + return {'probe_speed': probe_speed, + 'lift_speed': lift_speed, + 'samples': samples, + 'sample_retract_dist': sample_retract_dist, + 'samples_tolerance': samples_tolerance, + 'samples_tolerance_retries': samples_retries, + 'samples_result': samples_result} def _probe(self, speed): toolhead = self.printer.lookup_object('toolhead') curtime = self.printer.get_reactor().monotonic() @@ -126,169 +321,181 @@ class PrinterProbe: if "Timeout during endstop homing" in reason: reason += HINT_TIMEOUT raise self.printer.command_error(reason) - # get z compensation from axis_twist_compensation - axis_twist_compensation = self.printer.lookup_object( - 'axis_twist_compensation', None) - z_compensation = 0 - if axis_twist_compensation is not None: - z_compensation = ( - axis_twist_compensation.get_z_compensation_value(pos)) - # add z compensation to probe position - epos[2] += z_compensation - self.gcode.respond_info("probe at %.3f,%.3f is z=%.6f" - % (epos[0], epos[1], epos[2])) + # Allow axis_twist_compensation to update results + self.printer.send_event("probe:update_results", epos) + # Report results + gcode = self.printer.lookup_object('gcode') + gcode.respond_info("probe at %.3f,%.3f is z=%.6f" + % (epos[0], epos[1], epos[2])) return epos[:3] - def _move(self, coord, speed): - self.printer.lookup_object('toolhead').manual_move(coord, speed) - def _calc_mean(self, positions): - count = float(len(positions)) - return [sum([pos[i] for pos in positions]) / count - for i in range(3)] - def _calc_median(self, positions): - z_sorted = sorted(positions, key=(lambda p: p[2])) - middle = len(positions) // 2 - if (len(positions) & 1) == 1: - # odd number of samples - return z_sorted[middle] - # even number of samples - return self._calc_mean(z_sorted[middle-1:middle+1]) def run_probe(self, gcmd): - speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.) - lift_speed = self.get_lift_speed(gcmd) - sample_count = gcmd.get_int("SAMPLES", self.sample_count, minval=1) - sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST", - self.sample_retract_dist, above=0.) - samples_tolerance = gcmd.get_float("SAMPLES_TOLERANCE", - self.samples_tolerance, minval=0.) - samples_retries = gcmd.get_int("SAMPLES_TOLERANCE_RETRIES", - self.samples_retries, minval=0) - samples_result = gcmd.get("SAMPLES_RESULT", self.samples_result) - must_notify_multi_probe = not self.multi_probe_pending - if must_notify_multi_probe: - self.multi_probe_begin() - probexy = self.printer.lookup_object('toolhead').get_position()[:2] + if not self.multi_probe_pending: + self._probe_state_error() + params = self.get_probe_params(gcmd) + toolhead = self.printer.lookup_object('toolhead') + probexy = toolhead.get_position()[:2] retries = 0 positions = [] + sample_count = params['samples'] while len(positions) < sample_count: # Probe position - pos = self._probe(speed) + pos = self._probe(params['probe_speed']) positions.append(pos) # Check samples tolerance z_positions = [p[2] for p in positions] - if max(z_positions) - min(z_positions) > samples_tolerance: - if retries >= samples_retries: + if max(z_positions)-min(z_positions) > params['samples_tolerance']: + if retries >= params['samples_tolerance_retries']: raise gcmd.error("Probe samples exceed samples_tolerance") gcmd.respond_info("Probe samples exceed tolerance. Retrying...") retries += 1 positions = [] # Retract if len(positions) < sample_count: - self._move(probexy + [pos[2] + sample_retract_dist], lift_speed) - if must_notify_multi_probe: - self.multi_probe_end() - # Calculate and return result - if samples_result == 'median': - return self._calc_median(positions) - return self._calc_mean(positions) - cmd_PROBE_help = "Probe Z-height at current XY position" - def cmd_PROBE(self, gcmd): - pos = self.run_probe(gcmd) - gcmd.respond_info("Result is z=%.6f" % (pos[2],)) - self.last_z_result = pos[2] - cmd_QUERY_PROBE_help = "Return the status of the z-probe" - def cmd_QUERY_PROBE(self, gcmd): + toolhead.manual_move( + probexy + [pos[2] + params['sample_retract_dist']], + params['lift_speed']) + # Calculate result + epos = calc_probe_z_average(positions, params['samples_result']) + self.results.append(epos) + def pull_probed_results(self): + res = self.results + self.results = [] + return res + +# Helper to read the xyz probe offsets from the config +class ProbeOffsetsHelper: + def __init__(self, config): + self.x_offset = config.getfloat('x_offset', 0.) + self.y_offset = config.getfloat('y_offset', 0.) + self.z_offset = config.getfloat('z_offset') + def get_offsets(self): + return self.x_offset, self.y_offset, self.z_offset + + +###################################################################### +# Tools for utilizing the probe +###################################################################### + +# Helper code that can probe a series of points and report the +# position at each point. +class ProbePointsHelper: + def __init__(self, config, finalize_callback, default_points=None): + self.printer = config.get_printer() + self.finalize_callback = finalize_callback + self.probe_points = default_points + self.name = config.get_name() + self.gcode = self.printer.lookup_object('gcode') + # Read config settings + if default_points is None or config.get('points', None) is not None: + self.probe_points = config.getlists('points', seps=(',', '\n'), + parser=float, count=2) + def_move_z = config.getfloat('horizontal_move_z', 5.) + self.default_horizontal_move_z = def_move_z + self.speed = config.getfloat('speed', 50., above=0.) + self.use_offsets = False + # Internal probing state + self.lift_speed = self.speed + self.probe_offsets = (0., 0., 0.) + self.manual_results = [] + def minimum_points(self,n): + if len(self.probe_points) < n: + raise self.printer.config_error( + "Need at least %d probe points for %s" % (n, self.name)) + def update_probe_points(self, points, min_points): + self.probe_points = points + self.minimum_points(min_points) + def use_xy_offsets(self, use_offsets): + self.use_offsets = use_offsets + def get_lift_speed(self): + return self.lift_speed + def _move(self, coord, speed): + self.printer.lookup_object('toolhead').manual_move(coord, speed) + def _raise_tool(self, is_first=False): + speed = self.lift_speed + if is_first: + # Use full speed to first probe position + speed = self.speed + self._move([None, None, self.horizontal_move_z], speed) + def _invoke_callback(self, results): + # Flush lookahead queue toolhead = self.printer.lookup_object('toolhead') - print_time = toolhead.get_last_move_time() - res = self.mcu_probe.query_endstop(print_time) - self.last_state = res - gcmd.respond_info("probe: %s" % (["open", "TRIGGERED"][not not res],)) - def get_status(self, eventtime): - return {'name': self.name, - 'last_query': self.last_state, - 'last_z_result': self.last_z_result} - cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position" - def cmd_PROBE_ACCURACY(self, gcmd): - speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.) - lift_speed = self.get_lift_speed(gcmd) - sample_count = gcmd.get_int("SAMPLES", 10, minval=1) - sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST", - self.sample_retract_dist, above=0.) - toolhead = self.printer.lookup_object('toolhead') - pos = toolhead.get_position() - gcmd.respond_info("PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f" - " (samples=%d retract=%.3f" - " speed=%.1f lift_speed=%.1f)\n" - % (pos[0], pos[1], pos[2], - sample_count, sample_retract_dist, - speed, lift_speed)) - # Probe bed sample_count times - self.multi_probe_begin() - positions = [] - while len(positions) < sample_count: - # Probe position - pos = self._probe(speed) - positions.append(pos) - # Retract - liftpos = [None, None, pos[2] + sample_retract_dist] - self._move(liftpos, lift_speed) - self.multi_probe_end() - # Calculate maximum, minimum and average values - max_value = max([p[2] for p in positions]) - min_value = min([p[2] for p in positions]) - range_value = max_value - min_value - avg_value = self._calc_mean(positions)[2] - median = self._calc_median(positions)[2] - # calculate the standard deviation - deviation_sum = 0 - for i in range(len(positions)): - deviation_sum += pow(positions[i][2] - avg_value, 2.) - sigma = (deviation_sum / len(positions)) ** 0.5 - # Show information - gcmd.respond_info( - "probe accuracy results: maximum %.6f, minimum %.6f, range %.6f, " - "average %.6f, median %.6f, standard deviation %.6f" % ( - max_value, min_value, range_value, avg_value, median, sigma)) - def probe_calibrate_finalize(self, kin_pos): + toolhead.get_last_move_time() + # Invoke callback + res = self.finalize_callback(self.probe_offsets, results) + return res != "retry" + def _move_next(self, probe_num): + # Move to next XY probe point + nextpos = list(self.probe_points[probe_num]) + if self.use_offsets: + nextpos[0] -= self.probe_offsets[0] + nextpos[1] -= self.probe_offsets[1] + self._move(nextpos, self.speed) + def start_probe(self, gcmd): + manual_probe.verify_no_manual_probe(self.printer) + # Lookup objects + probe = self.printer.lookup_object('probe', None) + method = gcmd.get('METHOD', 'automatic').lower() + def_move_z = self.default_horizontal_move_z + self.horizontal_move_z = gcmd.get_float('HORIZONTAL_MOVE_Z', + def_move_z) + if probe is None or method == 'manual': + # Manual probe + self.lift_speed = self.speed + self.probe_offsets = (0., 0., 0.) + self.manual_results = [] + self._manual_probe_start() + return + # Perform automatic probing + self.lift_speed = probe.get_probe_params(gcmd)['lift_speed'] + self.probe_offsets = probe.get_offsets() + if self.horizontal_move_z < self.probe_offsets[2]: + raise gcmd.error("horizontal_move_z can't be less than" + " probe's z_offset") + probe_session = probe.start_probe_session(gcmd) + probe_num = 0 + while 1: + self._raise_tool(not probe_num) + if probe_num >= len(self.probe_points): + results = probe_session.pull_probed_results() + done = self._invoke_callback(results) + if done: + break + # Caller wants a "retry" - restart probing + probe_num = 0 + self._move_next(probe_num) + probe_session.run_probe(gcmd) + probe_num += 1 + probe_session.end_probe_session() + def _manual_probe_start(self): + self._raise_tool(not self.manual_results) + if len(self.manual_results) >= len(self.probe_points): + done = self._invoke_callback(self.manual_results) + if done: + return + # Caller wants a "retry" - clear results and restart probing + self.manual_results = [] + self._move_next(len(self.manual_results)) + gcmd = self.gcode.create_gcode_command("", "", {}) + manual_probe.ManualProbeHelper(self.printer, gcmd, + self._manual_probe_finalize) + def _manual_probe_finalize(self, kin_pos): if kin_pos is None: return - z_offset = self.probe_calibrate_z - kin_pos[2] - self.gcode.respond_info( - "%s: z_offset: %.3f\n" - "The SAVE_CONFIG command will update the printer config file\n" - "with the above and restart the printer." % (self.name, z_offset)) - configfile = self.printer.lookup_object('configfile') - configfile.set(self.name, 'z_offset', "%.3f" % (z_offset,)) - cmd_PROBE_CALIBRATE_help = "Calibrate the probe's z_offset" - def cmd_PROBE_CALIBRATE(self, gcmd): - manual_probe.verify_no_manual_probe(self.printer) - # Perform initial probe - lift_speed = self.get_lift_speed(gcmd) - curpos = self.run_probe(gcmd) - # Move away from the bed - self.probe_calibrate_z = curpos[2] - curpos[2] += 5. - self._move(curpos, lift_speed) - # Move the nozzle over the probe point - curpos[0] += self.x_offset - curpos[1] += self.y_offset - self._move(curpos, self.speed) - # Start manual probe - manual_probe.ManualProbeHelper(self.printer, gcmd, - self.probe_calibrate_finalize) - def cmd_Z_OFFSET_APPLY_PROBE(self,gcmd): - offset = self.gcode_move.get_status()['homing_origin'].z - configfile = self.printer.lookup_object('configfile') - if offset == 0: - self.gcode.respond_info("Nothing to do: Z Offset is 0") - else: - new_calibrate = self.z_offset - offset - self.gcode.respond_info( - "%s: z_offset: %.3f\n" - "The SAVE_CONFIG command will update the printer config file\n" - "with the above and restart the printer." - % (self.name, new_calibrate)) - configfile.set(self.name, 'z_offset', "%.3f" % (new_calibrate,)) - cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset" + self.manual_results.append(kin_pos) + self._manual_probe_start() + +# Helper to obtain a single probe measurement +def run_single_probe(probe, gcmd): + probe_session = probe.start_probe_session(gcmd) + probe_session.run_probe(gcmd) + pos = probe_session.pull_probed_results()[0] + probe_session.end_probe_session() + return pos + + +###################################################################### +# Handle [probe] config +###################################################################### # Endstop wrapper that enables probe specific features class ProbeEndstopWrapper: @@ -304,12 +511,7 @@ class ProbeEndstopWrapper: config, 'deactivate_gcode', '') # Create an "endstop" object to handle the probe pin ppins = self.printer.lookup_object('pins') - pin = config.get('pin') - pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True) - mcu = pin_params['chip'] - self.mcu_endstop = mcu.setup_pin('endstop', pin_params) - self.printer.register_event_handler('klippy:mcu_identify', - self._handle_mcu_identify) + self.mcu_endstop = ppins.setup_pin('endstop', config.get('pin')) # Wrappers self.get_mcu = self.mcu_endstop.get_mcu self.add_stepper = self.mcu_endstop.add_stepper @@ -319,11 +521,6 @@ class ProbeEndstopWrapper: self.query_endstop = self.mcu_endstop.query_endstop # multi probes state self.multi = 'OFF' - def _handle_mcu_identify(self): - kin = self.printer.lookup_object('toolhead').get_kinematics() - for stepper in kin.get_steppers(): - if stepper.is_active_axis('z'): - self.add_stepper(stepper) def _raise_probe(self): toolhead = self.printer.lookup_object('toolhead') start_pos = toolhead.get_position() @@ -361,100 +558,23 @@ class ProbeEndstopWrapper: def get_position_endstop(self): return self.position_endstop -# Helper code that can probe a series of points and report the -# position at each point. -class ProbePointsHelper: - def __init__(self, config, finalize_callback, default_points=None): +# Main external probe interface +class PrinterProbe: + def __init__(self, config): self.printer = config.get_printer() - self.finalize_callback = finalize_callback - self.probe_points = default_points - self.name = config.get_name() - self.gcode = self.printer.lookup_object('gcode') - # Read config settings - if default_points is None or config.get('points', None) is not None: - self.probe_points = config.getlists('points', seps=(',', '\n'), - parser=float, count=2) - def_move_z = config.getfloat('horizontal_move_z', 5.) - self.default_horizontal_move_z = def_move_z - self.speed = config.getfloat('speed', 50., above=0.) - self.use_offsets = False - # Internal probing state - self.lift_speed = self.speed - self.probe_offsets = (0., 0., 0.) - self.results = [] - def minimum_points(self,n): - if len(self.probe_points) < n: - raise self.printer.config_error( - "Need at least %d probe points for %s" % (n, self.name)) - def update_probe_points(self, points, min_points): - self.probe_points = points - self.minimum_points(min_points) - def use_xy_offsets(self, use_offsets): - self.use_offsets = use_offsets - def get_lift_speed(self): - return self.lift_speed - def _move_next(self): - toolhead = self.printer.lookup_object('toolhead') - # Lift toolhead - speed = self.lift_speed - if not self.results: - # Use full speed to first probe position - speed = self.speed - toolhead.manual_move([None, None, self.horizontal_move_z], speed) - # Check if done probing - if len(self.results) >= len(self.probe_points): - toolhead.get_last_move_time() - res = self.finalize_callback(self.probe_offsets, self.results) - if res != "retry": - return True - self.results = [] - # Move to next XY probe point - nextpos = list(self.probe_points[len(self.results)]) - if self.use_offsets: - nextpos[0] -= self.probe_offsets[0] - nextpos[1] -= self.probe_offsets[1] - toolhead.manual_move(nextpos, self.speed) - return False - def start_probe(self, gcmd): - manual_probe.verify_no_manual_probe(self.printer) - # Lookup objects - probe = self.printer.lookup_object('probe', None) - method = gcmd.get('METHOD', 'automatic').lower() - self.results = [] - def_move_z = self.default_horizontal_move_z - self.horizontal_move_z = gcmd.get_float('HORIZONTAL_MOVE_Z', - def_move_z) - if probe is None or method != 'automatic': - # Manual probe - self.lift_speed = self.speed - self.probe_offsets = (0., 0., 0.) - self._manual_probe_start() - return - # Perform automatic probing - self.lift_speed = probe.get_lift_speed(gcmd) - self.probe_offsets = probe.get_offsets() - if self.horizontal_move_z < self.probe_offsets[2]: - raise gcmd.error("horizontal_move_z can't be less than" - " probe's z_offset") - probe.multi_probe_begin() - while 1: - done = self._move_next() - if done: - break - pos = probe.run_probe(gcmd) - self.results.append(pos) - probe.multi_probe_end() - def _manual_probe_start(self): - done = self._move_next() - if not done: - gcmd = self.gcode.create_gcode_command("", "", {}) - manual_probe.ManualProbeHelper(self.printer, gcmd, - self._manual_probe_finalize) - def _manual_probe_finalize(self, kin_pos): - if kin_pos is None: - return - self.results.append(kin_pos) - self._manual_probe_start() + self.mcu_probe = ProbeEndstopWrapper(config) + self.cmd_helper = ProbeCommandHelper(config, self, + self.mcu_probe.query_endstop) + self.probe_offsets = ProbeOffsetsHelper(config) + self.probe_session = ProbeSessionHelper(config, self.mcu_probe) + def get_probe_params(self, gcmd=None): + return self.probe_session.get_probe_params(gcmd) + def get_offsets(self): + return self.probe_offsets.get_offsets() + def get_status(self, eventtime): + return self.cmd_helper.get_status(eventtime) + def start_probe_session(self, gcmd): + return self.probe_session.start_probe_session(gcmd) def load_config(config): - return PrinterProbe(config, ProbeEndstopWrapper(config)) + return PrinterProbe(config) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 858fb6e09..932d1bfa3 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -7,11 +7,14 @@ import logging, math, bisect import mcu from . import ldc1612, probe, manual_probe +OUT_OF_RANGE = 99.9 + # Tool for calibrating the sensor Z detection and applying that calibration class EddyCalibration: def __init__(self, config): self.printer = config.get_printer() self.name = config.get_name() + self.drift_comp = DummyDriftCompensation() # Current calibration data self.cal_freqs = [] self.cal_zpos = [] @@ -35,12 +38,14 @@ class EddyCalibration: self.cal_freqs = [c[0] for c in cal] self.cal_zpos = [c[1] for c in cal] def apply_calibration(self, samples): + cur_temp = self.drift_comp.get_temperature() for i, (samp_time, freq, dummy_z) in enumerate(samples): - pos = bisect.bisect(self.cal_freqs, freq) + adj_freq = self.drift_comp.adjust_freq(freq, cur_temp) + pos = bisect.bisect(self.cal_freqs, adj_freq) if pos >= len(self.cal_zpos): - zpos = -99.9 + zpos = -OUT_OF_RANGE elif pos == 0: - zpos = 99.9 + zpos = OUT_OF_RANGE else: # XXX - could further optimize and avoid div by zero this_freq = self.cal_freqs[pos] @@ -49,8 +54,12 @@ class EddyCalibration: prev_zpos = self.cal_zpos[pos - 1] gain = (this_zpos - prev_zpos) / (this_freq - prev_freq) offset = prev_zpos - prev_freq * gain - zpos = freq * gain + offset + zpos = adj_freq * gain + offset samples[i] = (samp_time, freq, round(zpos, 6)) + def freq_to_height(self, freq): + dummy_sample = [(0., freq, 0.)] + self.apply_calibration(dummy_sample) + return dummy_sample[0][2] def height_to_freq(self, height): # XXX - could optimize lookup rev_zpos = list(reversed(self.cal_zpos)) @@ -65,7 +74,8 @@ class EddyCalibration: prev_zpos = rev_zpos[pos - 1] gain = (this_freq - prev_freq) / (this_zpos - prev_zpos) offset = prev_freq - prev_zpos * gain - return height * gain + offset + freq = height * gain + offset + return self.drift_comp.unadjust_freq(freq) def do_calibration_moves(self, move_speed): toolhead = self.printer.lookup_object('toolhead') kin = toolhead.get_kinematics() @@ -80,19 +90,20 @@ class EddyCalibration: return True self.printer.lookup_object(self.name).add_client(handle_batch) toolhead.dwell(1.) - # Move to each 50um position - max_z = 4 - samp_dist = 0.050 - num_steps = int(max_z / samp_dist + .5) + 1 + self.drift_comp.note_z_calibration_start() + # Move to each 40um position + max_z = 4.0 + samp_dist = 0.040 + req_zpos = [i*samp_dist for i in range(int(max_z / samp_dist) + 1)] start_pos = toolhead.get_position() times = [] - for i in range(num_steps): + for zpos in req_zpos: # Move to next position (always descending to reduce backlash) hop_pos = list(start_pos) - hop_pos[2] += i * samp_dist + 0.500 + hop_pos[2] += zpos + 0.500 move(hop_pos, move_speed) next_pos = list(start_pos) - next_pos[2] += i * samp_dist + next_pos[2] += zpos move(next_pos, move_speed) # Note sample timing start_query_time = toolhead.get_last_move_time() + 0.050 @@ -106,6 +117,7 @@ class EddyCalibration: times.append((start_query_time, end_query_time, kin_pos[2])) toolhead.dwell(1.0) toolhead.wait_moves() + self.drift_comp.note_z_calibration_finish() # Finish data collection is_finished = True # Correlate query responses @@ -182,9 +194,116 @@ class EddyCalibration: # Start manual probe manual_probe.ManualProbeHelper(self.printer, gcmd, self.post_manual_probe) + def register_drift_compensation(self, comp): + self.drift_comp = comp -# Helper for implementing PROBE style commands +# Tool to gather samples and convert them to probe positions +class EddyGatherSamples: + def __init__(self, printer, sensor_helper, calibration, z_offset): + self._printer = printer + self._sensor_helper = sensor_helper + self._calibration = calibration + self._z_offset = z_offset + # Results storage + self._samples = [] + self._probe_times = [] + self._probe_results = [] + self._need_stop = False + # Start samples + if not self._calibration.is_calibrated(): + raise self._printer.command_error( + "Must calibrate probe_eddy_current first") + sensor_helper.add_client(self._add_measurement) + def _add_measurement(self, msg): + if self._need_stop: + del self._samples[:] + return False + self._samples.append(msg) + self._check_samples() + return True + def finish(self): + self._need_stop = True + def _await_samples(self): + # Make sure enough samples have been collected + reactor = self._printer.get_reactor() + mcu = self._sensor_helper.get_mcu() + while self._probe_times: + start_time, end_time, pos_time, toolhead_pos = self._probe_times[0] + systime = reactor.monotonic() + est_print_time = mcu.estimated_print_time(systime) + if est_print_time > end_time + 1.0: + raise self._printer.command_error( + "probe_eddy_current sensor outage") + reactor.pause(systime + 0.010) + def _pull_freq(self, start_time, end_time): + # Find average sensor frequency between time range + msg_num = discard_msgs = 0 + samp_sum = 0. + samp_count = 0 + while msg_num < len(self._samples): + msg = self._samples[msg_num] + msg_num += 1 + data = msg['data'] + if data[0][0] > end_time: + break + if data[-1][0] < start_time: + discard_msgs = msg_num + continue + for time, freq, z in data: + if time >= start_time and time <= end_time: + samp_sum += freq + samp_count += 1 + del self._samples[:discard_msgs] + if not samp_count: + # No sensor readings - raise error in pull_probed() + return 0. + return samp_sum / samp_count + def _lookup_toolhead_pos(self, pos_time): + toolhead = self._printer.lookup_object('toolhead') + kin = toolhead.get_kinematics() + kin_spos = {s.get_name(): s.mcu_to_commanded_position( + s.get_past_mcu_position(pos_time)) + for s in kin.get_steppers()} + return kin.calc_position(kin_spos) + def _check_samples(self): + while self._samples and self._probe_times: + start_time, end_time, pos_time, toolhead_pos = self._probe_times[0] + if self._samples[-1]['data'][-1][0] < end_time: + break + freq = self._pull_freq(start_time, end_time) + if pos_time is not None: + toolhead_pos = self._lookup_toolhead_pos(pos_time) + sensor_z = None + if freq: + sensor_z = self._calibration.freq_to_height(freq) + self._probe_results.append((sensor_z, toolhead_pos)) + self._probe_times.pop(0) + def pull_probed(self): + self._await_samples() + results = [] + for sensor_z, toolhead_pos in self._probe_results: + if sensor_z is None: + raise self._printer.command_error( + "Unable to obtain probe_eddy_current sensor readings") + if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: + raise self._printer.command_error( + "probe_eddy_current sensor not in valid range") + # Callers expect position relative to z_offset, so recalculate + bed_deviation = toolhead_pos[2] - sensor_z + toolhead_pos[2] = self._z_offset + bed_deviation + results.append(toolhead_pos) + del self._probe_results[:] + return results + def note_probe(self, start_time, end_time, toolhead_pos): + self._probe_times.append((start_time, end_time, None, toolhead_pos)) + self._check_samples() + def note_probe_and_position(self, start_time, end_time, pos_time): + self._probe_times.append((start_time, end_time, pos_time, None)) + self._check_samples() + +# Helper for implementing PROBE style commands (descend until trigger) class EddyEndstopWrapper: + REASON_SENSOR_ERROR = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 def __init__(self, config, sensor_helper, calibration): self._printer = config.get_printer() self._sensor_helper = sensor_helper @@ -192,35 +311,8 @@ class EddyEndstopWrapper: self._calibration = calibration self._z_offset = config.getfloat('z_offset', minval=0.) self._dispatch = mcu.TriggerDispatch(self._mcu) - self._samples = [] - self._is_sampling = self._start_from_home = self._need_stop = False self._trigger_time = 0. - 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 + self._gather = None # Interface for MCU_endstop def get_mcu(self): return self._mcu @@ -231,20 +323,21 @@ class EddyEndstopWrapper: def home_start(self, print_time, sample_time, sample_count, rest_time, triggered=True): self._trigger_time = 0. - self._start_measurements(is_home=True) trigger_freq = self._calibration.height_to_freq(self._z_offset) trigger_completion = self._dispatch.start(print_time) self._sensor_helper.setup_home( print_time, trigger_freq, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT) + mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.REASON_SENSOR_ERROR) return trigger_completion def home_wait(self, home_end_time): self._dispatch.wait_end(home_end_time) trigger_time = self._sensor_helper.clear_home() - self._stop_measurements(is_home=True) res = self._dispatch.stop() - if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - return -1. + if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + raise self._printer.command_error( + "Communication timeout during homing") + raise self._printer.command_error("Eddy current sensor error") if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: return 0. if self._mcu.is_fileoutput(): @@ -260,48 +353,19 @@ class EddyEndstopWrapper: trig_pos = phoming.probing_move(self, pos, speed) if not self._trigger_time: return trig_pos - # Wait for 200ms to elapse since trigger time - 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.: - break - reactor.pause(systime + need_delay) - # Find position since trigger - samples = self._samples - self._samples = [] + # Extract samples start_time = self._trigger_time + 0.050 end_time = start_time + 0.100 - samp_sum = 0. - samp_count = 0 - for msg in samples: - data = msg['data'] - if data[0][0] > end_time: - break - if data[-1][0] < start_time: - continue - for time, freq, z in data: - if time >= start_time and time <= end_time: - samp_sum += z - samp_count += 1 - if not samp_count: - raise self._printer.command_error( - "Unable to obtain probe_eddy_current sensor readings") - halt_z = samp_sum / samp_count - # Calculate reported "trigger" position toolhead = self._printer.lookup_object("toolhead") - new_pos = toolhead.get_position() - new_pos[2] += self._z_offset - halt_z - return new_pos + toolhead_pos = toolhead.get_position() + self._gather.note_probe(start_time, end_time, toolhead_pos) + return self._gather.pull_probed()[0] def multi_probe_begin(self): - if not self._calibration.is_calibrated(): - raise self._printer.command_error( - "Must calibrate probe_eddy_current first") - self._start_measurements() + self._gather = EddyGatherSamples(self._printer, self._sensor_helper, + self._calibration, self._z_offset) def multi_probe_end(self): - self._stop_measurements() + self._gather.finish() + self._gather = None def probe_prepare(self, hmove): pass def probe_finish(self, hmove): @@ -309,6 +373,46 @@ class EddyEndstopWrapper: def get_position_endstop(self): return self._z_offset +# Implementing probing with "METHOD=scan" +class EddyScanningProbe: + def __init__(self, printer, sensor_helper, calibration, z_offset, gcmd): + self._printer = printer + self._sensor_helper = sensor_helper + self._calibration = calibration + self._z_offset = z_offset + self._gather = EddyGatherSamples(printer, sensor_helper, + calibration, z_offset) + self._sample_time_delay = 0.050 + self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) + self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' + def _rapid_lookahead_cb(self, printtime): + start_time = printtime - self._sample_time / 2 + self._gather.note_probe_and_position( + start_time, start_time + self._sample_time, printtime) + def run_probe(self, gcmd): + toolhead = self._printer.lookup_object("toolhead") + if self._is_rapid: + toolhead.register_lookahead_callback(self._rapid_lookahead_cb) + return + printtime = toolhead.get_last_move_time() + toolhead.dwell(self._sample_time_delay + self._sample_time) + start_time = printtime + self._sample_time_delay + self._gather.note_probe_and_position( + start_time, start_time + self._sample_time, start_time) + def pull_probed_results(self): + if self._is_rapid: + # Flush lookahead (so all lookahead callbacks are invoked) + toolhead = self._printer.lookup_object("toolhead") + toolhead.get_last_move_time() + results = self._gather.pull_probed() + # Allow axis_twist_compensation to update results + for epos in results: + self._printer.send_event("probe:update_results", epos) + return results + def end_probe_session(self): + self._gather.finish() + self._gather = None + # Main "printer object" class PrinterEddyProbe: def __init__(self, config): @@ -319,11 +423,42 @@ class PrinterEddyProbe: sensor_type = config.getchoice('sensor_type', {s: s for s in sensors}) self.sensor_helper = sensors[sensor_type](config, self.calibration) # Probe interface - self.probe = EddyEndstopWrapper(config, self.sensor_helper, - self.calibration) - self.printer.add_object('probe', probe.PrinterProbe(config, self.probe)) + self.mcu_probe = EddyEndstopWrapper(config, self.sensor_helper, + self.calibration) + self.cmd_helper = probe.ProbeCommandHelper( + config, self, self.mcu_probe.query_endstop) + self.probe_offsets = probe.ProbeOffsetsHelper(config) + self.probe_session = probe.ProbeSessionHelper(config, self.mcu_probe) + self.printer.add_object('probe', self) def add_client(self, cb): self.sensor_helper.add_client(cb) + def get_probe_params(self, gcmd=None): + return self.probe_session.get_probe_params(gcmd) + def get_offsets(self): + return self.probe_offsets.get_offsets() + def get_status(self, eventtime): + return self.cmd_helper.get_status(eventtime) + def start_probe_session(self, gcmd): + method = gcmd.get('METHOD', 'automatic').lower() + if method in ('scan', 'rapid_scan'): + z_offset = self.get_offsets()[2] + return EddyScanningProbe(self.printer, self.sensor_helper, + self.calibration, z_offset, gcmd) + return self.probe_session.start_probe_session(gcmd) + def register_drift_compensation(self, comp): + self.calibration.register_drift_compensation(comp) + +class DummyDriftCompensation: + def get_temperature(self): + return 0. + def note_z_calibration_start(self): + pass + def note_z_calibration_finish(self): + pass + def adjust_freq(self, freq, temp=None): + return freq + def unadjust_freq(self, freq, temp=None): + return freq def load_config_prefix(config): return PrinterEddyProbe(config) diff --git a/klippy/extras/replicape.py b/klippy/extras/replicape.py index ab501cafc..f7f7bb64b 100644 --- a/klippy/extras/replicape.py +++ b/klippy/extras/replicape.py @@ -160,7 +160,7 @@ class Replicape: printer = config.get_printer() ppins = printer.lookup_object('pins') ppins.register_chip('replicape', self) - revisions = {'B3': 'B3'} + revisions = ['B3'] config.getchoice('revision', revisions) self.host_mcu = mcu.get_printer_mcu(printer, config.get('host_mcu')) # Setup enable pin diff --git a/klippy/extras/servo.py b/klippy/extras/servo.py index c05c9f819..344d6a31d 100644 --- a/klippy/extras/servo.py +++ b/klippy/extras/servo.py @@ -58,13 +58,15 @@ class PrinterServo: return width * self.width_to_value cmd_SET_SERVO_help = "Set servo angle" def cmd_SET_SERVO(self, gcmd): - print_time = self.printer.lookup_object('toolhead').get_last_move_time() width = gcmd.get_float('WIDTH', None) if width is not None: - self._set_pwm(print_time, self._get_pwm_from_pulse_width(width)) + value = self._get_pwm_from_pulse_width(width) else: angle = gcmd.get_float('ANGLE') - self._set_pwm(print_time, self._get_pwm_from_angle(angle)) + value = self._get_pwm_from_angle(angle) + toolhead = self.printer.lookup_object('toolhead') + toolhead.register_lookahead_callback((lambda pt: + self._set_pwm(pt, value))) def load_config_prefix(config): return PrinterServo(config) diff --git a/klippy/extras/sht3x.py b/klippy/extras/sht3x.py index 699d3f209..5a8785e8d 100644 --- a/klippy/extras/sht3x.py +++ b/klippy/extras/sht3x.py @@ -27,6 +27,13 @@ SHT3X_CMD = { 'LOW_REP': [0x24, 0x16], }, }, + 'PERIODIC': { + '2HZ': { + 'HIGH_REP': [0x22, 0x36], + 'MED_REP': [0x22, 0x20], + 'LOW_REP': [0x22, 0x2B], + }, + }, 'OTHER': { 'STATUS': { 'READ': [0xF3, 0x2D], @@ -72,10 +79,12 @@ class SHT3X: def _init_sht3x(self): # Device Soft Reset - self.i2c.i2c_write(SHT3X_CMD['OTHER']['SOFTRESET']) - - # Wait 2ms after reset - self.reactor.pause(self.reactor.monotonic() + .02) + self.i2c.i2c_write_wait_ack(SHT3X_CMD['OTHER']['BREAK']) + # Break takes ~ 1ms + self.reactor.pause(self.reactor.monotonic() + .0015) + self.i2c.i2c_write_wait_ack(SHT3X_CMD['OTHER']['SOFTRESET']) + # Wait <=1.5ms after reset + self.reactor.pause(self.reactor.monotonic() + .0015) status = self.i2c.i2c_read(SHT3X_CMD['OTHER']['STATUS']['READ'], 3) response = bytearray(status['response']) @@ -86,17 +95,17 @@ class SHT3X: if self._crc8(status) != checksum: logging.warning("sht3x: Reading status - checksum error!") + # Enable periodic mode + self.i2c.i2c_write_wait_ack( + SHT3X_CMD['PERIODIC']['2HZ']['HIGH_REP'] + ) + # Wait <=15.5ms for first measurment + self.reactor.pause(self.reactor.monotonic() + .0155) + def _sample_sht3x(self, eventtime): try: - # Read Temeprature - params = self.i2c.i2c_write( - SHT3X_CMD['MEASURE']['STRETCH_ENABLED']['HIGH_REP'] - ) - # Wait - self.reactor.pause(self.reactor.monotonic() - + .20) - - params = self.i2c.i2c_read([], 6) + # Read measurment + params = self.i2c.i2c_read(SHT3X_CMD['OTHER']['FETCH'], 6) response = bytearray(params['response']) rtemp = response[0] << 8 diff --git a/klippy/extras/smart_effector.py b/klippy/extras/smart_effector.py index c33de5275..6e5867893 100644 --- a/klippy/extras/smart_effector.py +++ b/klippy/extras/smart_effector.py @@ -48,7 +48,7 @@ class ControlPinHelper: bit_time += bit_step return bit_time -class SmartEffectorEndstopWrapper: +class SmartEffectorProbe: def __init__(self, config): self.printer = config.get_printer() self.gcode = self.printer.lookup_object('gcode') @@ -64,6 +64,12 @@ class SmartEffectorEndstopWrapper: self.query_endstop = self.probe_wrapper.query_endstop self.multi_probe_begin = self.probe_wrapper.multi_probe_begin self.multi_probe_end = self.probe_wrapper.multi_probe_end + self.get_position_endstop = self.probe_wrapper.get_position_endstop + # Common probe implementation helpers + self.cmd_helper = probe.ProbeCommandHelper( + config, self, self.probe_wrapper.query_endstop) + self.probe_offsets = probe.ProbeOffsetsHelper(config) + self.probe_session = probe.ProbeSessionHelper(config, self) # SmartEffector control control_pin = config.get('control_pin', None) if control_pin: @@ -78,6 +84,14 @@ class SmartEffectorEndstopWrapper: self.gcode.register_command("SET_SMART_EFFECTOR", self.cmd_SET_SMART_EFFECTOR, desc=self.cmd_SET_SMART_EFFECTOR_help) + def get_probe_params(self, gcmd=None): + return self.probe_session.get_probe_params(gcmd) + def get_offsets(self): + return self.probe_offsets.get_offsets() + def get_status(self, eventtime): + return self.cmd_helper.get_status(eventtime) + def start_probe_session(self, gcmd): + return self.probe_session.start_probe_session(gcmd) def probing_move(self, pos, speed): phoming = self.printer.lookup_object('homing') return phoming.probing_move(self, pos, speed) @@ -151,7 +165,6 @@ class SmartEffectorEndstopWrapper: gcmd.respond_info('SmartEffector sensitivity was reset') def load_config(config): - smart_effector = SmartEffectorEndstopWrapper(config) - config.get_printer().add_object('probe', - probe.PrinterProbe(config, smart_effector)) + smart_effector = SmartEffectorProbe(config) + config.get_printer().add_object('probe', smart_effector) return smart_effector diff --git a/klippy/extras/temperature_mcu.py b/klippy/extras/temperature_mcu.py index 585ec4c1d..be2cd145c 100644 --- a/klippy/extras/temperature_mcu.py +++ b/klippy/extras/temperature_mcu.py @@ -1,10 +1,11 @@ # Support for micro-controller chip based temperature sensors # -# Copyright (C) 2020 Kevin O'Connor +# Copyright (C) 2020-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import logging import mcu +from . import adc_temperature SAMPLE_TIME = 0.001 SAMPLE_COUNT = 8 @@ -31,30 +32,33 @@ class PrinterTemperatureMCU: self.mcu_adc = ppins.setup_pin('adc', '%s:ADC_TEMPERATURE' % (mcu_name,)) self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback) - query_adc = config.get_printer().load_object(config, 'query_adc') - query_adc.register_adc(config.get_name(), self.mcu_adc) + self.diag_helper = adc_temperature.HelperTemperatureDiagnostics( + config, self.mcu_adc, self.calc_temp) # Register callbacks if self.printer.get_start_args().get('debugoutput') is not None: - self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, - range_check_count=RANGE_CHECK_COUNT) + self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT) return self.printer.register_event_handler("klippy:mcu_identify", - self._mcu_identify) + self.handle_mcu_identify) + # Temperature interface def setup_callback(self, temperature_callback): self.temperature_callback = temperature_callback def get_report_time_delta(self): return REPORT_TIME - def adc_callback(self, read_time, read_value): - temp = self.base_temperature + read_value * self.slope - self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) def setup_minmax(self, min_temp, max_temp): self.min_temp = min_temp self.max_temp = max_temp + # Internal code + def adc_callback(self, read_time, read_value): + temp = self.base_temperature + read_value * self.slope + self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) + def calc_temp(self, adc): + return self.base_temperature + adc * self.slope def calc_adc(self, temp): return (temp - self.base_temperature) / self.slope def calc_base(self, temp, adc): return temp - adc * self.slope - def _mcu_identify(self): + def handle_mcu_identify(self): # Obtain mcu information mcu = self.mcu_adc.get_mcu() self.debug_read_cmd = mcu.lookup_query_command( @@ -89,10 +93,13 @@ class PrinterTemperatureMCU: self.slope = (self.temp2 - self.temp1) / (self.adc2 - self.adc1) self.base_temperature = self.calc_base(self.temp1, self.adc1) # Setup min/max checks - adc_range = [self.calc_adc(t) for t in [self.min_temp, self.max_temp]] - self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, - minval=min(adc_range), maxval=max(adc_range), - range_check_count=RANGE_CHECK_COUNT) + arange = [self.calc_adc(t) for t in [self.min_temp, self.max_temp]] + min_adc, max_adc = sorted(arange) + self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT, + minval=min_adc, maxval=max_adc, + range_check_count=RANGE_CHECK_COUNT) + self.diag_helper.setup_diag_minmax(self.min_temp, self.max_temp, + min_adc, max_adc) def config_unknown(self): raise self.printer.config_error("MCU temperature not supported on %s" % (self.mcu_type,)) diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py new file mode 100644 index 000000000..05eac34ef --- /dev/null +++ b/klippy/extras/temperature_probe.py @@ -0,0 +1,721 @@ +# Probe temperature sensor and drift calibration +# +# Copyright (C) 2024 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +from . import manual_probe + +KELVIN_TO_CELSIUS = -273.15 + +###################################################################### +# Polynomial Helper Classes and Functions +###################################################################### + +def calc_determinant(matrix): + m = matrix + aei = m[0][0] * m[1][1] * m[2][2] + bfg = m[1][0] * m[2][1] * m[0][2] + cdh = m[2][0] * m[0][1] * m[1][2] + ceg = m[2][0] * m[1][1] * m[0][2] + bdi = m[1][0] * m[0][1] * m[2][2] + afh = m[0][0] * m[2][1] * m[1][2] + return aei + bfg + cdh - ceg - bdi - afh + +class Polynomial2d: + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c + + def __call__(self, xval): + return self.c * xval * xval + self.b * xval + self.a + + def get_coefs(self): + return (self.a, self.b, self.c) + + def __str__(self): + return "%f, %f, %f" % (self.a, self.b, self.c) + + def __repr__(self): + parts = ["y(x) ="] + deg = 2 + for i, coef in enumerate((self.c, self.b, self.a)): + if round(coef, 8) == int(coef): + coef = int(coef) + if abs(coef) < 1e-10: + continue + cur_deg = deg - i + x_str = "x^%d" % (cur_deg,) if cur_deg > 1 else "x" * cur_deg + if len(parts) == 1: + parts.append("%f%s" % (coef, x_str)) + else: + sym = "-" if coef < 0 else "+" + parts.append("%s %f%s" % (sym, abs(coef), x_str)) + return " ".join(parts) + + @classmethod + def fit(cls, coords): + xlist = [c[0] for c in coords] + ylist = [c[1] for c in coords] + count = len(coords) + sum_x = sum(xlist) + sum_y = sum(ylist) + sum_x2 = sum([x**2 for x in xlist]) + sum_x3 = sum([x**3 for x in xlist]) + sum_x4 = sum([x**4 for x in xlist]) + sum_xy = sum([x * y for x, y in coords]) + sum_x2y = sum([y*x**2 for x, y in coords]) + vector_b = [sum_y, sum_xy, sum_x2y] + m = [ + [count, sum_x, sum_x2], + [sum_x, sum_x2, sum_x3], + [sum_x2, sum_x3, sum_x4] + ] + m0 = [vector_b, m[1], m[2]] + m1 = [m[0], vector_b, m[2]] + m2 = [m[0], m[1], vector_b] + det_m = calc_determinant(m) + a0 = calc_determinant(m0) / det_m + a1 = calc_determinant(m1) / det_m + a2 = calc_determinant(m2) / det_m + return cls(a0, a1, a2) + +class TemperatureProbe: + def __init__(self, config): + self.name = config.get_name() + self.printer = config.get_printer() + self.gcode = self.printer.lookup_object("gcode") + self.speed = config.getfloat("speed", None, above=0.) + self.horizontal_move_z = config.getfloat( + "horizontal_move_z", 2., above=0. + ) + self.resting_z = config.getfloat("resting_z", .4, above=0.) + self.cal_pos = config.getfloatlist( + "calibration_position", None, count=3 + ) + self.cal_bed_temp = config.getfloat( + "calibration_bed_temp", None, above=50. + ) + self.cal_extruder_temp = config.getfloat( + "calibration_extruder_temp", None, above=50. + ) + self.cal_extruder_z = config.getfloat( + "extruder_heating_z", 50., above=0. + ) + # Setup temperature sensor + smooth_time = config.getfloat("smooth_time", 2., above=0.) + self.inv_smooth_time = 1. / smooth_time + self.min_temp = config.getfloat( + "min_temp", KELVIN_TO_CELSIUS, minval=KELVIN_TO_CELSIUS + ) + self.max_temp = config.getfloat( + "max_temp", 99999999.9, above=self.min_temp + ) + pheaters = self.printer.load_object(config, "heaters") + self.sensor = pheaters.setup_sensor(config) + self.sensor.setup_minmax(self.min_temp, self.max_temp) + self.sensor.setup_callback(self._temp_callback) + pheaters.register_sensor(config, self) + self.last_temp_read_time = 0. + self.last_measurement = (0., 99999999., 0.,) + # Calibration State + self.cal_helper = None + self.next_auto_temp = 99999999. + self.target_temp = 0 + self.expected_count = 0 + self.sample_count = 0 + self.in_calibration = False + self.step = 2. + self.last_zero_pos = None + self.total_expansion = 0 + self.start_pos = [] + + # Register GCode Commands + pname = self.name.split(maxsplit=1)[-1] + self.gcode.register_mux_command( + "TEMPERATURE_PROBE_CALIBRATE", "PROBE", pname, + self.cmd_TEMPERATURE_PROBE_CALIBRATE, + desc=self.cmd_TEMPERATURE_PROBE_CALIBRATE_help + ) + + self.gcode.register_mux_command( + "TEMPERATURE_PROBE_ENABLE", "PROBE", pname, + self.cmd_TEMPERATURE_PROBE_ENABLE, + desc=self.cmd_TEMPERATURE_PROBE_ENABLE_help + ) + + # Register Drift Compensation Helper with probe + full_probe_name = "probe_eddy_current %s" % (pname,) + if config.has_section(full_probe_name): + pprobe = self.printer.load_object(config, full_probe_name) + self.cal_helper = EddyDriftCompensation(config, self) + pprobe.register_drift_compensation(self.cal_helper) + logging.info( + "%s: registered drift compensation with probe [%s]" + % (self.name, full_probe_name) + ) + else: + logging.info( + "%s: No probe named %s configured, thermal drift compensation " + "disabled." % (self.name, pname) + ) + + def _temp_callback(self, read_time, temp): + smoothed_temp, measured_min, measured_max = self.last_measurement + time_diff = read_time - self.last_temp_read_time + self.last_temp_read_time = read_time + temp_diff = temp - smoothed_temp + adj_time = min(time_diff * self.inv_smooth_time, 1.) + smoothed_temp += temp_diff * adj_time + measured_min = min(measured_min, smoothed_temp) + measured_max = max(measured_max, smoothed_temp) + self.last_measurement = (smoothed_temp, measured_min, measured_max) + if self.in_calibration and smoothed_temp >= self.next_auto_temp: + self.printer.get_reactor().register_async_callback( + self._check_kick_next + ) + + def _check_kick_next(self, eventtime): + smoothed_temp = self.last_measurement[0] + if self.in_calibration and smoothed_temp >= self.next_auto_temp: + self.next_auto_temp = 99999999. + self.gcode.run_script("TEMPERATURE_PROBE_NEXT") + + def get_temp(self, eventtime=None): + return self.last_measurement[0], self.target_temp + + def _collect_sample(self, kin_pos, tool_zero_z): + probe = self._get_probe() + x_offset, y_offset, _ = probe.get_offsets() + speeds = self._get_speeds() + lift_speed, _, move_speed = speeds + toolhead = self.printer.lookup_object("toolhead") + cur_pos = toolhead.get_position() + # Move to probe to sample collection position + cur_pos[2] += self.horizontal_move_z + toolhead.manual_move(cur_pos, lift_speed) + cur_pos[0] -= x_offset + cur_pos[1] -= y_offset + toolhead.manual_move(cur_pos, move_speed) + return self.cal_helper.collect_sample(kin_pos, tool_zero_z, speeds) + + def _prepare_next_sample(self, last_temp, tool_zero_z): + # Register our own abort command now that the manual + # probe has finished and unregistered + self.gcode.register_command( + "ABORT", self.cmd_TEMPERATURE_PROBE_ABORT, + desc=self.cmd_TEMPERATURE_PROBE_ABORT_help + ) + probe_speed = self._get_speeds()[1] + # Move tool down to the resting position + toolhead = self.printer.lookup_object("toolhead") + cur_pos = toolhead.get_position() + cur_pos[2] = tool_zero_z + self.resting_z + toolhead.manual_move(cur_pos, probe_speed) + cnt, exp_cnt = self.sample_count, self.expected_count + self.next_auto_temp = last_temp + self.step + self.gcode.respond_info( + "%s: collected sample %d/%d at temp %.2fC, next sample scheduled " + "at temp %.2fC" + % (self.name, cnt, exp_cnt, last_temp, self.next_auto_temp) + ) + + def _manual_probe_finalize(self, kin_pos): + if kin_pos is None: + # Calibration aborted + self._finalize_drift_cal(False) + return + if self.last_zero_pos is not None: + z_diff = self.last_zero_pos[2] - kin_pos[2] + self.total_expansion += z_diff + logging.info( + "Estimated Total Thermal Expansion: %.6f" + % (self.total_expansion,) + ) + self.last_zero_pos = kin_pos + toolhead = self.printer.lookup_object("toolhead") + tool_zero_z = toolhead.get_position()[2] + try: + last_temp = self._collect_sample(kin_pos, tool_zero_z) + except Exception: + self._finalize_drift_cal(False) + raise + self.sample_count += 1 + if last_temp >= self.target_temp: + # Calibration Done + self._finalize_drift_cal(True) + else: + try: + self._prepare_next_sample(last_temp, tool_zero_z) + if self.sample_count == 1: + self._set_bed_temp(self.cal_bed_temp) + except Exception: + self._finalize_drift_cal(False) + raise + + def _finalize_drift_cal(self, success, msg=None): + self.next_auto_temp = 99999999. + self.target_temp = 0 + self.expected_count = 0 + self.sample_count = 0 + self.step = 2. + self.in_calibration = False + self.last_zero_pos = None + self.total_expansion = 0 + self.start_pos = [] + # Unregister Temporary Commands + self.gcode.register_command("ABORT", None) + self.gcode.register_command("TEMPERATURE_PROBE_NEXT", None) + self.gcode.register_command("TEMPERATURE_PROBE_COMPLETE", None) + # Turn off heaters + self._set_extruder_temp(0) + self._set_bed_temp(0) + try: + self.cal_helper.finish_calibration(success) + except self.gcode.error as e: + success = False + msg = str(e) + if not success: + msg = msg or "%s: calibration aborted" % (self.name,) + self.gcode.respond_info(msg) + + def _get_probe(self): + probe = self.printer.lookup_object("probe") + if probe is None: + raise self.gcode.error("No probe configured") + return probe + + def _set_extruder_temp(self, temp, wait=False): + if self.cal_extruder_temp is None: + # Extruder temperature not configured + return + toolhead = self.printer.lookup_object("toolhead") + extr_name = toolhead.get_extruder().get_name() + self.gcode.run_script_from_command( + "SET_HEATER_TEMPERATURE HEATER=%s TARGET=%f" + % (extr_name, temp) + ) + if wait: + self.gcode.run_script_from_command( + "TEMPERATURE_WAIT SENSOR=%s MINIMUM=%f" + % (extr_name, temp) + ) + def _set_bed_temp(self, temp): + if self.cal_bed_temp is None: + # Bed temperature not configured + return + self.gcode.run_script_from_command( + "SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=%f" + % (temp,) + ) + + def _check_homed(self): + toolhead = self.printer.lookup_object("toolhead") + reactor = self.printer.get_reactor() + status = toolhead.get_status(reactor.monotonic()) + h_axes = status["homed_axes"] + for axis in "xyz": + if axis not in h_axes: + raise self.gcode.error( + "Printer must be homed before calibration" + ) + + def _move_to_start(self): + toolhead = self.printer.lookup_object("toolhead") + cur_pos = toolhead.get_position() + move_speed = self._get_speeds()[2] + if self.cal_pos is not None: + if self.cal_extruder_temp is not None: + # Move to extruder heating z position + cur_pos[2] = self.cal_extruder_z + toolhead.manual_move(cur_pos, move_speed) + toolhead.manual_move(self.cal_pos[:2], move_speed) + self._set_extruder_temp(self.cal_extruder_temp, True) + toolhead.manual_move(self.cal_pos, move_speed) + elif self.cal_extruder_temp is not None: + cur_pos[2] = self.cal_extruder_z + toolhead.manual_move(cur_pos, move_speed) + self._set_extruder_temp(self.cal_extruder_temp, True) + + def _get_speeds(self): + pparams = self._get_probe().get_probe_params() + probe_speed = pparams["probe_speed"] + lift_speed = pparams["lift_speed"] + move_speed = self.speed or max(probe_speed, lift_speed) + return lift_speed, probe_speed, move_speed + + cmd_TEMPERATURE_PROBE_CALIBRATE_help = ( + "Calibrate probe temperature drift compensation" + ) + def cmd_TEMPERATURE_PROBE_CALIBRATE(self, gcmd): + if self.cal_helper is None: + raise gcmd.error( + "No calibration helper registered for [%s]" + % (self.name,) + ) + self._check_homed() + probe = self._get_probe() + probe_name = probe.get_status(None)["name"] + short_name = probe_name.split(maxsplit=1)[-1] + if short_name != self.name.split(maxsplit=1)[-1]: + raise self.gcode.error( + "[%s] not linked to registered probe [%s]." + % (self.name, probe_name) + ) + manual_probe.verify_no_manual_probe(self.printer) + if self.in_calibration: + raise gcmd.error( + "Already in probe drift calibration. Use " + "TEMPERATURE_PROBE_COMPLETE or ABORT to exit." + ) + cur_temp = self.last_measurement[0] + target_temp = gcmd.get_float("TARGET", above=cur_temp) + step = gcmd.get_float("STEP", 2., minval=1.0) + expected_count = int( + (target_temp - cur_temp) / step + .5 + ) + if expected_count < 3: + raise gcmd.error( + "Invalid STEP and/or TARGET parameters resulted " + "in too few expected samples: %d" + % (expected_count,) + ) + try: + self.gcode.register_command( + "TEMPERATURE_PROBE_NEXT", self.cmd_TEMPERATURE_PROBE_NEXT, + desc=self.cmd_TEMPERATURE_PROBE_NEXT_help + ) + self.gcode.register_command( + "TEMPERATURE_PROBE_COMPLETE", + self.cmd_TEMPERATURE_PROBE_COMPLETE, + desc=self.cmd_TEMPERATURE_PROBE_NEXT_help + ) + except self.printer.config_error: + raise gcmd.error( + "Auxiliary Probe Drift Commands already registered. Use " + "TEMPERATURE_PROBE_COMPLETE or ABORT to exit." + ) + self.in_calibration = True + self.cal_helper.start_calibration() + self.target_temp = target_temp + self.step = step + self.sample_count = 0 + self.expected_count = expected_count + # If configured move to heating position and turn on extruder + try: + self._move_to_start() + except self.printer.command_error: + self._finalize_drift_cal(False, "Error during initial move") + raise + # Caputure start position and begin initial probe + toolhead = self.printer.lookup_object("toolhead") + self.start_pos = toolhead.get_position()[:2] + manual_probe.ManualProbeHelper( + self.printer, gcmd, self._manual_probe_finalize + ) + + cmd_TEMPERATURE_PROBE_NEXT_help = "Sample next probe drift temperature" + def cmd_TEMPERATURE_PROBE_NEXT(self, gcmd): + manual_probe.verify_no_manual_probe(self.printer) + self.next_auto_temp = 99999999. + toolhead = self.printer.lookup_object("toolhead") + # Lift and Move to nozzle back to start position + curpos = toolhead.get_position() + start_z = curpos[2] + lift_speed, probe_speed, move_speed = self._get_speeds() + # Move nozzle to the manual probing position + curpos[2] += self.horizontal_move_z + toolhead.manual_move(curpos, lift_speed) + curpos[0] = self.start_pos[0] + curpos[1] = self.start_pos[1] + toolhead.manual_move(curpos, move_speed) + curpos[2] = start_z + toolhead.manual_move(curpos, probe_speed) + self.gcode.register_command("ABORT", None) + manual_probe.ManualProbeHelper( + self.printer, gcmd, self._manual_probe_finalize + ) + + cmd_TEMPERATURE_PROBE_COMPLETE_help = "Finish Probe Drift Calibration" + def cmd_TEMPERATURE_PROBE_COMPLETE(self, gcmd): + manual_probe.verify_no_manual_probe(self.printer) + self._finalize_drift_cal(self.sample_count >= 3) + + cmd_TEMPERATURE_PROBE_ABORT_help = "Abort Probe Drift Calibration" + def cmd_TEMPERATURE_PROBE_ABORT(self, gcmd): + self._finalize_drift_cal(False) + + cmd_TEMPERATURE_PROBE_ENABLE_help = ( + "Set adjustment factor applied to drift correction" + ) + def cmd_TEMPERATURE_PROBE_ENABLE(self, gcmd): + if self.cal_helper is not None: + self.cal_helper.set_enabled(gcmd) + + def is_in_calibration(self): + return self.in_calibration + + def get_status(self, eventtime=None): + smoothed_temp, measured_min, measured_max = self.last_measurement + dcomp_enabled = False + if self.cal_helper is not None: + dcomp_enabled = self.cal_helper.is_enabled() + return { + "temperature": smoothed_temp, + "measured_min_temp": round(measured_min, 2), + "measured_max_temp": round(measured_max, 2), + "in_calibration": self.in_calibration, + "estimated_expansion": self.total_expansion, + "compensation_enabled": dcomp_enabled + } + + def stats(self, eventtime): + return False, '%s: temp=%.1f' % (self.name, self.last_measurement[0]) + + +##################################################################### +# +# Eddy Current Probe Drift Compensation Helper +# +##################################################################### + +DRIFT_SAMPLE_COUNT = 9 + +class EddyDriftCompensation: + def __init__(self, config, sensor): + self.printer = config.get_printer() + self.temp_sensor = sensor + self.name = config.get_name() + self.cal_temp = config.getfloat("calibration_temp", 0.) + self.drift_calibration = None + self.calibration_samples = None + self.max_valid_temp = config.getfloat("max_validation_temp", 60.) + self.dc_min_temp = config.getfloat("drift_calibration_min_temp", 0.) + dc = config.getlists( + "drift_calibration", None, seps=(',', '\n'), parser=float + ) + self.min_freq = 999999999999. + if dc is not None: + for coefs in dc: + if len(coefs) != 3: + raise config.error( + "Invalid polynomial in drift calibration" + ) + self.drift_calibration = [Polynomial2d(*coefs) for coefs in dc] + cal = self.drift_calibration + start_temp, end_temp = self.dc_min_temp, self.max_valid_temp + self._check_calibration(cal, start_temp, end_temp, config.error) + low_poly = self.drift_calibration[-1] + self.min_freq = min([low_poly(temp) for temp in range(121)]) + cal_str = "\n".join([repr(p) for p in cal]) + logging.info( + "%s: loaded temperature drift calibration. Min Temp: %.2f," + " Min Freq: %.6f\n%s" + % (self.name, self.dc_min_temp, self.min_freq, cal_str) + ) + else: + logging.info( + "%s: No drift calibration configured, disabling temperature " + "drift compensation" + % (self.name,) + ) + self.enabled = has_dc = self.drift_calibration is not None + if self.cal_temp < 1e-6 and has_dc: + self.enabled = False + logging.info( + "%s: No temperature saved for eddy probe calibration, " + "disabling temperature drift compensation." + % (self.name,) + ) + + def is_enabled(self): + return self.enabled + + def set_enabled(self, gcmd): + enabled = gcmd.get_int("ENABLE") + if enabled: + if self.drift_calibration is None: + raise gcmd.error( + "No drift calibration configured, cannot enable " + "temperature drift compensation" + ) + if self.cal_temp < 1e-6: + raise gcmd.error( + "Z Calibration temperature not configured, cannot enable " + "temperature drift compensation" + ) + self.enabled = enabled + + def note_z_calibration_start(self): + self.cal_temp = self.get_temperature() + + def note_z_calibration_finish(self): + self.cal_temp = (self.cal_temp + self.get_temperature()) / 2.0 + configfile = self.printer.lookup_object('configfile') + configfile.set(self.name, "calibration_temp", "%.6f " % (self.cal_temp)) + gcode = self.printer.lookup_object("gcode") + gcode.respond_info( + "%s: Z Calibration Temperature set to %.2f. " + "The SAVE_CONFIG command will update the printer config " + "file and restart the printer." + % (self.name, self.cal_temp) + ) + + def collect_sample(self, kin_pos, tool_zero_z, speeds): + if self.calibration_samples is None: + self.calibration_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)] + move_times = [] + temps = [0. for _ in range(DRIFT_SAMPLE_COUNT)] + probe_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)] + toolhead = self.printer.lookup_object("toolhead") + cur_pos = toolhead.get_position() + lift_speed, probe_speed, _ = speeds + + def _on_bulk_data_recd(msg): + if move_times: + idx, start_time, end_time = move_times[0] + cur_temp = self.get_temperature() + for sample in msg["data"]: + ptime = sample[0] + while ptime > end_time: + move_times.pop(0) + if not move_times: + return idx >= DRIFT_SAMPLE_COUNT - 1 + idx, start_time, end_time = move_times[0] + if ptime < start_time: + continue + temps[idx] = cur_temp + probe_samples[idx].append(sample) + return True + sect_name = "probe_eddy_current " + self.name.split(maxsplit=1)[-1] + self.printer.lookup_object(sect_name).add_client(_on_bulk_data_recd) + for i in range(DRIFT_SAMPLE_COUNT): + if i == 0: + # Move down to first sample location + cur_pos[2] = tool_zero_z + .05 + else: + # Sample each .5mm in z + cur_pos[2] += 1. + toolhead.manual_move(cur_pos, lift_speed) + cur_pos[2] -= .5 + toolhead.manual_move(cur_pos, probe_speed) + start = toolhead.get_last_move_time() + .05 + end = start + .1 + move_times.append((i, start, end)) + toolhead.dwell(.2) + toolhead.wait_moves() + # Wait for sample collection to finish + reactor = self.printer.get_reactor() + evttime = reactor.monotonic() + while move_times: + evttime = reactor.pause(evttime + .1) + sample_temp = sum(temps) / len(temps) + for i, data in enumerate(probe_samples): + freqs = [d[1] for d in data] + zvals = [d[2] for d in data] + avg_freq = sum(freqs) / len(freqs) + avg_z = sum(zvals) / len(zvals) + kin_z = i * .5 + .05 + kin_pos[2] + logging.info( + "Probe Values at Temp %.2fC, Z %.4fmm: Avg Freq = %.6f, " + "Avg Measured Z = %.6f" + % (sample_temp, kin_z, avg_freq, avg_z) + ) + self.calibration_samples[i].append((sample_temp, avg_freq)) + return sample_temp + + def start_calibration(self): + self.enabled = False + self.calibration_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)] + + def finish_calibration(self, success): + cal_samples = self.calibration_samples + self.calibration_samples = None + if not success: + return + gcode = self.printer.lookup_object("gcode") + if len(cal_samples) < 3: + raise gcode.error( + "calbration error, not enough samples" + ) + min_temp, _ = cal_samples[0][0] + max_temp, _ = cal_samples[-1][0] + polynomials = [] + for i, coords in enumerate(cal_samples): + height = .05 + i * .5 + poly = Polynomial2d.fit(coords) + polynomials.append(poly) + logging.info("Polynomial at Z=%.2f: %s" % (height, repr(poly))) + end_vld_temp = max(self.max_valid_temp, max_temp) + self._check_calibration(polynomials, min_temp, end_vld_temp) + coef_cfg = "\n" + "\n".join([str(p) for p in polynomials]) + configfile = self.printer.lookup_object('configfile') + configfile.set(self.name, "drift_calibration", coef_cfg) + configfile.set(self.name, "drift_calibration_min_temp", min_temp) + gcode.respond_info( + "%s: generated %d 2D polynomials\n" + "The SAVE_CONFIG command will update the printer config " + "file and restart the printer." + % (self.name, len(polynomials)) + ) + + def _check_calibration(self, calibration, start_temp, end_temp, error=None): + error = error or self.printer.command_error + start = int(start_temp) + end = int(end_temp) + 1 + for temp in range(start, end, 1): + last_freq = calibration[0](temp) + for i, poly in enumerate(calibration[1:]): + next_freq = poly(temp) + if next_freq >= last_freq: + # invalid polynomial + raise error( + "%s: invalid calibration detected, curve at index " + "%d overlaps previous curve at temp %dC." + % (self.name, i + 1, temp) + ) + last_freq = next_freq + + def adjust_freq(self, freq, origin_temp=None): + # Adjusts frequency from current temperature toward + # destination temperature + if not self.enabled or freq < self.min_freq: + return freq + if origin_temp is None: + origin_temp = self.get_temperature() + return self._calc_freq(freq, origin_temp, self.cal_temp) + + def unadjust_freq(self, freq, dest_temp=None): + # Given a frequency and its orignal sampled temp, find the + # offset frequency based on the current temp + if not self.enabled or freq < self.min_freq: + return freq + if dest_temp is None: + dest_temp = self.get_temperature() + return self._calc_freq(freq, self.cal_temp, dest_temp) + + def _calc_freq(self, freq, origin_temp, dest_temp): + high_freq = low_freq = None + dc = self.drift_calibration + for pos, poly in enumerate(dc): + high_freq = low_freq + low_freq = poly(origin_temp) + if freq >= low_freq: + if high_freq is None: + # Freqency above max calibration value + err = poly(dest_temp) - low_freq + return freq + err + t = min(1., max(0., (freq - low_freq) / (high_freq - low_freq))) + low_tgt_freq = poly(dest_temp) + high_tgt_freq = dc[pos-1](dest_temp) + return (1 - t) * low_tgt_freq + t * high_tgt_freq + # Frequency below minimum, no correction + return freq + + def get_temperature(self): + return self.temp_sensor.get_temp()[0] + + +def load_config_prefix(config): + return TemperatureProbe(config) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 8143882ab..1d8599e2e 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -278,16 +278,14 @@ class TMCCommandHelper: raise gcmd.error("Unknown field name '%s'" % (field_name,)) value = gcmd.get_int('VALUE', None) velocity = gcmd.get_float('VELOCITY', None, minval=0.) - tmc_frequency = self.mcu_tmc.get_tmc_frequency() - if tmc_frequency is None and velocity is not None: - raise gcmd.error("VELOCITY parameter not supported by this driver") if (value is None) == (velocity is None): raise gcmd.error("Specify either VALUE or VELOCITY") if velocity is not None: - step_dist = self.stepper.get_step_dist() - mres = self.fields.get_field("mres") - value = TMCtstepHelper(step_dist, mres, tmc_frequency, - velocity) + if self.mcu_tmc.get_tmc_frequency() is None: + raise gcmd.error( + "VELOCITY parameter not supported by this driver") + value = TMCtstepHelper(self.mcu_tmc, velocity, + pstepper=self.stepper) reg_val = self.fields.set_field(field_name, value) print_time = self.printer.lookup_object('toolhead').get_last_move_time() self.mcu_tmc.set_register(reg_name, reg_val, print_time) @@ -481,7 +479,7 @@ class TMCVirtualPinHelper: self.diag_pin_field = None self.mcu_endstop = None self.en_pwm = False - self.pwmthrs = self.coolthrs = 0 + self.pwmthrs = self.coolthrs = self.thigh = 0 # Register virtual_endstop pin name_parts = config.get_name().split() ppins = self.printer.lookup_object("pins") @@ -505,8 +503,8 @@ class TMCVirtualPinHelper: def handle_homing_move_begin(self, hmove): if self.mcu_endstop not in hmove.get_mcu_endstops(): return + # Enable/disable stealthchop self.pwmthrs = self.fields.get_field("tpwmthrs") - self.coolthrs = self.fields.get_field("tcoolthrs") reg = self.fields.lookup_register("en_pwm_mode", None) if reg is None: # On "stallguard4" drivers, "stealthchop" must be enabled @@ -520,12 +518,21 @@ class TMCVirtualPinHelper: self.fields.set_field("en_pwm_mode", 0) val = self.fields.set_field(self.diag_pin_field, 1) self.mcu_tmc.set_register("GCONF", val) + # Enable tcoolthrs (if not already) + self.coolthrs = self.fields.get_field("tcoolthrs") if self.coolthrs == 0: tc_val = self.fields.set_field("tcoolthrs", 0xfffff) self.mcu_tmc.set_register("TCOOLTHRS", tc_val) + # Disable thigh + reg = self.fields.lookup_register("thigh", None) + if reg is not None: + self.thigh = self.fields.get_field("thigh") + th_val = self.fields.set_field("thigh", 0) + self.mcu_tmc.set_register(reg, th_val) def handle_homing_move_end(self, hmove): if self.mcu_endstop not in hmove.get_mcu_endstops(): return + # Restore stealthchop/spreadcycle reg = self.fields.lookup_register("en_pwm_mode", None) if reg is None: tp_val = self.fields.set_field("tpwmthrs", self.pwmthrs) @@ -535,8 +542,14 @@ class TMCVirtualPinHelper: self.fields.set_field("en_pwm_mode", self.en_pwm) val = self.fields.set_field(self.diag_pin_field, 0) self.mcu_tmc.set_register("GCONF", val) + # Restore tcoolthrs tc_val = self.fields.set_field("tcoolthrs", self.coolthrs) self.mcu_tmc.set_register("TCOOLTHRS", tc_val) + # Restore thigh + reg = self.fields.lookup_register("thigh", None) + if reg is not None: + th_val = self.fields.set_field("thigh", self.thigh) + self.mcu_tmc.set_register(reg, th_val) ###################################################################### @@ -564,7 +577,7 @@ def TMCWaveTableHelper(config, mcu_tmc): set_config_field(config, "start_sin", 0) set_config_field(config, "start_sin90", 247) -# Helper to configure and query the microstep settings +# Helper to configure the microstep settings def TMCMicrostepHelper(config, mcu_tmc): fields = mcu_tmc.get_fields() stepper_name = " ".join(config.get_name().split()[1:]) @@ -572,27 +585,31 @@ def TMCMicrostepHelper(config, mcu_tmc): raise config.error( "Could not find config section '[%s]' required by tmc driver" % (stepper_name,)) - stepper_config = ms_config = config.getsection(stepper_name) - if (stepper_config.get('microsteps', None, note_valid=False) is None - and config.get('microsteps', None, note_valid=False) is not None): - # Older config format with microsteps in tmc config section - ms_config = config + sconfig = config.getsection(stepper_name) steps = {256: 0, 128: 1, 64: 2, 32: 3, 16: 4, 8: 5, 4: 6, 2: 7, 1: 8} - mres = ms_config.getchoice('microsteps', steps) + mres = sconfig.getchoice('microsteps', steps) fields.set_field("mres", mres) fields.set_field("intpol", config.getboolean("interpolate", True)) # Helper for calculating TSTEP based values from velocity -def TMCtstepHelper(step_dist, mres, tmc_freq, velocity): - if velocity > 0.: - step_dist_256 = step_dist / (1 << mres) - threshold = int(tmc_freq * step_dist_256 / velocity + .5) - return max(0, min(0xfffff, threshold)) - else: +def TMCtstepHelper(mcu_tmc, velocity, pstepper=None, config=None): + if velocity <= 0.: return 0xfffff + if pstepper is not None: + step_dist = pstepper.get_step_dist() + else: + stepper_name = " ".join(config.get_name().split()[1:]) + sconfig = config.getsection(stepper_name) + rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig) + step_dist = rotation_dist / steps_per_rotation + mres = mcu_tmc.get_fields().get_field("mres") + step_dist_256 = step_dist / (1 << mres) + tmc_freq = mcu_tmc.get_tmc_frequency() + threshold = int(tmc_freq * step_dist_256 / velocity + .5) + return max(0, min(0xfffff, threshold)) # Helper to configure stealthChop-spreadCycle transition velocity -def TMCStealthchopHelper(config, mcu_tmc, tmc_freq): +def TMCStealthchopHelper(config, mcu_tmc): fields = mcu_tmc.get_fields() en_pwm_mode = False velocity = config.getfloat('stealthchop_threshold', None, minval=0.) @@ -600,13 +617,7 @@ def TMCStealthchopHelper(config, mcu_tmc, tmc_freq): if velocity is not None: en_pwm_mode = True - - stepper_name = " ".join(config.get_name().split()[1:]) - sconfig = config.getsection(stepper_name) - rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig) - step_dist = rotation_dist / steps_per_rotation - mres = fields.get_field("mres") - tpwmthrs = TMCtstepHelper(step_dist, mres, tmc_freq, velocity) + tpwmthrs = TMCtstepHelper(mcu_tmc, velocity, config=config) fields.set_field("tpwmthrs", tpwmthrs) reg = fields.lookup_register("en_pwm_mode", None) @@ -615,3 +626,22 @@ def TMCStealthchopHelper(config, mcu_tmc, tmc_freq): else: # TMC2208 uses en_spreadCycle fields.set_field("en_spreadcycle", not en_pwm_mode) + +# Helper to configure StallGuard and CoolStep minimum velocity +def TMCVcoolthrsHelper(config, mcu_tmc): + fields = mcu_tmc.get_fields() + velocity = config.getfloat('coolstep_threshold', None, minval=0.) + tcoolthrs = 0 + if velocity is not None: + tcoolthrs = TMCtstepHelper(mcu_tmc, velocity, config=config) + fields.set_field("tcoolthrs", tcoolthrs) + +# Helper to configure StallGuard and CoolStep maximum velocity and +# SpreadCycle-FullStepping (High velocity) mode threshold. +def TMCVhighHelper(config, mcu_tmc): + fields = mcu_tmc.get_fields() + velocity = config.getfloat('high_velocity_threshold', None, minval=0.) + thigh = 0 + if velocity is not None: + thigh = TMCtstepHelper(mcu_tmc, velocity, config=config) + fields.set_field("thigh", thigh) diff --git a/klippy/extras/tmc2130.py b/klippy/extras/tmc2130.py index 62a9abbfe..20a25c66c 100644 --- a/klippy/extras/tmc2130.py +++ b/klippy/extras/tmc2130.py @@ -296,7 +296,9 @@ class TMC2130: self.get_status = cmdhelper.get_status # Setup basic register values tmc.TMCWaveTableHelper(config, self.mcu_tmc) - tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY) + tmc.TMCStealthchopHelper(config, self.mcu_tmc) + tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) + tmc.TMCVhighHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # CHOPCONF @@ -304,8 +306,16 @@ class TMC2130: set_config_field(config, "hstrt", 0) set_config_field(config, "hend", 7) set_config_field(config, "tbl", 1) + set_config_field(config, "vhighfs", 0) + set_config_field(config, "vhighchm", 0) # COOLCONF + set_config_field(config, "semin", 0) + set_config_field(config, "seup", 0) + set_config_field(config, "semax", 0) + set_config_field(config, "sedn", 0) + set_config_field(config, "seimin", 0) set_config_field(config, "sgt", 0) + set_config_field(config, "sfilt", 0) # IHOLDIRUN set_config_field(config, "iholddelay", 8) # PWMCONF diff --git a/klippy/extras/tmc2208.py b/klippy/extras/tmc2208.py index 421c53781..86476acc5 100644 --- a/klippy/extras/tmc2208.py +++ b/klippy/extras/tmc2208.py @@ -197,7 +197,7 @@ class TMC2208: self.get_status = cmdhelper.get_status # Setup basic register values self.fields.set_field("mstep_reg_select", True) - tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY) + tmc.TMCStealthchopHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF diff --git a/klippy/extras/tmc2209.py b/klippy/extras/tmc2209.py index c248c2d68..fbd8d1c10 100644 --- a/klippy/extras/tmc2209.py +++ b/klippy/extras/tmc2209.py @@ -73,7 +73,8 @@ class TMC2209: self.get_status = cmdhelper.get_status # Setup basic register values self.fields.set_field("mstep_reg_select", True) - tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY) + tmc.TMCStealthchopHelper(config, self.mcu_tmc) + tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF @@ -83,6 +84,12 @@ class TMC2209: set_config_field(config, "hstrt", 5) set_config_field(config, "hend", 0) set_config_field(config, "tbl", 2) + # COOLCONF + set_config_field(config, "semin", 0) + set_config_field(config, "seup", 0) + set_config_field(config, "semax", 0) + set_config_field(config, "sedn", 0) + set_config_field(config, "seimin", 0) # IHOLDIRUN set_config_field(config, "iholddelay", 8) # PWMCONF diff --git a/klippy/extras/tmc2240.py b/klippy/extras/tmc2240.py index 14d5dd918..21c835e26 100644 --- a/klippy/extras/tmc2240.py +++ b/klippy/extras/tmc2240.py @@ -95,7 +95,7 @@ Fields["DRV_STATUS"] = { "s2vsb": 0x01 << 13, "stealth": 0x01 << 14, "fsactive": 0x01 << 15, - "csactual": 0x1F << 16, + "cs_actual": 0x1F << 16, "stallguard": 0x01 << 24, "ot": 0x01 << 25, "otpw": 0x01 << 26, @@ -364,7 +364,10 @@ class TMC2240: # Setup basic register values tmc.TMCWaveTableHelper(config, self.mcu_tmc) self.fields.set_config_field(config, "offset_sin90", 0) - tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY) + tmc.TMCStealthchopHelper(config, self.mcu_tmc) + tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) + tmc.TMCVhighHelper(config, self.mcu_tmc) + # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF set_config_field(config, "multistep_filt", True) diff --git a/klippy/extras/tmc5160.py b/klippy/extras/tmc5160.py index 7ff47abf9..b773135c2 100644 --- a/klippy/extras/tmc5160.py +++ b/klippy/extras/tmc5160.py @@ -118,7 +118,7 @@ Fields["DRV_STATUS"] = { "s2vsb": 0x01 << 13, "stealth": 0x01 << 14, "fsactive": 0x01 << 15, - "csactual": 0xFF << 16, + "cs_actual": 0x1F << 16, "stallguard": 0x01 << 24, "ot": 0x01 << 25, "otpw": 0x01 << 26, @@ -242,6 +242,9 @@ Fields["TCOOLTHRS"] = { Fields["TSTEP"] = { "tstep": 0xfffff << 0 } +Fields["THIGH"] = { + "thigh": 0xfffff << 0 +} SignedFields = ["cur_a", "cur_b", "sgt", "xactual", "vactual", "pwm_scale_auto"] @@ -335,7 +338,10 @@ class TMC5160: self.get_status = cmdhelper.get_status # Setup basic register values tmc.TMCWaveTableHelper(config, self.mcu_tmc) - tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY) + tmc.TMCStealthchopHelper(config, self.mcu_tmc) + tmc.TMCVcoolthrsHelper(config, self.mcu_tmc) + tmc.TMCVhighHelper(config, self.mcu_tmc) + # Allow other registers to be set from the config set_config_field = self.fields.set_config_field # GCONF set_config_field(config, "multistep_filt", True) diff --git a/klippy/extras/tsl1401cl_filament_width_sensor.py b/klippy/extras/tsl1401cl_filament_width_sensor.py index fb2d97131..83480f467 100644 --- a/klippy/extras/tsl1401cl_filament_width_sensor.py +++ b/klippy/extras/tsl1401cl_filament_width_sensor.py @@ -33,7 +33,7 @@ class FilamentWidthSensor: # Start adc self.ppins = self.printer.lookup_object('pins') self.mcu_adc = self.ppins.setup_pin('adc', self.pin) - self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback) # extrude factor updating self.extrude_factor_update_timer = self.reactor.register_timer( diff --git a/klippy/gcode.py b/klippy/gcode.py index 7d980585f..15ab624aa 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -406,7 +406,7 @@ class GCodeIO: self._dump_debug() if self.is_fileinput: self.printer.request_exit('error_exit') - m112_r = re.compile('^(?:[nN][0-9]+)?\s*[mM]112(?:\s|$)') + m112_r = re.compile(r'^(?:[nN][0-9]+)?\s*[mM]112(?:\s|$)') def _process_data(self, eventtime): # Read input, separate by newline, and add to pending_commands try: diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index 6d576b5b5..0c4bb9255 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -23,7 +23,7 @@ class CartKinematics: self.dc_module = None if config.has_section('dual_carriage'): dc_config = config.getsection('dual_carriage') - dc_axis = dc_config.getchoice('axis', {'x': 'x', 'y': 'y'}) + dc_axis = dc_config.getchoice('axis', ['x', 'y']) self.dual_carriage_axis = {'x': 0, 'y': 1}[dc_axis] # setup second dual carriage rail self.rails.append(stepper.LookupMultiRail(dc_config)) @@ -52,20 +52,27 @@ class CartKinematics: def get_steppers(self): return [s for rail in self.rails for s in rail.get_steppers()] def calc_position(self, stepper_positions): - return [stepper_positions[rail.get_name()] for rail in self.rails] + rails = self.rails + if self.dc_module: + primary_rail = self.dc_module.get_primary_rail().get_rail() + rails = (rails[:self.dc_module.axis] + + [primary_rail] + rails[self.dc_module.axis+1:]) + return [stepper_positions[rail.get_name()] for rail in rails] def update_limits(self, i, range): l, h = self.limits[i] # Only update limits if this axis was already homed, # otherwise leave in un-homed state. if l <= h: self.limits[i] = range - def override_rail(self, i, rail): - self.rails[i] = rail def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - if i in homing_axes: - self.limits[i] = rail.get_range() + for axis in homing_axes: + if self.dc_module and axis == self.dc_module.axis: + rail = self.dc_module.get_primary_rail().get_rail() + else: + rail = self.rails[axis] + self.limits[axis] = rail.get_range() def note_z_not_homed(self): # Helper for Safe Z Home self.limits[2] = (1.0, -1.0) diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 692400378..7fb2e7ed5 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -18,7 +18,7 @@ class ExtruderStepper: self.stepper = stepper.PrinterStepper(config) ffi_main, ffi_lib = chelper.get_ffi() self.sk_extruder = ffi_main.gc(ffi_lib.extruder_stepper_alloc(), - ffi_lib.free) + ffi_lib.extruder_stepper_free) self.stepper.set_stepper_kinematics(self.sk_extruder) self.motion_queue = None # Register commands @@ -71,11 +71,14 @@ class ExtruderStepper: if not pressure_advance: new_smooth_time = 0. toolhead = self.printer.lookup_object("toolhead") - toolhead.note_step_generation_scan_time(new_smooth_time * .5, - old_delay=old_smooth_time * .5) + if new_smooth_time != old_smooth_time: + toolhead.note_step_generation_scan_time( + new_smooth_time * .5, old_delay=old_smooth_time * .5) ffi_main, ffi_lib = chelper.get_ffi() espa = ffi_lib.extruder_set_pressure_advance - espa(self.sk_extruder, pressure_advance, new_smooth_time) + toolhead.register_lookahead_callback( + lambda print_time: espa(self.sk_extruder, print_time, + pressure_advance, new_smooth_time)) self.pressure_advance = pressure_advance self.pressure_advance_smooth_time = smooth_time cmd_SET_PRESSURE_ADVANCE_help = "Set pressure advance parameters" diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index 1c2164eb7..265a0e6da 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -27,7 +27,7 @@ class HybridCoreXYKinematics: if config.has_section('dual_carriage'): dc_config = config.getsection('dual_carriage') # dummy for cartesian config users - dc_config.getchoice('axis', {'x': 'x'}, default='x') + dc_config.getchoice('axis', ['x'], default='x') # setup second dual carriage rail self.rails.append(stepper.PrinterRail(dc_config)) self.rails[1].get_endstops()[0][0].add_stepper( @@ -57,7 +57,7 @@ class HybridCoreXYKinematics: pos = [stepper_positions[rail.get_name()] for rail in self.rails] if (self.dc_module is not None and 'PRIMARY' == \ self.dc_module.get_status()['carriage_1']): - return [pos[0] - pos[1], pos[1], pos[2]] + return [pos[3] - pos[1], pos[1], pos[2]] else: return [pos[0] + pos[1], pos[1], pos[2]] def update_limits(self, i, range): @@ -66,13 +66,15 @@ class HybridCoreXYKinematics: # otherwise leave in un-homed state. if l <= h: self.limits[i] = range - def override_rail(self, i, rail): - self.rails[i] = rail def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - if i in homing_axes: - self.limits[i] = rail.get_range() + for axis in homing_axes: + if self.dc_module and axis == self.dc_module.axis: + rail = self.dc_module.get_primary_rail().get_rail() + else: + rail = self.rails[axis] + self.limits[axis] = rail.get_range() def note_z_not_homed(self): # Helper for Safe Z Home self.limits[2] = (1.0, -1.0) diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index 0eaea117e..2d89e3f7b 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -27,7 +27,7 @@ class HybridCoreXZKinematics: if config.has_section('dual_carriage'): dc_config = config.getsection('dual_carriage') # dummy for cartesian config users - dc_config.getchoice('axis', {'x': 'x'}, default='x') + dc_config.getchoice('axis', ['x'], default='x') # setup second dual carriage rail self.rails.append(stepper.PrinterRail(dc_config)) self.rails[2].get_endstops()[0][0].add_stepper( @@ -57,7 +57,7 @@ class HybridCoreXZKinematics: pos = [stepper_positions[rail.get_name()] for rail in self.rails] if (self.dc_module is not None and 'PRIMARY' == \ self.dc_module.get_status()['carriage_1']): - return [pos[0] - pos[2], pos[1], pos[2]] + return [pos[3] - pos[2], pos[1], pos[2]] else: return [pos[0] + pos[2], pos[1], pos[2]] def update_limits(self, i, range): @@ -66,13 +66,15 @@ class HybridCoreXZKinematics: # otherwise leave in un-homed state. if l <= h: self.limits[i] = range - def override_rail(self, i, rail): - self.rails[i] = rail def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - if i in homing_axes: - self.limits[i] = rail.get_range() + for axis in homing_axes: + if self.dc_module and axis == self.dc_module.axis: + rail = self.dc_module.get_primary_rail().get_rail() + else: + rail = self.rails[axis] + self.limits[axis] = rail.get_range() def note_z_not_homed(self): # Helper for Safe Z Home self.limits[2] = (1.0, -1.0) diff --git a/klippy/kinematics/idex_modes.py b/klippy/kinematics/idex_modes.py index 2ce91afe8..2f2da4168 100644 --- a/klippy/kinematics/idex_modes.py +++ b/klippy/kinematics/idex_modes.py @@ -4,7 +4,7 @@ # Copyright (C) 2023 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. -import math +import logging, math import chelper INACTIVE = 'INACTIVE' @@ -42,7 +42,12 @@ class DualCarriages: desc=self.cmd_RESTORE_DUAL_CARRIAGE_STATE_help) def get_rails(self): return self.dc - def toggle_active_dc_rail(self, index, override_rail=False): + def get_primary_rail(self): + for rail in self.dc: + if rail.mode == PRIMARY: + return rail + return None + def toggle_active_dc_rail(self, index): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() pos = toolhead.get_position() @@ -52,15 +57,11 @@ class DualCarriages: if i != index: if dc.is_active(): dc.inactivate(pos) - if override_rail: - kin.override_rail(3, dc_rail) target_dc = self.dc[index] if target_dc.mode != PRIMARY: newpos = pos[:self.axis] + [target_dc.get_axis_position(pos)] \ + pos[self.axis+1:] target_dc.activate(PRIMARY, newpos, old_position=pos) - if override_rail: - kin.override_rail(self.axis, target_dc.get_rail()) toolhead.set_position(newpos) kin.update_limits(self.axis, target_dc.get_rail().get_range()) def home(self, homing_state): @@ -72,10 +73,10 @@ class DualCarriages: # the same direction and the first carriage homes on the second one enumerated_dcs.reverse() for i, dc_rail in enumerated_dcs: - self.toggle_active_dc_rail(i, override_rail=True) + self.toggle_active_dc_rail(i) kin.home_axis(homing_state, self.axis, dc_rail.get_rail()) # Restore the original rails ordering - self.toggle_active_dc_rail(0, override_rail=True) + self.toggle_active_dc_rail(0) def get_status(self, eventtime=None): return {('carriage_%d' % (i,)) : dc.mode for (i, dc) in enumerate(self.dc)} @@ -201,14 +202,31 @@ class DualCarriages: move_speed = gcmd.get_float('MOVE_SPEED', 0., above=0.) toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() - pos = toolhead.get_position() if gcmd.get_int('MOVE', 1): + homing_speed = 99999999. + cur_pos = [] for i, dc in enumerate(self.dc): self.toggle_active_dc_rail(i) - saved_pos = saved_state['axes_positions'][i] - toolhead.manual_move( - pos[:self.axis] + [saved_pos] + pos[self.axis+1:], - move_speed or dc.get_rail().homing_speed) + homing_speed = min(homing_speed, dc.get_rail().homing_speed) + cur_pos.append(toolhead.get_position()) + move_pos = list(cur_pos[0]) + dl = [saved_state['axes_positions'][i] - cur_pos[i][self.axis] + for i in range(2)] + primary_ind = 0 if abs(dl[0]) >= abs(dl[1]) else 1 + self.toggle_active_dc_rail(primary_ind) + move_pos[self.axis] = saved_state['axes_positions'][primary_ind] + dc_mode = INACTIVE if min(abs(dl[0]), abs(dl[1])) < 0.000000001 \ + else COPY if dl[0] * dl[1] > 0 else MIRROR + if dc_mode != INACTIVE: + self.dc[1-primary_ind].activate(dc_mode, cur_pos[primary_ind]) + self.dc[1-primary_ind].override_axis_scaling( + abs(dl[1-primary_ind] / dl[primary_ind]), + cur_pos[primary_ind]) + toolhead.manual_move(move_pos, move_speed or homing_speed) + toolhead.flush_step_generation() + # Make sure the scaling coefficients are restored with the mode + self.dc[0].inactivate(move_pos) + self.dc[1].inactivate(move_pos) for i, dc in enumerate(self.dc): saved_mode = saved_state['carriage_modes'][i] self.activate_dc_mode(i, saved_mode) @@ -256,3 +274,8 @@ class DualCarriagesRail: self.scale = 0. self.apply_transform() self.mode = INACTIVE + def override_axis_scaling(self, new_scale, position): + old_axis_position = self.get_axis_position(position) + self.scale = math.copysign(new_scale, self.scale) + self.offset = old_axis_position - position[self.axis] * self.scale + self.apply_transform() diff --git a/klippy/klippy.py b/klippy/klippy.py index 097cff998..75ee6887a 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -1,7 +1,7 @@ #!/usr/bin/env python2 # Main code for host side printer firmware # -# Copyright (C) 2016-2020 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import sys, os, gc, optparse, logging, time, collections, importlib @@ -22,31 +22,6 @@ command to reload the config and restart the host software. Printer is halted """ -message_protocol_error1 = """ -This is frequently caused by running an older version of the -firmware on the MCU(s). Fix by recompiling and flashing the -firmware. -""" - -message_protocol_error2 = """ -Once the underlying issue is corrected, use the "RESTART" -command to reload the config and restart the host software. -""" - -message_mcu_connect_error = """ -Once the underlying issue is corrected, use the -"FIRMWARE_RESTART" command to reset the firmware, reload the -config, and restart the host software. -Error configuring printer -""" - -message_shutdown = """ -Once the underlying issue is corrected, use the -"FIRMWARE_RESTART" command to reset the firmware, reload the -config, and restart the host software. -Printer is shutdown -""" - class Printer: config_error = configfile.error command_error = gcode.CommandError @@ -85,6 +60,13 @@ class Printer: if (msg != message_ready and self.start_args.get('debuginput') is not None): self.request_exit('error_exit') + def update_error_msg(self, oldmsg, newmsg): + if (self.state_message != oldmsg + or self.state_message in (message_ready, message_startup) + or newmsg in (message_ready, message_startup)): + return + self.state_message = newmsg + logging.error(newmsg) def add_object(self, name, obj): if name in self.objects: raise self.config_error( @@ -143,33 +125,6 @@ class Printer: m.add_printer_objects(config) # Validate that there are no undefined parameters in the config file pconfig.check_unused_options(config) - def _build_protocol_error_message(self, e): - host_version = self.start_args['software_version'] - msg_update = [] - msg_updated = [] - for mcu_name, mcu in self.lookup_objects('mcu'): - try: - mcu_version = mcu.get_status()['mcu_version'] - except: - logging.exception("Unable to retrieve mcu_version from mcu") - continue - if mcu_version != host_version: - msg_update.append("%s: Current version %s" - % (mcu_name.split()[-1], mcu_version)) - else: - msg_updated.append("%s: Current version %s" - % (mcu_name.split()[-1], mcu_version)) - if not msg_update: - msg_update.append("") - if not msg_updated: - msg_updated.append("") - msg = ["MCU Protocol error", - message_protocol_error1, - "Your Klipper version is: %s" % (host_version,), - "MCU(s) which should be updated:"] - msg += msg_update + ["Up-to-date MCU(s):"] + msg_updated - msg += [message_protocol_error2, str(e)] - return "\n".join(msg) def _connect(self, eventtime): try: self._read_config() @@ -183,13 +138,17 @@ class Printer: self._set_state("%s\n%s" % (str(e), message_restart)) return except msgproto.error as e: - logging.exception("Protocol error") - self._set_state(self._build_protocol_error_message(e)) + msg = "Protocol error" + logging.exception(msg) + self._set_state(msg) + self.send_event("klippy:notify_mcu_error", msg, {"error": str(e)}) util.dump_mcu_build() return except mcu.error as e: - logging.exception("MCU error during connect") - self._set_state("%s%s" % (str(e), message_mcu_connect_error)) + msg = "MCU error during connect" + logging.exception(msg) + self._set_state(msg) + self.send_event("klippy:notify_mcu_error", msg, {"error": str(e)}) util.dump_mcu_build() return except Exception as e: @@ -241,12 +200,12 @@ class Printer: logging.info(info) if self.bglogger is not None: self.bglogger.set_rollover_info(name, info) - def invoke_shutdown(self, msg): + def invoke_shutdown(self, msg, details={}): if self.in_shutdown_state: return logging.error("Transition to shutdown state: %s", msg) self.in_shutdown_state = True - self._set_state("%s%s" % (msg, message_shutdown)) + self._set_state(msg) for cb in self.event_handlers.get("klippy:shutdown", []): try: cb() @@ -254,9 +213,10 @@ class Printer: logging.exception("Exception during shutdown handler") logging.info("Reactor garbage collection: %s", self.reactor.get_gc_stats()) - def invoke_async_shutdown(self, msg): + self.send_event("klippy:notify_mcu_shutdown", msg, details) + def invoke_async_shutdown(self, msg, details): self.reactor.register_async_callback( - (lambda e: self.invoke_shutdown(msg))) + (lambda e: self.invoke_shutdown(msg, details))) def register_event_handler(self, event, callback): self.event_handlers.setdefault(event, []).append(callback) def send_event(self, event, *params): diff --git a/klippy/mcu.py b/klippy/mcu.py index 2a01c5a7c..1122ff865 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -1,6 +1,6 @@ # Interface to Klipper micro-controller code # -# Copyright (C) 2016-2023 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import sys, os, zlib, logging, math @@ -87,7 +87,7 @@ class CommandWrapper: if cmd_queue is None: cmd_queue = serial.get_default_command_queue() self._cmd_queue = cmd_queue - self._msgtag = msgparser.lookup_msgtag(msgformat) & 0xffffffff + self._msgtag = msgparser.lookup_msgid(msgformat) & 0xffffffff def send(self, data=(), minclock=0, reqclock=0): cmd = self._cmd.encode(data) self._serial.raw_send(cmd, minclock, reqclock, self._cmd_queue) @@ -104,9 +104,9 @@ class CommandWrapper: class MCU_trsync: REASON_ENDSTOP_HIT = 1 - REASON_COMMS_TIMEOUT = 2 - REASON_HOST_REQUEST = 3 - REASON_PAST_END_TIME = 4 + REASON_HOST_REQUEST = 2 + REASON_PAST_END_TIME = 3 + REASON_COMMS_TIMEOUT = 4 def __init__(self, mcu, trdispatch): self._mcu = mcu self._trdispatch = trdispatch @@ -180,7 +180,7 @@ class MCU_trsync: if tc is not None: self._trigger_completion = None reason = params['trigger_reason'] - is_failure = (reason == self.REASON_COMMS_TIMEOUT) + is_failure = (reason >= self.REASON_COMMS_TIMEOUT) self._reactor.async_complete(tc, is_failure) elif self._home_end_clock is not None: clock = self._mcu.clock32_to_clock64(params['clock']) @@ -279,8 +279,9 @@ class TriggerDispatch: ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.trdispatch_stop(self._trdispatch) res = [trsync.stop() for trsync in self._trsyncs] - if any([r == MCU_trsync.REASON_COMMS_TIMEOUT for r in res]): - return MCU_trsync.REASON_COMMS_TIMEOUT + err_res = [r for r in res if r >= MCU_trsync.REASON_COMMS_TIMEOUT] + if err_res: + return err_res[0] return res[0] class MCU_endstop: @@ -334,8 +335,9 @@ class MCU_endstop: self._dispatch.wait_end(home_end_time) self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) res = self._dispatch.stop() - if res == MCU_trsync.REASON_COMMS_TIMEOUT: - return -1. + if res >= MCU_trsync.REASON_COMMS_TIMEOUT: + cmderr = self._mcu.get_printer().command_error + raise cmderr("Communication timeout during homing") if res != MCU_trsync.REASON_ENDSTOP_HIT: return 0. if self._mcu.is_fileoutput(): @@ -494,8 +496,8 @@ class MCU_adc: self._inv_max_adc = 0. def get_mcu(self): return self._mcu - def setup_minmax(self, sample_time, sample_count, - minval=0., maxval=1., range_check_count=0): + def setup_adc_sample(self, sample_time, sample_count, + minval=0., maxval=1., range_check_count=0): self._sample_time = sample_time self._sample_count = sample_count self._min_sample = minval @@ -572,9 +574,8 @@ class MCU: restart_methods = [None, 'arduino', 'cheetah', 'command', 'rpi_usb'] self._restart_method = 'command' if self._baud: - rmethods = {m: m for m in restart_methods} self._restart_method = config.getchoice('restart_method', - rmethods, None) + restart_methods, None) self._reset_cmd = self._config_reset_cmd = None self._is_mcu_bridge = False self._emergency_stop_cmd = None @@ -604,6 +605,7 @@ class MCU: self._mcu_tick_stddev = 0. self._mcu_tick_awake = 0. # Register handlers + printer.load_object(config, "error_mcu") printer.register_event_handler("klippy:firmware_restart", self._firmware_restart) printer.register_event_handler("klippy:mcu_identify", @@ -630,13 +632,13 @@ class MCU: if clock is not None: self._shutdown_clock = self.clock32_to_clock64(clock) self._shutdown_msg = msg = params['static_string_id'] - logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, params['#name'], + event_type = params['#name'] + self._printer.invoke_async_shutdown( + "MCU shutdown", {"reason": msg, "mcu": self._name, + "event_type": event_type}) + logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, event_type, self._shutdown_msg, self._clocksync.dump_debug(), self._serial.dump_debug()) - prefix = "MCU '%s' shutdown: " % (self._name,) - if params['#name'] == 'is_shutdown': - prefix = "Previous MCU '%s' shutdown: " % (self._name,) - self._printer.invoke_async_shutdown(prefix + msg + error_help(msg)) def _handle_starting(self, params): if not self._is_shutdown: self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart" @@ -1007,34 +1009,6 @@ class MCU: self._get_status_info['last_stats'] = last_stats return False, '%s: %s' % (self._name, stats) -Common_MCU_errors = { - ("Timer too close",): """ -This often indicates the host computer is overloaded. Check -for other processes consuming excessive CPU time, high swap -usage, disk errors, overheating, unstable voltage, or -similar system problems on the host computer.""", - ("Missed scheduling of next ",): """ -This is generally indicative of an intermittent -communication failure between micro-controller and host.""", - ("ADC out of range",): """ -This generally occurs when a heater temperature exceeds -its configured min_temp or max_temp.""", - ("Rescheduled timer in the past", "Stepper too far in past"): """ -This generally occurs when the micro-controller has been -requested to step at a rate higher than it is capable of -obtaining.""", - ("Command request",): """ -This generally occurs in response to an M112 G-Code command -or in response to an internal error in the host software.""", -} - -def error_help(msg): - for prefixes, help_msg in Common_MCU_errors.items(): - for prefix in prefixes: - if msg.startswith(prefix): - return help_msg - return "" - def add_printer_objects(config): printer = config.get_printer() reactor = printer.get_reactor() diff --git a/klippy/msgproto.py b/klippy/msgproto.py index f8a12530e..0fe765934 100644 --- a/klippy/msgproto.py +++ b/klippy/msgproto.py @@ -1,6 +1,6 @@ # Protocol definitions for firmware communication # -# Copyright (C) 2016-2021 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import json, zlib, logging @@ -160,8 +160,8 @@ def convert_msg_format(msgformat): return msgformat class MessageFormat: - def __init__(self, msgid, msgformat, enumerations={}): - self.msgid = msgid + def __init__(self, msgid_bytes, msgformat, enumerations={}): + self.msgid_bytes = msgid_bytes self.msgformat = msgformat self.debugformat = convert_msg_format(msgformat) self.name = msgformat.split()[0] @@ -169,19 +169,17 @@ class MessageFormat: self.param_types = [t for name, t in self.param_names] self.name_to_type = dict(self.param_names) def encode(self, params): - out = [] - out.append(self.msgid) + out = list(self.msgid_bytes) for i, t in enumerate(self.param_types): t.encode(out, params[i]) return out def encode_by_name(self, **params): - out = [] - out.append(self.msgid) + out = list(self.msgid_bytes) for name, t in self.param_names: t.encode(out, params[name]) return out def parse(self, s, pos): - pos += 1 + pos += len(self.msgid_bytes) out = {} for name, t in self.param_names: v, pos = t.parse(s, pos) @@ -198,13 +196,13 @@ class MessageFormat: class OutputFormat: name = '#output' - def __init__(self, msgid, msgformat): - self.msgid = msgid + def __init__(self, msgid_bytes, msgformat): + self.msgid_bytes = msgid_bytes self.msgformat = msgformat self.debugformat = convert_msg_format(msgformat) self.param_types = lookup_output_params(msgformat) def parse(self, s, pos): - pos += 1 + pos += len(self.msgid_bytes) out = [] for t in self.param_types: v, pos = t.parse(s, pos) @@ -219,7 +217,7 @@ class OutputFormat: class UnknownFormat: name = '#unknown' def parse(self, s, pos): - msgid = s[pos] + msgid, param_pos = PT_int32().parse(s, pos) msg = bytes(bytearray(s)) return {'#msgid': msgid, '#msg': msg}, len(s)-MESSAGE_TRAILER_SIZE def format_params(self, params): @@ -234,7 +232,8 @@ class MessageParser: self.messages = [] self.messages_by_id = {} self.messages_by_name = {} - self.msgtag_by_format = {} + self.msgid_by_format = {} + self.msgid_parser = PT_int32() self.config = {} self.version = self.build_versions = "" self.raw_identify_data = "" @@ -266,7 +265,7 @@ class MessageParser: out = ["seq: %02x" % (msgseq,)] pos = MESSAGE_HEADER_SIZE while 1: - msgid = s[pos] + msgid, param_pos = self.msgid_parser.parse(s, pos) mid = self.messages_by_id.get(msgid, self.unknown) params, pos = mid.parse(s, pos) out.append(mid.format_params(params)) @@ -283,14 +282,14 @@ class MessageParser: return "%s %s" % (name, msg) return str(params) def parse(self, s): - msgid = s[MESSAGE_HEADER_SIZE] + msgid, param_pos = self.msgid_parser.parse(s, MESSAGE_HEADER_SIZE) mid = self.messages_by_id.get(msgid, self.unknown) params, pos = mid.parse(s, MESSAGE_HEADER_SIZE) if pos != len(s)-MESSAGE_TRAILER_SIZE: self._error("Extra data at end of message") params['#name'] = mid.name return params - def encode(self, seq, cmd): + def encode_msgblock(self, seq, cmd): msglen = MESSAGE_MIN + len(cmd) seq = (seq & MESSAGE_SEQ_MASK) | MESSAGE_DEST out = [msglen, seq] + cmd @@ -317,11 +316,11 @@ class MessageParser: self._error("Command format mismatch: %s vs %s", msgformat, mp.msgformat) return mp - def lookup_msgtag(self, msgformat): - msgtag = self.msgtag_by_format.get(msgformat) - if msgtag is None: + def lookup_msgid(self, msgformat): + msgid = self.msgid_by_format.get(msgformat) + if msgid is None: self._error("Unknown command: %s", msgformat) - return msgtag + return msgid def create_command(self, msg): parts = msg.strip().split() if not parts: @@ -372,22 +371,22 @@ class MessageParser: start_value, count = value for i in range(count): enums[enum_root + str(start_enum + i)] = start_value + i - def _init_messages(self, messages, command_tags=[], output_tags=[]): - for msgformat, msgtag in messages.items(): + def _init_messages(self, messages, command_ids=[], output_ids=[]): + for msgformat, msgid in messages.items(): msgtype = 'response' - if msgtag in command_tags: + if msgid in command_ids: msgtype = 'command' - elif msgtag in output_tags: + elif msgid in output_ids: msgtype = 'output' - self.messages.append((msgtag, msgtype, msgformat)) - if msgtag < -32 or msgtag > 95: - self._error("Multi-byte msgtag not supported") - self.msgtag_by_format[msgformat] = msgtag - msgid = msgtag & 0x7f + self.messages.append((msgid, msgtype, msgformat)) + self.msgid_by_format[msgformat] = msgid + msgid_bytes = [] + self.msgid_parser.encode(msgid_bytes, msgid) if msgtype == 'output': - self.messages_by_id[msgid] = OutputFormat(msgid, msgformat) + self.messages_by_id[msgid] = OutputFormat(msgid_bytes, + msgformat) else: - msg = MessageFormat(msgid, msgformat, self.enumerations) + msg = MessageFormat(msgid_bytes, msgformat, self.enumerations) self.messages_by_id[msgid] = msg self.messages_by_name[msg.name] = msg def process_identify(self, data, decompress=True): diff --git a/klippy/serialhdl.py b/klippy/serialhdl.py index 6aee56481..30db61707 100644 --- a/klippy/serialhdl.py +++ b/klippy/serialhdl.py @@ -136,7 +136,7 @@ class SerialReader: can_filters=filters, bustype='socketcan') bus.send(set_id_msg) - except (can.CanError, os.error) as e: + except (can.CanError, os.error, IOError) as e: logging.warning("%sUnable to open CAN port: %s", self.warn_prefix, e) self.reactor.pause(self.reactor.monotonic() + 5.) diff --git a/klippy/stepper.py b/klippy/stepper.py index 56c8ec758..9b692904d 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -265,6 +265,7 @@ def parse_gear_ratio(config, note_valid): # Obtain "step distance" information from a config section def parse_step_distance(config, units_in_radians=None, note_valid=False): + # Check rotation_distance and gear_ratio if units_in_radians is None: # Caller doesn't know if units are in radians - infer it rd = config.get('rotation_distance', None, note_valid=False) @@ -276,7 +277,7 @@ def parse_step_distance(config, units_in_radians=None, note_valid=False): else: rotation_dist = config.getfloat('rotation_distance', above=0., note_valid=note_valid) - # Newer config format with rotation_distance + # Check microsteps and full_steps_per_rotation microsteps = config.getint('microsteps', minval=1, note_valid=note_valid) full_steps = config.getint('full_steps_per_rotation', 200, minval=1, note_valid=note_valid) diff --git a/scripts/buildcommands.py b/scripts/buildcommands.py index 236373c2f..b35873840 100644 --- a/scripts/buildcommands.py +++ b/scripts/buildcommands.py @@ -251,8 +251,9 @@ class HandleCommandGeneration: def __init__(self): self.commands = {} self.encoders = [] - self.msg_to_id = dict(msgproto.DefaultMessages) - self.messages_by_name = { m.split()[0]: m for m in self.msg_to_id } + self.msg_to_encid = dict(msgproto.DefaultMessages) + self.encid_to_msgid = {} + self.messages_by_name = { m.split()[0]: m for m in self.msg_to_encid } self.all_param_types = {} self.ctr_dispatch = { 'DECL_COMMAND_FLAGS': self.decl_command, @@ -280,37 +281,47 @@ class HandleCommandGeneration: def decl_output(self, req): msg = req.split(None, 1)[1] self.encoders.append((None, msg)) + def convert_encoded_msgid(self, encoded_msgid): + if encoded_msgid >= 0x80: + data = [(encoded_msgid >> 7) | 0x80, encoded_msgid & 0x7f] + else: + data = [encoded_msgid] + return msgproto.PT_int32().parse(data, 0)[0] def create_message_ids(self): # Create unique ids for each message type - msgid = max(self.msg_to_id.values()) + encoded_msgid = max(self.msg_to_encid.values()) mlist = list(self.commands.keys()) + [m for n, m in self.encoders] for msgname in mlist: msg = self.messages_by_name.get(msgname, msgname) - if msg not in self.msg_to_id: - msgid += 1 - self.msg_to_id[msg] = msgid - if msgid >= 128: - # The mcu currently assumes all message ids encode to one byte + if msg not in self.msg_to_encid: + encoded_msgid += 1 + self.msg_to_encid[msg] = encoded_msgid + if encoded_msgid >= 1<<14: + # The mcu currently assumes all message ids encode to 1 or 2 bytes error("Too many message ids") + self.encid_to_msgid = { + encoded_msgid: self.convert_encoded_msgid(encoded_msgid) + for encoded_msgid in self.msg_to_encid.values() + } def update_data_dictionary(self, data): - # Handle message ids over 96 (they are decoded as negative numbers) - msg_to_tag = {msg: msgid if msgid < 96 else msgid - 128 - for msg, msgid in self.msg_to_id.items()} - command_tags = [msg_to_tag[msg] + # Convert ids to standard form (use both positive and negative numbers) + msg_to_msgid = {msg: self.encid_to_msgid[encoded_msgid] + for msg, encoded_msgid in self.msg_to_encid.items()} + command_ids = [msg_to_msgid[msg] + for msgname, msg in self.messages_by_name.items() + if msgname in self.commands] + response_ids = [msg_to_msgid[msg] for msgname, msg in self.messages_by_name.items() - if msgname in self.commands] - response_tags = [msg_to_tag[msg] - for msgname, msg in self.messages_by_name.items() - if msgname not in self.commands] - data['commands'] = { msg: msgtag for msg, msgtag in msg_to_tag.items() - if msgtag in command_tags } - data['responses'] = { msg: msgtag for msg, msgtag in msg_to_tag.items() - if msgtag in response_tags } - output = {msg: msgtag for msg, msgtag in msg_to_tag.items() - if msgtag not in command_tags and msgtag not in response_tags} + if msgname not in self.commands] + data['commands'] = { msg: msgid for msg, msgid in msg_to_msgid.items() + if msgid in command_ids } + data['responses'] = { msg: msgid for msg, msgid in msg_to_msgid.items() + if msgid in response_ids } + output = {msg: msgid for msg, msgid in msg_to_msgid.items() + if msgid not in command_ids and msgid not in response_ids} if output: data['output'] = output - def build_parser(self, msgid, msgformat, msgtype): + def build_parser(self, encoded_msgid, msgformat, msgtype): if msgtype == "output": param_types = msgproto.lookup_output_params(msgformat) comment = "Output: " + msgformat @@ -327,17 +338,21 @@ class HandleCommandGeneration: params = 'command_parameters%d' % (paramid,) out = """ // %s - .msg_id=%d, + .encoded_msgid=%d, // msgid=%d .num_params=%d, .param_types = %s, -""" % (comment, msgid, len(types), params) +""" % (comment, encoded_msgid, self.encid_to_msgid[encoded_msgid], + len(types), params) if msgtype == 'response': num_args = (len(types) + types.count('PT_progmem_buffer') + types.count('PT_buffer')) out += " .num_args=%d," % (num_args,) else: + msgid_size = 1 + if encoded_msgid >= 0x80: + msgid_size = 2 max_size = min(msgproto.MESSAGE_MAX, - (msgproto.MESSAGE_MIN + 1 + (msgproto.MESSAGE_MIN + msgid_size + sum([t.max_length for t in param_types]))) out += " .max_size=%d," % (max_size,) return out @@ -347,22 +362,23 @@ class HandleCommandGeneration: encoder_code = [] did_output = {} for msgname, msg in self.encoders: - msgid = self.msg_to_id[msg] - if msgid in did_output: + encoded_msgid = self.msg_to_encid[msg] + if encoded_msgid in did_output: continue - did_output[msgid] = True + did_output[encoded_msgid] = True code = (' if (__builtin_strcmp(str, "%s") == 0)\n' - ' return &command_encoder_%s;\n' % (msg, msgid)) + ' return &command_encoder_%s;\n' + % (msg, encoded_msgid)) if msgname is None: - parsercode = self.build_parser(msgid, msg, 'output') + parsercode = self.build_parser(encoded_msgid, msg, 'output') output_code.append(code) else: - parsercode = self.build_parser(msgid, msg, 'command') + parsercode = self.build_parser(encoded_msgid, msg, 'command') encoder_code.append(code) encoder_defs.append( "const struct command_encoder command_encoder_%s PROGMEM = {" " %s\n};\n" % ( - msgid, parsercode)) + encoded_msgid, parsercode)) fmt = """ %s @@ -384,21 +400,21 @@ ctr_lookup_output(const char *str) "".join(encoder_code).strip(), "".join(output_code).strip()) def generate_commands_code(self): - cmd_by_id = { - self.msg_to_id[self.messages_by_name.get(msgname, msgname)]: cmd + cmd_by_encid = { + self.msg_to_encid[self.messages_by_name.get(msgname, msgname)]: cmd for msgname, cmd in self.commands.items() } - max_cmd_msgid = max(cmd_by_id.keys()) + max_cmd_encid = max(cmd_by_encid.keys()) index = [] externs = {} - for msgid in range(max_cmd_msgid+1): - if msgid not in cmd_by_id: + for encoded_msgid in range(max_cmd_encid+1): + if encoded_msgid not in cmd_by_encid: index.append(" {\n},") continue - funcname, flags, msgname = cmd_by_id[msgid] + funcname, flags, msgname = cmd_by_encid[encoded_msgid] msg = self.messages_by_name[msgname] externs[funcname] = 1 - parsercode = self.build_parser(msgid, msg, 'response') + parsercode = self.build_parser(encoded_msgid, msg, 'response') index.append(" {%s\n .flags=%s,\n .func=%s\n}," % ( parsercode, flags, funcname)) index = "".join(index).strip() @@ -411,7 +427,7 @@ const struct command_parser command_index[] PROGMEM = { %s }; -const uint8_t command_index_size PROGMEM = ARRAY_SIZE(command_index); +const uint16_t command_index_size PROGMEM = ARRAY_SIZE(command_index); """ return fmt % (externs, index) def generate_param_code(self): diff --git a/scripts/ci-install.sh b/scripts/ci-install.sh index a7d2599a2..f5a18612f 100755 --- a/scripts/ci-install.sh +++ b/scripts/ci-install.sh @@ -35,7 +35,7 @@ if [ ! -f ${PRU_FILE} ]; then cd ${BUILD_DIR} git config --global user.email "you@example.com" git config --global user.name "Your Name" - git clone https://github.com/dinuxbg/gnupru -b 2023.01 --depth 1 + git clone https://github.com/dinuxbg/gnupru -b 2024.05 --depth 1 cd gnupru export PREFIX=${PRU_DIR} ./download-and-prepare.sh 2>&1 | pv -nli 30 > ${BUILD_DIR}/gnupru-build.log diff --git a/scripts/graph_mesh.py b/scripts/graph_mesh.py new file mode 100755 index 000000000..3a331e5d5 --- /dev/null +++ b/scripts/graph_mesh.py @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 +# Bed Mesh data plotting and analysis +# +# Copyright (C) 2024 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import argparse +import sys +import os +import stat +import errno +import time +import socket +import re +import json +import collections +import numpy as np +import matplotlib +import matplotlib.cm as cm +import matplotlib.pyplot as plt +import matplotlib.animation as ani + +MESH_DUMP_REQUEST = json.dumps( + {"id": 1, "method": "bed_mesh/dump_mesh"} +) + +def sock_error_exit(msg): + sys.stderr.write(msg + "\n") + sys.exit(-1) + +def webhook_socket_create(uds_filename): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + while 1: + try: + sock.connect(uds_filename) + except socket.error as e: + if e.errno == errno.ECONNREFUSED: + time.sleep(0.1) + continue + sock_error_exit( + "Unable to connect socket %s [%d,%s]" + % (uds_filename, e.errno, errno.errorcode[e.errno]) + ) + break + print("Connected") + return sock + +def process_message(msg): + try: + resp = json.loads(msg) + except json.JSONDecodeError: + return None + if resp.get("id", -1) != 1: + return None + if "error" in resp: + err = resp["error"].get("message", "Unknown") + sock_error_exit( + "Error requesting mesh dump: %s" % (err,) + ) + return resp["result"] + + +def request_from_unixsocket(unix_sock_name): + print("Connecting to Unix Socket File '%s'" % (unix_sock_name,)) + whsock = webhook_socket_create(unix_sock_name) + whsock.settimeout(1.) + # send mesh query + whsock.send(MESH_DUMP_REQUEST.encode() + b"\x03") + sock_data = b"" + end_time = time.monotonic() + 10.0 + try: + while time.monotonic() < end_time: + try: + data = whsock.recv(4096) + except TimeoutError: + pass + else: + if not data: + sock_error_exit("Socket closed before mesh received") + parts = data.split(b"\x03") + parts[0] = sock_data + parts[0] + sock_data = parts.pop() + for msg in parts: + result = process_message(msg) + if result is not None: + return result + time.sleep(.1) + finally: + whsock.close() + sock_error_exit("Mesh dump request timed out") + +def request_from_websocket(url): + print("Connecting to websocket url '%s'" % (url,)) + try: + from websockets.sync.client import connect + except ModuleNotFoundError: + sock_error_exit("Python module 'websockets' not installed.") + raise + with connect(url) as websocket: + websocket.send(MESH_DUMP_REQUEST) + end_time = time.monotonic() + 20.0 + while time.monotonic() < end_time: + try: + msg = websocket.recv(10.) + except TimeoutError: + continue + result = process_message(msg) + if result is not None: + return result + time.sleep(.1) + sock_error_exit("Mesh dump request timed out") + +def request_mesh_data(input_name): + url_match = re.match(r"((?:https?)|(?:wss?))://(.+)", input_name.lower()) + if url_match is None: + file_path = os.path.abspath(os.path.expanduser(input_name)) + if not os.path.exists(file_path): + sock_error_exit("Path '%s' does not exist" % (file_path,)) + st_res = os.stat(file_path) + if stat.S_ISSOCK(st_res.st_mode): + return request_from_unixsocket(file_path) + else: + print("Reading mesh data from json file '%s'" % (file_path,)) + with open(file_path, "r") as f: + return json.load(f) + scheme = url_match.group(1) + host = url_match.group(2).rstrip("/") + scheme = scheme.replace("http", "ws") + url = "%s://%s/klippysocket" % (scheme, host) + return request_from_websocket(url) + +class PathAnimation: + instance = None + def __init__(self, artist, x_travel, y_travel): + self.travel_artist = artist + self.x_travel = x_travel + self.y_travel = y_travel + fig = plt.gcf() + self.animation = ani.FuncAnimation( + fig=fig, func=self.update, frames=self.gen_path_position(), + cache_frame_data=False, interval=60 + ) + PathAnimation.instance = self + + def gen_path_position(self): + count = 1 + x_travel, y_travel = self.x_travel, self.y_travel + last_x, last_y = x_travel[0], y_travel[0] + yield count + for xpos, ypos in zip(x_travel[1:], y_travel[1:]): + count += 1 + if xpos == last_x or ypos == last_y: + yield count + last_x, last_y = xpos, ypos + + def update(self, frame): + x_travel, y_travel = self.x_travel, self.y_travel + self.travel_artist.set_xdata(x_travel[:frame]) + self.travel_artist.set_ydata(y_travel[:frame]) + return (self.travel_artist,) + + +def _gen_mesh_coords(min_c, max_c, count): + dist = (max_c - min_c) / (count - 1) + return [min_c + i * dist for i in range(count)] + +def _plot_path(travel_path, probed, diff, cmd_args): + x_travel, y_travel = np.array(travel_path).transpose() + x_probed, y_probed = np.array(probed).transpose() + plt.xlabel("X") + plt.ylabel("Y") + # plot travel + travel_line = plt.plot(x_travel, y_travel, "b-")[0] + # plot intermediate points + plt.plot(x_probed, y_probed, "k.") + # plot start point + plt.plot([x_travel[0]], [y_travel[0]], "g>") + # plot stop point + plt.plot([x_travel[-1]], [y_travel[-1]], "r*") + if diff: + diff_x, diff_y = np.array(diff).transpose() + plt.plot(diff_x, diff_y, "m.") + if cmd_args.animate and cmd_args.output is None: + PathAnimation(travel_line, x_travel, y_travel) + +def _format_mesh_data(matrix, params): + min_pt = (params["min_x"], params["min_y"]) + max_pt = (params["max_x"], params["max_y"]) + xvals = _gen_mesh_coords(min_pt[0], max_pt[0], len(matrix[0])) + yvals = _gen_mesh_coords(min_pt[1], max_pt[0], len(matrix)) + x, y = np.meshgrid(xvals, yvals) + z = np.array(matrix) + return x, y, z + +def _set_xy_limits(mesh_data, cmd_args): + if not cmd_args.scale_plot: + return + ax = plt.gca() + axis_min = mesh_data["axis_minimum"] + axis_max = mesh_data["axis_maximum"] + ax.set_xlim((axis_min[0], axis_max[0])) + ax.set_ylim((axis_min[1], axis_max[1])) + +def _plot_mesh(ax, matrix, params, cmap=cm.viridis, label=None): + x, y, z = _format_mesh_data(matrix, params) + surface = ax.plot_surface(x, y, z, cmap=cmap, label=label) + scale = max(abs(z.min()), abs(z.max())) * 3 + return surface, scale + +def plot_probe_points(mesh_data, cmd_args): + """Plot original generated points""" + calibration = mesh_data["calibration"] + x, y = np.array(calibration["points"]).transpose() + plt.title("Generated Probe Points") + plt.xlabel("X") + plt.ylabel("Y") + plt.plot(x, y, "b.") + _set_xy_limits(mesh_data, cmd_args) + +def plot_probe_path(mesh_data, cmd_args): + """Plot probe travel path""" + calibration = mesh_data["calibration"] + orig_pts = calibration["points"] + path_pts = calibration["probe_path"] + diff = [pt for pt in orig_pts if pt not in path_pts] + plt.title("Probe Travel Path") + _plot_path(path_pts, path_pts[1:-1], diff, cmd_args) + _set_xy_limits(mesh_data, cmd_args) + +def plot_rapid_path(mesh_data, cmd_args): + """Plot rapid scan travel path""" + calibration = mesh_data["calibration"] + orig_pts = calibration["points"] + rapid_pts = calibration["rapid_path"] + rapid_path = [pt[0] for pt in rapid_pts] + probed = [pt for pt, is_ppt in rapid_pts if is_ppt] + diff = [pt for pt in orig_pts if pt not in probed] + plt.title("Rapid Scan Travel Path") + _plot_path(rapid_path, probed, diff, cmd_args) + _set_xy_limits(mesh_data, cmd_args) + +def plot_probed_matrix(mesh_data, cmd_args): + """Plot probed Z values""" + ax = plt.subplot(projection="3d") + profile = cmd_args.profile_name + if profile is not None: + req_mesh = mesh_data["profiles"].get(profile) + if req_mesh is None: + raise Exception("Profile %s not found" % (profile,)) + matrix = req_mesh["points"] + name = profile + else: + req_mesh = mesh_data["current_mesh"] + if not req_mesh: + raise Exception("No current mesh data in dump") + matrix = req_mesh["probed_matrix"] + name = req_mesh["name"] + params = req_mesh["mesh_params"] + surface, scale = _plot_mesh(ax, matrix, params) + ax.set_title("Probed Mesh (%s)" % (name,)) + ax.set(zlim=(-scale, scale)) + plt.gcf().colorbar(surface, shrink=.75) + _set_xy_limits(mesh_data, cmd_args) + +def plot_mesh_matrix(mesh_data, cmd_args): + """Plot mesh Z values""" + ax = plt.subplot(projection="3d") + req_mesh = mesh_data["current_mesh"] + if not req_mesh: + raise Exception("No current mesh data in dump") + matrix = req_mesh["mesh_matrix"] + params = req_mesh["mesh_params"] + surface, scale = _plot_mesh(ax, matrix, params) + name = req_mesh["name"] + ax.set_title("Interpolated Mesh (%s)" % (name,)) + ax.set(zlim=(-scale, scale)) + plt.gcf().colorbar(surface, shrink=.75) + _set_xy_limits(mesh_data, cmd_args) + +def plot_overlay(mesh_data, cmd_args): + """Plots the current probed mesh overlaid with a profile""" + ax = plt.subplot(projection="3d") + # Plot Profile + profile = cmd_args.profile_name + if profile is None: + raise Exception("A profile must be specified to plot an overlay") + req_mesh = mesh_data["profiles"].get(profile) + if req_mesh is None: + raise Exception("Profile %s not found" % (profile,)) + matrix = req_mesh["points"] + params = req_mesh["mesh_params"] + prof_surf, prof_scale = _plot_mesh(ax, matrix, params, label=profile) + # Plot Current + req_mesh = mesh_data["current_mesh"] + if not req_mesh: + raise Exception("No current mesh data in dump") + matrix = req_mesh["probed_matrix"] + params = req_mesh["mesh_params"] + cur_name = req_mesh["name"] + cur_surf, cur_scale = _plot_mesh(ax, matrix, params, cm.inferno, cur_name) + ax.set_title("Probed Mesh Overlay") + scale = max(cur_scale, prof_scale) + ax.set(zlim=(-scale, scale)) + ax.legend(loc='best') + plt.gcf().colorbar(prof_surf, shrink=.75) + _set_xy_limits(mesh_data, cmd_args) + +def plot_delta(mesh_data, cmd_args): + """Plots the delta between current probed mesh and a profile""" + ax = plt.subplot(projection="3d") + # Plot Profile + profile = cmd_args.profile_name + if profile is None: + raise Exception("A profile must be specified to plot an overlay") + req_mesh = mesh_data["profiles"].get(profile) + if req_mesh is None: + raise Exception("Profile %s not found" % (profile,)) + prof_matix = req_mesh["points"] + prof_params = req_mesh["mesh_params"] + req_mesh = mesh_data["current_mesh"] + if not req_mesh: + raise Exception("No current mesh data in dump") + cur_matrix = req_mesh["probed_matrix"] + cur_params = req_mesh["mesh_params"] + cur_name = req_mesh["name"] + # validate that the params match + pfields = ("x_count", "y_count", "min_x", "max_x", "min_y", "max_y") + for field in pfields: + if abs(prof_params[field] - cur_params[field]) >= 1e-6: + raise Exception( + "Values for field %s do not match, cant plot deviation" + ) + delta = np.array(cur_matrix) - np.array(prof_matix) + surface, scale = _plot_mesh(ax, delta, cur_params) + ax.set(zlim=(-scale, scale)) + ax.set_title("Probed Mesh Delta (%s, %s)" % (cur_name, profile)) + _set_xy_limits(mesh_data, cmd_args) + + +PLOT_TYPES = { + "points": plot_probe_points, + "path": plot_probe_path, + "rapid": plot_rapid_path, + "probedz": plot_probed_matrix, + "meshz": plot_mesh_matrix, + "overlay": plot_overlay, + "delta": plot_delta, +} + +def print_types(cmd_args): + typelist = [ + "%-10s%s" % (name, func.__doc__) for name, func in PLOT_TYPES.items() + ] + print("\n".join(typelist)) + +def plot_mesh_data(cmd_args): + mesh_data = request_mesh_data(cmd_args.input) + if cmd_args.output is not None: + matplotlib.use("svg") + + fig = plt.figure() + plot_func = PLOT_TYPES[cmd_args.type] + plot_func(mesh_data, cmd_args) + fig.set_size_inches(10, 8) + fig.tight_layout() + if cmd_args.output is None: + plt.show() + else: + fig.savefig(cmd_args.output) + +def _check_path_unique(name, path): + path = np.array(path) + unique_pts, counts = np.unique(path, return_counts=True, axis=0) + for idx, count in enumerate(counts): + if count != 1: + coord = unique_pts[idx] + print( + " WARNING: Backtracking or duplicate found in %s path at %s, " + "this may be due to multiple samples in a faulty region." + % (name, coord) + ) + +def _analyze_mesh(name, mesh_axes): + print("\nAnalyzing Probed Mesh %s..." % (name,)) + x, y, z = mesh_axes + min_idx, max_idx = z.argmin(), z.argmax() + min_x, min_y = x.flatten()[min_idx], y.flatten()[min_idx] + max_x, max_y = x.flatten()[max_idx], y.flatten()[max_idx] + + print( + " Min Coord (%.2f, %.2f), Max Coord (%.2f, %.2f), " + "Probe Count: (%d, %d)" % + (x.min(), y.min(), x.max(), y.max(), len(z), len(z[0])) + ) + print( + " Mesh range: min %.4f (%.2f, %.2f), max %.4f (%.2f, %.2f)" + % (z.min(), min_x, min_y, z.max(), max_x, max_y) + ) + print(" Mean: %.4f, Standard Deviation: %.4f" % (z.mean(), z.std())) + +def _compare_mesh(name_a, name_b, mesh_a, mesh_b): + ax, ay, az = mesh_a + bx, by, bz = mesh_b + if not np.array_equal(ax, bx) or not np.array_equal(ay, by): + return + delta = az - bz + abs_max = max(abs(delta.max()), abs(delta.min())) + abs_mean = sum([abs(z) for z in delta.flatten()]) / len(delta.flatten()) + min_idx, max_idx = delta.argmin(), delta.argmax() + min_x, min_y = ax.flatten()[min_idx], ay.flatten()[min_idx] + max_x, max_y = ax.flatten()[max_idx], ay.flatten()[max_idx] + print(" Delta from %s to %s..." % (name_a, name_b)) + print( + " Range: min %.4f (%.2f, %.2f), max %.4f (%.2f, %.2f)\n" + " Mean: %.6f, Standard Deviation: %.6f\n" + " Absolute Max: %.6f, Absolute Mean: %.6f" + % (delta.min(), min_x, min_y, delta.max(), max_x, max_y, + delta.mean(), delta.std(), abs_max, abs_mean) + ) + +def analyze(cmd_args): + mesh_data = request_mesh_data(cmd_args.input) + print("Analyzing Travel Path...") + calibration = mesh_data["calibration"] + org_pts = calibration["points"] + probe_path = calibration["probe_path"] + rapid_path = calibration["rapid_path"] + rapid_points = [pt for pt, is_pt in rapid_path if is_pt] + rapid_moves = [pt[0] for pt in rapid_path] + print(" Original point count: %d" % (len(org_pts))) + print(" Probe path count: %d" % (len(probe_path))) + print(" Rapid scan sample count: %d" % (len(probe_path))) + print(" Rapid scan move count: %d" % (len(rapid_moves))) + if np.array_equal(rapid_points, probe_path): + print(" Rapid scan points match probe path points") + else: + diff = [pt for pt in rapid_points if pt not in probe_path] + print( + " ERROR: Rapid scan points do not match probe points\n" + "difference: %s" % (diff,) + ) + _check_path_unique("probe", probe_path) + _check_path_unique("rapid scan", rapid_moves) + req_mesh = mesh_data["current_mesh"] + formatted_data = collections.OrderedDict() + if req_mesh: + matrix = req_mesh["probed_matrix"] + params = req_mesh["mesh_params"] + name = req_mesh["name"] + formatted_data[name] = _format_mesh_data(matrix, params) + profiles = mesh_data["profiles"] + for prof_name, prof_data in profiles.items(): + if prof_name in formatted_data: + continue + matrix = prof_data["points"] + params = prof_data["mesh_params"] + formatted_data[prof_name] = _format_mesh_data(matrix, params) + while formatted_data: + name, current_axes = formatted_data.popitem() + _analyze_mesh(name, current_axes) + for prof_name, prof_axes in formatted_data.items(): + _compare_mesh(name, prof_name, current_axes, prof_axes) + +def dump_request(cmd_args): + mesh_data = request_mesh_data(cmd_args.input) + outfile = cmd_args.output + if outfile is None: + postfix = time.strftime("%Y%m%d_%H%M%S") + outfile = "klipper-bedmesh-%s.json" % (postfix,) + outfile = os.path.abspath(os.path.expanduser(outfile)) + print("Saving Mesh Output to '%s'" % (outfile)) + with open(outfile, "w") as f: + f.write(json.dumps(mesh_data)) + +def main(): + parser = argparse.ArgumentParser(description="Graph Bed Mesh Data") + sub_parsers = parser.add_subparsers() + list_parser = sub_parsers.add_parser( + "list", help="List available plot types" + ) + list_parser.set_defaults(func=print_types) + plot_parser = sub_parsers.add_parser("plot", help="Plot a specified type") + analyze_parser = sub_parsers.add_parser( + "analyze", help="Perform analysis on mesh data" + ) + dump_parser = sub_parsers.add_parser( + "dump", help="Dump API response to json file" + ) + plot_parser.add_argument( + "-a", "--animate", action="store_true", + help="Animate paths in live preview" + ) + plot_parser.add_argument( + "-s", "--scale-plot", action="store_true", + help="Use axis limits reported by Klipper to scale plot X/Y" + ) + plot_parser.add_argument( + "-p", "--profile-name", type=str, default=None, + help="Optional name of a profile to plot for 'probedz'" + ) + plot_parser.add_argument( + "-o", "--output", type=str, default=None, + help="Output file path" + ) + plot_parser.add_argument( + "type", metavar="", type=str, choices=PLOT_TYPES.keys(), + help="Type of data to graph" + ) + plot_parser.add_argument( + "input", metavar="", + help="Path/url to Klipper Socket or path to json file" + ) + plot_parser.set_defaults(func=plot_mesh_data) + analyze_parser.add_argument( + "input", metavar="", + help="Path/url to Klipper Socket or path to json file" + ) + analyze_parser.set_defaults(func=analyze) + dump_parser.add_argument( + "-o", "--output", type=str, default=None, + help="Json output file path" + ) + dump_parser.add_argument( + "input", metavar="", + help="Path or url to Klipper Socket" + ) + dump_parser.set_defaults(func=dump_request) + cmd_args = parser.parse_args() + cmd_args.func(cmd_args) + + +if __name__ == "__main__": + main() diff --git a/scripts/motan/readlog.py b/scripts/motan/readlog.py index 1b44c9375..48284ec2b 100644 --- a/scripts/motan/readlog.py +++ b/scripts/motan/readlog.py @@ -293,7 +293,7 @@ class HandleStepPhase: self._pull_block(req_time) continue step_pos = step_data[data_pos][1] - return (step_pos - self.mcu_phase_offset) % self.phases + return (step_pos + self.mcu_phase_offset) % self.phases def _pull_block(self, req_time): step_data = self.step_data del step_data[:-1] diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index 4f84d7229..9924fefcd 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -31,6 +31,11 @@ BOARD_DEFS = { 'spi_bus': "spi1", "cs_pin": "PA4" }, + 'btt-skr-mini-v3-b0': { + 'mcu': "stm32g0b0xx", + 'spi_bus': "spi1", + "cs_pin": "PA4" + }, 'flyboard-mini': { 'mcu': "stm32f103xe", 'spi_bus': "spi2", @@ -41,6 +46,7 @@ BOARD_DEFS = { 'mcu': "stm32f103xe", 'spi_bus': "spi2", "cs_pin": "PA15", + "conversion_script": "scripts/update_mks_robin.py", "firmware_path": "Robin_e3.bin", "current_firmware_path": "Robin_e3.cur" }, @@ -128,6 +134,16 @@ BOARD_DEFS = { 'mcu': "stm32g0b1xx", 'spi_bus': "spi1", "cs_pin": "PB8" + }, + 'chitu-v6': { + 'mcu': "stm32f103xe", + 'spi_bus': "swspi", + 'spi_pins': "PC8,PD2,PC12", + "cs_pin": "PC11", + #'sdio_bus': 'sdio', + "conversion_script": "scripts/update_chitu.py", + "firmware_path": "update.cbd", + 'skip_verify': True } } @@ -152,6 +168,7 @@ BOARD_ALIASES = { 'btt-skr-mini-e3-v1.2': BOARD_DEFS['btt-skr-mini'], 'btt-skr-mini-e3-v2': BOARD_DEFS['btt-skr-mini'], 'btt-skr-mini-e3-v3': BOARD_DEFS['btt-skr-mini-v3'], + 'btt-skr-mini-e3-v3-b0': BOARD_DEFS['btt-skr-mini-v3-b0'], 'btt-skr-mini-mz': BOARD_DEFS['btt-skr-mini'], 'btt-skr-e3-dip': BOARD_DEFS['btt-skr-mini'], 'btt002-v1': BOARD_DEFS['btt-skr-mini'], @@ -176,7 +193,8 @@ BOARD_ALIASES = { 'fysetc-s6-v1.2': BOARD_DEFS['fysetc-spider'], 'fysetc-s6-v2': BOARD_DEFS['fysetc-spider'], 'robin_v3': BOARD_DEFS['monster8'], - 'btt-skrat-v1.0': BOARD_DEFS['btt-skrat'] + 'btt-skrat-v1.0': BOARD_DEFS['btt-skrat'], + 'chitu-v6': BOARD_DEFS['chitu-v6'] } def list_boards(): diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index a3231b693..cbe769e57 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -74,20 +74,19 @@ def translate_serial_to_tty(device): return ttyname, ttyname def check_need_convert(board_name, config): - if board_name.lower().startswith('mks-robin-e3'): - # we need to convert this file - robin_util = os.path.join( - fatfs_lib.KLIPPER_DIR, "scripts/update_mks_robin.py") - klipper_bin = config['klipper_bin_path'] - robin_bin = os.path.join( + conv_script = config.get("conversion_script") + if conv_script is None: + return + conv_util = os.path.join(fatfs_lib.KLIPPER_DIR, conv_script) + klipper_bin = config['klipper_bin_path'] + dest_bin = os.path.join( os.path.dirname(klipper_bin), os.path.basename(config['firmware_path'])) - cmd = "%s %s %s %s" % (sys.executable, robin_util, klipper_bin, - robin_bin) - output("Converting Klipper binary to MKS Robin format...") - os.system(cmd) - output_line("Done") - config['klipper_bin_path'] = robin_bin + cmd = "%s %s %s %s" % (sys.executable, conv_util, klipper_bin, dest_bin) + output("Converting Klipper binary to custom format...") + os.system(cmd) + output_line("Done") + config['klipper_bin_path'] = dest_bin ########################################################### diff --git a/src/Kconfig b/src/Kconfig index 7dcea3bab..1fdfe02cc 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -108,6 +108,14 @@ config WANT_LDC1612 bool depends on HAVE_GPIO_I2C default y +config WANT_HX71X + bool + depends on WANT_GPIO_BITBANGING + default y +config WANT_ADS1220 + bool + depends on HAVE_GPIO_SPI + default y config WANT_SOFTWARE_I2C bool depends on HAVE_GPIO && HAVE_GPIO_I2C @@ -118,7 +126,8 @@ config WANT_SOFTWARE_SPI default y config NEED_SENSOR_BULK bool - depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612 + depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612 || WANT_HX71X \ + || WANT_ADS1220 default y menu "Optional features (to reduce code size)" depends on HAVE_LIMITED_CODE_SIZE @@ -137,6 +146,12 @@ config WANT_LIS2DW config WANT_LDC1612 bool "Support ldc1612 eddy current sensor" depends on HAVE_GPIO_I2C +config WANT_HX71X + bool "Support HX711 and HX717 ADC chips" + depends on WANT_GPIO_BITBANGING +config WANT_ADS1220 + bool "Support ADS 1220 ADC chip" + depends on HAVE_GPIO_SPI config WANT_SOFTWARE_I2C bool "Support software based I2C \"bit-banging\"" depends on HAVE_GPIO && HAVE_GPIO_I2C diff --git a/src/Makefile b/src/Makefile index ed98172e4..86c7407e6 100644 --- a/src/Makefile +++ b/src/Makefile @@ -20,4 +20,6 @@ sensors-src-$(CONFIG_HAVE_GPIO_I2C) += sensor_mpu9250.c src-$(CONFIG_WANT_SENSORS) += $(sensors-src-y) src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c src-$(CONFIG_WANT_LDC1612) += sensor_ldc1612.c +src-$(CONFIG_WANT_HX71X) += sensor_hx71x.c +src-$(CONFIG_WANT_ADS1220) += sensor_ads1220.c src-$(CONFIG_NEED_SENSOR_BULK) += sensor_bulk.c diff --git a/src/atsam/Makefile b/src/atsam/Makefile index 7ab69b823..3595d0cef 100644 --- a/src/atsam/Makefile +++ b/src/atsam/Makefile @@ -20,7 +20,7 @@ CFLAGS-$(CONFIG_MACH_SAM4E) += -Ilib/sam4e/include CFLAGS-$(CONFIG_MACH_SAME70) += -Ilib/same70b/include CFLAGS += $(CFLAGS-y) -D__$(MCU)__ -mthumb -Ilib/cmsis-core -Ilib/fast-hash -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld $(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld diff --git a/src/atsamd/Makefile b/src/atsamd/Makefile index d241cd8cb..8b9722b62 100644 --- a/src/atsamd/Makefile +++ b/src/atsamd/Makefile @@ -14,7 +14,7 @@ CFLAGS-$(CONFIG_MACH_SAME51) += -Ilib/same51/include CFLAGS-$(CONFIG_MACH_SAMX5) += -mcpu=cortex-m4 -Ilib/same54/include CFLAGS += $(CFLAGS-y) -D__$(MCU)__ -mthumb -Ilib/cmsis-core -Ilib/fast-hash -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld $(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld diff --git a/src/command.c b/src/command.c index 39c09458b..d2d05aff9 100644 --- a/src/command.c +++ b/src/command.c @@ -1,6 +1,6 @@ // Code for parsing incoming commands and encoding outgoing messages // -// Copyright (C) 2016,2017 Kevin O'Connor +// Copyright (C) 2016-2024 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -69,6 +69,28 @@ parse_int(uint8_t **pp) return v; } +// Write an encoded msgid (optimized 2-byte VLQ encoder) +static uint8_t * +encode_msgid(uint8_t *p, uint_fast16_t encoded_msgid) +{ + if (encoded_msgid >= 0x80) + *p++ = (encoded_msgid >> 7) | 0x80; + *p++ = encoded_msgid & 0x7f; + return p; +} + +// Parse an encoded msgid (optimized 2-byte parser, return as positive number) +uint_fast16_t +command_parse_msgid(uint8_t **pp) +{ + uint8_t *p = *pp; + uint_fast16_t encoded_msgid = *p++; + if (encoded_msgid & 0x80) + encoded_msgid = ((encoded_msgid & 0x7f) << 7) | (*p++); + *pp = p; + return encoded_msgid; +} + // Parse an incoming command into 'args' uint8_t * command_parsef(uint8_t *p, uint8_t *maxend @@ -119,7 +141,7 @@ command_encodef(uint8_t *buf, const struct command_encoder *ce, va_list args) uint8_t *maxend = &p[max_size - MESSAGE_MIN]; uint_fast8_t num_params = READP(ce->num_params); const uint8_t *param_types = READP(ce->param_types); - *p++ = READP(ce->msg_id); + p = encode_msgid(p, READP(ce->encoded_msgid)); while (num_params--) { if (p > maxend) goto error; @@ -227,7 +249,7 @@ DECL_SHUTDOWN(sendf_shutdown); // Find the command handler associated with a command static const struct command_parser * -command_lookup_parser(uint_fast8_t cmdid) +command_lookup_parser(uint_fast16_t cmdid) { if (!cmdid || cmdid >= READP(command_index_size)) shutdown("Invalid command"); @@ -309,7 +331,7 @@ command_dispatch(uint8_t *buf, uint_fast8_t msglen) uint8_t *p = &buf[MESSAGE_HEADER_SIZE]; uint8_t *msgend = &buf[msglen-MESSAGE_TRAILER_SIZE]; while (p < msgend) { - uint_fast8_t cmdid = *p++; + uint_fast16_t cmdid = command_parse_msgid(&p); const struct command_parser *cp = command_lookup_parser(cmdid); uint32_t args[READP(cp->num_args)]; p = command_parsef(p, msgend, cp, args); diff --git a/src/command.h b/src/command.h index 894114d71..21b3f79b8 100644 --- a/src/command.h +++ b/src/command.h @@ -57,11 +57,13 @@ #define MESSAGE_SYNC 0x7E struct command_encoder { - uint8_t msg_id, max_size, num_params; + uint16_t encoded_msgid; + uint8_t max_size, num_params; const uint8_t *param_types; }; struct command_parser { - uint8_t msg_id, num_args, flags, num_params; + uint16_t encoded_msgid; + uint8_t num_args, flags, num_params; const uint8_t *param_types; void (*func)(uint32_t *args); }; @@ -72,6 +74,7 @@ enum { // command.c void *command_decode_ptr(uint32_t v); +uint_fast16_t command_parse_msgid(uint8_t **pp); uint8_t *command_parsef(uint8_t *p, uint8_t *maxend , const struct command_parser *cp, uint32_t *args); uint_fast8_t command_encode_and_frame( @@ -86,7 +89,7 @@ int_fast8_t command_find_and_dispatch(uint8_t *buf, uint_fast8_t buf_len // out/compile_time_request.c (auto generated file) extern const struct command_parser command_index[]; -extern const uint8_t command_index_size; +extern const uint16_t command_index_size; extern const uint8_t command_identify_data[]; extern const uint32_t command_identify_size; const struct command_encoder *ctr_lookup_encoder(const char *str); diff --git a/src/generic/armcm_link.lds.S b/src/generic/armcm_link.lds.S index 2f789f130..94dd2100d 100644 --- a/src/generic/armcm_link.lds.S +++ b/src/generic/armcm_link.lds.S @@ -69,5 +69,8 @@ SECTIONS // that isn't needed so no need to include them in the binary. *(.init) *(.fini) + // Don't include exception tables + *(.ARM.extab) + *(.ARM.exidx) } } diff --git a/src/hc32f460/Makefile b/src/hc32f460/Makefile index c44267369..85d2fa19d 100644 --- a/src/hc32f460/Makefile +++ b/src/hc32f460/Makefile @@ -7,7 +7,7 @@ dirs-y += src/hc32f460 src/generic lib/hc32f460/driver/src lib/hc32f460/mcu/comm CFLAGS += -mthumb -mcpu=cortex-m4 -Isrc/hc32f460 -Ilib/hc32f460/driver/inc -Ilib/hc32f460/mcu/common -Ilib/cmsis-core -DHC32F460 -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld $(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld diff --git a/src/lpc176x/Makefile b/src/lpc176x/Makefile index 7ed80b26b..6814969c5 100644 --- a/src/lpc176x/Makefile +++ b/src/lpc176x/Makefile @@ -7,7 +7,7 @@ dirs-y += src/lpc176x src/generic lib/lpc176x/device CFLAGS += -mthumb -mcpu=cortex-m3 -Ilib/lpc176x/device -Ilib/cmsis-core -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld $(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld diff --git a/src/pru/pru0.c b/src/pru/pru0.c index 57d55d279..8a11e1402 100644 --- a/src/pru/pru0.c +++ b/src/pru/pru0.c @@ -141,7 +141,7 @@ do_dispatch(uint8_t *buf, uint32_t msglen) uint8_t *msgend = &buf[msglen-MESSAGE_TRAILER_SIZE]; while (p < msgend) { // Parse command - uint_fast8_t cmdid = *p++; + uint_fast16_t cmdid = command_parse_msgid(&p); const struct command_parser *cp = &SHARED_MEM->command_index[cmdid]; if (!cmdid || cmdid >= SHARED_MEM->command_index_size || cp->num_args > ARRAY_SIZE(SHARED_MEM->next_command_args)) { diff --git a/src/rp2040/Makefile b/src/rp2040/Makefile index 641990140..b82503a39 100644 --- a/src/rp2040/Makefile +++ b/src/rp2040/Makefile @@ -46,7 +46,6 @@ $(OUT)klipper.uf2: $(OUT)klipper.elf $(OUT)lib/rp2040/elf2uf2/elf2uf2 $(Q)$(OUT)lib/rp2040/elf2uf2/elf2uf2 $< $@ rptarget-$(CONFIG_RP2040_HAVE_STAGE2) := $(OUT)klipper.uf2 -rplink-$(CONFIG_RP2040_HAVE_STAGE2) := $(OUT)src/rp2040/rp2040_link.ld stage2-$(CONFIG_RP2040_HAVE_STAGE2) := $(OUT)stage2.o # rp2040 building when using a bootloader @@ -55,13 +54,13 @@ $(OUT)klipper.bin: $(OUT)klipper.elf $(Q)$(OBJCOPY) -O binary $< $@ rptarget-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)klipper.bin -rplink-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)src/rp2040/rp2040_link.ld # Set klipper.elf linker rules target-y += $(rptarget-y) -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs -T $(rplink-y) +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano +CFLAGS_klipper.elf += -T $(OUT)src/rp2040/rp2040_link.ld OBJS_klipper.elf += $(stage2-y) -$(OUT)klipper.elf: $(stage2-y) $(rplink-y) +$(OUT)klipper.elf: $(stage2-y) $(OUT)src/rp2040/rp2040_link.ld # Flash rules lib/rp2040_flash/rp2040_flash: diff --git a/src/rp2040/rp2040_link.lds.S b/src/rp2040/rp2040_link.lds.S index 9b0264a2b..abc5be6c5 100644 --- a/src/rp2040/rp2040_link.lds.S +++ b/src/rp2040/rp2040_link.lds.S @@ -21,6 +21,16 @@ MEMORY ram (rwx) : ORIGIN = CONFIG_RAM_START , LENGTH = CONFIG_RAM_SIZE } +// Force flags for each output section to avoid RWX linker warning +PHDRS +{ + text_segment PT_LOAD FLAGS(5); // RX flags + ram_vectortable_segment PT_LOAD FLAGS(6); // RW flags + data_segment PT_LOAD FLAGS(6); // RW flags + bss_segment PT_LOAD FLAGS(6); // RW flags + stack_segment PT_LOAD FLAGS(6); // RW flags +} + SECTIONS { .text : { @@ -32,7 +42,7 @@ SECTIONS KEEP(*(.vector_table)) _text_vectortable_end = .; *(.text.armcm_boot*) - } > rom + } > rom :text_segment . = ALIGN(4); _data_flash = .; @@ -41,7 +51,7 @@ SECTIONS _ram_vectortable_start = .; . = . + ( _text_vectortable_end - _text_vectortable_start ) ; _ram_vectortable_end = .; - } > ram + } > ram :ram_vectortable_segment .data : AT (_data_flash) { @@ -53,7 +63,7 @@ SECTIONS *(.data .data.*); . = ALIGN(4); _data_end = .; - } > ram + } > ram :data_segment .bss (NOLOAD) : { @@ -63,19 +73,22 @@ SECTIONS *(COMMON) . = ALIGN(4); _bss_end = .; - } > ram + } > ram :bss_segment _stack_start = CONFIG_RAM_START + CONFIG_RAM_SIZE - CONFIG_STACK_SIZE ; .stack _stack_start (NOLOAD) : { . = . + CONFIG_STACK_SIZE; _stack_end = .; - } > ram + } > ram :stack_segment /DISCARD/ : { // The .init/.fini sections are used by __libc_init_array(), but // that isn't needed so no need to include them in the binary. *(.init) *(.fini) + // Don't include exception tables + *(.ARM.extab) + *(.ARM.exidx) } } diff --git a/src/sensor_ads1220.c b/src/sensor_ads1220.c new file mode 100644 index 000000000..ea33379a0 --- /dev/null +++ b/src/sensor_ads1220.c @@ -0,0 +1,163 @@ +// Support for ADS1220 ADC Chip +// +// Copyright (C) 2024 Gareth Farrington +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include "board/irq.h" // irq_disable +#include "board/gpio.h" // gpio_out_write +#include "board/misc.h" // timer_read_time +#include "basecmd.h" // oid_alloc +#include "command.h" // DECL_COMMAND +#include "sched.h" // sched_add_timer +#include "sensor_bulk.h" // sensor_bulk_report +#include "spicmds.h" // spidev_transfer +#include + +struct ads1220_adc { + struct timer timer; + uint32_t rest_ticks; + struct gpio_in data_ready; + struct spidev_s *spi; + uint8_t pending_flag, data_count; + struct sensor_bulk sb; +}; + +// Flag types +enum { + FLAG_PENDING = 1 << 0 +}; + +#define BYTES_PER_SAMPLE 4 + +static struct task_wake wake_ads1220; + +/**************************************************************** + * ADS1220 Sensor Support + ****************************************************************/ + +int8_t +ads1220_is_data_ready(struct ads1220_adc *ads1220) { + return gpio_in_read(ads1220->data_ready) == 0; +} + +// Event handler that wakes wake_ads1220() periodically +static uint_fast8_t +ads1220_event(struct timer *timer) +{ + struct ads1220_adc *ads1220 = container_of(timer, struct ads1220_adc, + timer); + uint32_t rest_ticks = ads1220->rest_ticks; + if (ads1220->pending_flag) { + ads1220->sb.possible_overflows++; + rest_ticks *= 4; + } else if (ads1220_is_data_ready(ads1220)) { + ads1220->pending_flag = 1; + sched_wake_task(&wake_ads1220); + rest_ticks *= 8; + } + ads1220->timer.waketime += rest_ticks; + return SF_RESCHEDULE; +} + +// Add a measurement to the buffer +static void +add_sample(struct ads1220_adc *ads1220, uint8_t oid, uint_fast32_t counts) +{ + ads1220->sb.data[ads1220->sb.data_count] = counts; + ads1220->sb.data[ads1220->sb.data_count + 1] = counts >> 8; + ads1220->sb.data[ads1220->sb.data_count + 2] = counts >> 16; + ads1220->sb.data[ads1220->sb.data_count + 3] = counts >> 24; + ads1220->sb.data_count += BYTES_PER_SAMPLE; + + if ((ads1220->sb.data_count + BYTES_PER_SAMPLE) > + ARRAY_SIZE(ads1220->sb.data)) { + sensor_bulk_report(&ads1220->sb, oid); + } +} + +// ADS1220 ADC query +void +ads1220_read_adc(struct ads1220_adc *ads1220, uint8_t oid) +{ + uint8_t msg[3] = {0, 0, 0}; + spidev_transfer(ads1220->spi, 1, sizeof(msg), msg); + ads1220->pending_flag = 0; + barrier(); + + // create 24 bit int from bytes + uint32_t counts = ((uint32_t)msg[0] << 16) + | ((uint32_t)msg[1] << 8) + | ((uint32_t)msg[2]); + + // extend 2's complement 24 bits to 32bits + if (counts & 0x800000) + counts |= 0xFF000000; + + add_sample(ads1220, oid, counts); +} + +// Create an ads1220 sensor +void +command_config_ads1220(uint32_t *args) +{ + struct ads1220_adc *ads1220 = oid_alloc(args[0] + , command_config_ads1220, sizeof(*ads1220)); + ads1220->timer.func = ads1220_event; + ads1220->pending_flag = 0; + ads1220->spi = spidev_oid_lookup(args[1]); + ads1220->data_ready = gpio_in_setup(args[2], 0); +} +DECL_COMMAND(command_config_ads1220, "config_ads1220 oid=%c" + " spi_oid=%c data_ready_pin=%u"); + +// start/stop capturing ADC data +void +command_query_ads1220(uint32_t *args) +{ + uint8_t oid = args[0]; + struct ads1220_adc *ads1220 = oid_lookup(oid, command_config_ads1220); + sched_del_timer(&ads1220->timer); + ads1220->pending_flag = 0; + ads1220->rest_ticks = args[1]; + if (!ads1220->rest_ticks) { + // End measurements + return; + } + // Start new measurements + sensor_bulk_reset(&ads1220->sb); + irq_disable(); + ads1220->timer.waketime = timer_read_time() + ads1220->rest_ticks; + sched_add_timer(&ads1220->timer); + irq_enable(); +} +DECL_COMMAND(command_query_ads1220, "query_ads1220 oid=%c rest_ticks=%u"); + +void +command_query_ads1220_status(const uint32_t *args) +{ + uint8_t oid = args[0]; + struct ads1220_adc *ads1220 = oid_lookup(oid, command_config_ads1220); + irq_disable(); + const uint32_t start_t = timer_read_time(); + uint8_t is_data_ready = ads1220_is_data_ready(ads1220); + irq_enable(); + uint8_t pending_bytes = is_data_ready ? BYTES_PER_SAMPLE : 0; + sensor_bulk_status(&ads1220->sb, oid, start_t, 0, pending_bytes); +} +DECL_COMMAND(command_query_ads1220_status, "query_ads1220_status oid=%c"); + +// Background task that performs measurements +void +ads1220_capture_task(void) +{ + if (!sched_check_wake(&wake_ads1220)) + return; + uint8_t oid; + struct ads1220_adc *ads1220; + foreach_oid(oid, ads1220, command_config_ads1220) { + if (ads1220->pending_flag) + ads1220_read_adc(ads1220, oid); + } +} +DECL_TASK(ads1220_capture_task); diff --git a/src/sensor_bulk.h b/src/sensor_bulk.h index 9c130bea3..c750dbdae 100644 --- a/src/sensor_bulk.h +++ b/src/sensor_bulk.h @@ -4,7 +4,7 @@ struct sensor_bulk { uint16_t sequence, possible_overflows; uint8_t data_count; - uint8_t data[52]; + uint8_t data[51]; }; void sensor_bulk_reset(struct sensor_bulk *sb); diff --git a/src/sensor_hx71x.c b/src/sensor_hx71x.c new file mode 100644 index 000000000..f20d88072 --- /dev/null +++ b/src/sensor_hx71x.c @@ -0,0 +1,253 @@ +// Support for bit-banging commands to HX711 and HX717 ADC chips +// +// Copyright (C) 2024 Gareth Farrington +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include "autoconf.h" // CONFIG_MACH_AVR +#include "board/gpio.h" // gpio_out_write +#include "board/irq.h" // irq_poll +#include "board/misc.h" // timer_read_time +#include "basecmd.h" // oid_alloc +#include "command.h" // DECL_COMMAND +#include "sched.h" // sched_add_timer +#include "sensor_bulk.h" // sensor_bulk_report +#include +#include + +struct hx71x_adc { + struct timer timer; + uint8_t gain_channel; // the gain+channel selection (1-4) + uint8_t flags; + uint32_t rest_ticks; + uint32_t last_error; + struct gpio_in dout; // pin used to receive data from the hx71x + struct gpio_out sclk; // pin used to generate clock for the hx71x + struct sensor_bulk sb; +}; + +enum { + HX_PENDING = 1<<0, HX_OVERFLOW = 1<<1, +}; + +#define BYTES_PER_SAMPLE 4 +#define SAMPLE_ERROR_DESYNC 1L << 31 +#define SAMPLE_ERROR_READ_TOO_LONG 1L << 30 + +static struct task_wake wake_hx71x; + + +/**************************************************************** + * Low-level bit-banging + ****************************************************************/ + +#define MIN_PULSE_TIME nsecs_to_ticks(200) + +static uint32_t +nsecs_to_ticks(uint32_t ns) +{ + return timer_from_us(ns * 1000) / 1000000; +} + +// Pause for 200ns +static void +hx71x_delay_noirq(void) +{ + if (CONFIG_MACH_AVR) { + // Optimize avr, as calculating time takes longer than needed delay + asm("nop\n nop"); + return; + } + uint32_t end = timer_read_time() + MIN_PULSE_TIME; + while (timer_is_before(timer_read_time(), end)) + ; +} + +// Pause for a minimum of 200ns +static void +hx71x_delay(void) +{ + if (CONFIG_MACH_AVR) + // Optimize avr, as calculating time takes longer than needed delay + return; + uint32_t end = timer_read_time() + MIN_PULSE_TIME; + while (timer_is_before(timer_read_time(), end)) + irq_poll(); +} + +// Read 'num_bits' from the sensor +static uint32_t +hx71x_raw_read(struct gpio_in dout, struct gpio_out sclk, int num_bits) +{ + uint32_t bits_read = 0; + while (num_bits--) { + irq_disable(); + gpio_out_toggle_noirq(sclk); + hx71x_delay_noirq(); + gpio_out_toggle_noirq(sclk); + uint_fast8_t bit = gpio_in_read(dout); + irq_enable(); + hx71x_delay(); + bits_read = (bits_read << 1) | bit; + } + return bits_read; +} + + +/**************************************************************** + * HX711 and HX717 Sensor Support + ****************************************************************/ + +// Check if data is ready +static uint_fast8_t +hx71x_is_data_ready(struct hx71x_adc *hx71x) +{ + return !gpio_in_read(hx71x->dout); +} + +// Event handler that wakes wake_hx71x() periodically +static uint_fast8_t +hx71x_event(struct timer *timer) +{ + struct hx71x_adc *hx71x = container_of(timer, struct hx71x_adc, timer); + uint32_t rest_ticks = hx71x->rest_ticks; + uint8_t flags = hx71x->flags; + if (flags & HX_PENDING) { + hx71x->sb.possible_overflows++; + hx71x->flags = HX_PENDING | HX_OVERFLOW; + rest_ticks *= 4; + } else if (hx71x_is_data_ready(hx71x)) { + // New sample pending + hx71x->flags = HX_PENDING; + sched_wake_task(&wake_hx71x); + rest_ticks *= 8; + } + hx71x->timer.waketime += rest_ticks; + return SF_RESCHEDULE; +} + +static void +add_sample(struct hx71x_adc *hx71x, uint8_t oid, uint32_t counts, + uint8_t force_flush) { + // Add measurement to buffer + hx71x->sb.data[hx71x->sb.data_count] = counts; + hx71x->sb.data[hx71x->sb.data_count + 1] = counts >> 8; + hx71x->sb.data[hx71x->sb.data_count + 2] = counts >> 16; + hx71x->sb.data[hx71x->sb.data_count + 3] = counts >> 24; + hx71x->sb.data_count += BYTES_PER_SAMPLE; + + if (hx71x->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(hx71x->sb.data) + || force_flush) + sensor_bulk_report(&hx71x->sb, oid); +} + +// hx71x ADC query +static void +hx71x_read_adc(struct hx71x_adc *hx71x, uint8_t oid) +{ + // Read from sensor + uint_fast8_t gain_channel = hx71x->gain_channel; + uint32_t adc = hx71x_raw_read(hx71x->dout, hx71x->sclk, 24 + gain_channel); + + // Clear pending flag (and note if an overflow occurred) + irq_disable(); + uint8_t flags = hx71x->flags; + hx71x->flags = 0; + irq_enable(); + + // Extract report from raw data + uint32_t counts = adc >> gain_channel; + if (counts & 0x800000) + counts |= 0xFF000000; + + // Check for errors + uint_fast8_t extras_mask = (1 << gain_channel) - 1; + if ((adc & extras_mask) != extras_mask) { + // Transfer did not complete correctly + hx71x->last_error = SAMPLE_ERROR_DESYNC; + } else if (flags & HX_OVERFLOW) { + // Transfer took too long + hx71x->last_error = SAMPLE_ERROR_READ_TOO_LONG; + } + + // forever send errors until reset + if (hx71x->last_error != 0) { + counts = hx71x->last_error; + } + + // Add measurement to buffer + add_sample(hx71x, oid, counts, false); +} + +// Create a hx71x sensor +void +command_config_hx71x(uint32_t *args) +{ + struct hx71x_adc *hx71x = oid_alloc(args[0] + , command_config_hx71x, sizeof(*hx71x)); + hx71x->timer.func = hx71x_event; + uint8_t gain_channel = args[1]; + if (gain_channel < 1 || gain_channel > 4) { + shutdown("HX71x gain/channel out of range 1-4"); + } + hx71x->gain_channel = gain_channel; + hx71x->dout = gpio_in_setup(args[2], 1); + hx71x->sclk = gpio_out_setup(args[3], 0); + gpio_out_write(hx71x->sclk, 1); // put chip in power down state +} +DECL_COMMAND(command_config_hx71x, "config_hx71x oid=%c gain_channel=%c" + " dout_pin=%u sclk_pin=%u"); + +// start/stop capturing ADC data +void +command_query_hx71x(uint32_t *args) +{ + uint8_t oid = args[0]; + struct hx71x_adc *hx71x = oid_lookup(oid, command_config_hx71x); + sched_del_timer(&hx71x->timer); + hx71x->flags = 0; + hx71x->last_error = 0; + hx71x->rest_ticks = args[1]; + if (!hx71x->rest_ticks) { + // End measurements + gpio_out_write(hx71x->sclk, 1); // put chip in power down state + return; + } + // Start new measurements + gpio_out_write(hx71x->sclk, 0); // wake chip from power down + sensor_bulk_reset(&hx71x->sb); + irq_disable(); + hx71x->timer.waketime = timer_read_time() + hx71x->rest_ticks; + sched_add_timer(&hx71x->timer); + irq_enable(); +} +DECL_COMMAND(command_query_hx71x, "query_hx71x oid=%c rest_ticks=%u"); + +void +command_query_hx71x_status(const uint32_t *args) +{ + uint8_t oid = args[0]; + struct hx71x_adc *hx71x = oid_lookup(oid, command_config_hx71x); + irq_disable(); + const uint32_t start_t = timer_read_time(); + uint8_t is_data_ready = hx71x_is_data_ready(hx71x); + irq_enable(); + uint8_t pending_bytes = is_data_ready ? BYTES_PER_SAMPLE : 0; + sensor_bulk_status(&hx71x->sb, oid, start_t, 0, pending_bytes); +} +DECL_COMMAND(command_query_hx71x_status, "query_hx71x_status oid=%c"); + +// Background task that performs measurements +void +hx71x_capture_task(void) +{ + if (!sched_check_wake(&wake_hx71x)) + return; + uint8_t oid; + struct hx71x_adc *hx71x; + foreach_oid(oid, hx71x, command_config_hx71x) { + if (hx71x->flags) + hx71x_read_adc(hx71x, oid); + } +} +DECL_TASK(hx71x_capture_task); diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 9258ce6dc..45e8b84e2 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -17,7 +17,7 @@ #include "trsync.h" // trsync_do_trigger enum { - LDC_PENDING = 1<<0, + LDC_PENDING = 1<<0, LDC_HAVE_INTB = 1<<1, LH_AWAIT_HOMING = 1<<1, LH_CAN_TRIGGER = 1<<2 }; @@ -27,16 +27,24 @@ struct ldc1612 { struct i2cdev_s *i2c; uint8_t flags; struct sensor_bulk sb; + struct gpio_in intb_pin; // homing struct trsync *ts; uint8_t homing_flags; - uint8_t trigger_reason; + uint8_t trigger_reason, error_reason; uint32_t trigger_threshold; uint32_t homing_clock; }; static struct task_wake ldc1612_wake; +// Check if the intb line is "asserted" +static int +check_intb_asserted(struct ldc1612 *ld) +{ + return !gpio_in_read(ld->intb_pin); +} + // Query ldc1612 data static uint_fast8_t ldc1612_event(struct timer *timer) @@ -44,8 +52,10 @@ ldc1612_event(struct timer *timer) struct ldc1612 *ld = container_of(timer, struct ldc1612, timer); if (ld->flags & LDC_PENDING) ld->sb.possible_overflows++; - ld->flags |= LDC_PENDING; - sched_wake_task(&ldc1612_wake); + if (!(ld->flags & LDC_HAVE_INTB) || check_intb_asserted(ld)) { + ld->flags |= LDC_PENDING; + sched_wake_task(&ldc1612_wake); + } ld->timer.waketime += ld->rest_ticks; return SF_RESCHEDULE; } @@ -60,6 +70,17 @@ command_config_ldc1612(uint32_t *args) } DECL_COMMAND(command_config_ldc1612, "config_ldc1612 oid=%c i2c_oid=%c"); +void +command_config_ldc1612_with_intb(uint32_t *args) +{ + command_config_ldc1612(args); + struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); + ld->intb_pin = gpio_in_setup(args[2], 1); + ld->flags = LDC_HAVE_INTB; +} +DECL_COMMAND(command_config_ldc1612_with_intb, + "config_ldc1612_with_intb oid=%c i2c_oid=%c intb_pin=%c"); + void command_ldc1612_setup_home(uint32_t *args) { @@ -74,11 +95,12 @@ command_ldc1612_setup_home(uint32_t *args) ld->homing_clock = args[1]; ld->ts = trsync_oid_lookup(args[3]); ld->trigger_reason = args[4]; + ld->error_reason = args[5]; ld->homing_flags = LH_AWAIT_HOMING | LH_CAN_TRIGGER; } DECL_COMMAND(command_ldc1612_setup_home, "ldc1612_setup_home oid=%c clock=%u threshold=%u" - " trsync_oid=%c trigger_reason=%c"); + " trsync_oid=%c trigger_reason=%c error_reason=%c"); void command_query_ldc1612_home_state(uint32_t *args) @@ -90,6 +112,32 @@ command_query_ldc1612_home_state(uint32_t *args) DECL_COMMAND(command_query_ldc1612_home_state, "query_ldc1612_home_state oid=%c"); +// Check if a sample should trigger a homing event +static void +check_home(struct ldc1612 *ld, uint32_t data) +{ + uint8_t homing_flags = ld->homing_flags; + if (!(homing_flags & LH_CAN_TRIGGER)) + return; + if (data > 0x0fffffff) { + // Sensor reports an issue - cancel homing + ld->homing_flags = 0; + trsync_do_trigger(ld->ts, ld->error_reason); + return; + } + uint32_t time = timer_read_time(); + if ((homing_flags & LH_AWAIT_HOMING) + && timer_is_before(time, ld->homing_clock)) + return; + homing_flags &= ~LH_AWAIT_HOMING; + if (data > ld->trigger_threshold) { + homing_flags = 0; + ld->homing_clock = time; + trsync_do_trigger(ld->ts, ld->trigger_reason); + } + ld->homing_flags = homing_flags; +} + // Chip registers #define REG_DATA0_MSB 0x00 #define REG_DATA0_LSB 0x01 @@ -117,14 +165,12 @@ read_reg_status(struct ldc1612 *ld) static void ldc1612_query(struct ldc1612 *ld, uint8_t oid) { - // Clear pending flag + // Check if data available (and clear INTB line) + uint16_t status = read_reg_status(ld); irq_disable(); ld->flags &= ~LDC_PENDING; irq_enable(); - - // Check if data available - uint16_t status = read_reg_status(ld); - if (status != 0x48) + if (!(status & 0x08)) return; // Read coil0 frequency @@ -134,21 +180,11 @@ ldc1612_query(struct ldc1612 *ld, uint8_t oid) ld->sb.data_count += BYTES_PER_SAMPLE; // Check for endstop trigger - uint8_t homing_flags = ld->homing_flags; - if (homing_flags & LH_CAN_TRIGGER) { - uint32_t time = timer_read_time(); - if (!(homing_flags & LH_AWAIT_HOMING) - || !timer_is_before(time, ld->homing_clock)) { - homing_flags &= ~LH_AWAIT_HOMING; - uint32_t data = (d[0] << 24L) | (d[1] << 16L) | (d[2] << 8) | d[3]; - if (data > ld->trigger_threshold) { - homing_flags = 0; - ld->homing_clock = time; - trsync_do_trigger(ld->ts, ld->trigger_reason); - } - ld->homing_flags = homing_flags; - } - } + uint32_t data = ((uint32_t)d[0] << 24) + | ((uint32_t)d[1] << 16) + | ((uint32_t)d[2] << 8) + | ((uint32_t)d[3]); + check_home(ld, data); // Flush local buffer if needed if (ld->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(ld->sb.data)) @@ -161,7 +197,7 @@ command_query_ldc1612(uint32_t *args) struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); sched_del_timer(&ld->timer); - ld->flags = 0; + ld->flags &= ~LDC_PENDING; if (!args[1]) // End measurements return; @@ -177,18 +213,29 @@ command_query_ldc1612(uint32_t *args) DECL_COMMAND(command_query_ldc1612, "query_ldc1612 oid=%c rest_ticks=%u"); void -command_query_ldc1612_status(uint32_t *args) +command_query_status_ldc1612(uint32_t *args) { struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); + if (ld->flags & LDC_HAVE_INTB) { + // Check if a sample is pending in the chip via the intb line + irq_disable(); + uint32_t time = timer_read_time(); + int p = check_intb_asserted(ld); + irq_enable(); + sensor_bulk_status(&ld->sb, args[0], time, 0, p ? BYTES_PER_SAMPLE : 0); + return; + } + + // Query sensor to see if a sample is pending uint32_t time1 = timer_read_time(); uint16_t status = read_reg_status(ld); uint32_t time2 = timer_read_time(); - uint32_t fifo = status == 0x48 ? BYTES_PER_SAMPLE : 0; + uint32_t fifo = status & 0x08 ? BYTES_PER_SAMPLE : 0; sensor_bulk_status(&ld->sb, args[0], time1, time2-time1, fifo); } -DECL_COMMAND(command_query_ldc1612_status, "query_ldc1612_status oid=%c"); +DECL_COMMAND(command_query_status_ldc1612, "query_status_ldc1612 oid=%c"); void ldc1612_task(void) diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index d14622a25..037e37bbe 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -88,6 +88,9 @@ choice config MACH_STM32G431 bool "STM32G431" select MACH_STM32G4 + config MACH_STM32G474 + bool "STM32G474" + select MACH_STM32G4 config MACH_STM32H723 bool "STM32H723" select MACH_STM32H7 @@ -181,6 +184,7 @@ config MCU default "stm32g0b0xx" if MACH_STM32G0B0 default "stm32g0b1xx" if MACH_STM32G0B1 default "stm32g431xx" if MACH_STM32G431 + default "stm32g474xx" if MACH_STM32G474 default "stm32h723xx" if MACH_STM32H723 default "stm32h743xx" if MACH_STM32H743 default "stm32h750xx" if MACH_STM32H750 @@ -199,6 +203,7 @@ config CLOCK_FREQ default 216000000 if MACH_STM32F765 default 64000000 if MACH_STM32G0 default 150000000 if MACH_STM32G431 + default 170000000 if MACH_STM32G474 default 400000000 if MACH_STM32H7 # 400Mhz is max Klipper currently supports default 80000000 if MACH_STM32L412 default 64000000 if MACH_N32G45x && STM32_CLOCK_REF_INTERNAL @@ -206,13 +211,13 @@ config CLOCK_FREQ config FLASH_SIZE hex - default 0x4000 if MACH_STM32F031 - default 0x8000 if MACH_STM32F042 + default 0x8000 if MACH_STM32F031 || MACH_STM32F042 default 0x20000 if MACH_STM32F070 || MACH_STM32F072 default 0x10000 if MACH_STM32F103 || MACH_STM32L412 # Flash size of stm32f103x8 (64KiB) default 0x40000 if MACH_STM32F2 || MACH_STM32F401 || MACH_STM32H723 default 0x80000 if MACH_STM32F4x5 || MACH_STM32F446 default 0x20000 if MACH_STM32G0 || MACH_STM32G431 + default 0x40000 if MACH_STM32G474 default 0x20000 if MACH_STM32H750 default 0x200000 if MACH_STM32H743 || MACH_STM32F765 default 0x20000 if MACH_N32G45x @@ -233,6 +238,7 @@ config RAM_SIZE default 0x2800 if MACH_STM32F103x6 default 0x5000 if MACH_STM32F103 && !MACH_STM32F103x6 # Ram size of stm32f103x8 default 0x8000 if MACH_STM32G431 + default 0x20000 if MACH_STM32G474 default 0xa000 if MACH_STM32L412 default 0x20000 if MACH_STM32F207 default 0x10000 if MACH_STM32F401 diff --git a/src/stm32/Makefile b/src/stm32/Makefile index 18af2e9d7..5f4d3af5c 100644 --- a/src/stm32/Makefile +++ b/src/stm32/Makefile @@ -31,7 +31,7 @@ CFLAGS-$(CONFIG_MACH_STM32H7) += -mcpu=cortex-m7 -Ilib/stm32h7/include CFLAGS-$(CONFIG_MACH_STM32L4) += -mcpu=cortex-m4 -Ilib/stm32l4/include CFLAGS += $(CFLAGS-y) -D$(MCU_UPPER) -mthumb -Ilib/cmsis-core -Ilib/fast-hash -CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld $(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld diff --git a/src/stm32/fdcan.c b/src/stm32/fdcan.c index b0e8c01d1..5344d26b1 100644 --- a/src/stm32/fdcan.c +++ b/src/stm32/fdcan.c @@ -60,16 +60,23 @@ || CONFIG_STM32_CANBUS_PB5_PB6 ||CONFIG_STM32_CANBUS_PB12_PB13) #define SOC_CAN FDCAN1 #define MSG_RAM (((struct fdcan_ram_layout*)SRAMCAN_BASE)->fdcan1) + #if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4 + #define CAN_IT0_IRQn FDCAN1_IT0_IRQn + #endif #else #define SOC_CAN FDCAN2 #define MSG_RAM (((struct fdcan_ram_layout*)SRAMCAN_BASE)->fdcan2) + #if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4 + #define CAN_IT0_IRQn FDCAN2_IT0_IRQn + #endif #endif #if CONFIG_MACH_STM32G0 #define CAN_IT0_IRQn TIM16_FDCAN_IT0_IRQn #define CAN_FUNCTION GPIO_FUNCTION(3) // Alternative function mapping number -#elif CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4 - #define CAN_IT0_IRQn FDCAN1_IT0_IRQn +#endif + +#if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4 #define CAN_FUNCTION GPIO_FUNCTION(9) // Alternative function mapping number #endif diff --git a/src/stm32/stm32f0_i2c.c b/src/stm32/stm32f0_i2c.c index 1441079f7..597b48460 100644 --- a/src/stm32/stm32f0_i2c.c +++ b/src/stm32/stm32f0_i2c.c @@ -22,6 +22,8 @@ struct i2c_info { DECL_CONSTANT_STR("BUS_PINS_i2c1_PF1_PF0", "PF1,PF0"); DECL_ENUMERATION("i2c_bus", "i2c1_PB8_PB9", 2); DECL_CONSTANT_STR("BUS_PINS_i2c1_PB8_PB9", "PB8,PB9"); + DECL_ENUMERATION("i2c_bus", "i2c1_PB8_PB7", 3); + DECL_CONSTANT_STR("BUS_PINS_i2c1_PB8_PB7", "PB8,PB7"); // Deprecated "i2c1a" style mappings DECL_ENUMERATION("i2c_bus", "i2c1", 0); DECL_CONSTANT_STR("BUS_PINS_i2c1", "PB6,PB7"); @@ -93,6 +95,7 @@ static const struct i2c_info i2c_bus[] = { { I2C1, GPIO('B', 6), GPIO('B', 7), GPIO_FUNCTION(1) }, { I2C1, GPIO('F', 1), GPIO('F', 0), GPIO_FUNCTION(1) }, { I2C1, GPIO('B', 8), GPIO('B', 9), GPIO_FUNCTION(1) }, + { I2C1, GPIO('B', 8), GPIO('B', 7), GPIO_FUNCTION(1) }, #elif CONFIG_MACH_STM32F7 { I2C1, GPIO('B', 6), GPIO('B', 7), GPIO_FUNCTION(1) }, #elif CONFIG_MACH_STM32G0 diff --git a/src/stm32/stm32g4.c b/src/stm32/stm32g4.c index 139ea8eaa..5255cb19a 100644 --- a/src/stm32/stm32g4.c +++ b/src/stm32/stm32g4.c @@ -22,6 +22,12 @@ lookup_clock_line(uint32_t periph_base) if (periph_base < APB2PERIPH_BASE) { uint32_t pos = (periph_base - APB1PERIPH_BASE) / 0x400; if (pos < 32) { +#if defined(FDCAN2_BASE) + if (periph_base == FDCAN2_BASE) + return (struct cline){.en = &RCC->APB1ENR1, + .rst = &RCC->APB1RSTR1, + .bit = 1 << 25}; +#endif return (struct cline){.en = &RCC->APB1ENR1, .rst = &RCC->APB1RSTR1, .bit = 1 << pos}; @@ -98,7 +104,7 @@ enable_clock_stm32g4(void) RCC->CR |= RCC_CR_PLLON; // Enable 48Mhz USB clock using clock recovery - if (CONFIG_USBSERIAL) { + if (CONFIG_USB) { RCC->CRRCR |= RCC_CRRCR_HSI48ON; while (!(RCC->CRRCR & RCC_CRRCR_HSI48RDY)) ; diff --git a/src/stm32/stm32h7_adc.c b/src/stm32/stm32h7_adc.c index e9dc8f845..6740edd3e 100644 --- a/src/stm32/stm32h7_adc.c +++ b/src/stm32/stm32h7_adc.c @@ -12,6 +12,8 @@ #include "internal.h" // GPIO #include "sched.h" // sched_shutdown +#define ADC_INVALID_PIN 0xFF + #define ADC_TEMPERATURE_PIN 0xfe DECL_ENUMERATION("pin", "ADC_TEMPERATURE", ADC_TEMPERATURE_PIN); @@ -24,8 +26,8 @@ DECL_CONSTANT("ADC_MAX", 4095); static const uint8_t adc_pins[] = { #if CONFIG_MACH_STM32H7 // ADC1 - 0, // PA0_C ADC12_INP0 - 0, // PA1_C ADC12_INP1 + ADC_INVALID_PIN, // PA0_C ADC12_INP0 + ADC_INVALID_PIN, // PA1_C ADC12_INP1 GPIO('F', 11), // ADC1_INP2 GPIO('A', 6), // ADC12_INP3 GPIO('C', 4), // ADC12_INP4 @@ -45,8 +47,8 @@ static const uint8_t adc_pins[] = { GPIO('A', 4), // ADC12_INP18 GPIO('A', 5), // ADC12_INP19 // ADC2 - 0, // PA0_C ADC12_INP0 - 0, // PA1_C ADC12_INP1 + ADC_INVALID_PIN, // PA0_C ADC12_INP0 + ADC_INVALID_PIN, // PA1_C ADC12_INP1 GPIO('F', 13), // ADC2_INP2 GPIO('A', 6), // ADC12_INP3 GPIO('C', 4), // ADC12_INP4 @@ -61,13 +63,13 @@ static const uint8_t adc_pins[] = { GPIO('C', 3), // ADC12_INP13 GPIO('A', 2), // ADC12_INP14 GPIO('A', 3), // ADC12_INP15 - 0, // dac_out1 - 0, // dac_out2 + ADC_INVALID_PIN, // dac_out1 + ADC_INVALID_PIN, // dac_out2 GPIO('A', 4), // ADC12_INP18 GPIO('A', 5), // ADC12_INP19 // ADC3 - 0, // PC2_C ADC3_INP0 - 0, // PC3_C ADC3_INP1 + ADC_INVALID_PIN, // PC2_C ADC3_INP0 + ADC_INVALID_PIN, // PC3_C ADC3_INP1 GPIO('F', 9) , // ADC3_INP2 GPIO('F', 7), // ADC3_INP3 GPIO('F', 5), // ADC3_INP4 @@ -85,14 +87,14 @@ static const uint8_t adc_pins[] = { GPIO('H', 5), // ADC3_INP16 #if CONFIG_MACH_STM32H723 ADC_TEMPERATURE_PIN, - 0, + ADC_INVALID_PIN, #else - 0, // Vbat/4 + ADC_INVALID_PIN, // Vbat/4 ADC_TEMPERATURE_PIN,// VSENSE #endif - 0, // VREFINT + ADC_INVALID_PIN, // VREFINT #elif CONFIG_MACH_STM32G4 - 0, // [0] vssa + ADC_INVALID_PIN, // [0] vssa GPIO('A', 0), // [1] GPIO('A', 1), // [2] GPIO('A', 2), // [3] @@ -105,14 +107,14 @@ static const uint8_t adc_pins[] = { GPIO('F', 0), // [10] GPIO('B', 12), // [11] GPIO('B', 1), // [12] - 0, // [13] opamp + ADC_INVALID_PIN, // [13] opamp GPIO('B', 11), // [14] GPIO('B', 0), // [15] ADC_TEMPERATURE_PIN, // [16] vtemp - 0, // [17] vbat/3 - 0, // [18] vref - 0, - 0, // [0] vssa ADC 2 + ADC_INVALID_PIN, // [17] vbat/3 + ADC_INVALID_PIN, // [18] vref + ADC_INVALID_PIN, + ADC_INVALID_PIN, // [0] vssa ADC 2 GPIO('A', 0), // [1] GPIO('A', 1), // [2] GPIO('A', 6), // [3] @@ -128,11 +130,11 @@ static const uint8_t adc_pins[] = { GPIO('A', 5), // [13] GPIO('B', 11), // [14] GPIO('B', 15), // [15] - 0, // [16] opamp + ADC_INVALID_PIN, // [16] opamp GPIO('A', 4), // [17] - 0, // [18] opamp + ADC_INVALID_PIN, // [18] opamp #else // stm32l4 - 0, // vref + ADC_INVALID_PIN, // vref GPIO('C', 0), // ADC12_IN1 .. 16 GPIO('C', 1), GPIO('C', 2), @@ -150,7 +152,7 @@ static const uint8_t adc_pins[] = { GPIO('B', 0), GPIO('B', 1), ADC_TEMPERATURE_PIN, // temp - 0, // vbat + ADC_INVALID_PIN, // vbat #endif }; @@ -189,7 +191,11 @@ gpio_adc_setup(uint32_t pin) if (chan >= 2 * ADCIN_BANK_SIZE) { chan -= 2 * ADCIN_BANK_SIZE; adc = ADC3; +#if CONFIG_MACH_STM32G4 + adc_common = ADC345_COMMON; +#else adc_common = ADC3_COMMON; +#endif } else #endif #ifdef ADC2 diff --git a/src/stm32/stm32l4.c b/src/stm32/stm32l4.c index 7db15fff0..ae099d6bc 100644 --- a/src/stm32/stm32l4.c +++ b/src/stm32/stm32l4.c @@ -96,7 +96,7 @@ enable_clock_stm32l4(void) RCC->CR |= RCC_CR_PLLON; // Enable 48Mhz USB clock using clock recovery - if (CONFIG_USBSERIAL) { + if (CONFIG_USB) { RCC->CRRCR |= RCC_CRRCR_HSI48ON; while (!(RCC->CRRCR & RCC_CRRCR_HSI48RDY)) ; diff --git a/test/configs/ar100.config b/test/configs/ar100.config index 6c9174824..a1335176f 100644 --- a/test/configs/ar100.config +++ b/test/configs/ar100.config @@ -4,3 +4,5 @@ CONFIG_WANT_DISPLAYS=n CONFIG_WANT_SOFTWARE_I2C=n CONFIG_WANT_SOFTWARE_SPI=n CONFIG_WANT_LIS2DW=n +CONFIG_WANT_HX71X=n +CONFIG_WANT_ADS1220=n diff --git a/test/configs/stm32f031.config b/test/configs/stm32f031.config index aa9c282be..a8c95cfe7 100644 --- a/test/configs/stm32f031.config +++ b/test/configs/stm32f031.config @@ -1,5 +1,9 @@ -# Base config file for STM32F031 boards +# Base config file for STM32F031x4 (16KB) boards with serial on PA14/PA15 CONFIG_MACH_STM32=y CONFIG_MACH_STM32F031=y -CONFIG_WANT_GPIO_BITBANGING=n +CONFIG_LOW_LEVEL_OPTIONS=y +CONFIG_STM32_SERIAL_USART2_ALT_PA15_PA14=y +CONFIG_STM32_CLOCK_REF_INTERNAL=y +CONFIG_STM32_FLASH_START_0000=y CONFIG_WANT_DISPLAYS=n +CONFIG_WANT_GPIO_BITBANGING=n diff --git a/test/configs/stm32f042.config b/test/configs/stm32f042.config index 7f1e879fb..53cf1281e 100644 --- a/test/configs/stm32f042.config +++ b/test/configs/stm32f042.config @@ -3,3 +3,6 @@ CONFIG_MACH_STM32=y CONFIG_MACH_STM32F042=y CONFIG_WANT_SOFTWARE_I2C=n CONFIG_WANT_LIS2DW=n +CONFIG_WANT_LDC1612=n +CONFIG_WANT_HX71X=n +CONFIG_WANT_ADS1220=n diff --git a/test/configs/stm32g474.config b/test/configs/stm32g474.config new file mode 100644 index 000000000..da38a9179 --- /dev/null +++ b/test/configs/stm32g474.config @@ -0,0 +1,3 @@ +# Base config file for STM32G474 ARM processor +CONFIG_MACH_STM32=y +CONFIG_MACH_STM32G474=y diff --git a/test/klippy/commands.test b/test/klippy/commands.test index 50e71ab3c..33c599614 100644 --- a/test/klippy/commands.test +++ b/test/klippy/commands.test @@ -40,7 +40,7 @@ SET_VELOCITY_LIMIT ACCEL=100 VELOCITY=20 SQUARE_CORNER_VELOCITY=1 ACCEL_TO_DECEL M204 S500 SET_PRESSURE_ADVANCE EXTRUDER=extruder ADVANCE=.001 -SET_PRESSURE_ADVANCE ADVANCE=.002 ADVANCE_LOOKAHEAD_TIME=.001 +SET_PRESSURE_ADVANCE ADVANCE=.002 SMOOTH_TIME=.001 # Restart command (must be last in test) RESTART diff --git a/test/klippy/dual_carriage.cfg b/test/klippy/dual_carriage.cfg index 9ae01c2bc..93c574440 100644 --- a/test/klippy/dual_carriage.cfg +++ b/test/klippy/dual_carriage.cfg @@ -61,7 +61,7 @@ pid_Kd: 114 min_temp: 0 max_temp: 250 -[gcode_macro PARK_extruder0] +[gcode_macro PARK_extruder] gcode: G90 G1 X0 diff --git a/test/klippy/dual_carriage.test b/test/klippy/dual_carriage.test index 5b2f9e65d..ed40c236e 100644 --- a/test/klippy/dual_carriage.test +++ b/test/klippy/dual_carriage.test @@ -17,6 +17,18 @@ G1 X190 F6000 SET_DUAL_CARRIAGE CARRIAGE=0 G1 X20 F6000 +# Save dual carriage state +SAVE_DUAL_CARRIAGE_STATE + +G1 X50 F6000 + +# Go back to alternate carriage +SET_DUAL_CARRIAGE CARRIAGE=1 +G1 X170 F6000 + +# Restore dual carriage state +RESTORE_DUAL_CARRIAGE_STATE + # Test changing extruders G1 X5 T1 diff --git a/test/klippy/pressure_advance.cfg b/test/klippy/pressure_advance.cfg new file mode 100644 index 000000000..d7123d08e --- /dev/null +++ b/test/klippy/pressure_advance.cfg @@ -0,0 +1,68 @@ +# Config for extruder testing +[stepper_x] +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PE5 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PJ1 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: ^PD3 +position_endstop: 0.5 +position_max: 200 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.500 +filament_diameter: 3.500 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 210 + +[extruder_stepper my_extra_stepper] +extruder: extruder +step_pin: PH5 +dir_pin: PH6 +enable_pin: !PB5 +microsteps: 16 +rotation_distance: 28.2 + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 diff --git a/test/klippy/pressure_advance.test b/test/klippy/pressure_advance.test new file mode 100644 index 000000000..c1ef052ad --- /dev/null +++ b/test/klippy/pressure_advance.test @@ -0,0 +1,38 @@ +# Extruder tests +DICTIONARY atmega2560.dict +CONFIG pressure_advance.cfg + +SET_PRESSURE_ADVANCE ADVANCE=0.1 +# Home and extrusion moves +G28 +G1 X20 Y20 Z1 F6000 +G1 E7 +G1 X25 Y25 E7.5 + +# Update pressure advance for my_extra_stepper +SET_PRESSURE_ADVANCE EXTRUDER=my_extra_stepper ADVANCE=0.02 +G1 X30 Y30 E8.0 + +# Unsync my_extra_stepper from extruder +SYNC_EXTRUDER_MOTION EXTRUDER=my_extra_stepper MOTION_QUEUE= + +# Update pressure advance for primary extruder +SET_PRESSURE_ADVANCE ADVANCE=0.01 +G1 X35 Y35 E8.5 + +# Update pressure advance both extruders +SET_PRESSURE_ADVANCE EXTRUDER=my_extra_stepper ADVANCE=0.05 +SET_PRESSURE_ADVANCE ADVANCE=0.05 +# Sync my_extra_stepper to extruder +SYNC_EXTRUDER_MOTION EXTRUDER=my_extra_stepper MOTION_QUEUE=extruder +G1 X40 Y40 E9.0 + +# Update smooth_time +SET_PRESSURE_ADVANCE SMOOTH_TIME=0.02 +SET_PRESSURE_ADVANCE EXTRUDER=my_extra_stepper SMOOTH_TIME=0.02 +G1 X45 Y45 E9.5 + +# Updating both smooth_time and pressure advance +SET_PRESSURE_ADVANCE SMOOTH_TIME=0.03 ADVANCE=0.1 +SET_PRESSURE_ADVANCE EXTRUDER=my_extra_stepper SMOOTH_TIME=0.03 ADVANCE=0.1 +G1 X50 Y50 E10.0 diff --git a/test/klippy/printers.test b/test/klippy/printers.test index e404f6f15..9f7ab3c20 100644 --- a/test/klippy/printers.test +++ b/test/klippy/printers.test @@ -204,6 +204,7 @@ CONFIG ../../config/printer-voxelab-aquila-2021.cfg # Printers using the stm32f401 DICTIONARY stm32f401.dict CONFIG ../../config/generic-fysetc-cheetah-v2.0.cfg +CONFIG ../../config/printer-artillery-genius-pro-2022.cfg CONFIG ../../config/printer-artillery-sidewinder-x2-2022.cfg CONFIG ../../config/printer-artillery-sidewinder-x3-plus-2024.cfg CONFIG ../../config/printer-creality-ender5-s1-2023.cfg @@ -246,6 +247,7 @@ CONFIG ../../config/generic-fysetc-spider.cfg CONFIG ../../config/generic-ldo-leviathan-v1.2.cfg CONFIG ../../config/generic-mks-rumba32-v1.0.cfg CONFIG ../../config/printer-ratrig-v-minion-2021.cfg +CONFIG ../../config/printer-tronxy-crux1-2022.cfg # Printers using the stm32h723 DICTIONARY stm32h723.dict