diff --git a/config/generic-bigtreetech-skr-mini-e3-v3.0.cfg b/config/generic-bigtreetech-skr-mini-e3-v3.0.cfg index 2ea064b09416..b6a98bb0224b 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/generic-mellow-fly-e3-v2.cfg b/config/generic-mellow-fly-e3-v2.cfg new file mode 100644 index 000000000000..6836ea328a0a --- /dev/null +++ b/config/generic-mellow-fly-e3-v2.cfg @@ -0,0 +1,232 @@ +# This file contains common pin mappings for the Mellow Fly-E3-v2. +# To use this config, the firmware should be compiled for the +# STM32F407 with a "32KiB bootloader". + +# The "make flash" command does not work on the Fly-E3-v2. Instead, +# after running "make", copy the generated "out/klipper.bin" file to a +# file named "firmware.bin" or "klipper.bin" on an SD card and then restart the Fly-E3-v2 +# with that SD card. + +# See docs/Config_Reference.md for a description of parameters. + +[mcu] +serial: /dev/serial/by-id/usb-Klipper_stm32f407xx_27004A001851323333353137-if00 + +[stepper_x] +step_pin: PE5 +dir_pin: PC0 +enable_pin: !PC1 +microsteps: 16 +rotation_distance: 30 +full_steps_per_rotation: 200 +endstop_pin: PE7 #X-STOP +position_endstop: 0 +position_max: 200 +homing_speed: 50 +second_homing_speed: 10 +homing_retract_dist: 5.0 +homing_positive_dir: false +step_pulse_duration: 0.000004 + +[stepper_y] +step_pin: PE4 +dir_pin: !PC13 +enable_pin: !PC14 +microsteps: 16 +rotation_distance: 30 +full_steps_per_rotation: 200 +endstop_pin: PE8 #Y-STOP +position_endstop: 0 +position_max: 200 +homing_speed: 50 +second_homing_speed: 10 +homing_retract_dist: 5.0 +homing_positive_dir: false +step_pulse_duration: 0.000004 + +[stepper_z] +step_pin: PE1 +dir_pin: !PB7 +enable_pin: !PE3 +microsteps: 16 +rotation_distance: 30 +full_steps_per_rotation: 200 +endstop_pin: PE9 #Z-STOP +position_min: 0 +position_endstop: 0 +position_max: 200 +homing_speed: 5 +second_homing_speed: 3 +homing_retract_dist: 5.0 +homing_positive_dir: false +step_pulse_duration: 0.000004 + +[extruder] +step_pin: PE2 +dir_pin: PD5 +enable_pin: !PD6 +microsteps: 16 +rotation_distance: 33.500 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PC6 #E0 + +######################################## +# Extruder 100K thermistor configuration +######################################## +sensor_type: ATC Semitec 104GT-2 +sensor_pin: PC4 #T0 TEMP +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 275 +######################################## +# Extruder MAX31865 PT100 2 wire config +######################################## +# sensor_type: MAX31865 +# sensor_pin: PD15 #PT-100 +# spi_speed: 4000000 +# spi_software_sclk_pin: PD12 +# spi_software_mosi_pin: PD11 +# spi_software_miso_pin: PD13 +# rtd_nominal_r: 100 +# rtd_reference_r: 430 +# rtd_num_of_wires: 2 +# rtd_use_50Hz_filter: True +min_temp: 0 +max_temp: 300 + +#[extruder1] +#step_pin: PE0 +#dir_pin: PD1 +#enable_pin: !PD3 +#microsteps: 16 +#heater_pin: PC7 #E1 +#sensor_pin: PC5 #T1 TEMP + +######################################## +# TMC2209 configuration +######################################## + +[tmc2209 stepper_x] +uart_pin: PC15 +interpolate: False +run_current: 0.3 +sense_resistor: 0.110 +stealthchop_threshold: 999999 + +[tmc2209 stepper_y] +uart_pin: PB6 +interpolate: False +run_current: 0.3 +sense_resistor: 0.110 +stealthchop_threshold: 999999 + +[tmc2209 stepper_z] +uart_pin: PD7 +interpolate: False +run_current: 0.4 +sense_resistor: 0.110 +stealthchop_threshold: 999999 + +[tmc2209 extruder] +uart_pin: PD4 +interpolate: False +run_current: 0.27 +sense_resistor: 0.075 +stealthchop_threshold: 999999 + +#[tmc2209 extruder1] +#uart_pin: PD0 +#interpolate: False +#run_current: 0.27 +#sense_resistor: 0.075 +#stealthchop_threshold: 999999 + + +####################################### +# Heated Bed +####################################### + +[heater_bed] +heater_pin: PB0 #BED +sensor_type: Generic 3950 +sensor_pin: PB1 #B-TEMP +max_power: 1.0 +min_temp: 0 +max_temp: 120 +control: pid +pid_kp: 58.437 +pid_ki: 2.347 +pid_kd: 363.769 + +####################################### +# LIGHTING +####################################### + +#[led Toolhead] +#white_pin: PA2 #FAN2 +#cycle_time: 0.010 +#initial_white: 0 + +####################################### +# COOLING +####################################### + +[heater_fan hotend_fan] +pin: PA1 #FAN1 +max_power: 1.0 +kick_start_time: 0.5 +heater: extruder +heater_temp: 50 +fan_speed: 1.0 + +[controller_fan controller_fan] +pin: PA0 #FAN0 +max_power: 1.0 +kick_start_time: 0.5 +heater: extruder +stepper: stepper_x, stepper_y, stepper_z +fan_speed: 1.0 +idle_timeout: 60 + +[fan] +pin: PA3 #FAN3 +max_power: 1.0 +off_below: 0.2 + +[temperature_sensor Mellow_Fly_E3_V2] +sensor_type: temperature_mcu +min_temp: 5 +max_temp: 80 + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 50 +max_z_accel: 100 + +######################################## +# EXP1 / EXP2 (display) pins +######################################## +[board_pins] +aliases: + EXP1_1=PD10, EXP1_3=PA8, EXP1_5=PE15, EXP1_7=PA14, EXP1_9=, + EXP1_2=PA9, EXP1_4=PA10, EXP1_6=PE14, EXP1_8=PA13, EXP1_10=<5V>, + # EXP2 header + EXP2_1=PA6, EXP2_3=PB11, EXP2_5=PB10, EXP2_7=PE13, EXP2_9=, + EXP2_2=PA5, EXP2_4=PA4, EXP2_6=PA7, EXP2_8=, EXP2_10=, + +# See the sample-lcd.cfg file for definitions of common LCD displays. + +####################################### +# BL-Touch +####################################### + +#[bltouch] +#sensor_pin: PC2 +#control_pin: PE6 +#z_offset: 0 diff --git a/config/printer-tronxy-crux1-2022.cfg b/config/printer-tronxy-crux1-2022.cfg new file mode 100644 index 000000000000..e3254d85b322 --- /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 4af1812a3a1d..3837f737ed99 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 1538f6257210..62f1dee84ed7 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/Benchmarks.md b/docs/Benchmarks.md index 1256c580119a..abe7d741aece 100644 --- a/docs/Benchmarks.md +++ b/docs/Benchmarks.md @@ -354,6 +354,26 @@ micro-controller. | 1 stepper (200Mhz) | 39 | | 3 stepper (200Mhz) | 181 | +### SAME70 step rate benchmark + +The following configuration sequence is used on the SAME70: +``` +allocate_oids count=3 +config_stepper oid=0 step_pin=PC18 dir_pin=PB5 invert_step=-1 step_pulse_ticks=0 +config_stepper oid=1 step_pin=PC16 dir_pin=PD10 invert_step=-1 step_pulse_ticks=0 +config_stepper oid=2 step_pin=PC28 dir_pin=PA4 invert_step=-1 step_pulse_ticks=0 +finalize_config crc=0 +``` + +The test was last run on commit `34e9ea55` with gcc version +`arm-none-eabi-gcc (NixOS 10.3-2021.10) 10.3.1` on a SAME70Q20B +micro-controller. + +| same70 | ticks | +| -------------------- | ----- | +| 1 stepper | 45 | +| 3 stepper | 190 | + ### AR100 step rate benchmark ### The following configuration sequence is used on AR100 CPU (Allwinner A64): @@ -366,7 +386,7 @@ finalize_config crc=0 ``` -The test was last run on commit `08d037c6` with gcc version +The test was last run on commit `b7978d37` with gcc version `or1k-linux-musl-gcc (GCC) 9.2.0` on an Allwinner A64-H micro-controller. diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index b5212ce19930..cb031ee2b388 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,19 @@ All dates in this document are approximate. ## Changes +20240912: `SET_PIN`, `SET_SERVO`, `SET_FAN_SPEED`, `M106`, and `M107` +commands are now collated. Previously, if many updates to the same +object were issued faster than the minimum scheduling time (typically +100ms) then actual updates could be queued far into the future. Now if +many updates are issued in rapid succession then it is possible that +only the latest request will be applied. If the previous behavior is +requried then consider adding explicit `G4` delay commands between +updates. + +20240912: Support for `maximum_mcu_duration` and `static_value` +parameters in `[output_pin]` config sections have been removed. These +options have been deprecated since 20240123. + 20240415: The `on_error_gcode` parameter in the `[virtual_sdcard]` config section now has a default. If this parameter is not specified it now defaults to `TURN_OFF_HEATERS`. If the previous behavior is diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index c9b13ea8a0d0..150485b81923 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] @@ -2411,6 +2418,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. @@ -4596,6 +4666,112 @@ 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. +``` + +#### HX711 +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 +#pga_bypass: False +# Disable the internal Programmable Gain Amplifier. If +# True the PGA will be disabled for gains 1, 2, and 4. The PGA is always +# enabled for gain settings 8 to 128, regardless of the pga_bypass setting. +# If AVSS is used as an input pga_bypass is forced to True. +# The default is False. +#sample_rate: 660 +# This chip supports two ranges of sample rates, Normal and Turbo. In turbo +# mode the chip's 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 +#input_mux: +# Input multiplexer configuration, select a pair of pins to use. The first pin +# is the positive, AINP, and the second pin is the negative, AINN. Valid +# values are: 'AIN0_AIN1', 'AIN0_AIN2', 'AIN0_AIN3', 'AIN1_AIN2', 'AIN1_AIN3', +# 'AIN2_AIN3', 'AIN1_AIN0', 'AIN3_AIN2', 'AIN0_AVSS', 'AIN1_AVSS', 'AIN2_AVSS' +# and 'AIN3_AVSS'. If AVSS is used the PGA is bypassed and the pga_bypass +# setting will be forced to True. +# The default is AIN0_AIN1. +#vref: +# The selected voltage reference. Valid values are: 'internal', 'REF0', 'REF1' +# and 'analog_supply'. Default is 'internal'. +``` + ## Board specific hardware support ### [sx1509] @@ -4859,8 +5035,9 @@ Most Klipper micro-controller implementations only support an micro-controller supports a 400000 speed (*fast mode*, 400kbit/s), but it must be [set in the operating system](RPi_microcontroller.md#optional-enabling-i2c) and the `i2c_speed` parameter is otherwise ignored. The Klipper -"RP2040" micro-controller and ATmega AVR family support a rate of 400000 -via the `i2c_speed` parameter. All other Klipper micro-controllers use a +"RP2040" micro-controller and ATmega AVR family and some STM32 +(F0, G0, G4, L4, F7, H7) support a rate of 400000 via the `i2c_speed` parameter. +All other Klipper micro-controllers use a 100000 rate and ignore the `i2c_speed` parameter. ``` diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 221c855b6d66..3c36a613c181 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/Features.md b/docs/Features.md index 9c61ccf72845..fa430789f621 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -190,6 +190,7 @@ represent total number of steps per second on the micro-controller. | AR100 | 3529K | 2507K | | STM32F407 | 3652K | 2459K | | STM32F446 | 3913K | 2634K | +| SAME70 | 6667K | 4737K | | STM32H743 | 9091K | 6061K | If unsure of the micro-controller on a particular board, find the diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 9a66bff6f23f..d8d7f7b2df39 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -486,6 +486,20 @@ enabled. `SET_FAN_SPEED FAN=config_name SPEED=` This command sets the speed of a fan. "speed" must be between 0.0 and 1.0. +`SET_FAN_SPEED PIN=config_name TEMPLATE= +[=]`: If `TEMPLATE` is specified then it assigns a +[display_template](Config_Reference.md#display_template) to the given +fan. For example, if one defined a `[display_template +my_fan_template]` config section then one could assign +`TEMPLATE=my_fan_template` here. The display_template should produce a +string containing a floating point number with the desired value. The +template will be continuously evaluated and the fan will be +automatically set to the resulting speed. One may set display_template +parameters to use during template evaluation (parameters will be +parsed as Python literals). If TEMPLATE is an empty string then this +command will clear any previous template assigned to the pin (one can +then use `SET_FAN_SPEED` commands to manage the values directly). + ### [filament_switch_sensor] The following command is available when a @@ -867,6 +881,20 @@ output `VALUE`. VALUE should be 0 or 1 for "digital" output pins. For PWM pins, set to a value between 0.0 and 1.0, or between 0.0 and `scale` if a scale is configured in the output_pin config section. +`SET_PIN PIN=config_name TEMPLATE= [=]`: +If `TEMPLATE` is specified then it assigns a +[display_template](Config_Reference.md#display_template) to the given +pin. For example, if one defined a `[display_template +my_pin_template]` config section then one could assign +`TEMPLATE=my_pin_template` here. The display_template should produce a +string containing a floating point number with the desired value. The +template will be continuously evaluated and the pin will be +automatically set to the resulting value. One may set display_template +parameters to use during template evaluation (parameters will be +parsed as Python literals). If TEMPLATE is an empty string then this +command will clear any previous template assigned to the pin (one can +then use `SET_PIN` commands to manage the values directly). + ### [palette2] The following commands are available when the @@ -1425,3 +1453,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 004d963a0796..ca64259aa0bc 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. + +## 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. -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. +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"). - -## 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 -`/tmp/printer`. Then click "Save". - -Enter the Settings tab again and under "Serial Connection" change the -"Serial Port" setting to `/tmp/printer`. +For common micro-controllers using RP2040 chips, the code can be flashed +with something similar to: -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". +``` +sudo service klipper stop +make flash FLASH_DEVICE=first +sudo service klipper start +``` -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.) +It is important to note that RP2040 chips may need to be put into Boot mode +before this operation. -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 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. -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`). +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/Measuring_Resonances.md b/docs/Measuring_Resonances.md index d5f7f54cc537..81c2eb96d2ce 100644 --- a/docs/Measuring_Resonances.md +++ b/docs/Measuring_Resonances.md @@ -212,12 +212,20 @@ sudo apt install python3-numpy python3-matplotlib libatlas-base-dev libopenblas- Next, in order to install NumPy in the Klipper environment, run the command: ``` -~/klippy-env/bin/pip install -v numpy +~/klippy-env/bin/pip install -v "numpy<1.26" ``` Note that, depending on the performance of the CPU, it may take *a lot* of time, up to 10-20 minutes. Be patient and wait for the completion of the installation. On some occasions, if the board has too little RAM -the installation may fail and you will need to enable swap. +the installation may fail and you will need to enable swap. Also note +the forced version, due to newer versions of NumPY having requirements +that may not be satisfied in some klipper python environments. + +Once installed please check that no errors show from the command: +``` +~/klippy-env/bin/python -c 'import numpy;' +``` +The correct output should simply be a new line. #### Configure ADXL345 With RPi diff --git a/docs/OctoPrint.md b/docs/OctoPrint.md new file mode 100644 index 000000000000..20f9246af547 --- /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.serial +``` +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 2b9253c26e67..1ab948910b49 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/Sponsors.md b/docs/Sponsors.md index a226bb57bb5c..0d0d71dc9907 100644 --- a/docs/Sponsors.md +++ b/docs/Sponsors.md @@ -17,7 +17,6 @@ serve the 3D printing community better. Follow them on ## Sponsors [](https://obico.io/klipper.html?source=klipper_sponsor) -[](https://peopoly.net) ## Klipper Developers diff --git a/docs/_klipper3d/mkdocs.yml b/docs/_klipper3d/mkdocs.yml index c5da747b5c37..02d32fadca66 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 e4199561d018..fa1261be97ee 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -142,8 +142,9 @@ 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 b8d1cc221e87..0cd6523c3514 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 a8a4a4ff76f3..5d497c3dd0df 100644 --- a/klippy/configfile.py +++ b/klippy/configfile.py @@ -1,12 +1,17 @@ # Code for reading and writing the Klipper config file # -# Copyright (C) 2016-2021 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import sys, os, glob, re, time, logging, configparser, io error = configparser.Error + +###################################################################### +# Config section parsing helper +###################################################################### + class sentinel: pass @@ -134,39 +139,114 @@ def deprecate(self, option, value=None): pconfig = self.printer.lookup_object("configfile") pconfig.deprecate(self.section, option, value, msg) + +###################################################################### +# Config file parsing (with include file support) +###################################################################### + +class ConfigFileReader: + def read_config_file(self, filename): + try: + f = open(filename, 'r') + data = f.read() + f.close() + except: + msg = "Unable to open config file %s" % (filename,) + logging.exception(msg) + raise error(msg) + return data.replace('\r\n', '\n') + def build_config_string(self, fileconfig): + sfile = io.StringIO() + fileconfig.write(sfile) + return sfile.getvalue().strip() + def append_fileconfig(self, fileconfig, data, filename): + if not data: + return + # Strip trailing comments + lines = data.split('\n') + for i, line in enumerate(lines): + pos = line.find('#') + if pos >= 0: + lines[i] = line[:pos] + sbuffer = io.StringIO('\n'.join(lines)) + if sys.version_info.major >= 3: + fileconfig.read_file(sbuffer, filename) + else: + fileconfig.readfp(sbuffer, filename) + def _create_fileconfig(self): + if sys.version_info.major >= 3: + fileconfig = configparser.RawConfigParser( + strict=False, inline_comment_prefixes=(';', '#')) + else: + fileconfig = configparser.RawConfigParser() + return fileconfig + def build_fileconfig(self, data, filename): + fileconfig = self._create_fileconfig() + self.append_fileconfig(fileconfig, data, filename) + return fileconfig + def _resolve_include(self, source_filename, include_spec, fileconfig, + visited): + dirname = os.path.dirname(source_filename) + include_spec = include_spec.strip() + include_glob = os.path.join(dirname, include_spec) + include_filenames = glob.glob(include_glob) + if not include_filenames and not glob.has_magic(include_glob): + # Empty set is OK if wildcard but not for direct file reference + raise error("Include file '%s' does not exist" % (include_glob,)) + include_filenames.sort() + for include_filename in include_filenames: + include_data = self.read_config_file(include_filename) + self._parse_config(include_data, include_filename, fileconfig, + visited) + return include_filenames + def _parse_config(self, data, filename, fileconfig, visited): + path = os.path.abspath(filename) + if path in visited: + raise error("Recursive include of config file '%s'" % (filename)) + visited.add(path) + lines = data.split('\n') + # Buffer lines between includes and parse as a unit so that overrides + # in includes apply linearly as they do within a single file + buf = [] + for line in lines: + # Process include or buffer line + mo = configparser.RawConfigParser.SECTCRE.match(line) + header = mo and mo.group('header') + if header and header.startswith('include '): + self.append_fileconfig(fileconfig, '\n'.join(buf), filename) + del buf[:] + include_spec = header[8:].strip() + self._resolve_include(filename, include_spec, fileconfig, + visited) + else: + buf.append(line) + self.append_fileconfig(fileconfig, '\n'.join(buf), filename) + visited.remove(path) + def build_fileconfig_with_includes(self, data, filename): + fileconfig = self._create_fileconfig() + self._parse_config(data, filename, fileconfig, set()) + return fileconfig + + +###################################################################### +# Config auto save helper +###################################################################### + AUTOSAVE_HEADER = """ #*# <---------------------- SAVE_CONFIG ----------------------> #*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated. #*# """ -class PrinterConfig: +class ConfigAutoSave: def __init__(self, printer): self.printer = printer - self.autosave = None - self.deprecated = {} - self.runtime_warnings = [] - self.deprecate_warnings = [] - self.status_raw_config = {} + self.fileconfig = None self.status_save_pending = {} - self.status_settings = {} - self.status_warnings = [] self.save_config_pending = False gcode = self.printer.lookup_object('gcode') gcode.register_command("SAVE_CONFIG", self.cmd_SAVE_CONFIG, desc=self.cmd_SAVE_CONFIG_help) - def get_printer(self): - return self.printer - def _read_config_file(self, filename): - try: - f = open(filename, 'r') - data = f.read() - f.close() - except: - msg = "Unable to open config file %s" % (filename,) - logging.exception(msg) - raise error(msg) - return data.replace('\r\n', '\n') def _find_autosave_data(self, data): regular_data = data autosave_data = "" @@ -175,7 +255,7 @@ def _find_autosave_data(self, data): regular_data = data[:pos] autosave_data = data[pos + len(AUTOSAVE_HEADER):].strip() # Check for errors and strip line prefixes - if "\n#*# " in regular_data: + if "\n#*# " in regular_data or autosave_data.find(AUTOSAVE_HEADER) >= 0: logging.warning("Can't read autosave from config file" " - autosave state corrupted") return data, "" @@ -192,7 +272,7 @@ def _find_autosave_data(self, data): return regular_data, "\n".join(out) comment_r = re.compile('[#;].*$') value_r = re.compile('[^A-Za-z0-9_].*$') - def _strip_duplicates(self, data, config): + def _strip_duplicates(self, data, fileconfig): # Comment out fields in 'data' that are defined in 'config' lines = data.split('\n') section = None @@ -210,152 +290,31 @@ def _strip_duplicates(self, data, config): section = pruned_line[1:-1].strip() continue field = self.value_r.sub('', pruned_line) - if config.fileconfig.has_option(section, field): + if fileconfig.has_option(section, field): is_dup_field = True lines[lineno] = '#' + lines[lineno] return "\n".join(lines) - def _parse_config_buffer(self, buffer, filename, fileconfig): - if not buffer: - return - data = '\n'.join(buffer) - del buffer[:] - sbuffer = io.StringIO(data) - if sys.version_info.major >= 3: - fileconfig.read_file(sbuffer, filename) - else: - fileconfig.readfp(sbuffer, filename) - def _resolve_include(self, source_filename, include_spec, fileconfig, - visited): - dirname = os.path.dirname(source_filename) - include_spec = include_spec.strip() - include_glob = os.path.join(dirname, include_spec) - include_filenames = glob.glob(include_glob) - if not include_filenames and not glob.has_magic(include_glob): - # Empty set is OK if wildcard but not for direct file reference - raise error("Include file '%s' does not exist" % (include_glob,)) - include_filenames.sort() - for include_filename in include_filenames: - include_data = self._read_config_file(include_filename) - self._parse_config(include_data, include_filename, fileconfig, - visited) - return include_filenames - def _parse_config(self, data, filename, fileconfig, visited): - path = os.path.abspath(filename) - if path in visited: - raise error("Recursive include of config file '%s'" % (filename)) - visited.add(path) - lines = data.split('\n') - # Buffer lines between includes and parse as a unit so that overrides - # in includes apply linearly as they do within a single file - buffer = [] - for line in lines: - # Strip trailing comment - pos = line.find('#') - if pos >= 0: - line = line[:pos] - # Process include or buffer line - mo = configparser.RawConfigParser.SECTCRE.match(line) - header = mo and mo.group('header') - if header and header.startswith('include '): - self._parse_config_buffer(buffer, filename, fileconfig) - include_spec = header[8:].strip() - self._resolve_include(filename, include_spec, fileconfig, - visited) - else: - buffer.append(line) - self._parse_config_buffer(buffer, filename, fileconfig) - visited.remove(path) - def _build_config_wrapper(self, data, filename): - if sys.version_info.major >= 3: - fileconfig = configparser.RawConfigParser( - strict=False, inline_comment_prefixes=(';', '#')) - else: - fileconfig = configparser.RawConfigParser() - self._parse_config(data, filename, fileconfig, set()) - return ConfigWrapper(self.printer, fileconfig, {}, 'printer') - def _build_config_string(self, config): - sfile = io.StringIO() - config.fileconfig.write(sfile) - return sfile.getvalue().strip() - def read_config(self, filename): - return self._build_config_wrapper(self._read_config_file(filename), - filename) - def read_main_config(self): + def load_main_config(self): filename = self.printer.get_start_args()['config_file'] - data = self._read_config_file(filename) + cfgrdr = ConfigFileReader() + data = cfgrdr.read_config_file(filename) regular_data, autosave_data = self._find_autosave_data(data) - regular_config = self._build_config_wrapper(regular_data, filename) - autosave_data = self._strip_duplicates(autosave_data, regular_config) - self.autosave = self._build_config_wrapper(autosave_data, filename) - cfg = self._build_config_wrapper(regular_data + autosave_data, filename) - return cfg - def check_unused_options(self, config): - fileconfig = config.fileconfig - objects = dict(self.printer.lookup_objects()) - # Determine all the fields that have been accessed - access_tracking = dict(config.access_tracking) - for section in self.autosave.fileconfig.sections(): - for option in self.autosave.fileconfig.options(section): - access_tracking[(section.lower(), option.lower())] = 1 - # Validate that there are no undefined parameters in the config file - valid_sections = { s: 1 for s, o in access_tracking } - for section_name in fileconfig.sections(): - section = section_name.lower() - if section not in valid_sections and section not in objects: - raise error("Section '%s' is not a valid config section" - % (section,)) - for option in fileconfig.options(section_name): - option = option.lower() - if (section, option) not in access_tracking: - raise error("Option '%s' is not valid in section '%s'" - % (option, section)) - # Setup get_status() - self._build_status(config) - def log_config(self, config): - lines = ["===== Config file =====", - self._build_config_string(config), - "======================="] - self.printer.set_rollover_info("config", "\n".join(lines)) - # Status reporting - def runtime_warning(self, msg): - logging.warning(msg) - res = {'type': 'runtime_warning', 'message': msg} - self.runtime_warnings.append(res) - self.status_warnings = self.runtime_warnings + self.deprecate_warnings - def deprecate(self, section, option, value=None, msg=None): - self.deprecated[(section, option, value)] = msg - def _build_status(self, config): - self.status_raw_config.clear() - for section in config.get_prefix_sections(''): - self.status_raw_config[section.get_name()] = section_status = {} - for option in section.get_prefix_options(''): - section_status[option] = section.get(option, note_valid=False) - self.status_settings = {} - for (section, option), value in config.access_tracking.items(): - self.status_settings.setdefault(section, {})[option] = value - self.deprecate_warnings = [] - for (section, option, value), msg in self.deprecated.items(): - if value is None: - res = {'type': 'deprecated_option'} - else: - res = {'type': 'deprecated_value', 'value': value} - res['message'] = msg - res['section'] = section - res['option'] = option - self.deprecate_warnings.append(res) - self.status_warnings = self.runtime_warnings + self.deprecate_warnings + regular_fileconfig = cfgrdr.build_fileconfig_with_includes( + regular_data, filename) + autosave_data = self._strip_duplicates(autosave_data, + regular_fileconfig) + self.fileconfig = cfgrdr.build_fileconfig(autosave_data, filename) + cfgrdr.append_fileconfig(regular_fileconfig, + autosave_data, '*AUTOSAVE*') + return regular_fileconfig, self.fileconfig def get_status(self, eventtime): - return {'config': self.status_raw_config, - 'settings': self.status_settings, - 'warnings': self.status_warnings, - 'save_config_pending': self.save_config_pending, + return {'save_config_pending': self.save_config_pending, 'save_config_pending_items': self.status_save_pending} - # Autosave functions def set(self, section, option, value): - if not self.autosave.fileconfig.has_section(section): - self.autosave.fileconfig.add_section(section) + if not self.fileconfig.has_section(section): + self.fileconfig.add_section(section) svalue = str(value) - self.autosave.fileconfig.set(section, option, svalue) + self.fileconfig.set(section, option, svalue) pending = dict(self.status_save_pending) if not section in pending or pending[section] is None: pending[section] = {} @@ -366,8 +325,8 @@ def set(self, section, option, value): self.save_config_pending = True logging.info("save_config: set [%s] %s = %s", section, option, svalue) def remove_section(self, section): - if self.autosave.fileconfig.has_section(section): - self.autosave.fileconfig.remove_section(section) + if self.fileconfig.has_section(section): + self.fileconfig.remove_section(section) pending = dict(self.status_save_pending) pending[section] = None self.status_save_pending = pending @@ -378,21 +337,20 @@ def remove_section(self, section): del pending[section] self.status_save_pending = pending self.save_config_pending = True - def _disallow_include_conflicts(self, regular_data, cfgname, gcode): - config = self._build_config_wrapper(regular_data, cfgname) - for section in self.autosave.fileconfig.sections(): - for option in self.autosave.fileconfig.options(section): - if config.fileconfig.has_option(section, option): + def _disallow_include_conflicts(self, regular_fileconfig): + for section in self.fileconfig.sections(): + for option in self.fileconfig.options(section): + if regular_fileconfig.has_option(section, option): msg = ("SAVE_CONFIG section '%s' option '%s' conflicts " "with included value" % (section, option)) - raise gcode.error(msg) + raise self.printer.command_error(msg) cmd_SAVE_CONFIG_help = "Overwrite config file and restart" def cmd_SAVE_CONFIG(self, gcmd): - if not self.autosave.fileconfig.sections(): + if not self.fileconfig.sections(): return - gcode = self.printer.lookup_object('gcode') # Create string containing autosave data - autosave_data = self._build_config_string(self.autosave) + cfgrdr = ConfigFileReader() + autosave_data = cfgrdr.build_config_string(self.fileconfig) lines = [('#*# ' + l).strip() for l in autosave_data.split('\n')] lines.insert(0, "\n" + AUTOSAVE_HEADER.rstrip()) @@ -401,16 +359,27 @@ def cmd_SAVE_CONFIG(self, gcmd): # Read in and validate current config file cfgname = self.printer.get_start_args()['config_file'] try: - data = self._read_config_file(cfgname) - regular_data, old_autosave_data = self._find_autosave_data(data) - config = self._build_config_wrapper(regular_data, cfgname) + data = cfgrdr.read_config_file(cfgname) except error as e: - msg = "Unable to parse existing config on SAVE_CONFIG" + msg = "Unable to read existing config on SAVE_CONFIG" logging.exception(msg) - raise gcode.error(msg) - regular_data = self._strip_duplicates(regular_data, self.autosave) - self._disallow_include_conflicts(regular_data, cfgname, gcode) + raise gcmd.error(msg) + regular_data, old_autosave_data = self._find_autosave_data(data) + regular_data = self._strip_duplicates(regular_data, self.fileconfig) data = regular_data.rstrip() + autosave_data + new_regular_data, new_autosave_data = self._find_autosave_data(data) + if not new_autosave_data: + raise gcmd.error( + "Existing config autosave is corrupted." + " Can't complete SAVE_CONFIG") + try: + regular_fileconfig = cfgrdr.build_fileconfig_with_includes( + new_regular_data, cfgname) + except error as e: + msg = "Unable to parse existing config on SAVE_CONFIG" + logging.exception(msg) + raise gcmd.error(msg) + self._disallow_include_conflicts(regular_fileconfig) # Determine filenames datestr = time.strftime("-%Y%m%d_%H%M%S") backup_name = cfgname + datestr @@ -430,6 +399,135 @@ def cmd_SAVE_CONFIG(self, gcmd): except: msg = "Unable to write config file during SAVE_CONFIG" logging.exception(msg) - raise gcode.error(msg) + raise gcmd.error(msg) # Request a restart + gcode = self.printer.lookup_object('gcode') gcode.request_restart('restart') + + +###################################################################### +# Config validation (check for undefined options) +###################################################################### + +class ConfigValidate: + def __init__(self, printer): + self.printer = printer + self.status_settings = {} + self.access_tracking = {} + self.autosave_options = {} + def start_access_tracking(self, autosave_fileconfig): + # Note autosave options for use during undefined options check + self.autosave_options = {} + for section in autosave_fileconfig.sections(): + for option in autosave_fileconfig.options(section): + self.autosave_options[(section.lower(), option.lower())] = 1 + self.access_tracking = {} + return self.access_tracking + def check_unused(self, fileconfig): + # Don't warn on fields set in autosave segment + access_tracking = dict(self.access_tracking) + access_tracking.update(self.autosave_options) + # Note locally used sections + valid_sections = { s: 1 for s, o in self.printer.lookup_objects() } + valid_sections.update({ s: 1 for s, o in access_tracking }) + # Validate that there are no undefined parameters in the config file + for section_name in fileconfig.sections(): + section = section_name.lower() + if section not in valid_sections: + raise error("Section '%s' is not a valid config section" + % (section,)) + for option in fileconfig.options(section_name): + option = option.lower() + if (section, option) not in access_tracking: + raise error("Option '%s' is not valid in section '%s'" + % (option, section)) + # Setup get_status() + self._build_status_settings() + # Clear tracking state + self.access_tracking.clear() + self.autosave_options.clear() + def _build_status_settings(self): + self.status_settings = {} + for (section, option), value in self.access_tracking.items(): + self.status_settings.setdefault(section, {})[option] = value + def get_status(self, eventtime): + return {'settings': self.status_settings} + + +###################################################################### +# Main printer config tracking +###################################################################### + +class PrinterConfig: + def __init__(self, printer): + self.printer = printer + self.autosave = ConfigAutoSave(printer) + self.validate = ConfigValidate(printer) + self.deprecated = {} + self.runtime_warnings = [] + self.deprecate_warnings = [] + self.status_raw_config = {} + self.status_warnings = [] + def get_printer(self): + return self.printer + def read_config(self, filename): + cfgrdr = ConfigFileReader() + data = cfgrdr.read_config_file(filename) + fileconfig = cfgrdr.build_fileconfig(data, filename) + return ConfigWrapper(self.printer, fileconfig, {}, 'printer') + def read_main_config(self): + fileconfig, autosave_fileconfig = self.autosave.load_main_config() + access_tracking = self.validate.start_access_tracking( + autosave_fileconfig) + config = ConfigWrapper(self.printer, fileconfig, + access_tracking, 'printer') + self._build_status_config(config) + return config + def log_config(self, config): + cfgrdr = ConfigFileReader() + lines = ["===== Config file =====", + cfgrdr.build_config_string(config.fileconfig), + "======================="] + self.printer.set_rollover_info("config", "\n".join(lines)) + def check_unused_options(self, config): + self.validate.check_unused(config.fileconfig) + # Deprecation warnings + def runtime_warning(self, msg): + logging.warning(msg) + res = {'type': 'runtime_warning', 'message': msg} + self.runtime_warnings.append(res) + self.status_warnings = self.runtime_warnings + self.deprecate_warnings + def deprecate(self, section, option, value=None, msg=None): + key = (section, option, value) + if key in self.deprecated and self.deprecated[key] == msg: + return + self.deprecated[key] = msg + self.deprecate_warnings = [] + for (section, option, value), msg in self.deprecated.items(): + if value is None: + res = {'type': 'deprecated_option'} + else: + res = {'type': 'deprecated_value', 'value': value} + res['message'] = msg + res['section'] = section + res['option'] = option + self.deprecate_warnings.append(res) + self.status_warnings = self.runtime_warnings + self.deprecate_warnings + # Status reporting + def _build_status_config(self, config): + self.status_raw_config = {} + for section in config.get_prefix_sections(''): + self.status_raw_config[section.get_name()] = section_status = {} + for option in section.get_prefix_options(''): + section_status[option] = section.get(option, note_valid=False) + def get_status(self, eventtime): + status = {'config': self.status_raw_config, + 'warnings': self.status_warnings} + status.update(self.autosave.get_status(eventtime)) + status.update(self.validate.get_status(eventtime)) + return status + # Autosave functions + def set(self, section, option, value): + self.autosave.set(section, option, value) + def remove_section(self, section): + self.autosave.remove_section(section) diff --git a/klippy/extras/adc_scaled.py b/klippy/extras/adc_scaled.py index c2d2cb877f78..80ea452f3109 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 @@ def __init__(self, main, pin_params): 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 @@ def _config_pin(self, config, name, callback): 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 b76e8c66fa85..c53ae7056adf 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 @@ def __init__(self, config, adc_convert): 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 @@ def adc_callback(self, read_time, read_value): 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 000000000000..fcae78bca807 --- /dev/null +++ b/klippy/extras/ads1220.py @@ -0,0 +1,216 @@ +# 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('sample_rate', self.sps_options, + default='660') + self.is_turbo = str(self.sps) in self.sps_turbo + # Input multiplexer: AINP and AINN + mux_options = {'AIN0_AIN1': 0b0000, 'AIN0_AIN2': 0b0001, + 'AIN0_AIN3': 0b0010, 'AIN1_AIN2': 0b0011, + 'AIN1_AIN3': 0b0100, 'AIN2_AIN3': 0b0101, + 'AIN1_AIN0': 0b0110, 'AIN3_AIN2': 0b0111, + 'AIN0_AVSS': 0b1000, 'AIN1_AVSS': 0b1001, + 'AIN2_AVSS': 0b1010, 'AIN3_AVSS': 0b1011} + self.mux = config.getchoice('input_mux', mux_options, + default='AIN0_AIN1') + # PGA Bypass + self.pga_bypass = config.getboolean('pga_bypass', default=False) + # bypass PGA when AVSS is the negative input + force_pga_bypass = self.mux >= 0b1000 + self.pga_bypass = force_pga_bypass or self.pga_bypass + # Voltage Reference + self.vref_options = {'internal': 0b0, 'REF0': 0b01, 'REF1': 0b10, + 'analog_supply': 0b11} + self.vref = config.getchoice('vref', self.vref_options, + default='internal') + # check for conflict between REF1 and AIN0/AIN3 + mux_conflict = [0b0000, 0b0001, 0b0010, 0b0100, 0b0101, 0b0110, 0b0111, + 0b1000, 0b1011] + if self.vref == 0b10 and self.mux in mux_conflict: + raise config.error("ADS1220 config error: AIN0/REFP1 and AIN3/REFN1" + " cant be used as a voltage reference and" + " an input at the same time") + # 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) @@ -866,11 +773,12 @@ def probe_finalize(self, offsets, positions): z_mesh.build_mesh(probed_matrix) except BedMeshError as e: raise self.gcode.error(str(e)) - if self.zero_reference_mode == ZrefMode.IN_MESH: + if self.probe_mgr.get_zero_ref_mode() == ZrefMode.IN_MESH: # The reference can be anywhere in the mesh, therefore # it is necessary to set the reference after the initial mesh # is generated to lookup the correct z value. - z_mesh.set_zero_reference(*self.zero_ref_pos) + zero_ref_pos = self.probe_mgr.get_zero_ref_pos() + z_mesh.set_zero_reference(*zero_ref_pos) self.bedmesh.set_mesh(z_mesh) self.gcode.respond_info("Mesh Bed Leveling Complete") if self._profile_name is not None: @@ -878,14 +786,15 @@ def probe_finalize(self, offsets, positions): def _dump_points(self, probed_pts, corrected_pts, offsets): # logs generated points with offset applied, points received # from the finalize callback, and the list of corrected points - max_len = max([len(self.points), len(probed_pts), len(corrected_pts)]) + points = self.probe_mgr.get_base_points() + max_len = max([len(points), len(probed_pts), len(corrected_pts)]) logging.info( "bed_mesh: calibration point dump\nIndex | %-17s| %-25s|" " Corrected Point" % ("Generated Point", "Probed Point")) for i in list(range(max_len)): gen_pt = probed_pt = corr_pt = "" - if i < len(self.points): - off_pt = [p - o for p, o in zip(self.points[i], offsets[:2])] + if i < len(points): + off_pt = [p - o for p, o in zip(points[i], offsets[:2])] gen_pt = "(%.2f, %.2f)" % tuple(off_pt) if i < len(probed_pts): probed_pt = "(%.2f, %.2f, %.4f)" % tuple(probed_pts[i]) @@ -894,6 +803,453 @@ def _dump_points(self, probed_pts, corrected_pts, offsets): 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/bme280.py b/klippy/extras/bme280.py index 262dc130f417..1c26bbee7f0a 100644 --- a/klippy/extras/bme280.py +++ b/klippy/extras/bme280.py @@ -83,6 +83,7 @@ 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 @@ def __init__(self, config): 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 @@ def __init__(self, config): 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 @@ def read_calibration_data_bmp180(calib_data_1): 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 @@ def read_calibration_data_bmp180(calib_data_1): 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 @@ def read_calibration_data_bmp180(calib_data_1): 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 @@ def read_calibration_data_bmp180(calib_data_1): elif self.chip_type == 'BMP388': self.dig = read_calibration_data_bmp388(cal_1) - 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) + 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) - 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 == '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): + # In normal mode data shadowing is performed + # So reading can be done while measurements are in process + try: if self.chip_type == 'BME280': data = self.read_register('PRESSURE_MSB', 8) elif self.chip_type == 'BMP280': @@ -462,36 +511,40 @@ def _sample_bmp388_press(self): 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 @@ def data_ready(stat): 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 @@ def _compensate_gas(self, gas_raw, gas_range): 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 @@ def _calculate_gas_heater_resistance(self, target_temp): * (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 @@ def read_register(self, reg_name, read_len): 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/bus.py b/klippy/extras/bus.py index 9b2ec371fdd4..b8236dc7c042 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -160,7 +160,7 @@ def __init__(self, mcu, bus, addr, speed, sw_pins=None): % (self.oid, speed, addr)) self.cmd_queue = self.mcu.alloc_command_queue() self.mcu.register_config_callback(self.build_config) - self.i2c_write_cmd = self.i2c_read_cmd = self.i2c_modify_bits_cmd = None + self.i2c_write_cmd = self.i2c_read_cmd = None def get_oid(self): return self.oid def get_mcu(self): @@ -180,9 +180,6 @@ def build_config(self): "i2c_read oid=%c reg=%*s read_len=%u", "i2c_read_response oid=%c response=%*s", oid=self.oid, cq=self.cmd_queue) - self.i2c_modify_bits_cmd = self.mcu.lookup_command( - "i2c_modify_bits oid=%c reg=%*s clear_set_bits=%*s", - cq=self.cmd_queue) def i2c_write(self, data, minclock=0, reqclock=0): if self.i2c_write_cmd is None: # Send setup message via mcu initialization @@ -192,21 +189,11 @@ def i2c_write(self, data, minclock=0, reqclock=0): 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, - minclock=0, reqclock=0): - clearset = clear_bits + set_bits - if self.i2c_modify_bits_cmd is None: - # Send setup message via mcu initialization - reg_msg = "".join(["%02x" % (x,) for x in reg]) - clearset_msg = "".join(["%02x" % (x,) for x in clearset]) - self.mcu.add_config_cmd( - "i2c_modify_bits oid=%d reg=%s clear_set_bits=%s" % ( - self.oid, reg_msg, clearset_msg), is_init=True) - return - self.i2c_modify_bits_cmd.send([self.oid, reg, clearset], - minclock=minclock, reqclock=reqclock) def MCU_I2C_from_config(config, default_addr=None, default_speed=100000): # Load bus parameters diff --git a/klippy/extras/buttons.py b/klippy/extras/buttons.py index 70d76a60e175..daa998a93dcd 100644 --- a/klippy/extras/buttons.py +++ b/klippy/extras/buttons.py @@ -104,7 +104,7 @@ def __init__(self, printer, pin, pullup): 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/controller_fan.py b/klippy/extras/controller_fan.py index df141e7eb7d1..b1286b59712a 100644 --- a/klippy/extras/controller_fan.py +++ b/klippy/extras/controller_fan.py @@ -62,9 +62,7 @@ def callback(self, eventtime): self.last_on += 1 if speed != self.last_speed: self.last_speed = speed - curtime = self.printer.get_reactor().monotonic() - print_time = self.fan.get_mcu().estimated_print_time(curtime) - self.fan.set_speed(print_time + PIN_MIN_TIME, speed) + self.fan.set_speed(speed) return eventtime + 1. def load_config_prefix(config): diff --git a/klippy/extras/dotstar.py b/klippy/extras/dotstar.py index 4186534fead6..0262c13d0ade 100644 --- a/klippy/extras/dotstar.py +++ b/klippy/extras/dotstar.py @@ -1,9 +1,9 @@ # Support for "dotstar" leds # -# Copyright (C) 2019-2022 Kevin O'Connor +# Copyright (C) 2019-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -from . import bus +from . import bus, led BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000 @@ -22,9 +22,8 @@ def __init__(self, config): self.spi = bus.MCU_SPI(mcu, None, None, 0, 500000, sw_spi_pins) # Initialize color data self.chain_count = config.getint('chain_count', 1, minval=1) - pled = printer.load_object(config, "led") - self.led_helper = pled.setup_helper(config, self.update_leds, - self.chain_count) + self.led_helper = led.LEDHelper(config, self.update_leds, + self.chain_count) self.prev_data = None # Register commands printer.register_event_handler("klippy:connect", self.handle_connect) diff --git a/klippy/extras/error_mcu.py b/klippy/extras/error_mcu.py new file mode 100644 index 000000000000..dc91c33a92c7 --- /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/fan.py b/klippy/extras/fan.py index d6e687218c5e..c5677ba068a3 100644 --- a/klippy/extras/fan.py +++ b/klippy/extras/fan.py @@ -1,17 +1,14 @@ # Printer cooling fan # -# 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. -from . import pulse_counter - -FAN_MIN_TIME = 0.100 +from . import pulse_counter, output_pin class Fan: def __init__(self, config, default_shutdown_speed=0.): self.printer = config.get_printer() - self.last_fan_value = 0. - self.last_fan_time = 0. + self.last_fan_value = self.last_req_value = 0. # Read config self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.) self.kick_start_time = config.getfloat('kick_start_time', 0.1, @@ -36,6 +33,10 @@ def __init__(self, config, default_shutdown_speed=0.): self.enable_pin = ppins.setup_pin('digital_out', enable_pin) self.enable_pin.setup_max_duration(0.) + # Create gcode request queue + self.gcrq = output_pin.GCodeRequestQueue(config, self.mcu_fan.get_mcu(), + self._apply_speed) + # Setup tachometer self.tachometer = FanTachometer(config) @@ -45,37 +46,37 @@ def __init__(self, config, default_shutdown_speed=0.): def get_mcu(self): return self.mcu_fan.get_mcu() - def set_speed(self, print_time, value): + def _apply_speed(self, print_time, value): if value < self.off_below: value = 0. value = max(0., min(self.max_power, value * self.max_power)) if value == self.last_fan_value: - return - print_time = max(self.last_fan_time + FAN_MIN_TIME, print_time) + return "discard", 0. if self.enable_pin: if value > 0 and self.last_fan_value == 0: self.enable_pin.set_digital(print_time, 1) elif value == 0 and self.last_fan_value > 0: self.enable_pin.set_digital(print_time, 0) - if (value and value < self.max_power and self.kick_start_time + if (value and self.kick_start_time and (not self.last_fan_value or value - self.last_fan_value > .5)): # Run fan at full speed for specified kick_start_time + self.last_req_value = value + self.last_fan_value = self.max_power self.mcu_fan.set_pwm(print_time, self.max_power) - print_time += self.kick_start_time + return "delay", self.kick_start_time + self.last_fan_value = self.last_req_value = value self.mcu_fan.set_pwm(print_time, value) - self.last_fan_time = print_time - self.last_fan_value = value + def set_speed(self, value, print_time=None): + self.gcrq.send_async_request(value, print_time) def set_speed_from_command(self, value): - toolhead = self.printer.lookup_object('toolhead') - toolhead.register_lookahead_callback((lambda pt: - self.set_speed(pt, value))) + self.gcrq.queue_gcode_request(value) def _handle_request_restart(self, print_time): - self.set_speed(print_time, 0.) + self.set_speed(0., print_time) def get_status(self, eventtime): tachometer_status = self.tachometer.get_status(eventtime) return { - 'speed': self.last_fan_value, + 'speed': self.last_req_value, 'rpm': tachometer_status['rpm'], } diff --git a/klippy/extras/fan_generic.py b/klippy/extras/fan_generic.py index def9a77e146b..20bd57e70fd0 100644 --- a/klippy/extras/fan_generic.py +++ b/klippy/extras/fan_generic.py @@ -1,9 +1,10 @@ # Support fans that are controlled by gcode # -# 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. -from . import fan +import logging +from . import fan, output_pin class PrinterFanGeneric: cmd_SET_FAN_SPEED_help = "Sets the speed of a fan" @@ -12,6 +13,9 @@ def __init__(self, config): self.fan = fan.Fan(config, default_shutdown_speed=0.) self.fan_name = config.get_name().split()[-1] + # Template handling + self.template_eval = output_pin.lookup_template_eval(config) + gcode = self.printer.lookup_object("gcode") gcode.register_mux_command("SET_FAN_SPEED", "FAN", self.fan_name, @@ -20,8 +24,21 @@ def __init__(self, config): def get_status(self, eventtime): return self.fan.get_status(eventtime) + def _template_update(self, text): + try: + value = float(text) + except ValueError as e: + logging.exception("fan_generic template render error") + self.fan.set_speed(value) def cmd_SET_FAN_SPEED(self, gcmd): - speed = gcmd.get_float('SPEED', 0.) + speed = gcmd.get_float('SPEED', None, 0.) + template = gcmd.get('TEMPLATE', None) + if (speed is None) == (template is None): + raise gcmd.error("SET_FAN_SPEED must specify SPEED or TEMPLATE") + # Check for template setting + if template is not None: + self.template_eval.set_template(gcmd, self._template_update) + return self.fan.set_speed_from_command(speed) def load_config_prefix(config): diff --git a/klippy/extras/gcode_arcs.py b/klippy/extras/gcode_arcs.py index 76c165dd5bca..3917dac30bf2 100644 --- a/klippy/extras/gcode_arcs.py +++ b/klippy/extras/gcode_arcs.py @@ -39,8 +39,6 @@ def __init__(self, config): 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 @@ def _cmd_inner(self, gcmd, clockwise): 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 @@ def _cmd_inner(self, gcmd, clockwise): # # 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 @@ def planArc(self, currentPos, targetPos, offset, clockwise, # 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 e080288741d7..8dab35226666 100644 --- a/klippy/extras/hall_filament_width_sensor.py +++ b/klippy/extras/hall_filament_width_sensor.py @@ -49,10 +49,10 @@ def __init__(self, config): # 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/heater_fan.py b/klippy/extras/heater_fan.py index ab4c8a81e7f6..3630366e915f 100644 --- a/klippy/extras/heater_fan.py +++ b/klippy/extras/heater_fan.py @@ -33,9 +33,7 @@ def callback(self, eventtime): speed = self.fan_speed if speed != self.last_speed: self.last_speed = speed - curtime = self.printer.get_reactor().monotonic() - print_time = self.fan.get_mcu().estimated_print_time(curtime) - self.fan.set_speed(print_time + PIN_MIN_TIME, speed) + self.fan.set_speed(speed) return eventtime + 1. def load_config_prefix(config): diff --git a/klippy/extras/heaters.py b/klippy/extras/heaters.py index 5480501326b4..7cb663a4a279 100644 --- a/klippy/extras/heaters.py +++ b/klippy/extras/heaters.py @@ -259,7 +259,8 @@ def load_config(self, config): try: dconfig = pconfig.read_config(filename) except Exception: - raise config.config_error("Cannot load config '%s'" % (filename,)) + logging.exception("Unable to load temperature_sensors.cfg") + raise config.error("Cannot load config '%s'" % (filename,)) for c in dconfig.get_prefix_sections(''): self.printer.load_object(dconfig, c.get_name()) def add_sensor_factory(self, sensor_type, sensor_factory): diff --git a/klippy/extras/homing.py b/klippy/extras/homing.py index 06b52f1ec56e..bbad53371594 100644 --- a/klippy/extras/homing.py +++ b/klippy/extras/homing.py @@ -1,6 +1,6 @@ # Helper code for implementing homing operations # -# 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 logging, math @@ -29,10 +29,17 @@ def __init__(self, stepper, endstop_name): self.endstop_name = endstop_name self.stepper_name = stepper.get_name() self.start_pos = stepper.get_mcu_position() + self.start_cmd_pos = stepper.mcu_to_commanded_position(self.start_pos) self.halt_pos = self.trig_pos = None def note_home_end(self, trigger_time): self.halt_pos = self.stepper.get_mcu_position() self.trig_pos = self.stepper.get_past_mcu_position(trigger_time) + def verify_no_probe_skew(self, haltpos): + new_start_pos = self.stepper.get_mcu_position(self.start_cmd_pos) + if new_start_pos != self.start_pos: + logging.warning( + "Stepper '%s' position skew after probe: pos %d now %d", + self.stepper.get_name(), self.start_pos, new_start_pos) # Implementation of homing/probing moves class HomingMove: @@ -121,6 +128,9 @@ def homing_move(self, movepos, speed, probe_pos=False, haltpos = trigpos = self.calc_toolhead_pos(kin_spos, trig_steps) if trig_steps != halt_steps: haltpos = self.calc_toolhead_pos(kin_spos, halt_steps) + self.toolhead.set_position(haltpos) + for sp in self.stepper_positions: + sp.verify_no_probe_skew(haltpos) else: haltpos = trigpos = movepos over_steps = {sp.stepper_name: sp.halt_pos - sp.trig_pos @@ -130,7 +140,7 @@ def homing_move(self, movepos, speed, probe_pos=False, halt_kin_spos = {s.get_name(): s.get_commanded_position() for s in kin.get_steppers()} haltpos = self.calc_toolhead_pos(halt_kin_spos, over_steps) - self.toolhead.set_position(haltpos) + self.toolhead.set_position(haltpos) # Signal homing/probing move complete try: self.printer.send_event("homing:homing_move_end", self) diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py new file mode 100644 index 000000000000..5c59ed0b4fdc --- /dev/null +++ b/klippy/extras/hx71x.py @@ -0,0 +1,168 @@ +# 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()} + + +def HX711(config): + return HX71xBase(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') + + +def HX717(config): + return HX71xBase(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/led.py b/klippy/extras/led.py index 2396a79e7d06..22b51e8e6117 100644 --- a/klippy/extras/led.py +++ b/klippy/extras/led.py @@ -1,13 +1,10 @@ # Support for PWM driven LEDs # -# Copyright (C) 2019-2022 Kevin O'Connor +# Copyright (C) 2019-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import logging, ast -from .display import display - -# Time between each led template update -RENDER_TIME = 0.500 +import logging +from . import output_pin # Helper code for common LED initialization and control class LEDHelper: @@ -22,14 +19,22 @@ def __init__(self, config, update_func, led_count=1): blue = config.getfloat('initial_BLUE', 0., minval=0., maxval=1.) white = config.getfloat('initial_WHITE', 0., minval=0., maxval=1.) self.led_state = [(red, green, blue, white)] * led_count + # Support setting an led template + self.template_eval = output_pin.lookup_template_eval(config) + self.tcallbacks = [(lambda text, s=self, index=i: + s._template_update(index, text)) + for i in range(led_count)] # Register commands name = config.get_name().split()[-1] gcode = self.printer.lookup_object('gcode') gcode.register_mux_command("SET_LED", "LED", name, self.cmd_SET_LED, desc=self.cmd_SET_LED_help) - def get_led_count(self): - return self.led_count - def set_color(self, index, color): + gcode.register_mux_command("SET_LED_TEMPLATE", "LED", name, + self.cmd_SET_LED_TEMPLATE, + desc=self.cmd_SET_LED_TEMPLATE_help) + def get_status(self, eventtime=None): + return {'color_data': self.led_state} + def _set_color(self, index, color): if index is None: new_led_state = [color] * self.led_count if self.led_state == new_led_state: @@ -41,7 +46,17 @@ def set_color(self, index, color): new_led_state[index - 1] = color self.led_state = new_led_state self.need_transmit = True - def check_transmit(self, print_time): + def _template_update(self, index, text): + try: + parts = [max(0., min(1., float(f))) + for f in text.split(',', 4)] + except ValueError as e: + logging.exception("led template render error") + parts = [] + if len(parts) < 4: + parts += [0.] * (4 - len(parts)) + self._set_color(index, tuple(parts)) + def _check_transmit(self, print_time=None): if not self.need_transmit: return self.need_transmit = False @@ -62,9 +77,9 @@ def cmd_SET_LED(self, gcmd): color = (red, green, blue, white) # Update and transmit data def lookahead_bgfunc(print_time): - self.set_color(index, color) + self._set_color(index, color) if transmit: - self.check_transmit(print_time) + self._check_transmit(print_time) if sync: #Sync LED Update with print time and send toolhead = self.printer.lookup_object('toolhead') @@ -72,112 +87,15 @@ def lookahead_bgfunc(print_time): else: #Send update now (so as not to wake toolhead and reset idle_timeout) lookahead_bgfunc(None) - def get_status(self, eventtime=None): - return {'color_data': self.led_state} - -# Main LED tracking code -class PrinterLED: - def __init__(self, config): - self.printer = config.get_printer() - self.led_helpers = {} - self.active_templates = {} - self.render_timer = None - # Load templates - dtemplates = display.lookup_display_templates(config) - self.templates = dtemplates.get_display_templates() - gcode_macro = self.printer.lookup_object("gcode_macro") - self.create_template_context = gcode_macro.create_template_context - # Register handlers - gcode = self.printer.lookup_object('gcode') - gcode.register_command("SET_LED_TEMPLATE", self.cmd_SET_LED_TEMPLATE, - desc=self.cmd_SET_LED_TEMPLATE_help) - def setup_helper(self, config, update_func, led_count=1): - led_helper = LEDHelper(config, update_func, led_count) - name = config.get_name().split()[-1] - self.led_helpers[name] = led_helper - return led_helper - def _activate_timer(self): - if self.render_timer is not None or not self.active_templates: - return - reactor = self.printer.get_reactor() - self.render_timer = reactor.register_timer(self._render, reactor.NOW) - def _activate_template(self, led_helper, index, template, lparams): - key = (led_helper, index) - if template is not None: - uid = (template,) + tuple(sorted(lparams.items())) - self.active_templates[key] = (uid, template, lparams) - return - if key in self.active_templates: - del self.active_templates[key] - def _render(self, eventtime): - if not self.active_templates: - # Nothing to do - unregister timer - reactor = self.printer.get_reactor() - reactor.register_timer(self.render_timer) - self.render_timer = None - return reactor.NEVER - # Setup gcode_macro template context - context = self.create_template_context(eventtime) - def render(name, **kwargs): - return self.templates[name].render(context, **kwargs) - context['render'] = render - # Render all templates - need_transmit = {} - rendered = {} - template_info = self.active_templates.items() - for (led_helper, index), (uid, template, lparams) in template_info: - color = rendered.get(uid) - if color is None: - try: - text = template.render(context, **lparams) - parts = [max(0., min(1., float(f))) - for f in text.split(',', 4)] - except Exception as e: - logging.exception("led template render error") - parts = [] - if len(parts) < 4: - parts += [0.] * (4 - len(parts)) - rendered[uid] = color = tuple(parts) - need_transmit[led_helper] = 1 - led_helper.set_color(index, color) - context.clear() # Remove circular references for better gc - # Transmit pending changes - for led_helper in need_transmit.keys(): - led_helper.check_transmit(None) - return eventtime + RENDER_TIME cmd_SET_LED_TEMPLATE_help = "Assign a display_template to an LED" def cmd_SET_LED_TEMPLATE(self, gcmd): - led_name = gcmd.get("LED") - led_helper = self.led_helpers.get(led_name) - if led_helper is None: - raise gcmd.error("Unknown LED '%s'" % (led_name,)) - led_count = led_helper.get_led_count() - index = gcmd.get_int("INDEX", None, minval=1, maxval=led_count) - template = None - lparams = {} - tpl_name = gcmd.get("TEMPLATE") - if tpl_name: - template = self.templates.get(tpl_name) - if template is None: - raise gcmd.error("Unknown display_template '%s'" % (tpl_name,)) - tparams = template.get_params() - for p, v in gcmd.get_command_parameters().items(): - if not p.startswith("PARAM_"): - continue - p = p.lower() - if p not in tparams: - raise gcmd.error("Invalid display_template parameter: %s" - % (p,)) - try: - lparams[p] = ast.literal_eval(v) - except ValueError as e: - raise gcmd.error("Unable to parse '%s' as a literal" % (v,)) + index = gcmd.get_int("INDEX", None, minval=1, maxval=self.led_count) + set_template = self.template_eval.set_template if index is not None: - self._activate_template(led_helper, index, template, lparams) + set_template(gcmd, self.tcallbacks[index-1], self._check_transmit) else: - for i in range(led_count): - self._activate_template(led_helper, i+1, template, lparams) - self._activate_timer() + for i in range(self.led_count): + set_template(gcmd, self.tcallbacks[i], self._check_transmit) PIN_MIN_TIME = 0.100 MAX_SCHEDULE_TIME = 5.0 @@ -205,8 +123,7 @@ def __init__(self, config): % (config.get_name(),)) self.last_print_time = 0. # Initialize color data - pled = printer.load_object(config, "led") - self.led_helper = pled.setup_helper(config, self.update_leds, 1) + self.led_helper = LEDHelper(config, self.update_leds, 1) self.prev_color = color = self.led_helper.get_status()['color_data'][0] for idx, mcu_pin in self.pins: mcu_pin.setup_start_value(color[idx], 0.) @@ -225,8 +142,5 @@ def update_leds(self, led_state, print_time): def get_status(self, eventtime=None): return self.led_helper.get_status(eventtime) -def load_config(config): - return PrinterLED(config) - def load_config_prefix(config): return PrinterPWMLED(config) diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py new file mode 100644 index 000000000000..14f3c2983f99 --- /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/neopixel.py b/klippy/extras/neopixel.py index b6daeb4d29e3..e72b8a91a188 100644 --- a/klippy/extras/neopixel.py +++ b/klippy/extras/neopixel.py @@ -4,6 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import logging +from . import led BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000 @@ -40,9 +41,7 @@ def __init__(self, config): if len(self.color_map) > MAX_MCU_SIZE: raise config.error("neopixel chain too long") # Initialize color data - pled = printer.load_object(config, "led") - self.led_helper = pled.setup_helper(config, self.update_leds, - chain_count) + self.led_helper = led.LEDHelper(config, self.update_leds, chain_count) self.color_data = bytearray(len(self.color_map)) self.update_color_data(self.led_helper.get_status()['color_data']) self.old_color_data = bytearray([d ^ 1 for d in self.color_data]) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index ef094674c1c1..24d27a62de4d 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -3,9 +3,180 @@ # Copyright (C) 2017-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. +import logging, ast +from .display import display + + +###################################################################### +# G-Code request queuing helper +###################################################################### PIN_MIN_TIME = 0.100 -RESEND_HOST_TIME = 0.300 + PIN_MIN_TIME + +# Helper code to queue g-code requests +class GCodeRequestQueue: + def __init__(self, config, mcu, callback): + self.printer = printer = config.get_printer() + self.mcu = mcu + self.callback = callback + self.rqueue = [] + self.next_min_flush_time = 0. + self.toolhead = None + mcu.register_flush_callback(self._flush_notification) + printer.register_event_handler("klippy:connect", self._handle_connect) + def _handle_connect(self): + self.toolhead = self.printer.lookup_object('toolhead') + def _flush_notification(self, print_time, clock): + rqueue = self.rqueue + while rqueue: + next_time = max(rqueue[0][0], self.next_min_flush_time) + if next_time > print_time: + return + # Skip requests that have been overridden with a following request + pos = 0 + while pos + 1 < len(rqueue) and rqueue[pos + 1][0] <= next_time: + pos += 1 + req_pt, req_val = rqueue[pos] + # Invoke callback for the request + min_wait = 0. + ret = self.callback(next_time, req_val) + if ret is not None: + # Handle special cases + action, min_wait = ret + if action == "discard": + del rqueue[:pos+1] + continue + if action == "delay": + pos -= 1 + del rqueue[:pos+1] + self.next_min_flush_time = next_time + max(min_wait, PIN_MIN_TIME) + # Ensure following queue items are flushed + self.toolhead.note_mcu_movequeue_activity(self.next_min_flush_time) + def _queue_request(self, print_time, value): + self.rqueue.append((print_time, value)) + self.toolhead.note_mcu_movequeue_activity(print_time) + def queue_gcode_request(self, value): + self.toolhead.register_lookahead_callback( + (lambda pt: self._queue_request(pt, value))) + def send_async_request(self, value, print_time=None): + if print_time is None: + systime = self.printer.get_reactor().monotonic() + print_time = self.mcu.estimated_print_time(systime + PIN_MIN_TIME) + while 1: + next_time = max(print_time, self.next_min_flush_time) + # Invoke callback for the request + action, min_wait = "normal", 0. + ret = self.callback(next_time, value) + if ret is not None: + # Handle special cases + action, min_wait = ret + if action == "discard": + break + self.next_min_flush_time = next_time + max(min_wait, PIN_MIN_TIME) + if action != "delay": + break + + +###################################################################### +# Template evaluation helper +###################################################################### + +# Time between each template update +RENDER_TIME = 0.500 + +# Main template evaluation code +class PrinterTemplateEvaluator: + def __init__(self, config): + self.printer = config.get_printer() + self.active_templates = {} + self.render_timer = None + # Load templates + dtemplates = display.lookup_display_templates(config) + self.templates = dtemplates.get_display_templates() + gcode_macro = self.printer.load_object(config, "gcode_macro") + self.create_template_context = gcode_macro.create_template_context + def _activate_timer(self): + if self.render_timer is not None or not self.active_templates: + return + reactor = self.printer.get_reactor() + self.render_timer = reactor.register_timer(self._render, reactor.NOW) + def _activate_template(self, callback, template, lparams, flush_callback): + if template is not None: + uid = (template,) + tuple(sorted(lparams.items())) + self.active_templates[callback] = ( + uid, template, lparams, flush_callback) + return + if callback in self.active_templates: + del self.active_templates[callback] + def _render(self, eventtime): + if not self.active_templates: + # Nothing to do - unregister timer + reactor = self.printer.get_reactor() + reactor.unregister_timer(self.render_timer) + self.render_timer = None + return reactor.NEVER + # Setup gcode_macro template context + context = self.create_template_context(eventtime) + def render(name, **kwargs): + return self.templates[name].render(context, **kwargs) + context['render'] = render + # Render all templates + flush_callbacks = {} + rendered = {} + template_info = self.active_templates.items() + for callback, (uid, template, lparams, flush_callback) in template_info: + text = rendered.get(uid) + if text is None: + try: + text = template.render(context, **lparams) + except Exception as e: + logging.exception("display template render error") + text = "" + rendered[uid] = text + if flush_callback is not None: + flush_callbacks[flush_callback] = 1 + callback(text) + context.clear() # Remove circular references for better gc + # Invoke optional flush callbacks + for flush_callback in flush_callbacks.keys(): + flush_callback() + return eventtime + RENDER_TIME + def set_template(self, gcmd, callback, flush_callback=None): + template = None + lparams = {} + tpl_name = gcmd.get("TEMPLATE") + if tpl_name: + template = self.templates.get(tpl_name) + if template is None: + raise gcmd.error("Unknown display_template '%s'" % (tpl_name,)) + tparams = template.get_params() + for p, v in gcmd.get_command_parameters().items(): + if not p.startswith("PARAM_"): + continue + p = p.lower() + if p not in tparams: + raise gcmd.error("Invalid display_template parameter: %s" + % (p,)) + try: + lparams[p] = ast.literal_eval(v) + except ValueError as e: + raise gcmd.error("Unable to parse '%s' as a literal" % (v,)) + self._activate_template(callback, template, lparams, flush_callback) + self._activate_timer() + +def lookup_template_eval(config): + printer = config.get_printer() + te = printer.lookup_object("template_evaluator", None) + if te is None: + te = PrinterTemplateEvaluator(config) + printer.add_object("template_evaluator", te) + return te + + +###################################################################### +# Main output pin handling +###################################################################### + MAX_SCHEDULE_TIME = 5.0 class PrinterOutputPin: @@ -24,30 +195,18 @@ def __init__(self, config): else: self.mcu_pin = ppins.setup_pin('digital_out', config.get('pin')) self.scale = 1. - self.last_print_time = 0. - # Support mcu checking for maximum duration - self.reactor = self.printer.get_reactor() - self.resend_timer = None - self.resend_interval = 0. - max_mcu_duration = config.getfloat('maximum_mcu_duration', 0., - minval=0.500, - maxval=MAX_SCHEDULE_TIME) - self.mcu_pin.setup_max_duration(max_mcu_duration) - if max_mcu_duration: - config.deprecate('maximum_mcu_duration') - self.resend_interval = max_mcu_duration - RESEND_HOST_TIME + self.mcu_pin.setup_max_duration(0.) # Determine start and shutdown values - static_value = config.getfloat('static_value', None, - minval=0., maxval=self.scale) - if static_value is not None: - config.deprecate('static_value') - self.last_value = self.shutdown_value = static_value / self.scale - else: - self.last_value = config.getfloat( - 'value', 0., minval=0., maxval=self.scale) / self.scale - self.shutdown_value = config.getfloat( - 'shutdown_value', 0., minval=0., maxval=self.scale) / self.scale + self.last_value = config.getfloat( + 'value', 0., minval=0., maxval=self.scale) / self.scale + self.shutdown_value = config.getfloat( + 'shutdown_value', 0., minval=0., maxval=self.scale) / self.scale self.mcu_pin.setup_start_value(self.last_value, self.shutdown_value) + # Create gcode request queue + self.gcrq = GCodeRequestQueue(config, self.mcu_pin.get_mcu(), + self._set_pin) + # Template handling + self.template_eval = lookup_template_eval(config) # Register commands pin_name = config.get_name().split()[1] gcode = self.printer.lookup_object('gcode') @@ -56,45 +215,36 @@ def __init__(self, config): desc=self.cmd_SET_PIN_help) def get_status(self, eventtime): return {'value': self.last_value} - def _set_pin(self, print_time, value, is_resend=False): - if value == self.last_value and not is_resend: - return - print_time = max(print_time, self.last_print_time + PIN_MIN_TIME) + def _set_pin(self, print_time, value): + if value == self.last_value: + return "discard", 0. + self.last_value = value if self.is_pwm: self.mcu_pin.set_pwm(print_time, value) else: self.mcu_pin.set_digital(print_time, value) - self.last_value = value - self.last_print_time = print_time - if self.resend_interval and self.resend_timer is None: - self.resend_timer = self.reactor.register_timer( - self._resend_current_val, self.reactor.NOW) + def _template_update(self, text): + try: + value = float(text) + except ValueError as e: + logging.exception("output_pin template render error") + self.gcrq.send_async_request(value) cmd_SET_PIN_help = "Set the value of an output pin" def cmd_SET_PIN(self, gcmd): + value = gcmd.get_float('VALUE', None, minval=0., maxval=self.scale) + template = gcmd.get('TEMPLATE', None) + if (value is None) == (template is None): + raise gcmd.error("SET_PIN command must specify VALUE or TEMPLATE") + # Check for template setting + if template is not None: + self.template_eval.set_template(gcmd, self._template_update) + return # Read requested value - value = gcmd.get_float('VALUE', minval=0., maxval=self.scale) value /= self.scale if not self.is_pwm and value not in [0., 1.]: raise gcmd.error("Invalid pin value") - # Obtain print_time and apply requested settings - toolhead = self.printer.lookup_object('toolhead') - toolhead.register_lookahead_callback( - lambda print_time: self._set_pin(print_time, value)) - - def _resend_current_val(self, eventtime): - if self.last_value == self.shutdown_value: - self.reactor.unregister_timer(self.resend_timer) - self.resend_timer = None - return self.reactor.NEVER - - systime = self.reactor.monotonic() - print_time = self.mcu_pin.get_mcu().estimated_print_time(systime) - time_diff = (self.last_print_time + self.resend_interval) - print_time - if time_diff > 0.: - # Reschedule for resend time - return systime + time_diff - self._set_pin(print_time + PIN_MIN_TIME, self.last_value, True) - return systime + self.resend_interval + # Queue requested value + self.gcrq.queue_gcode_request(value) def load_config_prefix(config): return PrinterOutputPin(config) diff --git a/klippy/extras/pca9533.py b/klippy/extras/pca9533.py index f84f9a65ad37..a94e1334e056 100644 --- a/klippy/extras/pca9533.py +++ b/klippy/extras/pca9533.py @@ -4,7 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import logging -from . import bus +from . import bus, led BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000 @@ -16,8 +16,7 @@ class PCA9533: def __init__(self, config): self.printer = config.get_printer() self.i2c = bus.MCU_I2C_from_config(config, default_addr=98) - pled = self.printer.load_object(config, "led") - self.led_helper = pled.setup_helper(config, self.update_leds, 1) + self.led_helper = led.LEDHelper(config, self.update_leds, 1) self.i2c.i2c_write([PCA9533_PWM0, 85]) self.i2c.i2c_write([PCA9533_PWM1, 170]) self.update_leds(self.led_helper.get_status()['color_data'], None) diff --git a/klippy/extras/pca9632.py b/klippy/extras/pca9632.py index 00876dc18aca..8a3551cf242a 100644 --- a/klippy/extras/pca9632.py +++ b/klippy/extras/pca9632.py @@ -3,7 +3,7 @@ # Copyright (C) 2022 Ricardo Alcantara # # This file may be distributed under the terms of the GNU GPLv3 license. -from . import bus, mcp4018 +from . import bus, led, mcp4018 BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000 @@ -34,8 +34,7 @@ def __init__(self, config): raise config.error("Invalid color_order '%s'" % (color_order,)) self.color_map = ["RGBW".index(c) for c in color_order] self.prev_regs = {} - pled = printer.load_object(config, "led") - self.led_helper = pled.setup_helper(config, self.update_leds, 1) + self.led_helper = led.LEDHelper(config, self.update_leds, 1) printer.register_event_handler("klippy:connect", self.handle_connect) def reg_write(self, reg, val, minclock=0): if self.prev_regs.get(reg) == val: diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index de1f84476f05..932d1bfa3fed 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -14,6 +14,7 @@ class EddyCalibration: def __init__(self, config): self.printer = config.get_printer() self.name = config.get_name() + self.drift_comp = DummyDriftCompensation() # Current calibration data self.cal_freqs = [] self.cal_zpos = [] @@ -37,8 +38,10 @@ def load_calibration(self, cal): self.cal_freqs = [c[0] for c in cal] self.cal_zpos = [c[1] for c in cal] def apply_calibration(self, samples): + cur_temp = self.drift_comp.get_temperature() for i, (samp_time, freq, dummy_z) in enumerate(samples): - pos = bisect.bisect(self.cal_freqs, freq) + adj_freq = self.drift_comp.adjust_freq(freq, cur_temp) + pos = bisect.bisect(self.cal_freqs, adj_freq) if pos >= len(self.cal_zpos): zpos = -OUT_OF_RANGE elif pos == 0: @@ -51,7 +54,7 @@ def apply_calibration(self, samples): prev_zpos = self.cal_zpos[pos - 1] gain = (this_zpos - prev_zpos) / (this_freq - prev_freq) offset = prev_zpos - prev_freq * gain - zpos = freq * gain + offset + zpos = adj_freq * gain + offset samples[i] = (samp_time, freq, round(zpos, 6)) def freq_to_height(self, freq): dummy_sample = [(0., freq, 0.)] @@ -71,7 +74,8 @@ def height_to_freq(self, height): prev_zpos = rev_zpos[pos - 1] gain = (this_freq - prev_freq) / (this_zpos - prev_zpos) offset = prev_freq - prev_zpos * gain - return height * gain + offset + freq = height * gain + offset + return self.drift_comp.unadjust_freq(freq) def do_calibration_moves(self, move_speed): toolhead = self.printer.lookup_object('toolhead') kin = toolhead.get_kinematics() @@ -86,6 +90,7 @@ def handle_batch(msg): return True self.printer.lookup_object(self.name).add_client(handle_batch) toolhead.dwell(1.) + self.drift_comp.note_z_calibration_start() # Move to each 40um position max_z = 4.0 samp_dist = 0.040 @@ -112,6 +117,7 @@ def handle_batch(msg): times.append((start_query_time, end_query_time, kin_pos[2])) toolhead.dwell(1.0) toolhead.wait_moves() + self.drift_comp.note_z_calibration_finish() # Finish data collection is_finished = True # Correlate query responses @@ -188,6 +194,8 @@ def cmd_EDDY_CALIBRATE(self, gcmd): # Start manual probe manual_probe.ManualProbeHelper(self.printer, gcmd, self.post_manual_probe) + def register_drift_compensation(self, comp): + self.drift_comp = comp # Tool to gather samples and convert them to probe positions class EddyGatherSamples: @@ -265,16 +273,18 @@ def _check_samples(self): freq = self._pull_freq(start_time, end_time) if pos_time is not None: toolhead_pos = self._lookup_toolhead_pos(pos_time) - self._probe_results.append((freq, toolhead_pos)) + sensor_z = None + if freq: + sensor_z = self._calibration.freq_to_height(freq) + self._probe_results.append((sensor_z, toolhead_pos)) self._probe_times.pop(0) def pull_probed(self): self._await_samples() results = [] - for freq, toolhead_pos in self._probe_results: - if not freq: + for sensor_z, toolhead_pos in self._probe_results: + if sensor_z is None: raise self._printer.command_error( "Unable to obtain probe_eddy_current sensor readings") - sensor_z = self._calibration.freq_to_height(freq) if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: raise self._printer.command_error( "probe_eddy_current sensor not in valid range") @@ -373,15 +383,27 @@ def __init__(self, printer, sensor_helper, calibration, z_offset, gcmd): self._gather = EddyGatherSamples(printer, sensor_helper, calibration, z_offset) self._sample_time_delay = 0.050 - self._sample_time = 0.100 + self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) + self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' + def _rapid_lookahead_cb(self, printtime): + start_time = printtime - self._sample_time / 2 + self._gather.note_probe_and_position( + start_time, start_time + self._sample_time, printtime) def run_probe(self, gcmd): toolhead = self._printer.lookup_object("toolhead") + if self._is_rapid: + toolhead.register_lookahead_callback(self._rapid_lookahead_cb) + return printtime = toolhead.get_last_move_time() toolhead.dwell(self._sample_time_delay + self._sample_time) start_time = printtime + self._sample_time_delay self._gather.note_probe_and_position( start_time, start_time + self._sample_time, start_time) def pull_probed_results(self): + if self._is_rapid: + # Flush lookahead (so all lookahead callbacks are invoked) + toolhead = self._printer.lookup_object("toolhead") + toolhead.get_last_move_time() results = self._gather.pull_probed() # Allow axis_twist_compensation to update results for epos in results: @@ -418,11 +440,25 @@ def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): method = gcmd.get('METHOD', 'automatic').lower() - if method == 'scan': + 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/servo.py b/klippy/extras/servo.py index c05c9f819353..f1ce997638f5 100644 --- a/klippy/extras/servo.py +++ b/klippy/extras/servo.py @@ -1,11 +1,11 @@ # Support for servos # -# Copyright (C) 2017-2020 Kevin O'Connor +# Copyright (C) 2017-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. +from . import output_pin SERVO_SIGNAL_PERIOD = 0.020 -PIN_MIN_TIME = 0.100 class PrinterServo: def __init__(self, config): @@ -18,7 +18,7 @@ def __init__(self, config): self.max_angle = config.getfloat('maximum_servo_angle', 180.) self.angle_to_width = (self.max_width - self.min_width) / self.max_angle self.width_to_value = 1. / SERVO_SIGNAL_PERIOD - self.last_value = self.last_value_time = 0. + self.last_value = 0. initial_pwm = 0. iangle = config.getfloat('initial_angle', None, minval=0., maxval=360.) if iangle is not None: @@ -33,6 +33,9 @@ def __init__(self, config): self.mcu_servo.setup_max_duration(0.) self.mcu_servo.setup_cycle_time(SERVO_SIGNAL_PERIOD) self.mcu_servo.setup_start_value(initial_pwm, 0.) + # Create gcode request queue + self.gcrq = output_pin.GCodeRequestQueue( + config, self.mcu_servo.get_mcu(), self._set_pwm) # Register commands servo_name = config.get_name().split()[1] gcode = self.printer.lookup_object('gcode') @@ -43,11 +46,9 @@ def get_status(self, eventtime): return {'value': self.last_value} def _set_pwm(self, print_time, value): if value == self.last_value: - return - print_time = max(print_time, self.last_value_time + PIN_MIN_TIME) - self.mcu_servo.set_pwm(print_time, value) + return "discard", 0. self.last_value = value - self.last_value_time = print_time + self.mcu_servo.set_pwm(print_time, value) def _get_pwm_from_angle(self, angle): angle = max(0., min(self.max_angle, angle)) width = self.min_width + angle * self.angle_to_width @@ -58,13 +59,13 @@ def _get_pwm_from_pulse_width(self, width): 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) + self.gcrq.queue_gcode_request(value) def load_config_prefix(config): return PrinterServo(config) diff --git a/klippy/extras/sht3x.py b/klippy/extras/sht3x.py index 699d3f2095f4..5a8785e8d3b2 100644 --- a/klippy/extras/sht3x.py +++ b/klippy/extras/sht3x.py @@ -27,6 +27,13 @@ '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 @@ def get_report_time_delta(self): 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 @@ def _init_sht3x(self): 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 726531421ac9..6e5867893183 100644 --- a/klippy/extras/smart_effector.py +++ b/klippy/extras/smart_effector.py @@ -64,6 +64,7 @@ def __init__(self, config): 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) diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index 3bfecb7831c3..070b913353d6 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -38,19 +38,17 @@ def __init__(self, config): REG_INPUT_DISABLE : 0, REG_ANALOG_DRIVER_ENABLE : 0} self.reg_i_on_dict = {reg : 0 for reg in REG_I_ON} def _build_config(self): - # Reset the chip + # Reset the chip, Default RegClock/RegMisc 0x0 self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % ( self._oid, REG_RESET, 0x12)) self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % ( self._oid, REG_RESET, 0x34)) # Enable Oscillator - self._mcu.add_config_cmd("i2c_modify_bits oid=%d reg=%02x" - " clear_set_bits=%02x%02x" % ( - self._oid, REG_CLOCK, 0, (1 << 6))) + self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % ( + self._oid, REG_CLOCK, (1 << 6))) # Setup Clock Divider - self._mcu.add_config_cmd("i2c_modify_bits oid=%d reg=%02x" - " clear_set_bits=%02x%02x" % ( - self._oid, REG_MISC, 0, (1 << 4))) + self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % ( + self._oid, REG_MISC, (1 << 4))) # Transfer all regs with their initial cached state for _reg, _data in self.reg_dict.items(): self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%04x" % ( diff --git a/klippy/extras/temperature_fan.py b/klippy/extras/temperature_fan.py index aee94f281594..a9aa4d0bab52 100644 --- a/klippy/extras/temperature_fan.py +++ b/klippy/extras/temperature_fan.py @@ -46,7 +46,7 @@ def __init__(self, config): self.cmd_SET_TEMPERATURE_FAN_TARGET, desc=self.cmd_SET_TEMPERATURE_FAN_TARGET_help) - def set_speed(self, read_time, value): + def set_tf_speed(self, read_time, value): if value <= 0.: value = 0. elif value < self.min_speed: @@ -60,7 +60,7 @@ def set_speed(self, read_time, value): speed_time = read_time + self.speed_delay self.next_speed_time = speed_time + 0.75 * MAX_FAN_TIME self.last_speed_value = value - self.fan.set_speed(speed_time, value) + self.fan.set_speed(value, speed_time) def temperature_callback(self, read_time, temp): self.last_temp = temp self.control.temperature_callback(read_time, temp) @@ -128,10 +128,10 @@ def temperature_callback(self, read_time, temp): and temp <= target_temp-self.max_delta): self.heating = True if self.heating: - self.temperature_fan.set_speed(read_time, 0.) + self.temperature_fan.set_tf_speed(read_time, 0.) else: - self.temperature_fan.set_speed(read_time, - self.temperature_fan.get_max_speed()) + self.temperature_fan.set_tf_speed( + read_time, self.temperature_fan.get_max_speed()) ###################################################################### # Proportional Integral Derivative (PID) control algo @@ -171,7 +171,7 @@ def temperature_callback(self, read_time, temp): # Calculate output co = self.Kp*temp_err + self.Ki*temp_integ - self.Kd*temp_deriv bounded_co = max(0., min(self.temperature_fan.get_max_speed(), co)) - self.temperature_fan.set_speed( + self.temperature_fan.set_tf_speed( read_time, max(self.temperature_fan.get_min_speed(), self.temperature_fan.get_max_speed() - bounded_co)) # Store state for next measurement diff --git a/klippy/extras/temperature_mcu.py b/klippy/extras/temperature_mcu.py index 585ec4c1d20d..be2cd145c455 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 @@ def __init__(self, config): 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 @@ def _mcu_identify(self): 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 000000000000..05eac34ef984 --- /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/tsl1401cl_filament_width_sensor.py b/klippy/extras/tsl1401cl_filament_width_sensor.py index fb2d97131130..83480f46714a 100644 --- a/klippy/extras/tsl1401cl_filament_width_sensor.py +++ b/klippy/extras/tsl1401cl_filament_width_sensor.py @@ -33,7 +33,7 @@ def __init__(self, config): # 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 7d980585ff25..15ab624aafaa 100644 --- a/klippy/gcode.py +++ b/klippy/gcode.py @@ -406,7 +406,7 @@ def _handle_shutdown(self): 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/extruder.py b/klippy/kinematics/extruder.py index 6924003783e8..7fb2e7ed543f 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -18,7 +18,7 @@ def __init__(self, config): 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 @@ def _set_pressure_advance(self, pressure_advance, smooth_time): 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/idex_modes.py b/klippy/kinematics/idex_modes.py index f2618d0805a7..2f2da4168358 100644 --- a/klippy/kinematics/idex_modes.py +++ b/klippy/kinematics/idex_modes.py @@ -4,7 +4,7 @@ # Copyright (C) 2023 Dmitry Butyugin # # This file may be distributed under the terms of the GNU GPLv3 license. -import math +import logging, math import chelper INACTIVE = 'INACTIVE' @@ -202,14 +202,31 @@ def cmd_RESTORE_DUAL_CARRIAGE_STATE(self, gcmd): move_speed = gcmd.get_float('MOVE_SPEED', 0., above=0.) toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() - pos = toolhead.get_position() if gcmd.get_int('MOVE', 1): + homing_speed = 99999999. + cur_pos = [] for i, dc in enumerate(self.dc): self.toggle_active_dc_rail(i) - saved_pos = saved_state['axes_positions'][i] - toolhead.manual_move( - pos[:self.axis] + [saved_pos] + pos[self.axis+1:], - move_speed or dc.get_rail().homing_speed) + homing_speed = min(homing_speed, dc.get_rail().homing_speed) + cur_pos.append(toolhead.get_position()) + move_pos = list(cur_pos[0]) + dl = [saved_state['axes_positions'][i] - cur_pos[i][self.axis] + for i in range(2)] + primary_ind = 0 if abs(dl[0]) >= abs(dl[1]) else 1 + self.toggle_active_dc_rail(primary_ind) + move_pos[self.axis] = saved_state['axes_positions'][primary_ind] + dc_mode = INACTIVE if min(abs(dl[0]), abs(dl[1])) < 0.000000001 \ + else COPY if dl[0] * dl[1] > 0 else MIRROR + if dc_mode != INACTIVE: + self.dc[1-primary_ind].activate(dc_mode, cur_pos[primary_ind]) + self.dc[1-primary_ind].override_axis_scaling( + abs(dl[1-primary_ind] / dl[primary_ind]), + cur_pos[primary_ind]) + toolhead.manual_move(move_pos, move_speed or homing_speed) + toolhead.flush_step_generation() + # Make sure the scaling coefficients are restored with the mode + self.dc[0].inactivate(move_pos) + self.dc[1].inactivate(move_pos) for i, dc in enumerate(self.dc): saved_mode = saved_state['carriage_modes'][i] self.activate_dc_mode(i, saved_mode) @@ -257,3 +274,8 @@ def inactivate(self, position): 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 097cff998c22..75ee6887ad71 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 @@ 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 @@ def _set_state(self, msg): 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 @@ def _read_config(self): 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 @@ def _connect(self, eventtime): 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 @@ def set_rollover_info(self, name, info, log=True): 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 @@ def invoke_shutdown(self, msg): 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 23ba07173257..eb71e6bc2a97 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 @@ -496,8 +496,8 @@ def __init__(self, mcu, pin_params): 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 @@ -605,6 +605,7 @@ def __init__(self, config, clocksync): self._mcu_tick_stddev = 0. self._mcu_tick_awake = 0. # Register handlers + printer.load_object(config, "error_mcu") printer.register_event_handler("klippy:firmware_restart", self._firmware_restart) printer.register_event_handler("klippy:mcu_identify", @@ -631,13 +632,13 @@ def _handle_shutdown(self, params): if clock is not None: self._shutdown_clock = self.clock32_to_clock64(clock) self._shutdown_msg = msg = params['static_string_id'] - 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" @@ -831,9 +832,10 @@ def _ready(self): systime = self._reactor.monotonic() get_clock = self._clocksync.get_clock calc_freq = get_clock(systime + 1) - get_clock(systime) + freq_diff = abs(mcu_freq - calc_freq) mcu_freq_mhz = int(mcu_freq / 1000000. + 0.5) calc_freq_mhz = int(calc_freq / 1000000. + 0.5) - if mcu_freq_mhz != calc_freq_mhz: + if freq_diff > mcu_freq*0.01 and mcu_freq_mhz != calc_freq_mhz: pconfig = self._printer.lookup_object('configfile') msg = ("MCU '%s' configured for %dMhz but running at %dMhz!" % (self._name, mcu_freq_mhz, calc_freq_mhz)) @@ -1008,34 +1010,6 @@ def stats(self, eventtime): 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/serialhdl.py b/klippy/serialhdl.py index 6aee564814d9..30db617074d7 100644 --- a/klippy/serialhdl.py +++ b/klippy/serialhdl.py @@ -136,7 +136,7 @@ def connect_canbus(self, canbus_uuid, canbus_nodeid, canbus_iface="can0"): 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 9b692904dcf9..05e56cca4327 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -138,8 +138,10 @@ def set_position(self, coord): def get_commanded_position(self): ffi_main, ffi_lib = chelper.get_ffi() return ffi_lib.itersolve_get_commanded_pos(self._stepper_kinematics) - def get_mcu_position(self): - mcu_pos_dist = self.get_commanded_position() + self._mcu_position_offset + def get_mcu_position(self, cmd_pos=None): + if cmd_pos is None: + cmd_pos = self.get_commanded_position() + mcu_pos_dist = cmd_pos + self._mcu_position_offset mcu_pos = mcu_pos_dist / self._step_dist if mcu_pos >= 0.: return int(mcu_pos + 0.5) diff --git a/scripts/flash_usb.py b/scripts/flash_usb.py index 33c437602d20..e5f5632a45a9 100755 --- a/scripts/flash_usb.py +++ b/scripts/flash_usb.py @@ -198,7 +198,7 @@ def flash_picoboot(device, binfile, sudo): # We need one level up to get access to busnum/devnum files usbdir = os.path.dirname(devpath) enter_bootloader(device) - wait_path(usbdir) + wait_path(usbdir + "/busnum") with open(usbdir + "/busnum") as f: bus = f.read().strip() with open(usbdir + "/devnum") as f: diff --git a/scripts/graph_mesh.py b/scripts/graph_mesh.py new file mode 100755 index 000000000000..3a331e5d5dca --- /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/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py index 4f84d7229c8e..9924fefcd70e 100644 --- a/scripts/spi_flash/board_defs.py +++ b/scripts/spi_flash/board_defs.py @@ -31,6 +31,11 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 a3231b693219..cbe769e573f1 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/scripts/whconsole.py b/scripts/whconsole.py index ba77ae9bba9c..5e76b3bcea61 100755 --- a/scripts/whconsole.py +++ b/scripts/whconsole.py @@ -38,25 +38,25 @@ def __init__(self, uds_filename): self.poll = select.poll() self.poll.register(sys.stdin, select.POLLIN | select.POLLHUP) self.poll.register(self.webhook_socket, select.POLLIN | select.POLLHUP) - self.kbd_data = self.socket_data = "" + self.kbd_data = self.socket_data = b"" def process_socket(self): data = self.webhook_socket.recv(4096) if not data: sys.stderr.write("Socket closed\n") sys.exit(0) - parts = data.split('\x03') + parts = data.split(b'\x03') parts[0] = self.socket_data + parts[0] self.socket_data = parts.pop() for line in parts: sys.stdout.write("GOT: %s\n" % (line,)) def process_kbd(self): data = os.read(self.kbd_fd, 4096) - parts = data.split('\n') + parts = data.split(b'\n') parts[0] = self.kbd_data + parts[0] self.kbd_data = parts.pop() for line in parts: line = line.strip() - if not line or line.startswith('#'): + if not line or line.startswith(b'#'): continue try: m = json.loads(line) @@ -65,7 +65,7 @@ def process_kbd(self): continue cm = json.dumps(m, separators=(',', ':')) sys.stdout.write("SEND: %s\n" % (cm,)) - self.webhook_socket.send("%s\x03" % (cm,)) + self.webhook_socket.send(cm.encode() + b"\x03") def run(self): while 1: res = self.poll.poll(1000.) diff --git a/src/Kconfig b/src/Kconfig index 7dcea3bab59d..5b42467cb763 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -13,11 +13,11 @@ choice config MACH_AVR bool "Atmega AVR" config MACH_ATSAM - bool "SAM3/SAM4/SAM E70 (Due and Duet)" + bool "SAM3/SAM4/SAM E70" config MACH_ATSAMD bool "SAMC21/SAMD21/SAMD51/SAME5x" config MACH_LPC176X - bool "LPC176x (Smoothieboard)" + bool "LPC176x" config MACH_STM32 bool "STMicroelectronics STM32" config MACH_HC32F460 @@ -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 ed98172e4e8d..86c7407e687f 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/Kconfig b/src/atsam/Kconfig index 4a10a0f7d162..4c20c2440064 100644 --- a/src/atsam/Kconfig +++ b/src/atsam/Kconfig @@ -22,19 +22,19 @@ config BOARD_DIRECTORY choice prompt "Processor model" config MACH_SAM3X8E - bool "SAM3x8e (Arduino Due)" + bool "SAM3x8e" select MACH_SAM3X config MACH_SAM3X8C - bool "SAM3x8c (Printrboard G2)" + bool "SAM3x8c" select MACH_SAM3X config MACH_SAM4S8C - bool "SAM4s8c (Duet Maestro)" + bool "SAM4s8c" select MACH_SAM4S config MACH_SAM4E8E - bool "SAM4e8e (Duet Wifi/Eth)" + bool "SAM4e8e" select MACH_SAM4E config MACH_SAME70Q20B - bool "SAME70Q20B (Duet 3 6HC)" + bool "SAME70Q20B" select MACH_SAME70 endchoice @@ -70,6 +70,7 @@ config CLOCK_FREQ config FLASH_SIZE hex + default 0x20000 if MACH_SAME70 default 0x80000 config FLASH_BOOT_ADDRESS @@ -84,8 +85,7 @@ config RAM_START config RAM_SIZE hex default 0x18000 if MACH_SAM3X - default 0x20000 if MACH_SAM4 - default 0x40000 if MACH_SAME70 + default 0x20000 if MACH_SAM4 || MACH_SAME70 config STACK_SIZE int @@ -93,9 +93,25 @@ config STACK_SIZE config FLASH_APPLICATION_ADDRESS hex - default 0x400000 if MACH_SAM4 || MACH_SAME70 + default 0x0 if MACH_SAME70 + default 0x400000 if MACH_SAM4 default 0x80000 +config ARMCM_ITCM_FLASH_MIRROR_START + depends on MACH_SAME70 + hex + default 0x400000 + +config ARMCM_DTCM_START + depends on MACH_SAME70 + hex + default 0x20000000 + +config ARMCM_DTCM_SIZE + depends on MACH_SAME70 + hex + default 0x20000 + choice prompt "Communication interface" config ATSAM_USB diff --git a/src/atsam/Makefile b/src/atsam/Makefile index 3595d0cef40f..15bef326201d 100644 --- a/src/atsam/Makefile +++ b/src/atsam/Makefile @@ -20,9 +20,10 @@ 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 += -nostdlib -lgcc -lc_nano -CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld -$(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld +samlink-y := $(OUT)src/generic/armcm_link.ld +samlink-$(CONFIG_MACH_SAME70) := $(OUT)src/atsam/same70_link.ld +CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano -T $(samlink-y) +$(OUT)klipper.elf: $(samlink-y) # Add source files src-y += atsam/main.c atsam/gpio.c atsam/i2c.c atsam/spi.c diff --git a/src/atsam/fdcan.c b/src/atsam/fdcan.c index a536a7be1ed6..deeb607063c1 100644 --- a/src/atsam/fdcan.c +++ b/src/atsam/fdcan.c @@ -71,8 +71,8 @@ struct fdcan_msg_ram { struct fdcan_fifo TXFIFO[3]; }; -// Message ram is in regular memory -static struct fdcan_msg_ram MSG_RAM; +// Message ram is in DTCM - locate it there to avoid cache. +static struct fdcan_msg_ram MSG_RAM __section(".dtcm.bss"); /**************************************************************** diff --git a/src/atsam/gpio.h b/src/atsam/gpio.h index e9fc4c3ed040..638d02da4d10 100644 --- a/src/atsam/gpio.h +++ b/src/atsam/gpio.h @@ -50,8 +50,8 @@ struct i2c_config { }; struct i2c_config i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr); -void i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); -void i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg - , uint8_t read_len, uint8_t *read); +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg + , uint8_t read_len, uint8_t *read); #endif // gpio.h diff --git a/src/atsam/i2c.c b/src/atsam/i2c.c index 0661727c274e..8c3dc8ad2428 100644 --- a/src/atsam/i2c.c +++ b/src/atsam/i2c.c @@ -10,6 +10,7 @@ #include "gpio.h" // i2c_setup #include "internal.h" // gpio_peripheral #include "sched.h" // sched_shutdown +#include "i2ccmds.h" // I2C_BUS_SUCCESS #if CONFIG_MACH_SAME70 #include "same70_i2c.h" // Fixes for upstream header changes @@ -126,7 +127,7 @@ i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr) return (struct i2c_config){ .twi=p_twi, .addr=addr}; } -void +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) { Twi *p_twi = config.twi; @@ -150,9 +151,11 @@ i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) p_twi->TWI_CR = TWI_CR_STOP; while (!(p_twi->TWI_SR & TWI_SR_TXCOMP)) ; + + return I2C_BUS_SUCCESS; } -void +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *read) { @@ -192,4 +195,6 @@ i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg while (!(p_twi->TWI_SR & TWI_SR_TXCOMP)) ; (void)p_twi->TWI_SR; + + return I2C_BUS_SUCCESS; } diff --git a/src/atsam/sam3_usb.c b/src/atsam/sam3_usb.c index a76d7c751a56..a26a6961b8b7 100644 --- a/src/atsam/sam3_usb.c +++ b/src/atsam/sam3_usb.c @@ -32,6 +32,7 @@ usb_write_packet(uint32_t ep, const uint8_t *data, uint32_t len) uint8_t *dest = usb_fifo(ep); while (len--) *dest++ = *data++; + __DMB(); } static void diff --git a/src/atsam/same70_link.lds.S b/src/atsam/same70_link.lds.S new file mode 100644 index 000000000000..8dd5e1b3bdbd --- /dev/null +++ b/src/atsam/same70_link.lds.S @@ -0,0 +1,81 @@ +// Generic ARM Cortex-M linker script +// +// Copyright (C) 2019-2024 Kevin O'Connor +// Copyright (C) 2024 Luke Vuksta +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include "autoconf.h" // CONFIG_FLASH_APPLICATION_ADDRESS + +OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") +OUTPUT_ARCH(arm) + +MEMORY +{ + rom (rx) : ORIGIN = CONFIG_FLASH_APPLICATION_ADDRESS , LENGTH = CONFIG_FLASH_SIZE + ram (rwx) : ORIGIN = CONFIG_RAM_START , LENGTH = CONFIG_RAM_SIZE + dtcm (rw) : ORIGIN = CONFIG_ARMCM_DTCM_START , LENGTH = CONFIG_ARMCM_DTCM_SIZE +} + +SECTIONS +{ + .text : AT (CONFIG_ARMCM_ITCM_FLASH_MIRROR_START) { + . = ALIGN(4); + _text_vectortable_start = .; + KEEP(*(.vector_table)) + _text_vectortable_end = .; + *(.text .text.*) + *(.ramfunc .ramfunc.*) + *(.rodata .rodata*) + } > rom + + _text_size = SIZEOF (.text); + . = CONFIG_ARMCM_ITCM_FLASH_MIRROR_START + _text_size; + . = ALIGN(4); + _data_flash = .; + + .data : AT (_data_flash) + { + . = ALIGN(4); + _data_start = .; + *(.data .data.*) + . = ALIGN(4); + _data_end = .; + } > ram + + .bss (NOLOAD) : + { + . = ALIGN(4); + _bss_start = .; + *(.bss .bss.*) + *(COMMON) + . = ALIGN(4); + _bss_end = .; + } > ram + + _stack_start = CONFIG_RAM_START + CONFIG_RAM_SIZE - CONFIG_STACK_SIZE ; + .stack _stack_start (NOLOAD) : + { + . = . + CONFIG_STACK_SIZE; + _stack_end = .; + } > ram + + .dtcm_bss (NOLOAD) : + { + . = ALIGN(4); + _dtcm_bss_start = .; + *(.dtcm.bss) + . = ALIGN(4); + _dtcm_bss_end = .; + } > dtcm + + /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/atsam/same70_sysinit.c b/src/atsam/same70_sysinit.c index 4cb5f48cc2df..d2b6ecb4715c 100644 --- a/src/atsam/same70_sysinit.c +++ b/src/atsam/same70_sysinit.c @@ -1,5 +1,6 @@ // This code is from lib/sam4e/gcc/system_sam4e.c and modified for the SAM E70 +#include // memset #include "internal.h" /* Clock Settings (300MHz) */ @@ -9,13 +10,17 @@ | CKGR_PLLAR_PLLACOUNT(0x3fU) \ | CKGR_PLLAR_DIVA_BYPASS) #define SYS_BOARD_MCKR (PMC_MCKR_MDIV_PCK_DIV2 | PMC_MCKR_CSS_PLLA_CLK) +#define RST_PARAMS ((0xA5 << RSTC_CR_KEY_Pos) | RSTC_CR_PROCRST) +#define GPNVM_TCM_MASK ((1 << 7) | (1 << 8)) /* Key to unlock MOR register */ #define SYS_CKGR_MOR_KEY_VALUE CKGR_MOR_KEY(0x37) -uint32_t SystemCoreClock = CHIP_FREQ_MAINCK_RC_12MHZ; +extern uint32_t _text_size; +extern uint32_t _dtcm_bss_start, _dtcm_bss_end; -void SystemInit( void ) +void +SystemInit( void ) { /* Set FWS according to SYS_BOARD_MCKR configuration */ EFC->EEFC_FMR = EEFC_FMR_FWS(6) | EEFC_FMR_CLOE; @@ -66,12 +71,84 @@ void SystemInit( void ) { } - SystemCoreClock = CHIP_FREQ_CPU_MAX; - - // Configure PCK6 for TC use + /* Configure PCK6 for TC use */ PMC->PMC_PCK[6] = PMC_PCK_CSS_MCK | PMC_PCK_PRES(2); while ( !(PMC->PMC_SR & PMC_SR_PCKRDY6) ) { } PMC->PMC_SCER |= PMC_SCER_PCK6; + + /* Check Tightly Coupled Memory (TCM) bits. */ + EFC->EEFC_FCR = (EEFC_FCR_FKEY_PASSWD | EEFC_FCR_FCMD_GGPB); + while ((EFC->EEFC_FSR & EEFC_FSR_FRDY) == 0) + ; + if ((EFC->EEFC_FRR & GPNVM_TCM_MASK) != GPNVM_TCM_MASK) + { + /* Configure TCM sizes to 128KiB (set GPNVM7 and GPNVM8). */ + EFC->EEFC_FCR = (EEFC_FCR_FKEY_PASSWD | EEFC_FCR_FCMD_SGPB + | EEFC_FCR_FARG(7)); + while ((EFC->EEFC_FSR & EEFC_FSR_FRDY) == 0) + ; + EFC->EEFC_FCR = (EEFC_FCR_FKEY_PASSWD| EEFC_FCR_FCMD_SGPB + | EEFC_FCR_FARG(8)); + while ((EFC->EEFC_FSR & EEFC_FSR_FRDY) == 0) + ; + /* Reboot required, but bits are set now and we will not + * return down this path. */ + __DSB(); + __ISB(); + RSTC->RSTC_CR = RST_PARAMS; + for (;;) + ; + } + + /* Clear Data Tightly Coupled Memory (DTCM) bss segment - this has to happen + * after we check that the DTCM is enabled. */ + memset(&_dtcm_bss_start, 0, (&_dtcm_bss_end - &_dtcm_bss_start) * 4); + + /* DMA copy flash to Instruction Tightly Coupled Memory (ITCM). Just use + * channel 0 since we have not done anything yet. */ + enable_pclock(ID_XDMAC); + /* Clear pending interrupts. */ + (void)REG_XDMAC_CIS0; + REG_XDMAC_CSA0 = CONFIG_ARMCM_ITCM_FLASH_MIRROR_START; + REG_XDMAC_CDA0 = CONFIG_FLASH_APPLICATION_ADDRESS; + REG_XDMAC_CUBC0 = XDMAC_CUBC_UBLEN((int)&_text_size); + + REG_XDMAC_CC0 = + XDMAC_CC_TYPE_MEM_TRAN | XDMAC_CC_MBSIZE_SINGLE | + XDMAC_CC_CSIZE_CHK_1 | XDMAC_CC_DWIDTH_WORD | + XDMAC_CC_SAM_INCREMENTED_AM | XDMAC_CC_DAM_INCREMENTED_AM | + XDMAC_CC_SIF_AHB_IF1 | XDMAC_CC_DIF_AHB_IF0; + + REG_XDMAC_CNDA0 = 0; + REG_XDMAC_CNDC0 = 0; + REG_XDMAC_CBC0 = 0; + REG_XDMAC_CDS_MSP0 = 0; + REG_XDMAC_CSUS0 = 0; + REG_XDMAC_CDUS0 = 0; + + REG_XDMAC_CIE0 = XDMAC_CIE_BIE; + + __DSB(); + __ISB(); + XDMAC->XDMAC_GE = XDMAC_GE_EN0; + while ( XDMAC->XDMAC_GS & XDMAC_GS_ST0 ) + ; + + while ( !(REG_XDMAC_CIS0 & XDMAC_CIS_BIS) ) + ; + + /* Enable ITCM. DTCM is enabled by default. */ + __DSB(); + __ISB(); + SCB->ITCMCR = (SCB_ITCMCR_EN_Msk | SCB_ITCMCR_RMW_Msk + | SCB_ITCMCR_RETEN_Msk); + __DSB(); + __ISB(); + + /* Use data cache rather than DTCM for two reasons: + * 1. It is hard to flash this device with GPNVM bits enabled. + * 2. It is about as fast. */ + SCB_EnableDCache(); } diff --git a/src/atsamd/Kconfig b/src/atsamd/Kconfig index 6ba0d7e6e6da..5711566a3de0 100644 --- a/src/atsamd/Kconfig +++ b/src/atsamd/Kconfig @@ -27,35 +27,38 @@ config BOARD_DIRECTORY choice prompt "Processor model" config MACH_SAMC21G18 - bool "SAMC21G18 (Duet 3 Toolboard 1LC)" + bool "SAMC21G18" select MACH_SAMC21 config MACH_SAMD21G18 - bool "SAMD21G18 (Arduino Zero)" + bool "SAMD21G18" select MACH_SAMD21 config MACH_SAMD21E18 - bool "SAMD21E18 (Adafruit Trinket M0)" + bool "SAMD21E18" select MACH_SAMD21 config MACH_SAMD21J18 - bool "SAMD21J18 (ReprapWorld Minitronics v2)" + bool "SAMD21J18" select MACH_SAMD21 config MACH_SAMD21E15 bool "SAMD21E15" select MACH_SAMD21 config MACH_SAMD51G19 - bool "SAMD51G19 (Adafruit ItsyBitsy M4)" + bool "SAMD51G19" select MACH_SAMD51 config MACH_SAMD51J19 - bool "SAMD51J19 (Adafruit Metro M4)" + bool "SAMD51J19" select MACH_SAMD51 config MACH_SAMD51N19 bool "SAMD51N19" select MACH_SAMD51 config MACH_SAMD51P20 - bool "SAMD51P20 (Adafruit Grand Central)" + bool "SAMD51P20" select MACH_SAMD51 config MACH_SAME51J19 bool "SAME51J19" select MACH_SAME51 + config MACH_SAME51N19 + bool "SAME51N19" + select MACH_SAME51 config MACH_SAME54P20 bool "SAME54P20" select MACH_SAME54 @@ -100,13 +103,14 @@ config MCU default "samd51n19a" if MACH_SAMD51N19 default "samd51p20a" if MACH_SAMD51P20 default "same51j19a" if MACH_SAME51J19 + default "same51n19a" if MACH_SAME51N19 default "same54p20a" if MACH_SAME54P20 config FLASH_SIZE hex default 0x8000 if MACH_SAMD21E15 default 0x40000 if MACH_SAMC21G18 || MACH_SAMD21G18 || MACH_SAMD21E18 || MACH_SAMD21J18 - default 0x80000 if MACH_SAMD51G19 || MACH_SAMD51J19 || MACH_SAMD51N19 || MACH_SAME51J19 + default 0x80000 if MACH_SAMD51G19 || MACH_SAMD51J19 || MACH_SAMD51N19 || MACH_SAME51J19 || MACH_SAME51N19 default 0x100000 if MACH_SAMD51P20 || MACH_SAME54P20 config FLASH_BOOT_ADDRESS @@ -121,7 +125,7 @@ config RAM_SIZE hex default 0x1000 if MACH_SAMD21E15 default 0x8000 if MACH_SAMC21G18 || MACH_SAMD21G18 || MACH_SAMD21E18 || MACH_SAMD21J18 - default 0x30000 if MACH_SAMD51G19 || MACH_SAMD51J19 || MACH_SAMD51N19 || MACH_SAME51J19 + default 0x30000 if MACH_SAMD51G19 || MACH_SAMD51J19 || MACH_SAMD51N19 || MACH_SAME51J19 || MACH_SAME51N19 default 0x40000 if MACH_SAMD51P20 || MACH_SAME54P20 config STACK_SIZE @@ -137,9 +141,9 @@ choice prompt "Bootloader offset" config SAMD_FLASH_START_2000 depends on MACH_SAMD21 - bool "8KiB bootloader (Arduino Zero)" + bool "8KiB bootloader" config SAMD_FLASH_START_4000 - bool "16KiB bootloader (Arduino M0, Duet 3 Bootloader)" + bool "16KiB bootloader" config SAMD_FLASH_START_0000 bool "No bootloader" endchoice diff --git a/src/atsamd/gpio.h b/src/atsamd/gpio.h index b8cb3e861d19..d29071dc6f77 100644 --- a/src/atsamd/gpio.h +++ b/src/atsamd/gpio.h @@ -51,8 +51,8 @@ struct i2c_config { }; struct i2c_config i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr); -void i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); -void i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg - , uint8_t read_len, uint8_t *read); +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg + , uint8_t read_len, uint8_t *read); #endif // gpio.h diff --git a/src/atsamd/i2c.c b/src/atsamd/i2c.c index 3c316142ce5b..18506a27c797 100644 --- a/src/atsamd/i2c.c +++ b/src/atsamd/i2c.c @@ -9,6 +9,7 @@ #include "command.h" // shutdown #include "gpio.h" // i2c_setup #include "sched.h" // sched_shutdown +#include "i2ccmds.h" // I2C_BUS_SUCCESS #define TIME_RISE 125ULL // 125 nanoseconds #define I2C_FREQ 100000 @@ -86,7 +87,7 @@ i2c_stop(SercomI2cm *si) si->CTRLB.reg = SERCOM_I2CM_CTRLB_CMD(3); } -void +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) { SercomI2cm *si = (SercomI2cm *)config.si; @@ -94,9 +95,11 @@ i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) while (write_len--) i2c_send_byte(si, *write++); i2c_stop(si); + + return I2C_BUS_SUCCESS; } -void +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *read) { @@ -143,4 +146,6 @@ i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg // read received data byte *read++ = si->DATA.reg; } + + return I2C_BUS_SUCCESS; } diff --git a/src/avr/gpio.h b/src/avr/gpio.h index 9d98ee709218..9f28a30acdad 100644 --- a/src/avr/gpio.h +++ b/src/avr/gpio.h @@ -50,8 +50,8 @@ struct i2c_config { }; struct i2c_config i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr); -void i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); -void i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *read); #endif // gpio.h diff --git a/src/avr/i2c.c b/src/avr/i2c.c index 658a30a12187..e8febfba973d 100644 --- a/src/avr/i2c.c +++ b/src/avr/i2c.c @@ -11,6 +11,7 @@ #include "gpio.h" // i2c_setup #include "internal.h" // GPIO #include "sched.h" // sched_shutdown +#include "i2ccmds.h" // I2C_BUS_SUCCESS DECL_ENUMERATION("i2c_bus", "twi", 0); @@ -94,7 +95,7 @@ i2c_stop(uint32_t timeout) TWCR = (1<ticks); gpio_in_reset(is->sda_in, 1); gpio_in_reset(is->scl_in, 1); @@ -137,7 +139,10 @@ i2c_software_start(struct i2c_software *is, uint8_t addr) i2c_delay(is->ticks); gpio_out_reset(is->scl_out, 0); - i2c_software_send_byte(is, addr); + ret = i2c_software_send_byte(is, addr); + if (ret == I2C_BUS_NACK) + return I2C_BUS_START_NACK; + return ret; } static void @@ -150,32 +155,52 @@ i2c_software_stop(struct i2c_software *is) gpio_in_reset(is->sda_in, 1); } -void +int i2c_software_write(struct i2c_software *is, uint8_t write_len, uint8_t *write) { - i2c_software_start(is, is->addr); - while (write_len--) - i2c_software_send_byte(is, *write++); + int ret = i2c_software_start(is, is->addr); + if (ret != I2C_BUS_SUCCESS) + goto out; + + while (write_len--) { + ret = i2c_software_send_byte(is, *write++); + if (ret != I2C_BUS_SUCCESS) + break; + } + +out: i2c_software_stop(is); + return ret; } -void +int i2c_software_read(struct i2c_software *is, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *read) { - uint8_t addr = is->addr | 0x01; - + int ret; if (reg_len) { // write the register - i2c_software_start(is, is->addr); - while(reg_len--) - i2c_software_send_byte(is, *reg++); + ret = i2c_software_start(is, is->addr); + if (ret != I2C_BUS_SUCCESS) + goto out; + while(reg_len--) { + ret = i2c_software_send_byte(is, *reg++); + if (ret != I2C_BUS_SUCCESS) + goto out; + } + } // start/re-start and read data - i2c_software_start(is, addr); + ret = i2c_software_start(is, is->addr | 0x01); + if (ret != I2C_BUS_SUCCESS) { + ret = I2C_BUS_START_READ_NACK; + goto out; + } while(read_len--) { *read = i2c_software_read_byte(is, read_len); read++; } +out: i2c_software_stop(is); + return ret; } diff --git a/src/i2c_software.h b/src/i2c_software.h index 9bd54f29af90..843ab461a87b 100644 --- a/src/i2c_software.h +++ b/src/i2c_software.h @@ -4,10 +4,10 @@ #include // uint8_t struct i2c_software *i2c_software_oid_lookup(uint8_t oid); -void i2c_software_write(struct i2c_software *sw_i2c - , uint8_t write_len, uint8_t *write); -void i2c_software_read(struct i2c_software *sw_i2c - , uint8_t reg_len, uint8_t *reg - , uint8_t read_len, uint8_t *read); +int i2c_software_write(struct i2c_software *sw_i2c + , uint8_t write_len, uint8_t *write); +int i2c_software_read(struct i2c_software *sw_i2c + , uint8_t reg_len, uint8_t *reg + , uint8_t read_len, uint8_t *read); #endif // i2c_software.h diff --git a/src/i2ccmds.c b/src/i2ccmds.c index 377a9537deaa..c51772f1da09 100644 --- a/src/i2ccmds.c +++ b/src/i2ccmds.c @@ -36,7 +36,7 @@ command_i2c_set_bus(uint32_t *args) { uint8_t addr = args[3] & 0x7f; struct i2cdev_s *i2c = i2cdev_oid_lookup(args[0]); - i2c->i2c_config = i2c_setup(args[1], args[2], addr); + i2c->i2c_hw = i2c_setup(args[1], args[2], addr); i2c->flags |= IF_HARDWARE; } DECL_COMMAND(command_i2c_set_bus, @@ -45,72 +45,64 @@ DECL_COMMAND(command_i2c_set_bus, void i2cdev_set_software_bus(struct i2cdev_s *i2c, struct i2c_software *is) { - i2c->i2c_software = is; + i2c->i2c_sw = is; i2c->flags |= IF_SOFTWARE; } -void -command_i2c_write(uint32_t *args) +void i2c_shutdown_on_err(int ret) +{ + switch (ret) { + case I2C_BUS_NACK: + shutdown("I2C NACK"); + case I2C_BUS_START_NACK: + shutdown("I2C START NACK"); + case I2C_BUS_START_READ_NACK: + shutdown("I2C START READ NACK"); + case I2C_BUS_TIMEOUT: + shutdown("I2C Timeout"); + } +} + +int i2c_dev_write(struct i2cdev_s *i2c, uint8_t write_len, uint8_t *data) { - uint8_t oid = args[0]; - struct i2cdev_s *i2c = oid_lookup(oid, command_config_i2c); - uint8_t data_len = args[1]; - uint8_t *data = command_decode_ptr(args[2]); uint_fast8_t flags = i2c->flags; if (CONFIG_WANT_SOFTWARE_I2C && flags & IF_SOFTWARE) - i2c_software_write(i2c->i2c_software, data_len, data); + return i2c_software_write(i2c->i2c_sw, write_len, data); else - i2c_write(i2c->i2c_config, data_len, data); + return i2c_write(i2c->i2c_hw, write_len, data); } -DECL_COMMAND(command_i2c_write, "i2c_write oid=%c data=%*s"); -void -command_i2c_read(uint32_t * args) +void command_i2c_write(uint32_t *args) { uint8_t oid = args[0]; struct i2cdev_s *i2c = oid_lookup(oid, command_config_i2c); - uint8_t reg_len = args[1]; - uint8_t *reg = command_decode_ptr(args[2]); - uint8_t data_len = args[3]; - uint8_t data[data_len]; + uint8_t data_len = args[1]; + uint8_t *data = command_decode_ptr(args[2]); + int ret = i2c_dev_write(i2c, data_len, data); + i2c_shutdown_on_err(ret); +} +DECL_COMMAND(command_i2c_write, "i2c_write oid=%c data=%*s"); + +int i2c_dev_read(struct i2cdev_s *i2c, uint8_t reg_len, uint8_t *reg + , uint8_t read_len, uint8_t *read) +{ uint_fast8_t flags = i2c->flags; if (CONFIG_WANT_SOFTWARE_I2C && flags & IF_SOFTWARE) - i2c_software_read(i2c->i2c_software, reg_len, reg, data_len, data); + return i2c_software_read(i2c->i2c_sw, reg_len, reg, read_len, read); else - i2c_read(i2c->i2c_config, reg_len, reg, data_len, data); - sendf("i2c_read_response oid=%c response=%*s", oid, data_len, data); + return i2c_read(i2c->i2c_hw, reg_len, reg, read_len, read); } -DECL_COMMAND(command_i2c_read, "i2c_read oid=%c reg=%*s read_len=%u"); -void -command_i2c_modify_bits(uint32_t *args) +void command_i2c_read(uint32_t *args) { uint8_t oid = args[0]; struct i2cdev_s *i2c = oid_lookup(oid, command_config_i2c); uint8_t reg_len = args[1]; uint8_t *reg = command_decode_ptr(args[2]); - uint32_t clear_set_len = args[3]; - if (clear_set_len % 2 != 0) - shutdown("i2c_modify_bits: Odd number of bits!"); - uint8_t data_len = clear_set_len/2; - uint8_t *clear_set = command_decode_ptr(args[4]); - uint8_t receive_data[reg_len + data_len]; - uint_fast8_t flags = i2c->flags; - memcpy(receive_data, reg, reg_len); - if (CONFIG_WANT_SOFTWARE_I2C && flags & IF_SOFTWARE) - i2c_software_read( - i2c->i2c_software, reg_len, reg, data_len, receive_data + reg_len); - else - i2c_read( - i2c->i2c_config, reg_len, reg, data_len, receive_data + reg_len); - for (int i = 0; i < data_len; i++) { - receive_data[reg_len + i] &= ~clear_set[i]; - receive_data[reg_len + i] |= clear_set[data_len + i]; - } - if (CONFIG_WANT_SOFTWARE_I2C && flags & IF_SOFTWARE) - i2c_software_write(i2c->i2c_software, reg_len + data_len, receive_data); - else - i2c_write(i2c->i2c_config, reg_len + data_len, receive_data); + uint8_t data_len = args[3]; + uint8_t data[data_len]; + int ret = i2c_dev_read(i2c, reg_len, reg, data_len, data); + i2c_shutdown_on_err(ret); + sendf("i2c_read_response oid=%c response=%*s", oid, data_len, data); } -DECL_COMMAND(command_i2c_modify_bits, - "i2c_modify_bits oid=%c reg=%*s clear_set_bits=%*s"); +DECL_COMMAND(command_i2c_read, "i2c_read oid=%c reg=%*s read_len=%u"); diff --git a/src/i2ccmds.h b/src/i2ccmds.h index 9ce54aa060b2..55c97b6b5af1 100644 --- a/src/i2ccmds.h +++ b/src/i2ccmds.h @@ -4,13 +4,28 @@ #include #include "board/gpio.h" // i2c_config +// I2C ERROR Codes +enum { + I2C_BUS_SUCCESS, + I2C_BUS_NACK, + I2C_BUS_TIMEOUT, + I2C_BUS_START_NACK, + I2C_BUS_START_READ_NACK, +}; + struct i2cdev_s { - struct i2c_config i2c_config; - struct i2c_software *i2c_software; + union { + struct i2c_config i2c_hw; + struct i2c_software *i2c_sw; + }; uint8_t flags; }; struct i2cdev_s *i2cdev_oid_lookup(uint8_t oid); void i2cdev_set_software_bus(struct i2cdev_s *i2c, struct i2c_software *is); +int i2c_dev_read(struct i2cdev_s *i2c, uint8_t reg_len, uint8_t *reg + , uint8_t read_len, uint8_t *read); +int i2c_dev_write(struct i2cdev_s *i2c, uint8_t write_len, uint8_t *data); +void i2c_shutdown_on_err(int ret); #endif diff --git a/src/linux/gpio.h b/src/linux/gpio.h index df9de752566e..d72007c2ce3b 100644 --- a/src/linux/gpio.h +++ b/src/linux/gpio.h @@ -49,8 +49,8 @@ struct i2c_config { }; struct i2c_config i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr); -void i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); -void i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *read); #endif // gpio.h diff --git a/src/linux/i2c.c b/src/linux/i2c.c index b328dc56eaa6..a728752aa7c2 100644 --- a/src/linux/i2c.c +++ b/src/linux/i2c.c @@ -13,6 +13,7 @@ #include "command.h" // shutdown #include "internal.h" // report_errno #include "sched.h" // sched_shutdown +#include "i2ccmds.h" // I2C_BUS_SUCCESS DECL_ENUMERATION_RANGE("i2c_bus", "i2c.0", 0, 15); @@ -84,18 +85,28 @@ i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr) return (struct i2c_config){.fd=fd, .addr=addr}; } -void +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *data) { - int ret = write(config.fd, data, write_len); - if (ret != write_len) { - if (ret < 0) - report_errno("write value i2c", ret); - try_shutdown("Unable write i2c device"); + struct i2c_rdwr_ioctl_data i2c_data; + struct i2c_msg msgs[1]; + msgs[0].addr = config.addr; + msgs[0].flags = 0x0; + msgs[0].len = write_len; + msgs[0].buf = data; + i2c_data.nmsgs = 1; + i2c_data.msgs = &msgs[0]; + + int ret = ioctl(config.fd, I2C_RDWR, &i2c_data); + + if (ret < 0) { + return I2C_BUS_NACK; } + + return I2C_BUS_SUCCESS; } -void +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *data) { @@ -121,7 +132,9 @@ i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg int ret = ioctl(config.fd, I2C_RDWR, &i2c_data); - if(ret < 0) { - try_shutdown("Unable to read i2c device"); + if (ret < 0) { + return I2C_BUS_NACK; } + + return I2C_BUS_SUCCESS; } diff --git a/src/linux/timer.c b/src/linux/timer.c index 21be01312460..8eda62a54ddc 100644 --- a/src/linux/timer.c +++ b/src/linux/timer.c @@ -152,7 +152,7 @@ timer_dispatch(void) // Check if there are too many repeat timers if (diff < (int32_t)(-timer_from_us(100000))) try_shutdown("Rescheduled timer in the past"); - if (sched_tasks_busy()) + if (sched_check_set_tasks_busy()) return; repeat_count = TIMER_IDLE_REPEAT_COUNT; } diff --git a/src/lpc176x/Kconfig b/src/lpc176x/Kconfig index 390a081f034a..ee7580301020 100644 --- a/src/lpc176x/Kconfig +++ b/src/lpc176x/Kconfig @@ -65,7 +65,7 @@ config STACK_SIZE choice prompt "Bootloader offset" config LPC_FLASH_START_4000 - bool "16KiB bootloader (Smoothieware bootloader)" + bool "16KiB bootloader" config LPC_FLASH_START_0000 bool "No bootloader" endchoice diff --git a/src/lpc176x/gpio.h b/src/lpc176x/gpio.h index e03afbb44910..c1507c64c265 100644 --- a/src/lpc176x/gpio.h +++ b/src/lpc176x/gpio.h @@ -51,8 +51,8 @@ struct i2c_config { }; struct i2c_config i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr); -void i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); -void i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg - , uint8_t read_len, uint8_t *read); +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg + , uint8_t read_len, uint8_t *read); #endif // gpio.h diff --git a/src/lpc176x/i2c.c b/src/lpc176x/i2c.c index 7ed631af1f15..519dd504ca5a 100644 --- a/src/lpc176x/i2c.c +++ b/src/lpc176x/i2c.c @@ -9,6 +9,7 @@ #include "gpio.h" // i2c_setup #include "internal.h" // gpio_peripheral #include "sched.h" // sched_shutdown +#include "i2ccmds.h" // I2C_BUS_SUCCESS struct i2c_info { LPC_I2C_TypeDef *i2c; @@ -122,7 +123,7 @@ i2c_stop(LPC_I2C_TypeDef *i2c, uint32_t timeout) i2c_wait(i2c, IF_STOP, timeout); } -void +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) { LPC_I2C_TypeDef *i2c = config.i2c; @@ -133,9 +134,11 @@ i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) while (write_len--) i2c_send_byte(i2c, *write++, timeout); i2c_stop(i2c, timeout); + + return I2C_BUS_SUCCESS; } -void +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *read) { @@ -159,4 +162,6 @@ i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg read++; } i2c_stop(i2c, timeout); + + return I2C_BUS_SUCCESS; } diff --git a/src/rp2040/gpio.h b/src/rp2040/gpio.h index ae6083780ce9..0dd393bfed42 100644 --- a/src/rp2040/gpio.h +++ b/src/rp2040/gpio.h @@ -50,8 +50,8 @@ struct i2c_config { }; struct i2c_config i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr); -void i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); -void i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg - , uint8_t read_len, uint8_t *read); +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg + , uint8_t read_len, uint8_t *read); #endif // gpio.h diff --git a/src/rp2040/i2c.c b/src/rp2040/i2c.c index 8a32417badd4..8ec2bd7a6684 100644 --- a/src/rp2040/i2c.c +++ b/src/rp2040/i2c.c @@ -11,6 +11,7 @@ #include "internal.h" // pclock, gpio_peripheral #include "hardware/regs/resets.h" // RESETS_RESET_I2C*_BITS #include "hardware/structs/i2c.h" +#include "i2ccmds.h" // I2C_BUS_SUCCESS struct i2c_info { i2c_hw_t *i2c; @@ -132,8 +133,8 @@ i2c_stop(i2c_hw_t *i2c) i2c->enable = 0; } -static void -i2c_do_write(i2c_hw_t *i2c, uint8_t addr, uint8_t write_len, uint8_t *write +static int +i2c_do_write(i2c_hw_t *i2c, uint8_t write_len, uint8_t *write , uint8_t send_stop, uint32_t timeout) { for (int i = 0; i < write_len; i++) { @@ -143,7 +144,7 @@ i2c_do_write(i2c_hw_t *i2c, uint8_t addr, uint8_t write_len, uint8_t *write // Wait until there's a spot in the TX FIFO while (i2c->txflr == 16) { if (!timer_is_before(timer_read_time(), timeout)) - shutdown("i2c timeout"); + return I2C_BUS_TIMEOUT; } i2c->data_cmd = first << I2C_IC_DATA_CMD_RESTART_LSB @@ -152,24 +153,39 @@ i2c_do_write(i2c_hw_t *i2c, uint8_t addr, uint8_t write_len, uint8_t *write } if (!send_stop) - return; + return I2C_BUS_SUCCESS; // Drain the transmit buffer while (i2c->txflr != 0) { if (!timer_is_before(timer_read_time(), timeout)) - shutdown("i2c timeout"); + return I2C_BUS_TIMEOUT; + } + + if (i2c->raw_intr_stat & I2C_IC_RAW_INTR_STAT_TX_ABRT_BITS) { + uint32_t abort_source = i2c->tx_abrt_source; + if (abort_source & I2C_IC_TX_ABRT_SOURCE_ABRT_7B_ADDR_NOACK_BITS) + { + i2c->clr_tx_abrt; + return I2C_BUS_START_NACK; + } + if (abort_source & I2C_IC_TX_ABRT_SOURCE_ABRT_TXDATA_NOACK_BITS) + { + i2c->clr_tx_abrt; + return I2C_BUS_NACK; + } } + return I2C_BUS_SUCCESS; } -static void -i2c_do_read(i2c_hw_t *i2c, uint8_t addr, uint8_t read_len, uint8_t *read +static int +i2c_do_read(i2c_hw_t *i2c, uint8_t read_len, uint8_t *read , uint32_t timeout) { int have_read = 0; int to_send = read_len; while (have_read < read_len) { if (!timer_is_before(timer_read_time(), timeout)) - shutdown("i2c timeout"); + return I2C_BUS_TIMEOUT; if (to_send > 0 && i2c->txflr < 16) { int first = to_send == read_len; @@ -186,30 +202,47 @@ i2c_do_read(i2c_hw_t *i2c, uint8_t addr, uint8_t read_len, uint8_t *read *read++ = i2c->data_cmd & 0xFF; have_read++; } + + if (i2c->raw_intr_stat & I2C_IC_RAW_INTR_STAT_TX_ABRT_BITS) { + uint32_t abort_source = i2c->tx_abrt_source; + if (abort_source & I2C_IC_TX_ABRT_SOURCE_ABRT_7B_ADDR_NOACK_BITS) { + i2c->clr_tx_abrt; + return I2C_BUS_START_READ_NACK; + } + } } + + return I2C_BUS_SUCCESS; } -void +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) { i2c_hw_t *i2c = (i2c_hw_t*)config.i2c; uint32_t timeout = timer_read_time() + timer_from_us(5000); + int ret; i2c_start(i2c, config.addr); - i2c_do_write(i2c, config.addr, write_len, write, 1, timeout); + ret = i2c_do_write(i2c, write_len, write, 1, timeout); i2c_stop(i2c); + return ret; } -void +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *read) { i2c_hw_t *i2c = (i2c_hw_t*)config.i2c; uint32_t timeout = timer_read_time() + timer_from_us(5000); + int ret = I2C_BUS_SUCCESS; i2c_start(i2c, config.addr); if (reg_len != 0) - i2c_do_write(i2c, config.addr, reg_len, reg, 0, timeout); - i2c_do_read(i2c, config.addr, read_len, read, timeout); + ret = i2c_do_write(i2c, reg_len, reg, 0, timeout); + if (ret != I2C_BUS_SUCCESS) + goto out; + ret = i2c_do_read(i2c, read_len, read, timeout); +out: i2c_stop(i2c); + return ret; } diff --git a/src/sched.c b/src/sched.c index 44cce558ee9e..51d159bc6e5c 100644 --- a/src/sched.c +++ b/src/sched.c @@ -1,6 +1,6 @@ // Basic scheduling functions and startup/shutdown code. // -// Copyright (C) 2016-2021 Kevin O'Connor +// Copyright (C) 2016-2024 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -19,7 +19,7 @@ static struct timer periodic_timer, sentinel_timer, deleted_timer; static struct { struct timer *timer_list, *last_insert; - int8_t tasks_status; + int8_t tasks_status, tasks_busy; uint8_t shutdown_status, shutdown_reason; } SchedStatus = {.timer_list = &periodic_timer, .last_insert = &periodic_timer}; @@ -205,11 +205,15 @@ sched_wake_tasks(void) SchedStatus.tasks_status = TS_REQUESTED; } -// Check if tasks need to be run +// Check if tasks busy (called from low-level timer dispatch code) uint8_t -sched_tasks_busy(void) +sched_check_set_tasks_busy(void) { - return SchedStatus.tasks_status >= TS_REQUESTED; + // Return busy if tasks never idle between two consecutive calls + if (SchedStatus.tasks_busy >= TS_REQUESTED) + return 1; + SchedStatus.tasks_busy = SchedStatus.tasks_status; + return 0; } // Note that a task is ready to run @@ -243,7 +247,7 @@ run_tasks(void) irq_disable(); if (SchedStatus.tasks_status != TS_REQUESTED) { // Sleep processor (only run timers) until tasks woken - SchedStatus.tasks_status = TS_IDLE; + SchedStatus.tasks_status = SchedStatus.tasks_busy = TS_IDLE; do { irq_wait(); } while (SchedStatus.tasks_status != TS_REQUESTED); diff --git a/src/sched.h b/src/sched.h index 0a2bdc4bcab0..9ec96631eeb4 100644 --- a/src/sched.h +++ b/src/sched.h @@ -31,7 +31,7 @@ void sched_del_timer(struct timer *del); unsigned int sched_timer_dispatch(void); void sched_timer_reset(void); void sched_wake_tasks(void); -uint8_t sched_tasks_busy(void); +uint8_t sched_check_set_tasks_busy(void); void sched_wake_task(struct task_wake *w); uint8_t sched_check_wake(struct task_wake *w); uint8_t sched_is_shutdown(void); diff --git a/src/sensor_ads1220.c b/src/sensor_ads1220.c new file mode 100644 index 000000000000..ea33379a0b6d --- /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_hx71x.c b/src/sensor_hx71x.c new file mode 100644 index 000000000000..f20d880726fe --- /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 01cf3ee04597..8b67884f15c9 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -7,7 +7,6 @@ #include // memcpy #include "basecmd.h" // oid_alloc -#include "board/gpio.h" // i2c_read #include "board/irq.h" // irq_disable #include "board/misc.h" // timer_read_time #include "command.h" // DECL_COMMAND @@ -147,7 +146,8 @@ check_home(struct ldc1612 *ld, uint32_t data) static void read_reg(struct ldc1612 *ld, uint8_t reg, uint8_t *res) { - i2c_read(ld->i2c->i2c_config, sizeof(reg), ®, 2, res); + int ret = i2c_dev_read(ld->i2c, sizeof(reg), ®, 2, res); + i2c_shutdown_on_err(ret); } // Read the status register on the ldc1612 @@ -180,7 +180,10 @@ ldc1612_query(struct ldc1612 *ld, uint8_t oid) ld->sb.data_count += BYTES_PER_SAMPLE; // Check for endstop trigger - uint32_t data = (d[0] << 24L) | (d[1] << 16L) | (d[2] << 8) | d[3]; + uint32_t data = ((uint32_t)d[0] << 24) + | ((uint32_t)d[1] << 16) + | ((uint32_t)d[2] << 8) + | ((uint32_t)d[3]); check_home(ld, data); // Flush local buffer if needed diff --git a/src/sensor_mpu9250.c b/src/sensor_mpu9250.c index 23c029211f72..7476734c4f0a 100644 --- a/src/sensor_mpu9250.c +++ b/src/sensor_mpu9250.c @@ -13,7 +13,6 @@ #include "command.h" // DECL_COMMAND #include "sched.h" // DECL_TASK #include "sensor_bulk.h" // sensor_bulk_report -#include "board/gpio.h" // i2c_read #include "i2ccmds.h" // i2cdev_oid_lookup // Chip registers @@ -71,13 +70,21 @@ mp9250_reschedule_timer(struct mpu9250 *mp) irq_enable(); } +static void +read_mpu(struct i2cdev_s *i2c, uint8_t reg_len, uint8_t *reg + , uint8_t read_len, uint8_t *read) +{ + int ret = i2c_dev_read(i2c, reg_len, reg, read_len, read); + i2c_shutdown_on_err(ret); +} + // Reads the fifo byte count from the device. static uint16_t get_fifo_status(struct mpu9250 *mp) { uint8_t reg[] = {AR_FIFO_COUNT_H}; uint8_t msg[2]; - i2c_read(mp->i2c->i2c_config, sizeof(reg), reg, sizeof(msg), msg); + read_mpu(mp->i2c, sizeof(reg), reg, sizeof(msg), msg); uint16_t fifo_bytes = ((msg[0] & 0x1f) << 8) | msg[1]; if (fifo_bytes > mp->fifo_max) mp->fifo_max = fifo_bytes; @@ -95,8 +102,7 @@ mp9250_query(struct mpu9250 *mp, uint8_t oid) // If we have enough bytes to fill the buffer do it and send report if (mp->fifo_pkts_bytes >= BYTES_PER_BLOCK) { uint8_t reg = AR_FIFO; - i2c_read(mp->i2c->i2c_config, sizeof(reg), ® - , BYTES_PER_BLOCK, &mp->sb.data[0]); + read_mpu(mp->i2c, sizeof(reg), ®, BYTES_PER_BLOCK, &mp->sb.data[0]); mp->sb.data_count = BYTES_PER_BLOCK; mp->fifo_pkts_bytes -= BYTES_PER_BLOCK; sensor_bulk_report(&mp->sb, oid); @@ -144,8 +150,7 @@ command_query_mpu9250_status(uint32_t *args) // Detect if a FIFO overrun occurred uint8_t int_reg[] = {AR_INT_STATUS}; uint8_t int_msg; - i2c_read(mp->i2c->i2c_config, sizeof(int_reg), int_reg, sizeof(int_msg), - &int_msg); + read_mpu(mp->i2c, sizeof(int_reg), int_reg, sizeof(int_msg), &int_msg); if (int_msg & FIFO_OVERFLOW_INT) mp->sb.possible_overflows++; @@ -153,7 +158,7 @@ command_query_mpu9250_status(uint32_t *args) uint8_t reg[] = {AR_FIFO_COUNT_H}; uint8_t msg[2]; uint32_t time1 = timer_read_time(); - i2c_read(mp->i2c->i2c_config, sizeof(reg), reg, sizeof(msg), msg); + read_mpu(mp->i2c, sizeof(reg), reg, sizeof(msg), msg); uint32_t time2 = timer_read_time(); uint16_t fifo_bytes = ((msg[0] & 0x1f) << 8) | msg[1]; diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index 037e37bbe949..4112e8334c6d 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -416,6 +416,10 @@ choice bool "Serial (on USART3 PD9/PD8)" if LOW_LEVEL_OPTIONS depends on !MACH_STM32F0 && !MACH_STM32F401 select SERIAL + config STM32_SERIAL_USART3_ALT_PC11_PC10 + bool "Serial (on USART3 PC11/PC10)" if LOW_LEVEL_OPTIONS + depends on MACH_STM32G474 + select SERIAL config STM32_SERIAL_UART4 bool "Serial (on UART4 PA0/PA1)" depends on MACH_STM32H7 diff --git a/src/stm32/gpio.h b/src/stm32/gpio.h index 281e3a8db44c..78f567b8149e 100644 --- a/src/stm32/gpio.h +++ b/src/stm32/gpio.h @@ -51,8 +51,8 @@ struct i2c_config { }; struct i2c_config i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr); -void i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); -void i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg - , uint8_t read_len, uint8_t *read); +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write); +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg + , uint8_t read_len, uint8_t *read); #endif // gpio.h diff --git a/src/stm32/i2c.c b/src/stm32/i2c.c index f5bbd01dd04a..441f611a89f8 100644 --- a/src/stm32/i2c.c +++ b/src/stm32/i2c.c @@ -11,6 +11,7 @@ #include "internal.h" // GPIO #include "sched.h" // sched_shutdown #include "board/irq.h" //irq_disable +#include "i2ccmds.h" // I2C_BUS_SUCCESS struct i2c_info { I2C_TypeDef *i2c; @@ -97,6 +98,8 @@ i2c_wait(I2C_TypeDef *i2c, uint32_t set, uint32_t clear, uint32_t timeout) uint32_t sr1 = i2c->SR1; if ((sr1 & set) == set && (sr1 & clear) == 0) return sr1; + if (sr1 & I2C_SR1_AF) + shutdown("I2C NACK error encountered"); if (!timer_is_before(timer_read_time(), timeout)) shutdown("i2c timeout"); } @@ -147,7 +150,7 @@ i2c_stop(I2C_TypeDef *i2c, uint32_t timeout) i2c_wait(i2c, 0, I2C_SR1_TXE, timeout); } -void +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) { I2C_TypeDef *i2c = config.i2c; @@ -157,9 +160,11 @@ i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) while (write_len--) i2c_send_byte(i2c, *write++, timeout); i2c_stop(i2c, timeout); + + return I2C_BUS_SUCCESS; } -void +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *read) { @@ -180,4 +185,6 @@ i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg read++; } i2c_wait(i2c, 0, I2C_SR1_RXNE, timeout); + + return I2C_BUS_SUCCESS; } diff --git a/src/stm32/stm32f0_i2c.c b/src/stm32/stm32f0_i2c.c index 597b48460eb7..ef12145f7de2 100644 --- a/src/stm32/stm32f0_i2c.c +++ b/src/stm32/stm32f0_i2c.c @@ -9,6 +9,7 @@ #include "gpio.h" // i2c_setup #include "internal.h" // GPIO #include "sched.h" // sched_shutdown +#include "i2ccmds.h" // I2C_BUS_SUCCESS struct i2c_info { I2C_TypeDef *i2c; @@ -148,57 +149,83 @@ i2c_setup(uint32_t bus, uint32_t rate, uint8_t addr) gpio_peripheral(ii->sda_pin, ii->function | GPIO_OPEN_DRAIN, 1); // Set 100Khz frequency and enable - i2c->TIMINGR = ((0xB << I2C_TIMINGR_PRESC_Pos) - | (0x13 << I2C_TIMINGR_SCLL_Pos) - | (0xF << I2C_TIMINGR_SCLH_Pos) - | (0x2 << I2C_TIMINGR_SDADEL_Pos) - | (0x4 << I2C_TIMINGR_SCLDEL_Pos)); + uint32_t nom_i2c_clock = 8000000; // 8mhz internal clock = 125ns ticks + uint32_t scll = 40; // 40 * 125ns = 5us + uint32_t sclh = 32; // 32 * 125ns = 4us + uint32_t sdadel = 4; // 4 * 125ns = 500ns + uint32_t scldel = 10; // 10 * 125ns = 1250ns + // Clamp the rate to 400Khz + if (rate >= 400000) { + scll = 10; // 10 * 125ns = 1250ns + sclh = 4; // 4 * 125 = 500ns + sdadel = 3; // 3 * 125 = 375ns + scldel = 4; // 4 * 125 = 500ns + } + + uint32_t pclk = get_pclock_frequency((uint32_t)i2c); + uint32_t presc = DIV_ROUND_UP(pclk, nom_i2c_clock); + i2c->TIMINGR = (((presc - 1) << I2C_TIMINGR_PRESC_Pos) + | ((scll - 1) << I2C_TIMINGR_SCLL_Pos) + | ((sclh - 1) << I2C_TIMINGR_SCLH_Pos) + | (sdadel << I2C_TIMINGR_SDADEL_Pos) + | ((scldel - 1) << I2C_TIMINGR_SCLDEL_Pos)); i2c->CR1 = I2C_CR1_PE; } return (struct i2c_config){ .i2c=i2c, .addr=addr<<1 }; } -static uint32_t +static int i2c_wait(I2C_TypeDef *i2c, uint32_t set, uint32_t timeout) { for (;;) { uint32_t isr = i2c->ISR; if (isr & set) - return isr; + return I2C_BUS_SUCCESS; + if (isr & I2C_ISR_NACKF) + return I2C_BUS_NACK; if (!timer_is_before(timer_read_time(), timeout)) - shutdown("i2c timeout"); + return I2C_BUS_TIMEOUT; } } -void +int i2c_write(struct i2c_config config, uint8_t write_len, uint8_t *write) { I2C_TypeDef *i2c = config.i2c; uint32_t timeout = timer_read_time() + timer_from_us(5000); + int ret = I2C_BUS_SUCCESS; // Send start and address i2c->CR2 = (I2C_CR2_START | config.addr | (write_len << I2C_CR2_NBYTES_Pos) | I2C_CR2_AUTOEND); while (write_len--) { - i2c_wait(i2c, I2C_ISR_TXIS, timeout); + ret = i2c_wait(i2c, I2C_ISR_TXIS, timeout); + if (ret != I2C_BUS_SUCCESS) + goto abrt; i2c->TXDR = *write++; } - i2c_wait(i2c, I2C_ISR_TXE, timeout); + return i2c_wait(i2c, I2C_ISR_TXE, timeout); +abrt: + i2c->CR2 |= I2C_CR2_STOP; + return ret; } -void +int i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg , uint8_t read_len, uint8_t *read) { I2C_TypeDef *i2c = config.i2c; uint32_t timeout = timer_read_time() + timer_from_us(5000); + int ret = I2C_BUS_SUCCESS; // Send start, address, reg i2c->CR2 = (I2C_CR2_START | config.addr | (reg_len << I2C_CR2_NBYTES_Pos)); while (reg_len--) { - i2c_wait(i2c, I2C_ISR_TXIS, timeout); + ret = i2c_wait(i2c, I2C_ISR_TXIS, timeout); + if (ret != I2C_BUS_SUCCESS) + goto abrt; i2c->TXDR = *reg++; } i2c_wait(i2c, I2C_ISR_TC, timeout); @@ -207,8 +234,13 @@ i2c_read(struct i2c_config config, uint8_t reg_len, uint8_t *reg i2c->CR2 = (I2C_CR2_START | I2C_CR2_RD_WRN | config.addr | (read_len << I2C_CR2_NBYTES_Pos) | I2C_CR2_AUTOEND); while (read_len--) { - i2c_wait(i2c, I2C_ISR_RXNE, timeout); + ret = i2c_wait(i2c, I2C_ISR_RXNE, timeout); + if (ret != I2C_BUS_SUCCESS) + goto abrt; *read++ = i2c->RXDR; } - i2c_wait(i2c, I2C_ISR_STOPF, timeout); + return i2c_wait(i2c, I2C_ISR_STOPF, timeout); +abrt: + i2c->CR2 |= I2C_CR2_STOP; + return ret; } diff --git a/src/stm32/stm32f0_serial.c b/src/stm32/stm32f0_serial.c index c987f149e561..964601b68b75 100644 --- a/src/stm32/stm32f0_serial.c +++ b/src/stm32/stm32f0_serial.c @@ -71,6 +71,14 @@ #define USARTx_FUNCTION GPIO_FUNCTION(CONFIG_MACH_STM32G0 ? 0 : 7) #define USARTx USART3 #define USARTx_IRQn USART3_IRQn +#elif CONFIG_STM32_SERIAL_USART3_ALT_PC11_PC10 + // Currently only supports STM32G474. + DECL_CONSTANT_STR("RESERVE_PINS_serial", "PC11,PC10"); + #define GPIO_Rx GPIO('C', 11) + #define GPIO_Tx GPIO('C', 10) + #define USARTx_FUNCTION GPIO_FUNCTION(7) + #define USARTx USART3 + #define USARTx_IRQn USART3_IRQn #elif CONFIG_STM32_SERIAL_UART4 DECL_CONSTANT_STR("RESERVE_PINS_serial", "PA1,PA0"); #define GPIO_Rx GPIO('A', 1) diff --git a/src/stm32/stm32g4.c b/src/stm32/stm32g4.c index 1eed3ec188d6..6d6d1c0d3a81 100644 --- a/src/stm32/stm32g4.c +++ b/src/stm32/stm32g4.c @@ -12,7 +12,7 @@ #include "internal.h" // enable_pclock #include "sched.h" // sched_main -#define FREQ_PERIPH_DIV 1 +#define FREQ_PERIPH_DIV 2 #define FREQ_PERIPH (CONFIG_CLOCK_FREQ / FREQ_PERIPH_DIV) // Map a peripheral address to its enable bits @@ -104,7 +104,7 @@ enable_clock_stm32g4(void) RCC->CR |= RCC_CR_PLLON; // Enable 48Mhz USB clock using clock recovery - if (CONFIG_USBSERIAL) { + if (CONFIG_USB) { RCC->CRRCR |= RCC_CRRCR_HSI48ON; while (!(RCC->CRRCR & RCC_CRRCR_HSI48RDY)) ; @@ -142,7 +142,7 @@ clock_setup(void) RCC->PLLCFGR |= RCC_PLLCFGR_PLLREN; // Switch system clock to PLL - RCC->CFGR = RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE1_DIV1 | RCC_CFGR_PPRE2_DIV1 + RCC->CFGR = RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE1_DIV2 | RCC_CFGR_PPRE2_DIV2 | RCC_CFGR_SW_PLL; while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL) ; diff --git a/src/stm32/stm32h7_adc.c b/src/stm32/stm32h7_adc.c index 3c217ca273c6..6740edd3e1ab 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 }; diff --git a/src/stm32/stm32l4.c b/src/stm32/stm32l4.c index 7db15fff0ba2..ae099d6bc6a5 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/src/trsync.c b/src/trsync.c index 3bf7aa669afd..5625280a6b71 100644 --- a/src/trsync.c +++ b/src/trsync.c @@ -1,6 +1,6 @@ // Handling of synchronized "trigger" dispatch // -// 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. @@ -23,13 +23,14 @@ enum { TSF_CAN_TRIGGER=1<<0, TSF_REPORT=1<<2 }; static struct task_wake trsync_wake; -// Activate a trigger (caller must disable IRQs) +// Activate a trigger void trsync_do_trigger(struct trsync *ts, uint8_t reason) { + irqstatus_t flag = irq_save(); uint8_t flags = ts->flags; if (!(flags & TSF_CAN_TRIGGER)) - return; + goto done; ts->trigger_reason = reason; ts->flags = (flags & ~TSF_CAN_TRIGGER) | TSF_REPORT; // Dispatch signals @@ -42,6 +43,8 @@ trsync_do_trigger(struct trsync *ts, uint8_t reason) func(tss, reason); } sched_wake_task(&trsync_wake); +done: + irq_restore(flag); } // Timeout handler diff --git a/test/configs/ar100.config b/test/configs/ar100.config index 6c9174824b5e..a1335176fb78 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/stm32f042.config b/test/configs/stm32f042.config index 12cc0922e45e..53cf1281e80b 100644 --- a/test/configs/stm32f042.config +++ b/test/configs/stm32f042.config @@ -4,3 +4,5 @@ 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/klippy/commands.test b/test/klippy/commands.test index 50e71ab3c671..33c59961432c 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 9ae01c2bc1be..93c5744404f1 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 5b2f9e65d3ed..ed40c236e5b5 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/load_cell.cfg b/test/klippy/load_cell.cfg new file mode 100644 index 000000000000..fa599d10e366 --- /dev/null +++ b/test/klippy/load_cell.cfg @@ -0,0 +1,23 @@ +# Test config for load_cell +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: none +max_velocity: 300 +max_accel: 3000 + +[load_cell my_ads1220] +sensor_type: ads1220 +cs_pin: PA0 +data_ready_pin: PA1 + +[load_cell my_hx711] +sensor_type: hx711 +sclk_pin: PA2 +dout_pin: PA3 + +[load_cell my_hx717] +sensor_type: hx717 +sclk_pin: PA4 +dout_pin: PA5 diff --git a/test/klippy/load_cell.test b/test/klippy/load_cell.test new file mode 100644 index 000000000000..880f840aa388 --- /dev/null +++ b/test/klippy/load_cell.test @@ -0,0 +1,5 @@ +# Tests for loadcell sensors +DICTIONARY atmega2560.dict +CONFIG load_cell.cfg + +G4 P1000 diff --git a/test/klippy/pressure_advance.cfg b/test/klippy/pressure_advance.cfg new file mode 100644 index 000000000000..d7123d08e094 --- /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 000000000000..c1ef052ad6b4 --- /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 ba7adb614f0c..40a6118c0eeb 100644 --- a/test/klippy/printers.test +++ b/test/klippy/printers.test @@ -224,6 +224,7 @@ CONFIG ../../config/generic-bigtreetech-skr-2.cfg CONFIG ../../config/generic-flyboard.cfg CONFIG ../../config/generic-I3DBEEZ9.cfg CONFIG ../../config/generic-mellow-fly-cdy-v3.cfg +CONFIG ../../config/generic-mellow-fly-e3-v2.cfg CONFIG ../../config/generic-mellow-super-infinty-hv.cfg CONFIG ../../config/generic-mks-monster8.cfg CONFIG ../../config/generic-mks-robin-nano-v3.cfg @@ -247,6 +248,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