Merge branch 'master' into nm_vsdlist_sort

This commit is contained in:
Niccolò Maggioni 2024-09-08 12:10:47 +02:00
commit 12eb7fb1f8
No known key found for this signature in database
GPG key ID: 4874B0C841E33264
103 changed files with 5715 additions and 1246 deletions

View file

@ -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

View file

@ -0,0 +1,126 @@
# This file contains pin mappings for the Artillery Genius Pro (2022)
# with a Artillery_Ruby-v1.2 board. To use this config, during "make menuconfig"
# select the STM32F401 with "No bootloader" and USB (on PA11/PA12)
# communication.
# To flash this firmware, set the physical bridge between +3.3V and Boot0 PIN
# on Artillery_Ruby mainboard. Then run the command:
# make flash FLASH_DEVICE=/dev/serial/by-id/usb-Klipper_stm32f401xc_*-if00
# See docs/Config_Reference.md for a description of parameters.
[extruder]
max_extrude_only_distance: 700.0
step_pin: PA7
dir_pin: PA6
enable_pin: !PC4
microsteps: 16
rotation_distance: 7.1910
nozzle_diameter: 0.400
filament_diameter: 1.750
heater_pin: PC9
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PC0
min_temp: 0
max_temp: 250
control: pid
pid_Kp: 23.223
pid_Ki: 1.518
pid_Kd: 88.826
[stepper_x]
step_pin: !PB14
dir_pin: PB13
enable_pin: !PB15
microsteps: 16
rotation_distance: 40
endstop_pin: !PA2
position_endstop: 0
position_max: 220
homing_speed: 60
[stepper_y]
step_pin: PB10
dir_pin: PB2
enable_pin: !PB12
microsteps: 16
rotation_distance: 40
endstop_pin: !PA1
position_endstop: 0
position_max: 220
homing_speed: 60
[stepper_z]
step_pin: PB0
dir_pin: !PC5
enable_pin: !PB1
microsteps: 16
rotation_distance: 8
endstop_pin: probe:z_virtual_endstop
position_max: 250
position_min: -5
[heater_bed]
heater_pin: PA8
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PC1
min_temp: 0
max_temp: 130
control: pid
pid_Kp: 23.223
pid_Ki: 1.518
pid_Kd: 88.826
[bed_screws]
screw1: 38,45
screw2: 180,45
screw3: 180,180
screw4: 38,180
[fan]
pin: PC8
off_below: 0.1
[heater_fan hotend_fan]
pin: PC7
heater: extruder
heater_temp: 50.0
[controller_fan stepper_fan]
pin: PC6
idle_timeout: 300
[mcu]
serial: /dev/serial/by-id/usb-Klipper_stm32f401xc_
[printer]
kinematics: cartesian
max_velocity: 500
max_accel: 4000
max_z_velocity: 50
square_corner_velocity: 5.0
max_z_accel: 100
[bltouch]
sensor_pin: PC2
control_pin: PC3
x_offset:27.25
y_offset:-12.8
z_offset: 0.25
speed:10
samples:1
samples_result:average
[bed_mesh]
speed: 800
mesh_min: 30, 20
mesh_max: 210, 200
probe_count: 5,5
algorithm: bicubic
move_check_distance: 3.0
[safe_z_home]
home_xy_position: 110,110
speed: 100
z_hop: 10
z_hop_speed: 5

View file

@ -98,6 +98,10 @@ z_offset: 0.0
speed: 2.0
samples: 5
[safe_z_home]
home_xy_position: 117, 117
z_hop: 10
[filament_switch_sensor filament_sensor]
pause_on_runout: true
switch_pin: ^!PA7

View file

@ -98,6 +98,10 @@ z_offset: 0.0
speed: 2.0
samples: 5
[safe_z_home]
home_xy_position: 117, 117
z_hop: 10
[filament_switch_sensor filament_sensor]
pause_on_runout: true
switch_pin: ^!PA7

View file

@ -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

View file

@ -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`.

View file

@ -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=<name> METHOD=[manual | automatic] [<probe_parameter>=<value>]
[<mesh_parameter>=<value>] [ADAPTIVE=[0|1] [ADAPTIVE_MARGIN=<value>]`\
`BED_MESH_CALIBRATE PROFILE=<name> METHOD=[manual | automatic | scan | rapid_scan] \
[<probe_parameter>=<value>] [<mesh_parameter>=<value>] [ADAPTIVE=[0|1] \
[ADAPTIVE_MARGIN=<value>]`\
_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] <plot type> <input>
positional arguments:
<plot type> Type of data to graph
<input> 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 <input>`
- `-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 <input>
```
As with the `plot` command, the `<input>` 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 <output file name> <input>
```
The `<input>` 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`

View file

@ -998,6 +998,13 @@ Visual Examples:
#adaptive_margin:
# An optional margin (in mm) to be added around the bed area used by
# the defined print objects when generating an adaptive mesh.
#scan_overshoot:
# The maximum amount of travel (in mm) available outside of the mesh.
# For rectangular beds this applies to travel on the X axis, and for round beds
# it applies to the entire radius. The tool must be able to travel the amount
# specified outside of the mesh. This value is used to optimize the travel
# path when performing a "rapid scan". The minimum value that may be specified
# is 1. The default is no overshoot.
```
### [bed_tilt]
@ -2007,6 +2014,9 @@ Support for eddy current inductive probes. One may define this section
sensor_type: ldc1612
# The sensor chip used to perform eddy current measurements. This
# parameter must be provided and must be set to ldc1612.
#intb_pin:
# MCU gpio pin connected to the ldc1612 sensor's INTB pin (if
# available). The default is to not use the INTB pin.
#z_offset:
# The nominal distance (in mm) between the nozzle and bed that a
# probing attempt should stop at. This parameter must be provided.
@ -2391,6 +2401,69 @@ temperature sensors that are reported via the M105 command.
# parameter.
```
### [temperature_probe]
Reports probe coil temperature. Includes optional thermal drift
calibration for eddy current based probes. A `[temperature_probe]`
section may be linked to a `[probe_eddy_current]` by using the same
postfix for both sections.
```
[temperature_probe my_probe]
#sensor_type:
#sensor_pin:
#min_temp:
#max_temp:
# Temperature sensor configuration.
# See the "extruder" section for the definition of the above
# parameters.
#smooth_time:
# A time value (in seconds) over which temperature measurements will
# be smoothed to reduce the impact of measurement noise. The default
# is 2.0 seconds.
#gcode_id:
# See the "heater_generic" section for the definition of this
# parameter.
#speed:
# The travel speed [mm/s] for xy moves during calibration. Default
# is the speed defined by the probe.
#horizontal_move_z:
# The z distance [mm] from the bed at which xy moves will occur
# during calibration. Default is 2mm.
#resting_z:
# The z distance [mm] from the bed at which the tool will rest
# to heat the probe coil during calibration. Default is .4mm
#calibration_position:
# The X, Y, Z position where the tool should be moved when
# probe drift calibration initializes. This is the location
# where the first manual probe will occur. If omitted, the
# default behavior is not to move the tool prior to the first
# manual probe.
#calibration_bed_temp:
# The maximum safe bed temperature (in C) used to heat the probe
# during probe drift calibration. When set, the calibration
# procedure will turn on the bed after the first sample is
# taken. When the calibration procedure is complete the bed
# temperature will be set to zero. When omitted the default
# behavior is not to set the bed temperature.
#calibration_extruder_temp:
# The extruder temperature (in C) set probe during drift calibration.
# When this option is supplied the procedure will wait for until the
# specified temperature is reached before requesting the first manual
# probe. When the calibration procedure is complete the extruder
# temperature will be set to 0. When omitted the default behavior is
# not to set the extruder temperature.
#extruder_heating_z: 50.
# The Z location where extruder heating will occur if the
# "calibration_extruder_temp" option is set. Its recommended to heat
# the extruder some distance from the bed to minimize its impact on
# the probe coil temperature. The default is 50.
#max_validation_temp: 60.
# The maximum temperature used to validate the calibration. It is
# recommended to set this to a value between 100 and 120 for enclosed
# printers. The default is 60.
```
## Temperature sensors
Klipper includes definitions for many types of temperature sensors.
@ -3317,6 +3390,18 @@ run_current:
# set, "stealthChop" mode will be enabled if the stepper motor
# velocity is below this value. The default is 0, which disables
# "stealthChop" mode.
#coolstep_threshold:
# The velocity (in mm/s) to set the TMC driver internal "CoolStep"
# threshold to. If set, the coolstep feature will be enabled when
# the stepper motor velocity is near or above this value. Important
# - if coolstep_threshold is set and "sensorless homing" is used,
# then one must ensure that the homing speed is above the coolstep
# threshold! The default is to not enable the coolstep feature.
#high_velocity_threshold:
# The velocity (in mm/s) to set the TMC driver internal "high
# velocity" threshold (THIGH) to. This is typically used to disable
# the "CoolStep" feature at high speeds. The default is to not set a
# TMC "high velocity" threshold.
#driver_MSLUT0: 2863314260
#driver_MSLUT1: 1251300522
#driver_MSLUT2: 608774441
@ -3347,11 +3432,19 @@ run_current:
#driver_TOFF: 4
#driver_HEND: 7
#driver_HSTRT: 0
#driver_VHIGHFS: 0
#driver_VHIGHCHM: 0
#driver_PWM_AUTOSCALE: True
#driver_PWM_FREQ: 1
#driver_PWM_GRAD: 4
#driver_PWM_AMPL: 128
#driver_SGT: 0
#driver_SEMIN: 0
#driver_SEUP: 0
#driver_SEMAX: 0
#driver_SEDN: 0
#driver_SEIMIN: 0
#driver_SFILT: 0
# Set the given register during the configuration of the TMC2130
# chip. This may be used to set custom motor parameters. The
# defaults for each parameter are next to the parameter name in the
@ -3448,6 +3541,13 @@ run_current:
#sense_resistor: 0.110
#stealthchop_threshold: 0
# See the "tmc2208" section for the definition of these parameters.
#coolstep_threshold:
# The velocity (in mm/s) to set the TMC driver internal "CoolStep"
# threshold to. If set, the coolstep feature will be enabled when
# the stepper motor velocity is near or above this value. Important
# - if coolstep_threshold is set and "sensorless homing" is used,
# then one must ensure that the homing speed is above the coolstep
# threshold! The default is to not enable the coolstep feature.
#uart_address:
# The address of the TMC2209 chip for UART messages (an integer
# between 0 and 3). This is typically used when multiple TMC2209
@ -3467,6 +3567,11 @@ run_current:
#driver_PWM_GRAD: 14
#driver_PWM_OFS: 36
#driver_SGTHRS: 0
#driver_SEMIN: 0
#driver_SEUP: 0
#driver_SEMAX: 0
#driver_SEDN: 0
#driver_SEIMIN: 0
# Set the given register during the configuration of the TMC2209
# chip. This may be used to set custom motor parameters. The
# defaults for each parameter are next to the parameter name in the
@ -3601,6 +3706,18 @@ run_current:
# set, "stealthChop" mode will be enabled if the stepper motor
# velocity is below this value. The default is 0, which disables
# "stealthChop" mode.
#coolstep_threshold:
# The velocity (in mm/s) to set the TMC driver internal "CoolStep"
# threshold to. If set, the coolstep feature will be enabled when
# the stepper motor velocity is near or above this value. Important
# - if coolstep_threshold is set and "sensorless homing" is used,
# then one must ensure that the homing speed is above the coolstep
# threshold! The default is to not enable the coolstep feature.
#high_velocity_threshold:
# The velocity (in mm/s) to set the TMC driver internal "high
# velocity" threshold (THIGH) to. This is typically used to disable
# the "CoolStep" feature at high speeds. The default is to not set a
# TMC "high velocity" threshold.
#driver_MSLUT0: 2863314260
#driver_MSLUT1: 1251300522
#driver_MSLUT2: 608774441
@ -3722,6 +3839,18 @@ run_current:
# set, "stealthChop" mode will be enabled if the stepper motor
# velocity is below this value. The default is 0, which disables
# "stealthChop" mode.
#coolstep_threshold:
# The velocity (in mm/s) to set the TMC driver internal "CoolStep"
# threshold to. If set, the coolstep feature will be enabled when
# the stepper motor velocity is near or above this value. Important
# - if coolstep_threshold is set and "sensorless homing" is used,
# then one must ensure that the homing speed is above the coolstep
# threshold! The default is to not enable the coolstep feature.
#high_velocity_threshold:
# The velocity (in mm/s) to set the TMC driver internal "high
# velocity" threshold (THIGH) to. This is typically used to disable
# the "CoolStep" feature at high speeds. The default is to not set a
# TMC "high velocity" threshold.
#driver_MSLUT0: 2863314260
#driver_MSLUT1: 1251300522
#driver_MSLUT2: 608774441
@ -4535,6 +4664,95 @@ adc2:
# above parameters.
```
## Load Cells
### [load_cell]
Load Cell. Uses an ADC sensor attached to a load cell to create a digital
scale.
```
[load_cell]
sensor_type:
# This must be one of the supported sensor types, see below.
```
#### XH711
This is a 24 bit low sample rate chip using "bit-bang" communications. It is
suitable for filament scales.
```
[load_cell]
sensor_type: hx711
sclk_pin:
# The pin connected to the HX711 clock line. This parameter must be provided.
dout_pin:
# The pin connected to the HX711 data output line. This parameter must be
# provided.
#gain: A-128
# Valid values for gain are: A-128, A-64, B-32. The default is A-128.
# 'A' denotes the input channel and the number denotes the gain. Only the 3
# listed combinations are supported by the chip. Note that changing the gain
# setting also selects the channel being read.
#sample_rate: 80
# Valid values for sample_rate are 80 or 10. The default value is 80.
# This must match the wiring of the chip. The sample rate cannot be changed
# in software.
```
#### HX717
This is the 4x higher sample rate version of the HX711, suitable for probing.
```
[load_cell]
sensor_type: hx717
sclk_pin:
# The pin connected to the HX717 clock line. This parameter must be provided.
dout_pin:
# The pin connected to the HX717 data output line. This parameter must be
# provided.
#gain: A-128
# Valid values for gain are A-128, B-64, A-64, B-8.
# 'A' denotes the input channel and the number denotes the gain setting.
# Only the 4 listed combinations are supported by the chip. Note that
# changing the gain setting also selects the channel being read.
#sample_rate: 320
# Valid values for sample_rate are: 10, 20, 80, 320. The default is 320.
# This must match the wiring of the chip. The sample rate cannot be changed
# in software.
```
#### ADS1220
The ADS1220 is a 24 bit ADC supporting up to a 2Khz sample rate configurable in
software.
```
[load_cell]
sensor_type: ads1220
cs_pin:
# The pin connected to the ADS1220 chip select line. This parameter must
# be provided.
#spi_speed: 512000
# This chip supports 2 speeds: 256000 or 512000. The faster speed is only
# enabled when one of the Turbo sample rates is used. The correct spi_speed
# is selected based on the sample rate.
#spi_bus:
#spi_software_sclk_pin:
#spi_software_mosi_pin:
#spi_software_miso_pin:
# See the "common SPI settings" section for a description of the
# above parameters.
data_ready_pin:
# Pin connected to the ADS1220 data ready line. This parameter must be
# provided.
#gain: 128
# Valid gain values are 128, 64, 32, 16, 8, 4, 2, 1
# The default is 128
#sample_rate: 660
# This chip supports two ranges of sample rates, Normal and Turbo. In turbo
# mode the chips c internal clock runs twice as fast and the SPI communication
# speed is also doubled.
# Normal sample rates: 20, 45, 90, 175, 330, 600, 1000
# Turbo sample rates: 40, 90, 180, 350, 660, 1200, 2000
# The default is 660
```
## Board specific hardware support
### [sx1509]

View file

@ -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.

View file

@ -1415,3 +1415,39 @@ command will probe the points specified in the config and then make independent
adjustments to each Z stepper to compensate for tilt. See the PROBE command for
details on the optional probe parameters. The optional `HORIZONTAL_MOVE_Z`
value overrides the `horizontal_move_z` option specified in the config file.
### [temperature_probe]
The following commands are available when a
[temperature_probe config section](Config_Reference.md#temperature_probe)
is enabled.
#### TEMPERATURE_PROBE_CALIBRATE
`TEMPERATURE_PROBE_CALIBRATE [PROBE=<probe name>] [TARGET=<value>] [STEP=<value>]`:
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.

View file

@ -1,15 +1,20 @@
# Installation
These instructions assume the software will run on a Raspberry Pi
computer in conjunction with OctoPrint. It is recommended that a
Raspberry Pi 2 (or later) be used as the host machine (see the
These instructions assume the software will run on a linux based host
running a Klipper compatible front end. It is recommended that a
SBC(Small Board Computer) such as a Raspberry Pi or Debian based Linux
device be used as the host machine (see the
[FAQ](FAQ.md#can-i-run-klipper-on-something-other-than-a-raspberry-pi-3)
for other machines).
for other options).
For the purposes of these instructions host relates to the Linux device and
mcu relates to the printboard. SBC relates to the term Small Board Computer
such as the Raspberry Pi.
## Obtain a Klipper Configuration File
Most Klipper settings are determined by a "printer configuration file"
that will be stored on the Raspberry Pi. An appropriate configuration
printer.cfg, that will be stored on the host. An appropriate configuration
file can often be found by looking in the Klipper
[config directory](../config/) for a file starting with a "printer-"
prefix that corresponds to the target printer. The Klipper
@ -35,38 +40,51 @@ printer configuration file, then start with the closest example
[config file](../config/) and use the Klipper
[config reference](Config_Reference.md) for further information.
## Prepping an OS image
## Interacting with Klipper
Start by installing [OctoPi](https://github.com/guysoft/OctoPi) on the
Raspberry Pi computer. Use OctoPi v0.17.0 or later - see the
[OctoPi releases](https://github.com/guysoft/OctoPi/releases) for
release information. One should verify that OctoPi boots and that the
OctoPrint web server works. After connecting to the OctoPrint web
page, follow the prompt to upgrade OctoPrint to v1.4.2 or later.
Klipper is a 3d printer firmware, so it needs some way for the user to
interact with it.
After installing OctoPi and upgrading OctoPrint, it will be necessary
to ssh into the target machine to run a handful of system commands. If
using a Linux or MacOS desktop, then the "ssh" software should already
be installed on the desktop. There are free ssh clients available for
other desktops (eg,
[PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/)). Use the
ssh utility to connect to the Raspberry Pi (`ssh pi@octopi` -- password
is "raspberry") and run the following commands:
Currently the best choices are front ends that retrieve information through
the [Moonraker web API](https://moonraker.readthedocs.io/) and there is also
the option to use [Octoprint](https://octoprint.org/) to control Klipper.
```
git clone https://github.com/Klipper3d/klipper
./klipper/scripts/install-octopi.sh
```
The choice is up to the user on what to use, but the underlying Klipper is the
same in all cases. We encourage users to research the options available and
make an informed decision.
The above will download Klipper, install some system dependencies,
setup Klipper to run at system startup, and start the Klipper host
software. It will require an internet connection and it may take a few
minutes to complete.
## Obtaining an OS image for SBC's
There are many ways to obtain an OS image for Klipper for SBC use, most depend on
what front end you wish to use. Some manafactures of these SBC boards also provide
their own Klipper-centric images.
The two main Moonraker based front ends are [Fluidd](https://docs.fluidd.xyz/)
and [Mainsail](https://docs.mainsail.xyz/), the latter of which has a premade install
image ["MainsailOS"](http://docs.mainsailOS.xyz), this has the option for Raspberry Pi
and some OrangePi varianta.
Fluidd can be installed via KIAUH(Klipper Install And Update Helper), which
is explained below and is a 3rd party installer for all things Klipper.
OctoPrint can be installed via the popular OctoPi image or via KIAUH, this
process is explained in [OctoPrint.md](OctoPrint.md)
## Installing via KIAUH
Normally you would start with a base image for your SBC, RPiOS Lite for example,
or in the case of a x86 Linux device, Ubuntu Server. Please note that Desktop
variants are not recommended due to certain helper programs that can stop some
Klipper functions working and even mask access to some print boards.
KIAUH can be used to install Klipper and its associated programs on a variety
of Linux based systems that run a form of Debian. More information can be found
at https://github.com/dw-0/kiauh
## Building and flashing the micro-controller
To compile the micro-controller code, start by running these commands
on the Raspberry Pi:
on your host device:
```
cd ~/klipper/
@ -108,10 +126,21 @@ It should report something similar to the following:
It's common for each printer to have its own unique serial port name.
This unique name will be used when flashing the micro-controller. It's
possible there may be multiple lines in the above output - if so,
choose the line corresponding to the micro-controller (see the
choose the line corresponding to the micro-controller. If many
items are listed and the choice is ambiguous, unplug the board and
run the command again, the missing item will be your print board(see the
[FAQ](FAQ.md#wheres-my-serial-port) for more information).
For common micro-controllers, the code can be flashed with something
For common micro-controllers with STM32 or clone chips, LPC chips and
others it is usual that these need an initial Klipper flash via SD card.
When flashing with this method, it is important to make sure that the
print board is not connected with USB to the host, due to some boards
being able to feed power back to the board and stopping a flash from
occuring.
For common micro-controllers using Atmega chips, for example the 2560,
the code can be flashed with something
similar to:
```
@ -123,53 +152,38 @@ sudo service klipper start
Be sure to update the FLASH_DEVICE with the printer's unique serial
port name.
When flashing for the first time, make sure that OctoPrint is not
connected directly to the printer (from the OctoPrint web page, under
the "Connection" section, click "Disconnect").
For common micro-controllers using RP2040 chips, the code can be flashed
with something similar to:
## Configuring OctoPrint to use Klipper
```
sudo service klipper stop
make flash FLASH_DEVICE=first
sudo service klipper start
```
The OctoPrint web server needs to be configured to communicate with
the Klipper host software. Using a web browser, login to the OctoPrint
web page and then configure the following items:
It is important to note that RP2040 chips may need to be put into Boot mode
before this operation.
Navigate to the Settings tab (the wrench icon at the top of the
page). Under "Serial Connection" in "Additional serial ports" add
`/tmp/printer`. Then click "Save".
Enter the Settings tab again and under "Serial Connection" change the
"Serial Port" setting to `/tmp/printer`.
In the Settings tab, navigate to the "Behavior" sub-tab and select the
"Cancel any ongoing prints but stay connected to the printer"
option. Click "Save".
From the main page, under the "Connection" section (at the top left of
the page) make sure the "Serial Port" is set to `/tmp/printer` and
click "Connect". (If `/tmp/printer` is not an available selection then
try reloading the page.)
Once connected, navigate to the "Terminal" tab and type "status"
(without the quotes) into the command entry box and click "Send". The
terminal window will likely report there is an error opening the
config file - that means OctoPrint is successfully communicating with
Klipper. Proceed to the next section.
## Configuring Klipper
The next step is to copy the
[printer configuration file](#obtain-a-klipper-configuration-file) to
the Raspberry Pi.
the host.
Arguably the easiest way to set the Klipper configuration file is to
use a desktop editor that supports editing files over the "scp" and/or
"sftp" protocols. There are freely available tools that support this
(eg, Notepad++, WinSCP, and Cyberduck). Load the printer config file
in the editor and then save it as a file named `printer.cfg` in the
home directory of the pi user (ie, `/home/pi/printer.cfg`).
Arguably the easiest way to set the Klipper configuration file is using the
built in editors in Mainsail or Fluidd. These will allow the user to open
the configuration examples and save them to be printer.cfg.
Another option is to use a desktop editor that supports editing files
over the "scp" and/or "sftp" protocols. There are freely available tools
that support this (eg, Notepad++, WinSCP, and Cyberduck).
Load the printer config file in the editor and then save it as a file
named "printer.cfg" in the home directory of the pi user
(ie, /home/pi/printer.cfg).
Alternatively, one can also copy and edit the file directly on the
Raspberry Pi via ssh. That may look something like the following (be
host via ssh. That may look something like the following (be
sure to update the command to use the appropriate printer config
filename):
@ -201,7 +215,7 @@ serial: /dev/serial/by-id/usb-1a86_USB2.0-Serial-if00-port0
```
After creating and editing the file it will be necessary to issue a
"restart" command in the OctoPrint web terminal to load the config. A
"restart" command in the command console to load the config. A
"status" command will report the printer is ready if the Klipper
config file is successfully read and the micro-controller is
successfully found and configured.
@ -211,10 +225,10 @@ Klipper to report a configuration error. If an error occurs, make any
necessary corrections to the printer config file and issue "restart"
until "status" reports the printer is ready.
Klipper reports error messages via the OctoPrint terminal tab. The
"status" command can be used to re-report error messages. The default
Klipper startup script also places a log in **/tmp/klippy.log** which
provides more detailed information.
Klipper reports error messages via the command console and via pop up in
Fluidd and Mainsail. The "status" command can be used to re-report error
messages. A log is available and usually located in ~/printer_data/logs
this is named klippy.log
After Klipper reports that the printer is ready, proceed to the
[config check document](Config_checks.md) to perform some basic checks

79
docs/OctoPrint.md Normal file
View file

@ -0,0 +1,79 @@
# OctoPrint for Klipper
Klipper has a few options for its front ends, Octoprint was the first
and original front end for Klipper. This document will give
a brief overview of installing with this option.
## Install with OctoPi
Start by installing [OctoPi](https://github.com/guysoft/OctoPi) on the
Raspberry Pi computer. Use OctoPi v0.17.0 or later - see the
[OctoPi releases](https://github.com/guysoft/OctoPi/releases) for
release information.
One should verify that OctoPi boots and that the
OctoPrint web server works. After connecting to the OctoPrint web
page, follow the prompt to upgrade OctoPrint if needed.
After installing OctoPi and upgrading OctoPrint, it will be necessary
to ssh into the target machine to run a handful of system commands.
Start by running these commands on your host device:
__If you do not have git installed, please do so with:__
```
sudo apt install git
```
then proceed:
```
cd ~
git clone https://github.com/Klipper3d/klipper
./klipper/scripts/install-octopi.sh
```
The above will download Klipper, install the needed system dependencies,
setup Klipper to run at system startup, and start the Klipper host
software. It will require an internet connection and it may take a few
minutes to complete.
## Installing with KIAUH
KIAUH can be used to install OctoPrint on a variety of Linux based systems
that run a form of Debian. More information can be found
at https://github.com/dw-0/kiauh
## Configuring OctoPrint to use Klipper
The OctoPrint web server needs to be configured to communicate with the Klipper
host software. Using a web browser, login to the OctoPrint web page and then
configure the following items:
Navigate to the Settings tab (the wrench icon at the top of the page).
Under "Serial Connection" in "Additional serial ports" add:
```
~/printer_data/comms/klippy.sock
```
Then click "Save".
_In some older setups this address may be `/tmp/printer`_
Enter the Settings tab again and under "Serial Connection" change the "Serial Port"
setting to the one added above.
In the Settings tab, navigate to the "Behavior" sub-tab and select the
"Cancel any ongoing prints but stay connected to the printer" option. Click "Save".
From the main page, under the "Connection" section (at the top left of the page)
make sure the "Serial Port" is set to the new additional one added
and click "Connect". (If it is not in the available selection then
try reloading the page.)
Once connected, navigate to the "Terminal" tab and type "status" (without the quotes)
into the command entry box and click "Send". The terminal window will likely report
there is an error opening the config file - that means OctoPrint is successfully
communicating with Klipper.
Please proceed to [Installation.md](Installation.md) and the
_Building and flashing the micro-controller_ section

View file

@ -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

View file

@ -1,5 +1,5 @@
# Python virtualenv module requirements for mkdocs
jinja2==3.1.3
jinja2==3.1.4
mkdocs==1.2.4
mkdocs-material==8.1.3
mkdocs-simple-hooks==0.1.3

View file

@ -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

View file

@ -142,8 +142,9 @@ defs_kin_winch = """
defs_kin_extruder = """
struct stepper_kinematics *extruder_stepper_alloc(void);
void extruder_stepper_free(struct stepper_kinematics *sk);
void extruder_set_pressure_advance(struct stepper_kinematics *sk
, double pressure_advance, double smooth_time);
, double print_time, double pressure_advance, double smooth_time);
"""
defs_kin_shaper = """

View file

@ -9,9 +9,15 @@
#include <string.h> // 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);
}

View file

@ -69,6 +69,8 @@ class ConfigWrapper:
return self._get_wrapper(self.fileconfig.getboolean, option, default,
note_valid=note_valid)
def getchoice(self, option, choices, default=sentinel, note_valid=True):
if type(choices) == type([]):
choices = {i: i for i in choices}
if choices and type(list(choices.keys())[0]) == int:
c = self.getint(option, default, note_valid=note_valid)
else:

View file

@ -7,7 +7,6 @@
SAMPLE_TIME = 0.001
SAMPLE_COUNT = 8
REPORT_TIME = 0.300
RANGE_CHECK_COUNT = 4
class MCU_scaled_adc:
def __init__(self, main, pin_params):
@ -18,7 +17,7 @@ class MCU_scaled_adc:
qname = main.name + ":" + pin_params['pin']
query_adc.register_adc(qname, self._mcu_adc)
self._callback = None
self.setup_minmax = self._mcu_adc.setup_minmax
self.setup_adc_sample = self._mcu_adc.setup_adc_sample
self.get_mcu = self._mcu_adc.get_mcu
def _handle_callback(self, read_time, read_value):
max_adc = self._main.last_vref[1]
@ -54,8 +53,7 @@ class PrinterADCScaled:
ppins = self.printer.lookup_object('pins')
mcu_adc = ppins.setup_pin('adc', pin_name)
mcu_adc.setup_adc_callback(REPORT_TIME, callback)
mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, minval=0., maxval=1.,
range_check_count=RANGE_CHECK_COUNT)
mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT)
query_adc = config.get_printer().load_object(config, 'query_adc')
query_adc.register_adc(self.name + ":" + name, mcu_adc)
return mcu_adc

View file

@ -1,6 +1,6 @@
# Obtain temperature using linear interpolation of ADC values
#
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2016-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, bisect
@ -22,8 +22,8 @@ class PrinterADCtoTemperature:
ppins = config.get_printer().lookup_object('pins')
self.mcu_adc = ppins.setup_pin('adc', config.get('sensor_pin'))
self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback)
query_adc = config.get_printer().load_object(config, 'query_adc')
query_adc.register_adc(config.get_name(), self.mcu_adc)
self.diag_helper = HelperTemperatureDiagnostics(
config, self.mcu_adc, adc_convert.calc_temp)
def setup_callback(self, temperature_callback):
self.temperature_callback = temperature_callback
def get_report_time_delta(self):
@ -32,10 +32,44 @@ class PrinterADCtoTemperature:
temp = self.adc_convert.calc_temp(read_value)
self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp)
def setup_minmax(self, min_temp, max_temp):
adc_range = [self.adc_convert.calc_adc(t) for t in [min_temp, max_temp]]
self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT,
minval=min(adc_range), maxval=max(adc_range),
range_check_count=RANGE_CHECK_COUNT)
arange = [self.adc_convert.calc_adc(t) for t in [min_temp, max_temp]]
min_adc, max_adc = sorted(arange)
self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT,
minval=min_adc, maxval=max_adc,
range_check_count=RANGE_CHECK_COUNT)
self.diag_helper.setup_diag_minmax(min_temp, max_temp, min_adc, max_adc)
# Tool to register with query_adc and report extra info on ADC range errors
class HelperTemperatureDiagnostics:
def __init__(self, config, mcu_adc, calc_temp_cb):
self.printer = config.get_printer()
self.name = config.get_name()
self.mcu_adc = mcu_adc
self.calc_temp_cb = calc_temp_cb
self.min_temp = self.max_temp = self.min_adc = self.max_adc = None
query_adc = self.printer.load_object(config, 'query_adc')
query_adc.register_adc(self.name, self.mcu_adc)
error_mcu = self.printer.load_object(config, 'error_mcu')
error_mcu.add_clarify("ADC out of range", self._clarify_adc_range)
def setup_diag_minmax(self, min_temp, max_temp, min_adc, max_adc):
self.min_temp, self.max_temp = min_temp, max_temp
self.min_adc, self.max_adc = min_adc, max_adc
def _clarify_adc_range(self, msg, details):
if self.min_temp is None:
return None
last_value, last_read_time = self.mcu_adc.get_last_value()
if not last_read_time:
return None
if last_value >= self.min_adc and last_value <= self.max_adc:
return None
tempstr = "?"
try:
last_temp = self.calc_temp_cb(last_value)
tempstr = "%.3f" % (last_temp,)
except e:
logging.exception("Error in calc_temp callback")
return ("Sensor '%s' temperature %s not in range %.3f:%.3f"
% (self.name, tempstr, self.min_temp, self.max_temp))
######################################################################

187
klippy/extras/ads1220.py Normal file
View file

@ -0,0 +1,187 @@
# ADS1220 Support
#
# Copyright (C) 2024 Gareth Farrington <gareth@waves.ky>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import bulk_sensor, bus
#
# Constants
#
BYTES_PER_SAMPLE = 4 # samples are 4 byte wide unsigned integers
MAX_SAMPLES_PER_MESSAGE = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE
UPDATE_INTERVAL = 0.10
RESET_CMD = 0x06
START_SYNC_CMD = 0x08
RREG_CMD = 0x20
WREG_CMD = 0x40
NOOP_CMD = 0x0
RESET_STATE = bytearray([0x0, 0x0, 0x0, 0x0])
# turn bytearrays into pretty hex strings: [0xff, 0x1]
def hexify(byte_array):
return "[%s]" % (", ".join([hex(b) for b in byte_array]))
class ADS1220():
def __init__(self, config):
self.printer = printer = config.get_printer()
self.name = config.get_name().split()[-1]
self.last_error_count = 0
self.consecutive_fails = 0
# Chip options
# Gain
self.gain_options = {'1': 0x0, '2': 0x1, '4': 0x2, '8': 0x3, '16': 0x4,
'32': 0x5, '64': 0x6, '128': 0x7}
self.gain = config.getchoice('gain', self.gain_options, default='128')
# Sample rate
self.sps_normal = {'20': 20, '45': 45, '90': 90, '175': 175,
'330': 330, '600': 600, '1000': 1000}
self.sps_turbo = {'40': 40, '90': 90, '180': 180, '350': 350,
'660': 660, '1200': 1200, '2000': 2000}
self.sps_options = self.sps_normal.copy()
self.sps_options.update(self.sps_turbo)
self.sps = config.getchoice('sps', self.sps_options, default='660')
self.is_turbo = str(self.sps) in self.sps_turbo
# SPI Setup
spi_speed = 512000 if self.is_turbo else 256000
self.spi = bus.MCU_SPI_from_config(config, 1, default_speed=spi_speed)
self.mcu = mcu = self.spi.get_mcu()
self.oid = mcu.create_oid()
# Data Ready (DRDY) Pin
drdy_pin = config.get('data_ready_pin')
ppins = printer.lookup_object('pins')
drdy_ppin = ppins.lookup_pin(drdy_pin)
self.data_ready_pin = drdy_ppin['pin']
drdy_pin_mcu = drdy_ppin['chip']
if drdy_pin_mcu != self.mcu:
raise config.error("ADS1220 config error: SPI communication and"
" data_ready_pin must be on the same MCU")
# Bulk Sensor Setup
self.bulk_queue = bulk_sensor.BulkDataQueue(self.mcu, oid=self.oid)
# Clock tracking
chip_smooth = self.sps * UPDATE_INTERVAL * 2
# Measurement conversion
self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, "<i")
# Process messages in batches
self.batch_bulk = bulk_sensor.BatchBulkHelper(
self.printer, self._process_batch, self._start_measurements,
self._finish_measurements, UPDATE_INTERVAL)
# publish raw samples to the socket
hdr = {'header': ('time', 'counts', 'value')}
self.batch_bulk.add_mux_endpoint("ads1220/dump_ads1220", "sensor",
self.name, hdr)
# Command Configuration
mcu.add_config_cmd(
"config_ads1220 oid=%d spi_oid=%d data_ready_pin=%s"
% (self.oid, self.spi.get_oid(), self.data_ready_pin))
mcu.add_config_cmd("query_ads1220 oid=%d rest_ticks=0"
% (self.oid,), on_restart=True)
mcu.register_config_callback(self._build_config)
self.query_ads1220_cmd = None
def _build_config(self):
cmdqueue = self.spi.get_command_queue()
self.query_ads1220_cmd = self.mcu.lookup_command(
"query_ads1220 oid=%c rest_ticks=%u", cq=cmdqueue)
self.ffreader.setup_query_command("query_ads1220_status oid=%c",
oid=self.oid, cq=cmdqueue)
def get_mcu(self):
return self.mcu
def get_samples_per_second(self):
return self.sps
# returns a tuple of the minimum and maximum value of the sensor, used to
# detect if a data value is saturated
def get_range(self):
return -0x800000, 0x7FFFFF
# add_client interface, direct pass through to bulk_sensor API
def add_client(self, callback):
self.batch_bulk.add_client(callback)
# Measurement decoding
def _convert_samples(self, samples):
adc_factor = 1. / (1 << 23)
count = 0
for ptime, val in samples:
samples[count] = (round(ptime, 6), val, round(val * adc_factor, 9))
count += 1
del samples[count:]
# Start, stop, and process message batches
def _start_measurements(self):
self.last_error_count = 0
self.consecutive_fails = 0
# Start bulk reading
self.reset_chip()
self.setup_chip()
rest_ticks = self.mcu.seconds_to_clock(1. / (10. * self.sps))
self.query_ads1220_cmd.send([self.oid, rest_ticks])
logging.info("ADS1220 starting '%s' measurements", self.name)
# Initialize clock tracking
self.ffreader.note_start()
def _finish_measurements(self):
# don't use serial connection after shutdown
if self.printer.is_shutdown():
return
# Halt bulk reading
self.query_ads1220_cmd.send_wait_ack([self.oid, 0])
self.ffreader.note_end()
logging.info("ADS1220 finished '%s' measurements", self.name)
def _process_batch(self, eventtime):
samples = self.ffreader.pull_samples()
self._convert_samples(samples)
return {'data': samples, 'errors': self.last_error_count,
'overflows': self.ffreader.get_last_overflows()}
def reset_chip(self):
# the reset command takes 50us to complete
self.send_command(RESET_CMD)
# read startup register state and validate
val = self.read_reg(0x0, 4)
if val != RESET_STATE:
raise self.printer.command_error(
"Invalid ads1220 reset state (got %s vs %s).\n"
"This is generally indicative of connection problems\n"
"(e.g. faulty wiring) or a faulty ADS1220 chip."
% (hexify(val), hexify(RESET_STATE)))
def setup_chip(self):
continuous = 0x1 # enable continuous conversions
mode = 0x2 if self.is_turbo else 0x0 # turbo mode
sps_list = self.sps_turbo if self.is_turbo else self.sps_normal
data_rate = list(sps_list.keys()).index(str(self.sps))
reg_values = [(self.gain << 1),
(data_rate << 5) | (mode << 3) | (continuous << 2)]
self.write_reg(0x0, reg_values)
# start measurements immediately
self.send_command(START_SYNC_CMD)
def read_reg(self, reg, byte_count):
read_command = [RREG_CMD | (reg << 2) | (byte_count - 1)]
read_command += [NOOP_CMD] * byte_count
params = self.spi.spi_transfer(read_command)
return bytearray(params['response'][1:])
def send_command(self, cmd):
self.spi.spi_send([cmd])
def write_reg(self, reg, register_bytes):
write_command = [WREG_CMD | (reg << 2) | (len(register_bytes) - 1)]
write_command.extend(register_bytes)
self.spi.spi_send(write_command)
stored_val = self.read_reg(reg, len(register_bytes))
if register_bytes != stored_val:
raise self.printer.command_error(
"Failed to set ADS1220 register [0x%x] to %s: got %s. "
"This may be a connection problem (e.g. faulty wiring)" % (
reg, hexify(register_bytes), hexify(stored_val)))
ADS1220_SENSOR_TYPE = {"ads1220": ADS1220}

View file

@ -5,7 +5,7 @@
# This file may be distributed under the terms of the GNU GPLv3 license.
import math
from . import manual_probe as ManualProbe, bed_mesh as BedMesh
from . import manual_probe, bed_mesh, probe
DEFAULT_SAMPLE_COUNT = 3
@ -38,10 +38,13 @@ class AxisTwistCompensation:
# setup calibrater
self.calibrater = Calibrater(self, config)
# register events
self.printer.register_event_handler("probe:update_results",
self._update_z_compensation_value)
def get_z_compensation_value(self, pos):
def _update_z_compensation_value(self, pos):
if not self.z_compensations:
return 0
return
x_coord = pos[0]
z_compensations = self.z_compensations
@ -50,12 +53,12 @@ class AxisTwistCompensation:
/ (sample_count - 1))
interpolate_t = (x_coord - self.calibrate_start_x) / spacing
interpolate_i = int(math.floor(interpolate_t))
interpolate_i = BedMesh.constrain(interpolate_i, 0, sample_count - 2)
interpolate_i = bed_mesh.constrain(interpolate_i, 0, sample_count - 2)
interpolate_t -= interpolate_i
interpolated_z_compensation = BedMesh.lerp(
interpolated_z_compensation = bed_mesh.lerp(
interpolate_t, z_compensations[interpolate_i],
z_compensations[interpolate_i + 1])
return interpolated_z_compensation
pos[2] += interpolated_z_compensation
def clear_compensations(self):
self.z_compensations = []
@ -95,7 +98,7 @@ class Calibrater:
config = self.printer.lookup_object('configfile')
raise config.error(
"AXIS_TWIST_COMPENSATION requires [probe] to be defined")
self.lift_speed = self.probe.get_lift_speed()
self.lift_speed = self.probe.get_probe_params()['lift_speed']
self.probe_x_offset, self.probe_y_offset, _ = \
self.probe.get_offsets()
@ -134,7 +137,7 @@ class Calibrater:
nozzle_points, self.probe_x_offset, self.probe_y_offset)
# verify no other manual probe is in progress
ManualProbe.verify_no_manual_probe(self.printer)
manual_probe.verify_no_manual_probe(self.printer)
# begin calibration
self.current_point_index = 0
@ -186,7 +189,8 @@ class Calibrater:
probe_points[self.current_point_index][1], None))
# probe the point
self.current_measured_z = self.probe.run_probe(self.gcmd)[2]
pos = probe.run_single_probe(self.probe, self.gcmd)
self.current_measured_z = pos[2]
# horizontal_move_z (to prevent probe trigger or hitting bed)
self._move_helper((None, None, self.horizontal_move_z))
@ -195,7 +199,7 @@ class Calibrater:
self._move_helper((nozzle_points[self.current_point_index]))
# start the manual (nozzle) probe
ManualProbe.ManualProbeHelper(
manual_probe.ManualProbeHelper(
self.printer, self.gcmd,
self._manual_probe_callback_factory(
probe_points, nozzle_points, interval))

View file

@ -121,6 +121,11 @@ class BedMesh:
self.gcode.register_command(
'BED_MESH_OFFSET', self.cmd_BED_MESH_OFFSET,
desc=self.cmd_BED_MESH_OFFSET_help)
# Register dump webhooks
webhooks = self.printer.lookup_object('webhooks')
webhooks.register_endpoint(
"bed_mesh/dump_mesh", self._handle_dump_request
)
# Register transform
gcode_move = self.printer.load_object(config, 'gcode_move')
gcode_move.set_move_transform(self)
@ -282,6 +287,31 @@ class BedMesh:
gcode_move.reset_last_position()
else:
gcmd.respond_info("No mesh loaded to offset")
def _handle_dump_request(self, web_request):
eventtime = self.printer.get_reactor().monotonic()
prb = self.printer.lookup_object("probe", None)
th_sts = self.printer.lookup_object("toolhead").get_status(eventtime)
result = {"current_mesh": {}, "profiles": self.pmgr.get_profiles()}
if self.z_mesh is not None:
result["current_mesh"] = {
"name": self.z_mesh.get_profile_name(),
"probed_matrix": self.z_mesh.get_probed_matrix(),
"mesh_matrix": self.z_mesh.get_mesh_matrix(),
"mesh_params": self.z_mesh.get_mesh_params()
}
mesh_args = web_request.get_dict("mesh_args", {})
gcmd = None
if mesh_args:
gcmd = self.gcode.create_gcode_command("", "", mesh_args)
with self.gcode.get_mutex():
result["calibration"] = self.bmc.dump_calibration(gcmd)
else:
result["calibration"] = self.bmc.dump_calibration()
offsets = [0, 0, 0] if prb is None else prb.get_offsets()
result["probe_offsets"] = offsets
result["axis_minimum"] = th_sts["axis_minimum"]
result["axis_maximum"] = th_sts["axis_maximum"]
web_request.send(result)
class ZrefMode:
@ -298,130 +328,24 @@ class BedMeshCalibrate:
self.radius = self.origin = None
self.mesh_min = self.mesh_max = (0., 0.)
self.adaptive_margin = config.getfloat('adaptive_margin', 0.0)
self.zero_ref_pos = config.getfloatlist(
"zero_reference_position", None, count=2
)
self.zero_reference_mode = ZrefMode.DISABLED
self.faulty_regions = []
self.substituted_indices = collections.OrderedDict()
self.bedmesh = bedmesh
self.mesh_config = collections.OrderedDict()
self._init_mesh_config(config)
self._generate_points(config.error)
self.probe_mgr = ProbeManager(
config, self.orig_config, self.probe_finalize
)
try:
self.probe_mgr.generate_points(
self.mesh_config, self.mesh_min, self.mesh_max,
self.radius, self.origin
)
except BedMeshError as e:
raise config.error(str(e))
self._profile_name = "default"
self.probe_helper = probe.ProbePointsHelper(
config, self.probe_finalize, self._get_adjusted_points())
self.probe_helper.minimum_points(3)
self.probe_helper.use_xy_offsets(True)
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command(
'BED_MESH_CALIBRATE', self.cmd_BED_MESH_CALIBRATE,
desc=self.cmd_BED_MESH_CALIBRATE_help)
def _generate_points(self, error, probe_method="automatic"):
x_cnt = self.mesh_config['x_count']
y_cnt = self.mesh_config['y_count']
min_x, min_y = self.mesh_min
max_x, max_y = self.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 error("bed_mesh: min/max points too close together")
if self.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 self.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 <= self.radius:
points.append(
(self.origin[0] + pos_x, self.origin[1] + pos_y))
pos_y += y_dist
self.points = points
if self.zero_ref_pos is None or probe_method == "manual":
# Zero Reference Disabled
self.zero_reference_mode = ZrefMode.DISABLED
elif within(self.zero_ref_pos, self.mesh_min, self.mesh_max):
# Zero Reference position within mesh
self.zero_reference_mode = ZrefMode.IN_MESH
else:
# Zero Reference position outside of mesh
self.zero_reference_mode = ZrefMode.PROBE
if not self.faulty_regions:
return
self.substituted_indices.clear()
if self.zero_reference_mode == ZrefMode.PROBE:
# Cannot probe a reference within a faulty region
for min_c, max_c in self.faulty_regions:
if within(self.zero_ref_pos, min_c, max_c):
opt = "zero_reference_position"
raise error(
"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
if probe_method == "manual":
return
last_y = self.points[0][1]
is_reversed = False
for i, coord in enumerate(self.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 self.radius is None:
if within(ac, (min_x, min_y), (max_x, max_y), .000001):
valid_coords.append(ac)
else:
dist_from_origin = math.sqrt(ac[0]*ac[0] + ac[1]*ac[1])
if dist_from_origin <= self.radius:
valid_coords.append(ac)
if not valid_coords:
raise error("bed_mesh: Unable to generate coordinates"
" for faulty region at index: %d" % (i))
self.substituted_indices[i] = valid_coords
def print_generated_points(self, print_func):
x_offset = y_offset = 0.
probe = self.printer.lookup_object('probe', None)
@ -429,20 +353,23 @@ class BedMeshCalibrate:
x_offset, y_offset = probe.get_offsets()[:2]
print_func("bed_mesh: generated points\nIndex"
" | Tool Adjusted | Probe")
for i, (x, y) in enumerate(self.points):
points = self.probe_mgr.get_base_points()
for i, (x, y) in enumerate(points):
adj_pt = "(%.1f, %.1f)" % (x - x_offset, y - y_offset)
mesh_pt = "(%.1f, %.1f)" % (x, y)
print_func(
" %-4d| %-16s| %s" % (i, adj_pt, mesh_pt))
if self.zero_ref_pos is not None:
zero_ref_pos = self.probe_mgr.get_zero_ref_pos()
if zero_ref_pos is not None:
print_func(
"bed_mesh: zero_reference_position is (%.2f, %.2f)"
% (self.zero_ref_pos[0], self.zero_ref_pos[1])
% (zero_ref_pos[0], zero_ref_pos[1])
)
if self.substituted_indices:
substitutes = self.probe_mgr.get_substitutes()
if substitutes:
print_func("bed_mesh: faulty region points")
for i, v in self.substituted_indices.items():
pt = self.points[i]
for i, v in substitutes.items():
pt = points[i]
print_func("%d (%.2f, %.2f), substituted points: %s"
% (i, pt[0], pt[1], repr(v)))
def _init_mesh_config(self, config):
@ -481,42 +408,6 @@ class BedMeshCalibrate:
config.get('algorithm', 'lagrange').strip().lower()
orig_cfg['tension'] = mesh_cfg['tension'] = config.getfloat(
'bicubic_tension', .2, minval=0., maxval=2.)
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))
self._verify_algorithm(config.error)
def _verify_algorithm(self, error):
params = self.mesh_config
@ -652,8 +543,11 @@ class BedMeshCalibrate:
self.origin = adapted_origin
self.mesh_min = (-self.radius, -self.radius)
self.mesh_max = (self.radius, self.radius)
new_probe_count = max(new_x_probe_count, new_y_probe_count)
# Adaptive meshes require odd number of points
new_probe_count += 1 - (new_probe_count % 2)
self.mesh_config["x_count"] = self.mesh_config["y_count"] = \
max(new_x_probe_count, new_y_probe_count)
new_probe_count
else:
self.mesh_min = adjusted_mesh_min
self.mesh_max = adjusted_mesh_max
@ -700,6 +594,12 @@ class BedMeshCalibrate:
self.mesh_config['y_count'] = y_cnt
need_cfg_update = True
if "MESH_PPS" in params:
xpps, ypps = parse_gcmd_pair(gcmd, 'MESH_PPS', minval=0)
self.mesh_config['mesh_x_pps'] = xpps
self.mesh_config['mesh_y_pps'] = ypps
need_cfg_update = True
if "ALGORITHM" in params:
self.mesh_config['algo'] = gcmd.get('ALGORITHM').strip().lower()
need_cfg_update = True
@ -709,47 +609,50 @@ class BedMeshCalibrate:
if need_cfg_update:
self._verify_algorithm(gcmd.error)
self._generate_points(gcmd.error, probe_method)
self.probe_mgr.generate_points(
self.mesh_config, self.mesh_min, self.mesh_max,
self.radius, self.origin, probe_method
)
gcmd.respond_info("Generating new points...")
self.print_generated_points(gcmd.respond_info)
pts = self._get_adjusted_points()
self.probe_helper.update_probe_points(pts, 3)
msg = "\n".join(["%s: %s" % (k, v)
for k, v in self.mesh_config.items()])
logging.info("Updated Mesh Configuration:\n" + msg)
else:
self._generate_points(gcmd.error, probe_method)
pts = self._get_adjusted_points()
self.probe_helper.update_probe_points(pts, 3)
def _get_adjusted_points(self):
adj_pts = []
if self.substituted_indices:
last_index = 0
for i, pts in self.substituted_indices.items():
adj_pts.extend(self.points[last_index:i])
adj_pts.extend(pts)
# Add one to the last index to skip the point
# we are replacing
last_index = i + 1
adj_pts.extend(self.points[last_index:])
else:
adj_pts = list(self.points)
if self.zero_reference_mode == ZrefMode.PROBE:
adj_pts.append(self.zero_ref_pos)
return adj_pts
self.probe_mgr.generate_points(
self.mesh_config, self.mesh_min, self.mesh_max,
self.radius, self.origin, probe_method
)
def dump_calibration(self, gcmd=None):
if gcmd is not None and gcmd.get_command_parameters():
self.update_config(gcmd)
cfg = dict(self.mesh_config)
cfg["mesh_min"] = self.mesh_min
cfg["mesh_max"] = self.mesh_max
cfg["origin"] = self.origin
cfg["radius"] = self.radius
return {
"points": self.probe_mgr.get_base_points(),
"config": cfg,
"probe_path": self.probe_mgr.get_std_path(),
"rapid_path": list(self.probe_mgr.iter_rapid_path())
}
cmd_BED_MESH_CALIBRATE_help = "Perform Mesh Bed Leveling"
def cmd_BED_MESH_CALIBRATE(self, gcmd):
self._profile_name = gcmd.get('PROFILE', "default")
if not self._profile_name.strip():
raise gcmd.error("Value for parameter 'PROFILE' must be specified")
self.bedmesh.set_mesh(None)
self.update_config(gcmd)
self.probe_helper.start_probe(gcmd)
try:
self.update_config(gcmd)
except BedMeshError as e:
raise gcmd.error(str(e))
self.probe_mgr.start_probe(gcmd)
def probe_finalize(self, offsets, positions):
x_offset, y_offset, z_offset = offsets
z_offset = offsets[2]
positions = [[round(p[0], 2), round(p[1], 2), p[2]]
for p in positions]
if self.zero_reference_mode == ZrefMode.PROBE:
if self.probe_mgr.get_zero_ref_mode() == ZrefMode.PROBE:
ref_pos = positions.pop()
logging.info(
"bed_mesh: z-offset replaced with probed z value at "
@ -757,23 +660,26 @@ class BedMeshCalibrate:
% (ref_pos[0], ref_pos[1], ref_pos[2])
)
z_offset = ref_pos[2]
base_points = self.probe_mgr.get_base_points()
params = dict(self.mesh_config)
params['min_x'] = min(positions, key=lambda p: p[0])[0] + x_offset
params['max_x'] = max(positions, key=lambda p: p[0])[0] + x_offset
params['min_y'] = min(positions, key=lambda p: p[1])[1] + y_offset
params['max_y'] = max(positions, key=lambda p: p[1])[1] + y_offset
params['min_x'] = min(base_points, key=lambda p: p[0])[0]
params['max_x'] = max(base_points, key=lambda p: p[0])[0]
params['min_y'] = min(base_points, key=lambda p: p[1])[1]
params['max_y'] = max(base_points, key=lambda p: p[1])[1]
x_cnt = params['x_count']
y_cnt = params['y_count']
if self.substituted_indices:
substitutes = self.probe_mgr.get_substitutes()
probed_pts = positions
if substitutes:
# Replace substituted points with the original generated
# point. Its Z Value is the average probed Z of the
# substituted points.
corrected_pts = []
idx_offset = 0
start_idx = 0
for i, pts in self.substituted_indices.items():
fpt = [p - o for p, o in zip(self.points[i], offsets[:2])]
for i, pts in substitutes.items():
fpt = [p - o for p, o in zip(base_points[i], offsets[:2])]
# offset the index to account for additional samples
idx = i + idx_offset
# Add "normal" points
@ -789,38 +695,42 @@ class BedMeshCalibrate:
% (i, fpt[0], fpt[1], avg_z, avg_z - z_offset))
corrected_pts.append(fpt)
corrected_pts.extend(positions[start_idx:])
# validate corrected positions
if len(self.points) != len(corrected_pts):
self._dump_points(positions, corrected_pts, offsets)
raise self.gcode.error(
"bed_mesh: invalid position list size, "
"generated count: %d, probed count: %d"
% (len(self.points), len(corrected_pts)))
for gen_pt, probed in zip(self.points, corrected_pts):
off_pt = [p - o for p, o in zip(gen_pt, offsets[:2])]
if not isclose(off_pt[0], probed[0], abs_tol=.1) or \
not isclose(off_pt[1], probed[1], abs_tol=.1):
self._dump_points(positions, corrected_pts, offsets)
raise self.gcode.error(
"bed_mesh: point mismatch, orig = (%.2f, %.2f)"
", probed = (%.2f, %.2f)"
% (off_pt[0], off_pt[1], probed[0], probed[1]))
positions = corrected_pts
# validate length of result
if len(base_points) != len(positions):
self._dump_points(probed_pts, positions, offsets)
raise self.gcode.error(
"bed_mesh: invalid position list size, "
"generated count: %d, probed count: %d"
% (len(base_points), len(positions))
)
probed_matrix = []
row = []
prev_pos = positions[0]
for pos in positions:
prev_pos = base_points[0]
for pos, result in zip(base_points, positions):
offset_pos = [p - o for p, o in zip(pos, offsets[:2])]
if (
not isclose(offset_pos[0], result[0], abs_tol=.5) or
not isclose(offset_pos[1], result[1], abs_tol=.5)
):
logging.info(
"bed_mesh: point deviation > .5mm: orig pt = (%.2f, %.2f)"
", probed pt = (%.2f, %.2f)"
% (offset_pos[0], offset_pos[1], result[0], result[1])
)
z_pos = result[2] - z_offset
if not isclose(pos[1], prev_pos[1], abs_tol=.1):
# y has changed, append row and start new
probed_matrix.append(row)
row = []
if pos[0] > prev_pos[0]:
# probed in the positive direction
row.append(pos[2] - z_offset)
row.append(z_pos)
else:
# probed in the negative direction
row.insert(0, pos[2] - z_offset)
row.insert(0, z_pos)
prev_pos = pos
# append last row
probed_matrix.append(row)
@ -863,11 +773,12 @@ class BedMeshCalibrate:
z_mesh.build_mesh(probed_matrix)
except BedMeshError as e:
raise self.gcode.error(str(e))
if self.zero_reference_mode == ZrefMode.IN_MESH:
if self.probe_mgr.get_zero_ref_mode() == ZrefMode.IN_MESH:
# The reference can be anywhere in the mesh, therefore
# it is necessary to set the reference after the initial mesh
# is generated to lookup the correct z value.
z_mesh.set_zero_reference(*self.zero_ref_pos)
zero_ref_pos = self.probe_mgr.get_zero_ref_pos()
z_mesh.set_zero_reference(*zero_ref_pos)
self.bedmesh.set_mesh(z_mesh)
self.gcode.respond_info("Mesh Bed Leveling Complete")
if self._profile_name is not None:
@ -875,14 +786,15 @@ class BedMeshCalibrate:
def _dump_points(self, probed_pts, corrected_pts, offsets):
# logs generated points with offset applied, points received
# from the finalize callback, and the list of corrected points
max_len = max([len(self.points), len(probed_pts), len(corrected_pts)])
points = self.probe_mgr.get_base_points()
max_len = max([len(points), len(probed_pts), len(corrected_pts)])
logging.info(
"bed_mesh: calibration point dump\nIndex | %-17s| %-25s|"
" Corrected Point" % ("Generated Point", "Probed Point"))
for i in list(range(max_len)):
gen_pt = probed_pt = corr_pt = ""
if i < len(self.points):
off_pt = [p - o for p, o in zip(self.points[i], offsets[:2])]
if i < len(points):
off_pt = [p - o for p, o in zip(points[i], offsets[:2])]
gen_pt = "(%.2f, %.2f)" % tuple(off_pt)
if i < len(probed_pts):
probed_pt = "(%.2f, %.2f, %.4f)" % tuple(probed_pts[i])
@ -891,6 +803,453 @@ class BedMeshCalibrate:
logging.info(
" %-4d| %-17s| %-25s| %s" % (i, gen_pt, probed_pt, corr_pt))
class ProbeManager:
def __init__(self, config, orig_config, finalize_cb):
self.printer = config.get_printer()
self.cfg_overshoot = config.getfloat("scan_overshoot", 0, minval=1.)
self.orig_config = orig_config
self.faulty_regions = []
self.overshoot = self.cfg_overshoot
self.zero_ref_pos = config.getfloatlist(
"zero_reference_position", None, count=2
)
self.zref_mode = ZrefMode.DISABLED
self.base_points = []
self.substitutes = collections.OrderedDict()
self.is_round = orig_config["radius"] is not None
self.probe_helper = probe.ProbePointsHelper(config, finalize_cb, [])
self.probe_helper.use_xy_offsets(True)
self.rapid_scan_helper = RapidScanHelper(config, self, finalize_cb)
self._init_faulty_regions(config)
def _init_faulty_regions(self, config):
for i in list(range(1, 100, 1)):
start = config.getfloatlist("faulty_region_%d_min" % (i,), None,
count=2)
if start is None:
break
end = config.getfloatlist("faulty_region_%d_max" % (i,), count=2)
# Validate the corners. If necessary reorganize them.
# c1 = min point, c3 = max point
# c4 ---- c3
# | |
# c1 ---- c2
c1 = [min([s, e]) for s, e in zip(start, end)]
c3 = [max([s, e]) for s, e in zip(start, end)]
c2 = [c1[0], c3[1]]
c4 = [c3[0], c1[1]]
# Check for overlapping regions
for j, (prev_c1, prev_c3) in enumerate(self.faulty_regions):
prev_c2 = [prev_c1[0], prev_c3[1]]
prev_c4 = [prev_c3[0], prev_c1[1]]
# Validate that no existing corner is within the new region
for coord in [prev_c1, prev_c2, prev_c3, prev_c4]:
if within(coord, c1, c3):
raise config.error(
"bed_mesh: Existing faulty_region_%d %s overlaps "
"added faulty_region_%d %s"
% (j+1, repr([prev_c1, prev_c3]),
i, repr([c1, c3])))
# Validate that no new corner is within an existing region
for coord in [c1, c2, c3, c4]:
if within(coord, prev_c1, prev_c3):
raise config.error(
"bed_mesh: Added faulty_region_%d %s overlaps "
"existing faulty_region_%d %s"
% (i, repr([c1, c3]),
j+1, repr([prev_c1, prev_c3])))
self.faulty_regions.append((c1, c3))
def start_probe(self, gcmd):
method = gcmd.get("METHOD", "automatic").lower()
can_scan = False
pprobe = self.printer.lookup_object("probe", None)
if pprobe is not None:
probe_name = pprobe.get_status(None).get("name", "")
can_scan = probe_name.startswith("probe_eddy_current")
if method == "rapid_scan" and can_scan:
self.rapid_scan_helper.perform_rapid_scan(gcmd)
else:
self.probe_helper.start_probe(gcmd)
def get_zero_ref_pos(self):
return self.zero_ref_pos
def get_zero_ref_mode(self):
return self.zref_mode
def get_substitutes(self):
return self.substitutes
def generate_points(
self, mesh_config, mesh_min, mesh_max, radius, origin,
probe_method="automatic"
):
x_cnt = mesh_config['x_count']
y_cnt = mesh_config['y_count']
min_x, min_y = mesh_min
max_x, max_y = mesh_max
x_dist = (max_x - min_x) / (x_cnt - 1)
y_dist = (max_y - min_y) / (y_cnt - 1)
# floor distances down to next hundredth
x_dist = math.floor(x_dist * 100) / 100
y_dist = math.floor(y_dist * 100) / 100
if x_dist < 1. or y_dist < 1.:
raise BedMeshError("bed_mesh: min/max points too close together")
if radius is not None:
# round bed, min/max needs to be recalculated
y_dist = x_dist
new_r = (x_cnt // 2) * x_dist
min_x = min_y = -new_r
max_x = max_y = new_r
else:
# rectangular bed, only re-calc max_x
max_x = min_x + x_dist * (x_cnt - 1)
pos_y = min_y
points = []
for i in range(y_cnt):
for j in range(x_cnt):
if not i % 2:
# move in positive directon
pos_x = min_x + j * x_dist
else:
# move in negative direction
pos_x = max_x - j * x_dist
if radius is None:
# rectangular bed, append
points.append((pos_x, pos_y))
else:
# round bed, check distance from origin
dist_from_origin = math.sqrt(pos_x*pos_x + pos_y*pos_y)
if dist_from_origin <= radius:
points.append(
(origin[0] + pos_x, origin[1] + pos_y))
pos_y += y_dist
if self.zero_ref_pos is None or probe_method == "manual":
# Zero Reference Disabled
self.zref_mode = ZrefMode.DISABLED
elif within(self.zero_ref_pos, mesh_min, mesh_max):
# Zero Reference position within mesh
self.zref_mode = ZrefMode.IN_MESH
else:
# Zero Reference position outside of mesh
self.zref_mode = ZrefMode.PROBE
self.base_points = points
self.substitutes.clear()
# adjust overshoot
og_min_x = self.orig_config["mesh_min"][0]
og_max_x = self.orig_config["mesh_max"][0]
add_ovs = min(max(0, min_x - og_min_x), max(0, og_max_x - max_x))
self.overshoot = self.cfg_overshoot + math.floor(add_ovs)
min_pt, max_pt = (min_x, min_y), (max_x, max_y)
self._process_faulty_regions(min_pt, max_pt, radius)
self.probe_helper.update_probe_points(self.get_std_path(), 3)
def _process_faulty_regions(self, min_pt, max_pt, radius):
if not self.faulty_regions:
return
# Cannot probe a reference within a faulty region
if self.zref_mode == ZrefMode.PROBE:
for min_c, max_c in self.faulty_regions:
if within(self.zero_ref_pos, min_c, max_c):
opt = "zero_reference_position"
raise BedMeshError(
"bed_mesh: Cannot probe zero reference position at "
"(%.2f, %.2f) as it is located within a faulty region."
" Check the value for option '%s'"
% (self.zero_ref_pos[0], self.zero_ref_pos[1], opt,)
)
# Check to see if any points fall within faulty regions
last_y = self.base_points[0][1]
is_reversed = False
for i, coord in enumerate(self.base_points):
if not isclose(coord[1], last_y):
is_reversed = not is_reversed
last_y = coord[1]
adj_coords = []
for min_c, max_c in self.faulty_regions:
if within(coord, min_c, max_c, tol=.00001):
# Point lies within a faulty region
adj_coords = [
(min_c[0], coord[1]), (coord[0], min_c[1]),
(coord[0], max_c[1]), (max_c[0], coord[1])]
if is_reversed:
# Swap first and last points for zig-zag pattern
first = adj_coords[0]
adj_coords[0] = adj_coords[-1]
adj_coords[-1] = first
break
if not adj_coords:
# coord is not located within a faulty region
continue
valid_coords = []
for ac in adj_coords:
# make sure that coordinates are within the mesh boundary
if radius is None:
if within(ac, min_pt, max_pt, .000001):
valid_coords.append(ac)
else:
dist_from_origin = math.sqrt(ac[0]*ac[0] + ac[1]*ac[1])
if dist_from_origin <= radius:
valid_coords.append(ac)
if not valid_coords:
raise BedMeshError(
"bed_mesh: Unable to generate coordinates"
" for faulty region at index: %d" % (i)
)
self.substitutes[i] = valid_coords
def get_base_points(self):
return self.base_points
def get_std_path(self):
path = []
for idx, pt in enumerate(self.base_points):
if idx in self.substitutes:
for sub_pt in self.substitutes[idx]:
path.append(sub_pt)
else:
path.append(pt)
if self.zref_mode == ZrefMode.PROBE:
path.append(self.zero_ref_pos)
return path
def iter_rapid_path(self):
ascnd_x = True
last_base_pt = last_mv_pt = self.base_points[0]
# Generate initial move point
if self.overshoot:
overshoot = min(8, self.overshoot)
last_mv_pt = (last_base_pt[0] - overshoot, last_base_pt[1])
yield last_mv_pt, False
for idx, pt in enumerate(self.base_points):
# increasing Y indicates direction change
dir_change = not isclose(pt[1], last_base_pt[1], abs_tol=1e-6)
if idx in self.substitutes:
fp_gen = self._gen_faulty_path(
last_mv_pt, idx, ascnd_x, dir_change
)
for sub_pt, is_smp in fp_gen:
yield sub_pt, is_smp
last_mv_pt = sub_pt
else:
if dir_change:
for dpt in self._gen_dir_change(last_mv_pt, pt, ascnd_x):
yield dpt, False
yield pt, True
last_mv_pt = pt
last_base_pt = pt
ascnd_x ^= dir_change
if self.zref_mode == ZrefMode.PROBE:
if self.overshoot:
ovs = min(4, self.overshoot)
ovs = ovs if ascnd_x else -ovs
yield (last_mv_pt[0] + ovs, last_mv_pt[1]), False
yield self.zero_ref_pos, True
def _gen_faulty_path(self, last_pt, idx, ascnd_x, dir_change):
subs = self.substitutes[idx]
sub_cnt = len(subs)
if dir_change:
for dpt in self._gen_dir_change(last_pt, subs[0], ascnd_x):
yield dpt, False
if self.is_round:
# No faulty region path handling for round beds
for pt in subs:
yield pt, True
return
# Check to see if this is the first corner
first_corner = False
sorted_sub_idx = sorted(self.substitutes.keys())
if sub_cnt == 2 and idx < len(sorted_sub_idx):
first_corner = sorted_sub_idx[idx] == idx
yield subs[0], True
if sub_cnt == 1:
return
last_pt, next_pt = subs[:2]
if sub_cnt == 2:
if first_corner or dir_change:
# horizontal move first
yield (next_pt[0], last_pt[1]), False
else:
yield (last_pt[0], next_pt[1]), False
yield next_pt, True
elif sub_cnt >= 3:
if dir_change:
# first move should be a vertical switch up. If overshoot
# is available, simulate another direction change. Otherwise
# move inward 2 mm, then up through the faulty region.
if self.overshoot:
for dpt in self._gen_dir_change(last_pt, next_pt, ascnd_x):
yield dpt, False
else:
shift = -2 if ascnd_x else 2
yield (last_pt[0] + shift, last_pt[1]), False
yield (last_pt[0] + shift, next_pt[1]), False
yield next_pt, True
last_pt, next_pt = subs[1:3]
else:
# vertical move
yield (last_pt[0], next_pt[1]), False
yield next_pt, True
last_pt, next_pt = subs[1:3]
if sub_cnt == 4:
# Vertical switch up within faulty region
shift = 2 if ascnd_x else -2
yield (last_pt[0] + shift, last_pt[1]), False
yield (next_pt[0] - shift, next_pt[1]), False
yield next_pt, True
last_pt, next_pt = subs[2:4]
# horizontal move before final point
yield (next_pt[0], last_pt[1]), False
yield next_pt, True
def _gen_dir_change(self, last_pt, next_pt, ascnd_x):
if not self.overshoot:
return
# overshoot X beyond the outer point
xdir = 1 if ascnd_x else -1
overshoot = 2. if self.overshoot >= 3. else self.overshoot
ovr_pt = (last_pt[0] + overshoot * xdir, last_pt[1])
yield ovr_pt
if self.overshoot < 3.:
# No room to generate an arc, move up to next y
yield (next_pt[0] + overshoot * xdir, next_pt[1])
else:
# generate arc
STEP_ANGLE = 3
START_ANGLE = 270
ydiff = abs(next_pt[1] - last_pt[1])
xdiff = abs(next_pt[0] - last_pt[0])
max_radius = min(self.overshoot - 2, 8)
radius = min(ydiff / 2, max_radius)
origin = [ovr_pt[0], last_pt[1] + radius]
next_origin_y = next_pt[1] - radius
# determine angle
if xdiff < .01:
# Move is aligned on the x-axis
angle = 90
if next_origin_y - origin[1] < .05:
# The move can be completed in a single arc
angle = 180
else:
angle = int(math.degrees(math.atan(ydiff / xdiff)))
if (
(ascnd_x and next_pt[0] < last_pt[0]) or
(not ascnd_x and next_pt[0] > last_pt[0])
):
angle = 180 - angle
count = int(angle // STEP_ANGLE)
# Gen first arc
step = STEP_ANGLE * xdir
start = START_ANGLE + step
for arc_pt in self._gen_arc(origin, radius, start, step, count):
yield arc_pt
if angle == 180:
# arc complete
return
# generate next arc
origin = [next_pt[0] + overshoot * xdir, next_origin_y]
# start at the angle where the last arc finished
start = START_ANGLE + count * step
# recalculate the count to make sure we generate a full 180
# degrees. Add a step for the repeated connecting angle
count = 61 - count
for arc_pt in self._gen_arc(origin, radius, start, step, count):
yield arc_pt
def _gen_arc(self, origin, radius, start, step, count):
end = start + step * count
# create a segent for every 3 degress of travel
for angle in range(start, end, step):
rad = math.radians(angle % 360)
opp = math.sin(rad) * radius
adj = math.cos(rad) * radius
yield (origin[0] + adj, origin[1] + opp)
MAX_HIT_DIST = 2.
MM_WIN_SPEED = 125
class RapidScanHelper:
def __init__(self, config, probe_mgr, finalize_cb):
self.printer = config.get_printer()
self.probe_manager = probe_mgr
self.speed = config.getfloat("speed", 50., above=0.)
self.scan_height = config.getfloat("horizontal_move_z", 5.)
self.finalize_callback = finalize_cb
def perform_rapid_scan(self, gcmd):
speed = gcmd.get_float("SCAN_SPEED", self.speed)
scan_height = gcmd.get_float("HORIZONTAL_MOVE_Z", self.scan_height)
gcmd.respond_info(
"Beginning rapid surface scan at height %.2f..." % (scan_height)
)
pprobe = self.printer.lookup_object("probe")
toolhead = self.printer.lookup_object("toolhead")
# Calculate time window around which a sample is valid. Current
# assumption is anything within 2mm is usable, so:
# window = 2 / max_speed
#
# TODO: validate maximum speed allowed based on sample rate of probe
# Scale the hit distance window for speeds lower than 125mm/s. The
# lower the speed the less the window shrinks.
scale = max(0, 1 - speed / MM_WIN_SPEED) + 1
hit_dist = min(MAX_HIT_DIST, scale * speed / MM_WIN_SPEED)
half_window = hit_dist / speed
gcmd.respond_info(
"Sample hit distance +/- %.4fmm, time window +/- ms %.4f"
% (hit_dist, half_window * 1000)
)
gcmd_params = gcmd.get_command_parameters()
gcmd_params["SAMPLE_TIME"] = half_window * 2
self._raise_tool(gcmd, scan_height)
probe_session = pprobe.start_probe_session(gcmd)
offsets = pprobe.get_offsets()
initial_move = True
for pos, is_probe_pt in self.probe_manager.iter_rapid_path():
pos = self._apply_offsets(pos[:2], offsets)
toolhead.manual_move(pos, speed)
if initial_move:
initial_move = False
self._move_to_scan_height(gcmd, scan_height)
if is_probe_pt:
probe_session.run_probe(gcmd)
results = probe_session.pull_probed_results()
toolhead.get_last_move_time()
self.finalize_callback(offsets, results)
probe_session.end_probe_session()
def _raise_tool(self, gcmd, scan_height):
# If the nozzle is below scan height raise the tool
toolhead = self.printer.lookup_object("toolhead")
pprobe = self.printer.lookup_object("probe")
cur_pos = toolhead.get_position()
if cur_pos[2] >= scan_height:
return
pparams = pprobe.get_probe_params(gcmd)
lift_speed = pparams["lift_speed"]
cur_pos[2] = self.scan_height + .5
toolhead.manual_move(cur_pos, lift_speed)
def _move_to_scan_height(self, gcmd, scan_height):
time_window = gcmd.get_float("SAMPLE_TIME")
toolhead = self.printer.lookup_object("toolhead")
pprobe = self.printer.lookup_object("probe")
cur_pos = toolhead.get_position()
pparams = pprobe.get_probe_params(gcmd)
lift_speed = pparams["lift_speed"]
probe_speed = pparams["probe_speed"]
cur_pos[2] = scan_height + .5
toolhead.manual_move(cur_pos, lift_speed)
cur_pos[2] = scan_height
toolhead.manual_move(cur_pos, probe_speed)
toolhead.dwell(time_window / 2 + .01)
def _apply_offsets(self, point, offsets):
return [(pos - ofs) for pos, ofs in zip(point, offsets)]
class MoveSplitter:
def __init__(self, config, gcode):

View file

@ -1,6 +1,6 @@
# BLTouch support
#
# Copyright (C) 2018-2021 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
@ -23,13 +23,9 @@ Commands = {
}
# BLTouch "endstop" wrapper
class BLTouchEndstopWrapper:
class BLTouchProbe:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
self.printer.register_event_handler('klippy:mcu_identify',
self.handle_mcu_identify)
self.position_endstop = config.getfloat('z_offset', minval=0.)
self.stow_on_each_sample = config.getboolean('stow_on_each_sample',
True)
@ -44,12 +40,9 @@ class BLTouchEndstopWrapper:
self.next_cmd_time = self.action_end_time = 0.
self.finish_home_complete = self.wait_trigger_complete = None
# Create an "endstop" object to handle the sensor pin
pin = config.get('sensor_pin')
pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True)
mcu = pin_params['chip']
self.mcu_endstop = mcu.setup_pin('endstop', pin_params)
self.mcu_endstop = ppins.setup_pin('endstop', config.get('sensor_pin'))
# output mode
omodes = {'5V': '5V', 'OD': 'OD', None: None}
omodes = ['5V', 'OD', None]
self.output_mode = config.getchoice('set_output_mode', omodes, None)
# Setup for sensor test
self.next_test_time = 0.
@ -65,19 +58,30 @@ class BLTouchEndstopWrapper:
self.get_steppers = self.mcu_endstop.get_steppers
self.home_wait = self.mcu_endstop.home_wait
self.query_endstop = self.mcu_endstop.query_endstop
# multi probes state
self.multi = 'OFF'
# Common probe implementation helpers
self.cmd_helper = probe.ProbeCommandHelper(
config, self, self.mcu_endstop.query_endstop)
self.probe_offsets = probe.ProbeOffsetsHelper(config)
self.probe_session = probe.ProbeSessionHelper(config, self)
# Register BLTOUCH_DEBUG command
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command("BLTOUCH_DEBUG", self.cmd_BLTOUCH_DEBUG,
desc=self.cmd_BLTOUCH_DEBUG_help)
self.gcode.register_command("BLTOUCH_STORE", self.cmd_BLTOUCH_STORE,
desc=self.cmd_BLTOUCH_STORE_help)
# multi probes state
self.multi = 'OFF'
def handle_mcu_identify(self):
kin = self.printer.lookup_object('toolhead').get_kinematics()
for stepper in kin.get_steppers():
if stepper.is_active_axis('z'):
self.add_stepper(stepper)
# Register events
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
def get_probe_params(self, gcmd=None):
return self.probe_session.get_probe_params(gcmd)
def get_offsets(self):
return self.probe_offsets.get_offsets()
def get_status(self, eventtime):
return self.cmd_helper.get_status(eventtime)
def start_probe_session(self, gcmd):
return self.probe_session.start_probe_session(gcmd)
def handle_connect(self):
self.sync_mcu_print_time()
self.next_cmd_time += 0.200
@ -116,7 +120,11 @@ class BLTouchEndstopWrapper:
self.mcu_endstop.home_start(self.action_end_time, ENDSTOP_SAMPLE_TIME,
ENDSTOP_SAMPLE_COUNT, ENDSTOP_REST_TIME,
triggered=triggered)
trigger_time = self.mcu_endstop.home_wait(self.action_end_time + 0.100)
try:
trigger_time = self.mcu_endstop.home_wait(
self.action_end_time + 0.100)
except self.printer.command_error as e:
return False
return trigger_time > 0.
def raise_probe(self):
self.sync_mcu_print_time()
@ -274,6 +282,6 @@ class BLTouchEndstopWrapper:
self.sync_print_time()
def load_config(config):
blt = BLTouchEndstopWrapper(config)
config.get_printer().add_object('probe', probe.PrinterProbe(config, blt))
blt = BLTouchProbe(config)
config.get_printer().add_object('probe', blt)
return blt

View file

@ -83,6 +83,7 @@ BMP180_REGS = {
STATUS_MEASURING = 1 << 3
STATUS_IM_UPDATE = 1
MODE = 1
MODE_PERIODIC = 3
RUN_GAS = 1 << 4
NB_CONV_0 = 0
EAS_NEW_DATA = 1 << 7
@ -143,6 +144,7 @@ class BME280:
pow(2, self.os_temp - 1), pow(2, self.os_hum - 1),
pow(2, self.os_pres - 1)))
logging.info("BMxx80: IIR: %dx" % (pow(2, self.iir_filter) - 1))
self.iir_filter = self.iir_filter & 0x07
self.temp = self.pressure = self.humidity = self.gas = self.t_fine = 0.
self.min_temp = self.max_temp = self.range_switching_error = 0.
@ -155,6 +157,7 @@ class BME280:
return
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
self.last_gas_time = 0
def handle_connect(self):
self._init_bmxx80()
@ -281,7 +284,7 @@ class BME280:
self.chip_type, self.i2c.i2c_address))
# Reset chip
self.write_register('RESET', [RESET_CHIP_VALUE])
self.write_register('RESET', [RESET_CHIP_VALUE], wait=True)
self.reactor.pause(self.reactor.monotonic() + .5)
# Make sure non-volatile memory has been copied to registers
@ -293,15 +296,15 @@ class BME280:
status = self.read_register('STATUS', 1)[0]
if self.chip_type == 'BME680':
self.max_sample_time = 0.5
self.max_sample_time = \
(1.25 + (2.3 * self.os_temp) + ((2.3 * self.os_pres) + .575)
+ ((2.3 * self.os_hum) + .575)) / 1000
self.sample_timer = self.reactor.register_timer(self._sample_bme680)
self.chip_registers = BME680_REGS
elif self.chip_type == 'BMP180':
self.max_sample_time = (1.25 + ((2.3 * self.os_pres) + .575)) / 1000
self.sample_timer = self.reactor.register_timer(self._sample_bmp180)
self.chip_registers = BMP180_REGS
elif self.chip_type == 'BMP388':
self.max_sample_time = 0.5
self.chip_registers = BMP388_REGS
self.write_register(
"PWR_CTRL",
@ -318,15 +321,18 @@ class BME280:
self.write_register("INT_CTRL", [BMP388_REG_VAL_DRDY_EN])
self.sample_timer = self.reactor.register_timer(self._sample_bmp388)
else:
elif self.chip_type == 'BME280':
self.max_sample_time = \
(1.25 + (2.3 * self.os_temp) + ((2.3 * self.os_pres) + .575)
+ ((2.3 * self.os_hum) + .575)) / 1000
self.sample_timer = self.reactor.register_timer(self._sample_bme280)
self.chip_registers = BME280_REGS
if self.chip_type in ('BME680', 'BME280'):
self.write_register('CONFIG', (self.iir_filter & 0x07) << 2)
else:
self.max_sample_time = \
(1.25 + (2.3 * self.os_temp)
+ ((2.3 * self.os_pres) + .575)) / 1000
self.sample_timer = self.reactor.register_timer(self._sample_bme280)
self.chip_registers = BME280_REGS
# Read out and calculate the trimming parameters
if self.chip_type == 'BMP180':
@ -347,21 +353,64 @@ class BME280:
elif self.chip_type == 'BMP388':
self.dig = read_calibration_data_bmp388(cal_1)
if self.chip_type in ('BME280', 'BMP280'):
max_standby_time = REPORT_TIME - self.max_sample_time
# 0.5 ms
t_sb = 0
if self.chip_type == 'BME280':
if max_standby_time > 1:
t_sb = 5
elif max_standby_time > 0.5:
t_sb = 4
elif max_standby_time > 0.25:
t_sb = 3
elif max_standby_time > 0.125:
t_sb = 2
elif max_standby_time > 0.0625:
t_sb = 1
elif max_standby_time > 0.020:
t_sb = 7
elif max_standby_time > 0.010:
t_sb = 6
else:
if max_standby_time > 4:
t_sb = 7
elif max_standby_time > 2:
t_sb = 6
elif max_standby_time > 1:
t_sb = 5
elif max_standby_time > 0.5:
t_sb = 4
elif max_standby_time > 0.25:
t_sb = 3
elif max_standby_time > 0.125:
t_sb = 2
elif max_standby_time > 0.0625:
t_sb = 1
cfg = t_sb << 5 | self.iir_filter << 2
self.write_register('CONFIG', cfg)
if self.chip_type == 'BME280':
self.write_register('CTRL_HUM', self.os_hum)
# Enter normal (periodic) mode
meas = self.os_temp << 5 | self.os_pres << 2 | MODE_PERIODIC
self.write_register('CTRL_MEAS', meas, wait=True)
if self.chip_type == 'BME680':
self.write_register('CONFIG', self.iir_filter << 2)
# Should be set once and reused on every mode register write
self.write_register('CTRL_HUM', self.os_hum & 0x07)
gas_wait_0 = self._calc_gas_heater_duration(self.gas_heat_duration)
self.write_register('GAS_WAIT_0', [gas_wait_0])
res_heat_0 = self._calc_gas_heater_resistance(self.gas_heat_temp)
self.write_register('RES_HEAT_0', [res_heat_0])
# Set initial heater current to reach Gas heater target on start
self.write_register('IDAC_HEAT_0', 96)
def _sample_bme280(self, eventtime):
# Enter forced mode
if self.chip_type == 'BME280':
self.write_register('CTRL_HUM', self.os_hum)
meas = self.os_temp << 5 | self.os_pres << 2 | MODE
self.write_register('CTRL_MEAS', meas)
# In normal mode data shadowing is performed
# So reading can be done while measurements are in process
try:
# wait until results are ready
status = self.read_register('STATUS', 1)[0]
while status & STATUS_MEASURING:
self.reactor.pause(
self.reactor.monotonic() + self.max_sample_time)
status = self.read_register('STATUS', 1)[0]
if self.chip_type == 'BME280':
data = self.read_register('PRESSURE_MSB', 8)
elif self.chip_type == 'BMP280':
@ -462,36 +511,40 @@ class BME280:
return comp_press
def _sample_bme680(self, eventtime):
self.write_register('CTRL_HUM', self.os_hum & 0x07)
meas = self.os_temp << 5 | self.os_pres << 2
self.write_register('CTRL_MEAS', [meas])
gas_wait_0 = self._calculate_gas_heater_duration(self.gas_heat_duration)
self.write_register('GAS_WAIT_0', [gas_wait_0])
res_heat_0 = self._calculate_gas_heater_resistance(self.gas_heat_temp)
self.write_register('RES_HEAT_0', [res_heat_0])
gas_config = RUN_GAS | NB_CONV_0
self.write_register('CTRL_GAS_1', [gas_config])
def data_ready(stat):
def data_ready(stat, run_gas):
new_data = (stat & EAS_NEW_DATA)
gas_done = not (stat & GAS_DONE)
meas_done = not (stat & MEASURE_DONE)
if not run_gas:
gas_done = True
return new_data and gas_done and meas_done
run_gas = False
# Check VOC once a while
if self.reactor.monotonic() - self.last_gas_time > 3:
gas_config = RUN_GAS | NB_CONV_0
self.write_register('CTRL_GAS_1', [gas_config])
run_gas = True
# Enter forced mode
meas = meas | MODE
self.write_register('CTRL_MEAS', meas)
meas = self.os_temp << 5 | self.os_pres << 2 | MODE
self.write_register('CTRL_MEAS', meas, wait=True)
max_sample_time = self.max_sample_time
if run_gas:
max_sample_time += self.gas_heat_duration / 1000
self.reactor.pause(self.reactor.monotonic() + max_sample_time)
try:
# wait until results are ready
status = self.read_register('EAS_STATUS_0', 1)[0]
while not data_ready(status):
while not data_ready(status, run_gas):
self.reactor.pause(
self.reactor.monotonic() + self.max_sample_time)
status = self.read_register('EAS_STATUS_0', 1)[0]
data = self.read_register('PRESSURE_MSB', 8)
gas_data = self.read_register('GAS_R_MSB', 2)
gas_data = [0, 0]
if run_gas:
gas_data = self.read_register('GAS_R_MSB', 2)
except Exception:
logging.exception("BME680: Error reading data")
self.temp = self.pressure = self.humidity = self.gas = .0
@ -515,6 +568,10 @@ class BME280:
gas_raw = (gas_data[0] << 2) | ((gas_data[1] & 0xC0) >> 6)
gas_range = (gas_data[1] & 0x0F)
self.gas = self._compensate_gas(gas_raw, gas_range)
# Disable gas measurement on success
gas_config = NB_CONV_0
self.write_register('CTRL_GAS_1', [gas_config])
self.last_gas_time = self.reactor.monotonic()
if self.temp < self.min_temp or self.temp > self.max_temp:
self.printer.invoke_shutdown(
@ -643,7 +700,7 @@ class BME280:
gas_raw - 512. + var1)
return gas
def _calculate_gas_heater_resistance(self, target_temp):
def _calc_gas_heater_resistance(self, target_temp):
amb_temp = self.temp
heater_data = self.read_register('RES_HEAT_VAL', 3)
res_heat_val = get_signed_byte(heater_data[0])
@ -658,7 +715,7 @@ class BME280:
* (1. / (1. + (res_heat_val * 0.002)))) - 25))
return int(res_heat)
def _calculate_gas_heater_duration(self, duration_ms):
def _calc_gas_heater_duration(self, duration_ms):
if duration_ms >= 4032:
duration_reg = 0xff
else:
@ -719,12 +776,15 @@ class BME280:
params = self.i2c.i2c_read(regs, read_len)
return bytearray(params['response'])
def write_register(self, reg_name, data):
def write_register(self, reg_name, data, wait = False):
if type(data) is not list:
data = [data]
reg = self.chip_registers[reg_name]
data.insert(0, reg)
self.i2c.i2c_write(data)
if not wait:
self.i2c.i2c_write(data)
else:
self.i2c.i2c_write_wait_ack(data)
def get_status(self, eventtime):
data = {

View file

@ -198,7 +198,7 @@ class ClockSyncRegression:
inv_freq = clock_to_print_time(base_mcu + inv_cfreq) - base_time
return base_time, base_chip, inv_freq
MAX_BULK_MSG_SIZE = 52
MAX_BULK_MSG_SIZE = 51
# Read sensor_bulk_data and calculate timestamps for devices that take
# samples at a fixed frequency (and produce fixed data size samples).

View file

@ -192,6 +192,9 @@ class MCU_I2C:
return
self.i2c_write_cmd.send([self.oid, data],
minclock=minclock, reqclock=reqclock)
def i2c_write_wait_ack(self, data, minclock=0, reqclock=0):
self.i2c_write_cmd.send_wait_ack([self.oid, data],
minclock=minclock, reqclock=reqclock)
def i2c_read(self, write, read_len):
return self.i2c_read_cmd.send([self.oid, write, read_len])
def i2c_modify_bits(self, reg, clear_bits, set_bits,

View file

@ -104,7 +104,7 @@ class MCU_ADC_buttons:
self.max_value = 0.
ppins = printer.lookup_object('pins')
self.mcu_adc = ppins.setup_pin('adc', self.pin)
self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback)
query_adc = printer.lookup_object('query_adc')
query_adc.register_adc('adc_button:' + pin.strip(), self.mcu_adc)

View file

@ -8,7 +8,7 @@ import logging
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
LINE_LENGTH_DEFAULT=20
LINE_LENGTH_OPTIONS={16:16, 20:20}
LINE_LENGTH_OPTIONS=[16, 20]
TextGlyphs = { 'right_arrow': b'\x7e' }

View file

@ -9,7 +9,7 @@ import logging
from .. import bus
LINE_LENGTH_DEFAULT=20
LINE_LENGTH_OPTIONS={16:16, 20:20}
LINE_LENGTH_OPTIONS=[16, 20]
TextGlyphs = { 'right_arrow': b'\x7e' }

View file

@ -18,7 +18,7 @@ class MenuKeys:
# Register rotary encoder
encoder_pins = config.get('encoder_pins', None)
encoder_steps_per_detent = config.getchoice('encoder_steps_per_detent',
{2: 2, 4: 4}, 4)
[2, 4], 4)
if encoder_pins is not None:
try:
pin1, pin2 = encoder_pins.split(',')

133
klippy/extras/error_mcu.py Normal file
View file

@ -0,0 +1,133 @@
# More verbose information on micro-controller errors
#
# Copyright (C) 2024 Kevin O'Connor <kevin@koconnor.net>
#
# 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("<none>")
if not msg_updated:
msg_updated.append("<none>")
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)

View file

@ -39,8 +39,6 @@ class ArcSupport:
self.gcode.register_command("G18", self.cmd_G18)
self.gcode.register_command("G19", self.cmd_G19)
self.Coord = self.gcode.Coord
# backwards compatibility, prior implementation only supported XY
self.plane = ARC_PLANE_X_Y
@ -64,52 +62,36 @@ class ArcSupport:
if not gcodestatus['absolute_coordinates']:
raise gcmd.error("G2/G3 does not support relative move mode")
currentPos = gcodestatus['gcode_position']
absolut_extrude = gcodestatus['absolute_extrude']
# Parse parameters
asTarget = self.Coord(x=gcmd.get_float("X", currentPos[0]),
y=gcmd.get_float("Y", currentPos[1]),
z=gcmd.get_float("Z", currentPos[2]),
e=None)
asTarget = [gcmd.get_float("X", currentPos[0]),
gcmd.get_float("Y", currentPos[1]),
gcmd.get_float("Z", currentPos[2])]
if gcmd.get_float("R", None) is not None:
raise gcmd.error("G2/G3 does not support R moves")
# determine the plane coordinates and the helical axis
asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('IJ') ]
I = gcmd.get_float('I', 0.)
J = gcmd.get_float('J', 0.)
asPlanar = (I, J)
axes = (X_AXIS, Y_AXIS, Z_AXIS)
if self.plane == ARC_PLANE_X_Z:
asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('IK') ]
K = gcmd.get_float('K', 0.)
asPlanar = (I, K)
axes = (X_AXIS, Z_AXIS, Y_AXIS)
elif self.plane == ARC_PLANE_Y_Z:
asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('JK') ]
K = gcmd.get_float('K', 0.)
asPlanar = (J, K)
axes = (Y_AXIS, Z_AXIS, X_AXIS)
if not (asPlanar[0] or asPlanar[1]):
raise gcmd.error("G2/G3 requires IJ, IK or JK parameters")
asE = gcmd.get_float("E", None)
asF = gcmd.get_float("F", None)
# Build list of linear coordinates to move
coords = self.planArc(currentPos, asTarget, asPlanar,
clockwise, *axes)
e_per_move = e_base = 0.
if asE is not None:
if gcodestatus['absolute_extrude']:
e_base = currentPos[3]
e_per_move = (asE - e_base) / len(coords)
# Convert coords into G1 commands
for coord in coords:
g1_params = {'X': coord[0], 'Y': coord[1], 'Z': coord[2]}
if e_per_move:
g1_params['E'] = e_base + e_per_move
if gcodestatus['absolute_extrude']:
e_base += e_per_move
if asF is not None:
g1_params['F'] = asF
g1_gcmd = self.gcode.create_gcode_command("G1", "G1", g1_params)
self.gcode_move.cmd_G1(g1_gcmd)
# Build linear coordinates to move
self.planArc(currentPos, asTarget, asPlanar, clockwise,
gcmd, absolut_extrude, *axes)
# function planArc() originates from marlin plan_arc()
# https://github.com/MarlinFirmware/Marlin
@ -120,6 +102,7 @@ class ArcSupport:
#
# alpha and beta axes are the current plane, helical axis is linear travel
def planArc(self, currentPos, targetPos, offset, clockwise,
gcmd, absolut_extrude,
alpha_axis, beta_axis, helical_axis):
# todo: sometimes produces full circles
@ -159,23 +142,42 @@ class ArcSupport:
# Generate coordinates
theta_per_segment = angular_travel / segments
linear_per_segment = linear_travel / segments
coords = []
for i in range(1, int(segments)):
asE = gcmd.get_float("E", None)
asF = gcmd.get_float("F", None)
e_per_move = e_base = 0.
if asE is not None:
if absolut_extrude:
e_base = currentPos[3]
e_per_move = (asE - e_base) / segments
for i in range(1, int(segments) + 1):
dist_Helical = i * linear_per_segment
cos_Ti = math.cos(i * theta_per_segment)
sin_Ti = math.sin(i * theta_per_segment)
c_theta = i * theta_per_segment
cos_Ti = math.cos(c_theta)
sin_Ti = math.sin(c_theta)
r_P = -offset[0] * cos_Ti + offset[1] * sin_Ti
r_Q = -offset[0] * sin_Ti - offset[1] * cos_Ti
# Coord doesn't support index assignment, create list
c = [None, None, None, None]
c = [None, None, None]
c[alpha_axis] = center_P + r_P
c[beta_axis] = center_Q + r_Q
c[helical_axis] = currentPos[helical_axis] + dist_Helical
coords.append(self.Coord(*c))
coords.append(targetPos)
return coords
if i == segments:
c = targetPos
# Convert coords into G1 commands
g1_params = {'X': c[0], 'Y': c[1], 'Z': c[2]}
if e_per_move:
g1_params['E'] = e_base + e_per_move
if absolut_extrude:
e_base += e_per_move
if asF is not None:
g1_params['F'] = asF
g1_gcmd = self.gcode.create_gcode_command("G1", "G1", g1_params)
self.gcode_move.cmd_G1(g1_gcmd)
def load_config(config):
return ArcSupport(config)

View file

@ -49,10 +49,10 @@ class HallFilamentWidthSensor:
# Start adc
self.ppins = self.printer.lookup_object('pins')
self.mcu_adc = self.ppins.setup_pin('adc', self.pin1)
self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback)
self.mcu_adc2 = self.ppins.setup_pin('adc', self.pin2)
self.mcu_adc2.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc2.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc2.setup_adc_callback(ADC_REPORT_TIME, self.adc2_callback)
# extrude factor updating
self.extrude_factor_update_timer = self.reactor.register_timer(

View file

@ -98,11 +98,14 @@ class HomingMove:
trigger_times = {}
move_end_print_time = self.toolhead.get_last_move_time()
for mcu_endstop, name in self.endstops:
trigger_time = mcu_endstop.home_wait(move_end_print_time)
try:
trigger_time = mcu_endstop.home_wait(move_end_print_time)
except self.printer.command_error as e:
if error is None:
error = "Error during homing %s: %s" % (name, str(e))
continue
if trigger_time > 0.:
trigger_times[name] = trigger_time
elif trigger_time < 0. and error is None:
error = "Communication timeout during homing %s" % (name,)
elif check_triggered and error is None:
error = "No trigger on %s after full movement" % (name,)
# Determine stepper halt positions

170
klippy/extras/hx71x.py Normal file
View file

@ -0,0 +1,170 @@
# HX711/HX717 Support
#
# Copyright (C) 2024 Gareth Farrington <gareth@waves.ky>
#
# 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, "<i")
# Process messages in batches
self.batch_bulk = bulk_sensor.BatchBulkHelper(
self.printer, self._process_batch, self._start_measurements,
self._finish_measurements, UPDATE_INTERVAL)
# publish raw samples to the socket
dump_path = "%s/dump_%s" % (sensor_type, sensor_type)
hdr = {'header': ('time', 'counts', 'value')}
self.batch_bulk.add_mux_endpoint(dump_path, "sensor", self.name, hdr)
# Command Configuration
self.query_hx71x_cmd = None
mcu.add_config_cmd(
"config_hx71x oid=%d gain_channel=%d dout_pin=%s sclk_pin=%s"
% (self.oid, self.gain_channel, self.dout_pin, self.sclk_pin))
mcu.add_config_cmd("query_hx71x oid=%d rest_ticks=0"
% (self.oid,), on_restart=True)
mcu.register_config_callback(self._build_config)
def _build_config(self):
self.query_hx71x_cmd = self.mcu.lookup_command(
"query_hx71x oid=%c rest_ticks=%u")
self.ffreader.setup_query_command("query_hx71x_status oid=%c",
oid=self.oid,
cq=self.mcu.alloc_command_queue())
def get_mcu(self):
return self.mcu
def get_samples_per_second(self):
return self.sps
# returns a tuple of the minimum and maximum value of the sensor, used to
# detect if a data value is saturated
def get_range(self):
return -0x800000, 0x7FFFFF
# add_client interface, direct pass through to bulk_sensor API
def add_client(self, callback):
self.batch_bulk.add_client(callback)
# Measurement decoding
def _convert_samples(self, samples):
adc_factor = 1. / (1 << 23)
count = 0
for ptime, val in samples:
if val == SAMPLE_ERROR_DESYNC or val == SAMPLE_ERROR_LONG_READ:
self.last_error_count += 1
break # additional errors are duplicates
samples[count] = (round(ptime, 6), val, round(val * adc_factor, 9))
count += 1
del samples[count:]
# Start, stop, and process message batches
def _start_measurements(self):
self.consecutive_fails = 0
self.last_error_count = 0
# Start bulk reading
rest_ticks = self.mcu.seconds_to_clock(1. / (10. * self.sps))
self.query_hx71x_cmd.send([self.oid, rest_ticks])
logging.info("%s starting '%s' measurements",
self.sensor_type, self.name)
# Initialize clock tracking
self.ffreader.note_start()
def _finish_measurements(self):
# don't use serial connection after shutdown
if self.printer.is_shutdown():
return
# Halt bulk reading
self.query_hx71x_cmd.send_wait_ack([self.oid, 0])
self.ffreader.note_end()
logging.info("%s finished '%s' measurements",
self.sensor_type, self.name)
def _process_batch(self, eventtime):
prev_overflows = self.ffreader.get_last_overflows()
prev_error_count = self.last_error_count
samples = self.ffreader.pull_samples()
self._convert_samples(samples)
overflows = self.ffreader.get_last_overflows() - prev_overflows
errors = self.last_error_count - prev_error_count
if errors > 0:
logging.error("%s: Forced sensor restart due to error", self.name)
self._finish_measurements()
self._start_measurements()
elif overflows > 0:
self.consecutive_fails += 1
if self.consecutive_fails > 4:
logging.error("%s: Forced sensor restart due to overflows",
self.name)
self._finish_measurements()
self._start_measurements()
else:
self.consecutive_fails = 0
return {'data': samples, 'errors': self.last_error_count,
'overflows': self.ffreader.get_last_overflows()}
class HX711(HX71xBase):
def __init__(self, config):
super(HX711, self).__init__(config, "hx711",
# HX711 sps options
{80: 80, 10: 10}, 80,
# HX711 gain/channel options
{'A-128': 1, 'B-32': 2, 'A-64': 3}, 'A-128')
class HX717(HX71xBase):
def __init__(self, config):
super(HX717, self).__init__(config, "hx717",
# HX717 sps options
{320: 320, 80: 80, 20: 20, 10: 10}, 320,
# HX717 gain/channel options
{'A-128': 1, 'B-64': 2, 'A-64': 3,
'B-8': 4}, 'A-128')
HX71X_SENSOR_TYPES = {
"hx711": HX711,
"hx717": HX717
}

View file

@ -87,8 +87,17 @@ class LDC1612:
self.oid = oid = mcu.create_oid()
self.query_ldc1612_cmd = None
self.ldc1612_setup_home_cmd = self.query_ldc1612_home_state_cmd = None
mcu.add_config_cmd("config_ldc1612 oid=%d i2c_oid=%d"
% (oid, self.i2c.get_oid()))
if config.get('intb_pin', None) is not None:
ppins = config.get_printer().lookup_object("pins")
pin_params = ppins.lookup_pin(config.get('intb_pin'))
if pin_params['chip'] != mcu:
raise config.error("ldc1612 intb_pin must be on same mcu")
mcu.add_config_cmd(
"config_ldc1612_with_intb oid=%d i2c_oid=%d intb_pin=%s"
% (oid, self.i2c.get_oid(), pin_params['pin']))
else:
mcu.add_config_cmd("config_ldc1612 oid=%d i2c_oid=%d"
% (oid, self.i2c.get_oid()))
mcu.add_config_cmd("query_ldc1612 oid=%d rest_ticks=0"
% (oid,), on_restart=True)
mcu.register_config_callback(self._build_config)
@ -108,11 +117,11 @@ class LDC1612:
cmdqueue = self.i2c.get_command_queue()
self.query_ldc1612_cmd = self.mcu.lookup_command(
"query_ldc1612 oid=%c rest_ticks=%u", cq=cmdqueue)
self.ffreader.setup_query_command("query_ldc1612_status oid=%c",
self.ffreader.setup_query_command("query_status_ldc1612 oid=%c",
oid=self.oid, cq=cmdqueue)
self.ldc1612_setup_home_cmd = self.mcu.lookup_command(
"ldc1612_setup_home oid=%c clock=%u threshold=%u"
" trsync_oid=%c trigger_reason=%c", cq=cmdqueue)
" trsync_oid=%c trigger_reason=%c error_reason=%c", cq=cmdqueue)
self.query_ldc1612_home_state_cmd = self.mcu.lookup_query_command(
"query_ldc1612_home_state oid=%c",
"ldc1612_home_state oid=%c homing=%c trigger_clock=%u",
@ -129,13 +138,14 @@ class LDC1612:
def add_client(self, cb):
self.batch_bulk.add_client(cb)
# Homing
def setup_home(self, print_time, trigger_freq, trsync_oid, reason):
def setup_home(self, print_time, trigger_freq,
trsync_oid, hit_reason, err_reason):
clock = self.mcu.print_time_to_clock(print_time)
tfreq = int(trigger_freq * (1<<28) / float(LDC1612_FREQ) + 0.5)
self.ldc1612_setup_home_cmd.send(
[self.oid, clock, tfreq, trsync_oid, reason])
[self.oid, clock, tfreq, trsync_oid, hit_reason, err_reason])
def clear_home(self):
self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0])
self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0, 0])
if self.mcu.is_fileoutput():
return 0.
params = self.query_ldc1612_home_state_cmd.send([self.oid])

View file

@ -0,0 +1,30 @@
# Load Cell Implementation
#
# Copyright (C) 2024 Gareth Farrington <gareth@waves.ky>
#
# 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)

View file

@ -1,6 +1,6 @@
# Z-Probe support
#
# Copyright (C) 2017-2021 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2017-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
@ -13,44 +13,176 @@ consider reducing the Z axis minimum position so the probe
can travel further (the Z minimum position can be negative).
"""
class PrinterProbe:
# Calculate the average Z from a set of positions
def calc_probe_z_average(positions, method='average'):
if method != 'median':
# Use mean average
count = float(len(positions))
return [sum([pos[i] for pos in positions]) / count
for i in range(3)]
# Use median
z_sorted = sorted(positions, key=(lambda p: p[2]))
middle = len(positions) // 2
if (len(positions) & 1) == 1:
# odd number of samples
return z_sorted[middle]
# even number of samples
return calc_probe_z_average(z_sorted[middle-1:middle+1], 'average')
######################################################################
# Probe device implementation helpers
######################################################################
# Helper to implement common probing commands
class ProbeCommandHelper:
def __init__(self, config, probe, query_endstop=None):
self.printer = config.get_printer()
self.probe = probe
self.query_endstop = query_endstop
self.name = config.get_name()
gcode = self.printer.lookup_object('gcode')
# QUERY_PROBE command
self.last_state = False
gcode.register_command('QUERY_PROBE', self.cmd_QUERY_PROBE,
desc=self.cmd_QUERY_PROBE_help)
# PROBE command
self.last_z_result = 0.
gcode.register_command('PROBE', self.cmd_PROBE,
desc=self.cmd_PROBE_help)
# PROBE_CALIBRATE command
self.probe_calibrate_z = 0.
gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE,
desc=self.cmd_PROBE_CALIBRATE_help)
# Other commands
gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY,
desc=self.cmd_PROBE_ACCURACY_help)
gcode.register_command('Z_OFFSET_APPLY_PROBE',
self.cmd_Z_OFFSET_APPLY_PROBE,
desc=self.cmd_Z_OFFSET_APPLY_PROBE_help)
def _move(self, coord, speed):
self.printer.lookup_object('toolhead').manual_move(coord, speed)
def get_status(self, eventtime):
return {'name': self.name,
'last_query': self.last_state,
'last_z_result': self.last_z_result}
cmd_QUERY_PROBE_help = "Return the status of the z-probe"
def cmd_QUERY_PROBE(self, gcmd):
if self.query_endstop is None:
raise gcmd.error("Probe does not support QUERY_PROBE")
toolhead = self.printer.lookup_object('toolhead')
print_time = toolhead.get_last_move_time()
res = self.query_endstop(print_time)
self.last_state = res
gcmd.respond_info("probe: %s" % (["open", "TRIGGERED"][not not res],))
cmd_PROBE_help = "Probe Z-height at current XY position"
def cmd_PROBE(self, gcmd):
pos = run_single_probe(self.probe, gcmd)
gcmd.respond_info("Result is z=%.6f" % (pos[2],))
self.last_z_result = pos[2]
def probe_calibrate_finalize(self, kin_pos):
if kin_pos is None:
return
z_offset = self.probe_calibrate_z - kin_pos[2]
gcode = self.printer.lookup_object('gcode')
gcode.respond_info(
"%s: z_offset: %.3f\n"
"The SAVE_CONFIG command will update the printer config file\n"
"with the above and restart the printer." % (self.name, z_offset))
configfile = self.printer.lookup_object('configfile')
configfile.set(self.name, 'z_offset', "%.3f" % (z_offset,))
cmd_PROBE_CALIBRATE_help = "Calibrate the probe's z_offset"
def cmd_PROBE_CALIBRATE(self, gcmd):
manual_probe.verify_no_manual_probe(self.printer)
params = self.probe.get_probe_params(gcmd)
# Perform initial probe
curpos = run_single_probe(self.probe, gcmd)
# Move away from the bed
self.probe_calibrate_z = curpos[2]
curpos[2] += 5.
self._move(curpos, params['lift_speed'])
# Move the nozzle over the probe point
x_offset, y_offset, z_offset = self.probe.get_offsets()
curpos[0] += x_offset
curpos[1] += y_offset
self._move(curpos, params['probe_speed'])
# Start manual probe
manual_probe.ManualProbeHelper(self.printer, gcmd,
self.probe_calibrate_finalize)
cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position"
def cmd_PROBE_ACCURACY(self, gcmd):
params = self.probe.get_probe_params(gcmd)
sample_count = gcmd.get_int("SAMPLES", 10, minval=1)
toolhead = self.printer.lookup_object('toolhead')
pos = toolhead.get_position()
gcmd.respond_info("PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f"
" (samples=%d retract=%.3f"
" speed=%.1f lift_speed=%.1f)\n"
% (pos[0], pos[1], pos[2],
sample_count, params['sample_retract_dist'],
params['probe_speed'], params['lift_speed']))
# Create dummy gcmd with SAMPLES=1
fo_params = dict(gcmd.get_command_parameters())
fo_params['SAMPLES'] = '1'
gcode = self.printer.lookup_object('gcode')
fo_gcmd = gcode.create_gcode_command("", "", fo_params)
# Probe bed sample_count times
probe_session = self.probe.start_probe_session(fo_gcmd)
probe_num = 0
while probe_num < sample_count:
# Probe position
probe_session.run_probe(fo_gcmd)
probe_num += 1
# Retract
pos = toolhead.get_position()
liftpos = [None, None, pos[2] + params['sample_retract_dist']]
self._move(liftpos, params['lift_speed'])
positions = probe_session.pull_probed_results()
probe_session.end_probe_session()
# Calculate maximum, minimum and average values
max_value = max([p[2] for p in positions])
min_value = min([p[2] for p in positions])
range_value = max_value - min_value
avg_value = calc_probe_z_average(positions, 'average')[2]
median = calc_probe_z_average(positions, 'median')[2]
# calculate the standard deviation
deviation_sum = 0
for i in range(len(positions)):
deviation_sum += pow(positions[i][2] - avg_value, 2.)
sigma = (deviation_sum / len(positions)) ** 0.5
# Show information
gcmd.respond_info(
"probe accuracy results: maximum %.6f, minimum %.6f, range %.6f, "
"average %.6f, median %.6f, standard deviation %.6f" % (
max_value, min_value, range_value, avg_value, median, sigma))
cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset"
def cmd_Z_OFFSET_APPLY_PROBE(self, gcmd):
gcode_move = self.printer.lookup_object("gcode_move")
offset = gcode_move.get_status()['homing_origin'].z
if offset == 0:
gcmd.respond_info("Nothing to do: Z Offset is 0")
return
z_offset = self.probe.get_offsets()[2]
new_calibrate = z_offset - offset
gcmd.respond_info(
"%s: z_offset: %.3f\n"
"The SAVE_CONFIG command will update the printer config file\n"
"with the above and restart the printer."
% (self.name, new_calibrate))
configfile = self.printer.lookup_object('configfile')
configfile.set(self.name, 'z_offset', "%.3f" % (new_calibrate,))
# Homing via probe:z_virtual_endstop
class HomingViaProbeHelper:
def __init__(self, config, mcu_probe):
self.printer = config.get_printer()
self.name = config.get_name()
self.mcu_probe = mcu_probe
self.speed = config.getfloat('speed', 5.0, above=0.)
self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.)
self.x_offset = config.getfloat('x_offset', 0.)
self.y_offset = config.getfloat('y_offset', 0.)
self.z_offset = config.getfloat('z_offset')
self.probe_calibrate_z = 0.
self.multi_probe_pending = False
self.last_state = False
self.last_z_result = 0.
self.gcode_move = self.printer.load_object(config, "gcode_move")
# Infer Z position to move to during a probe
if config.has_section('stepper_z'):
zconfig = config.getsection('stepper_z')
self.z_position = zconfig.getfloat('position_min', 0.,
note_valid=False)
else:
pconfig = config.getsection('printer')
self.z_position = pconfig.getfloat('minimum_z_position', 0.,
note_valid=False)
# Multi-sample support (for improved accuracy)
self.sample_count = config.getint('samples', 1, minval=1)
self.sample_retract_dist = config.getfloat('sample_retract_dist', 2.,
above=0.)
atypes = {'median': 'median', 'average': 'average'}
self.samples_result = config.getchoice('samples_result', atypes,
'average')
self.samples_tolerance = config.getfloat('samples_tolerance', 0.100,
minval=0.)
self.samples_retries = config.getint('samples_tolerance_retries', 0,
minval=0)
# Register z_virtual_endstop pin
self.printer.lookup_object('pins').register_chip('probe', self)
# Register homing event handlers
# Register event handlers
self.printer.register_event_handler('klippy:mcu_identify',
self._handle_mcu_identify)
self.printer.register_event_handler("homing:homing_move_begin",
self._handle_homing_move_begin)
self.printer.register_event_handler("homing:homing_move_end",
@ -61,19 +193,11 @@ class PrinterProbe:
self._handle_home_rails_end)
self.printer.register_event_handler("gcode:command_error",
self._handle_command_error)
# Register PROBE/QUERY_PROBE commands
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command('PROBE', self.cmd_PROBE,
desc=self.cmd_PROBE_help)
self.gcode.register_command('QUERY_PROBE', self.cmd_QUERY_PROBE,
desc=self.cmd_QUERY_PROBE_help)
self.gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE,
desc=self.cmd_PROBE_CALIBRATE_help)
self.gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY,
desc=self.cmd_PROBE_ACCURACY_help)
self.gcode.register_command('Z_OFFSET_APPLY_PROBE',
self.cmd_Z_OFFSET_APPLY_PROBE,
desc=self.cmd_Z_OFFSET_APPLY_PROBE_help)
def _handle_mcu_identify(self):
kin = self.printer.lookup_object('toolhead').get_kinematics()
for stepper in kin.get_steppers():
if stepper.is_active_axis('z'):
self.mcu_probe.add_stepper(stepper)
def _handle_homing_move_begin(self, hmove):
if self.mcu_probe in hmove.get_mcu_endstops():
self.mcu_probe.probe_prepare(hmove)
@ -83,35 +207,106 @@ class PrinterProbe:
def _handle_home_rails_begin(self, homing_state, rails):
endstops = [es for rail in rails for es, name in rail.get_endstops()]
if self.mcu_probe in endstops:
self.multi_probe_begin()
self.mcu_probe.multi_probe_begin()
self.multi_probe_pending = True
def _handle_home_rails_end(self, homing_state, rails):
endstops = [es for rail in rails for es, name in rail.get_endstops()]
if self.mcu_probe in endstops:
self.multi_probe_end()
def _handle_command_error(self):
try:
self.multi_probe_end()
except:
logging.exception("Multi-probe end")
def multi_probe_begin(self):
self.mcu_probe.multi_probe_begin()
self.multi_probe_pending = True
def multi_probe_end(self):
if self.multi_probe_pending:
if self.multi_probe_pending and self.mcu_probe in endstops:
self.multi_probe_pending = False
self.mcu_probe.multi_probe_end()
def _handle_command_error(self):
if self.multi_probe_pending:
self.multi_probe_pending = False
try:
self.mcu_probe.multi_probe_end()
except:
logging.exception("Homing multi-probe end")
def setup_pin(self, pin_type, pin_params):
if pin_type != 'endstop' or pin_params['pin'] != 'z_virtual_endstop':
raise pins.error("Probe virtual endstop only useful as endstop pin")
if pin_params['invert'] or pin_params['pullup']:
raise pins.error("Can not pullup/invert probe virtual endstop")
return self.mcu_probe
def get_lift_speed(self, gcmd=None):
if gcmd is not None:
return gcmd.get_float("LIFT_SPEED", self.lift_speed, above=0.)
return self.lift_speed
def get_offsets(self):
return self.x_offset, self.y_offset, self.z_offset
# Helper to track multiple probe attempts in a single command
class ProbeSessionHelper:
def __init__(self, config, mcu_probe):
self.printer = config.get_printer()
self.mcu_probe = mcu_probe
gcode = self.printer.lookup_object('gcode')
self.dummy_gcode_cmd = gcode.create_gcode_command("", "", {})
# Infer Z position to move to during a probe
if config.has_section('stepper_z'):
zconfig = config.getsection('stepper_z')
self.z_position = zconfig.getfloat('position_min', 0.,
note_valid=False)
else:
pconfig = config.getsection('printer')
self.z_position = pconfig.getfloat('minimum_z_position', 0.,
note_valid=False)
self.homing_helper = HomingViaProbeHelper(config, mcu_probe)
# Configurable probing speeds
self.speed = config.getfloat('speed', 5.0, above=0.)
self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.)
# Multi-sample support (for improved accuracy)
self.sample_count = config.getint('samples', 1, minval=1)
self.sample_retract_dist = config.getfloat('sample_retract_dist', 2.,
above=0.)
atypes = ['median', 'average']
self.samples_result = config.getchoice('samples_result', atypes,
'average')
self.samples_tolerance = config.getfloat('samples_tolerance', 0.100,
minval=0.)
self.samples_retries = config.getint('samples_tolerance_retries', 0,
minval=0)
# Session state
self.multi_probe_pending = False
self.results = []
# Register event handlers
self.printer.register_event_handler("gcode:command_error",
self._handle_command_error)
def _handle_command_error(self):
if self.multi_probe_pending:
try:
self.end_probe_session()
except:
logging.exception("Multi-probe end")
def _probe_state_error(self):
raise self.printer.command_error(
"Internal probe error - start/end probe session mismatch")
def start_probe_session(self, gcmd):
if self.multi_probe_pending:
self._probe_state_error()
self.mcu_probe.multi_probe_begin()
self.multi_probe_pending = True
self.results = []
return self
def end_probe_session(self):
if not self.multi_probe_pending:
self._probe_state_error()
self.results = []
self.multi_probe_pending = False
self.mcu_probe.multi_probe_end()
def get_probe_params(self, gcmd=None):
if gcmd is None:
gcmd = self.dummy_gcode_cmd
probe_speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.)
lift_speed = gcmd.get_float("LIFT_SPEED", self.lift_speed, above=0.)
samples = gcmd.get_int("SAMPLES", self.sample_count, minval=1)
sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST",
self.sample_retract_dist, above=0.)
samples_tolerance = gcmd.get_float("SAMPLES_TOLERANCE",
self.samples_tolerance, minval=0.)
samples_retries = gcmd.get_int("SAMPLES_TOLERANCE_RETRIES",
self.samples_retries, minval=0)
samples_result = gcmd.get("SAMPLES_RESULT", self.samples_result)
return {'probe_speed': probe_speed,
'lift_speed': lift_speed,
'samples': samples,
'sample_retract_dist': sample_retract_dist,
'samples_tolerance': samples_tolerance,
'samples_tolerance_retries': samples_retries,
'samples_result': samples_result}
def _probe(self, speed):
toolhead = self.printer.lookup_object('toolhead')
curtime = self.printer.get_reactor().monotonic()
@ -126,169 +321,181 @@ class PrinterProbe:
if "Timeout during endstop homing" in reason:
reason += HINT_TIMEOUT
raise self.printer.command_error(reason)
# get z compensation from axis_twist_compensation
axis_twist_compensation = self.printer.lookup_object(
'axis_twist_compensation', None)
z_compensation = 0
if axis_twist_compensation is not None:
z_compensation = (
axis_twist_compensation.get_z_compensation_value(pos))
# add z compensation to probe position
epos[2] += z_compensation
self.gcode.respond_info("probe at %.3f,%.3f is z=%.6f"
% (epos[0], epos[1], epos[2]))
# Allow axis_twist_compensation to update results
self.printer.send_event("probe:update_results", epos)
# Report results
gcode = self.printer.lookup_object('gcode')
gcode.respond_info("probe at %.3f,%.3f is z=%.6f"
% (epos[0], epos[1], epos[2]))
return epos[:3]
def _move(self, coord, speed):
self.printer.lookup_object('toolhead').manual_move(coord, speed)
def _calc_mean(self, positions):
count = float(len(positions))
return [sum([pos[i] for pos in positions]) / count
for i in range(3)]
def _calc_median(self, positions):
z_sorted = sorted(positions, key=(lambda p: p[2]))
middle = len(positions) // 2
if (len(positions) & 1) == 1:
# odd number of samples
return z_sorted[middle]
# even number of samples
return self._calc_mean(z_sorted[middle-1:middle+1])
def run_probe(self, gcmd):
speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.)
lift_speed = self.get_lift_speed(gcmd)
sample_count = gcmd.get_int("SAMPLES", self.sample_count, minval=1)
sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST",
self.sample_retract_dist, above=0.)
samples_tolerance = gcmd.get_float("SAMPLES_TOLERANCE",
self.samples_tolerance, minval=0.)
samples_retries = gcmd.get_int("SAMPLES_TOLERANCE_RETRIES",
self.samples_retries, minval=0)
samples_result = gcmd.get("SAMPLES_RESULT", self.samples_result)
must_notify_multi_probe = not self.multi_probe_pending
if must_notify_multi_probe:
self.multi_probe_begin()
probexy = self.printer.lookup_object('toolhead').get_position()[:2]
if not self.multi_probe_pending:
self._probe_state_error()
params = self.get_probe_params(gcmd)
toolhead = self.printer.lookup_object('toolhead')
probexy = toolhead.get_position()[:2]
retries = 0
positions = []
sample_count = params['samples']
while len(positions) < sample_count:
# Probe position
pos = self._probe(speed)
pos = self._probe(params['probe_speed'])
positions.append(pos)
# Check samples tolerance
z_positions = [p[2] for p in positions]
if max(z_positions) - min(z_positions) > samples_tolerance:
if retries >= samples_retries:
if max(z_positions)-min(z_positions) > params['samples_tolerance']:
if retries >= params['samples_tolerance_retries']:
raise gcmd.error("Probe samples exceed samples_tolerance")
gcmd.respond_info("Probe samples exceed tolerance. Retrying...")
retries += 1
positions = []
# Retract
if len(positions) < sample_count:
self._move(probexy + [pos[2] + sample_retract_dist], lift_speed)
if must_notify_multi_probe:
self.multi_probe_end()
# Calculate and return result
if samples_result == 'median':
return self._calc_median(positions)
return self._calc_mean(positions)
cmd_PROBE_help = "Probe Z-height at current XY position"
def cmd_PROBE(self, gcmd):
pos = self.run_probe(gcmd)
gcmd.respond_info("Result is z=%.6f" % (pos[2],))
self.last_z_result = pos[2]
cmd_QUERY_PROBE_help = "Return the status of the z-probe"
def cmd_QUERY_PROBE(self, gcmd):
toolhead.manual_move(
probexy + [pos[2] + params['sample_retract_dist']],
params['lift_speed'])
# Calculate result
epos = calc_probe_z_average(positions, params['samples_result'])
self.results.append(epos)
def pull_probed_results(self):
res = self.results
self.results = []
return res
# Helper to read the xyz probe offsets from the config
class ProbeOffsetsHelper:
def __init__(self, config):
self.x_offset = config.getfloat('x_offset', 0.)
self.y_offset = config.getfloat('y_offset', 0.)
self.z_offset = config.getfloat('z_offset')
def get_offsets(self):
return self.x_offset, self.y_offset, self.z_offset
######################################################################
# Tools for utilizing the probe
######################################################################
# Helper code that can probe a series of points and report the
# position at each point.
class ProbePointsHelper:
def __init__(self, config, finalize_callback, default_points=None):
self.printer = config.get_printer()
self.finalize_callback = finalize_callback
self.probe_points = default_points
self.name = config.get_name()
self.gcode = self.printer.lookup_object('gcode')
# Read config settings
if default_points is None or config.get('points', None) is not None:
self.probe_points = config.getlists('points', seps=(',', '\n'),
parser=float, count=2)
def_move_z = config.getfloat('horizontal_move_z', 5.)
self.default_horizontal_move_z = def_move_z
self.speed = config.getfloat('speed', 50., above=0.)
self.use_offsets = False
# Internal probing state
self.lift_speed = self.speed
self.probe_offsets = (0., 0., 0.)
self.manual_results = []
def minimum_points(self,n):
if len(self.probe_points) < n:
raise self.printer.config_error(
"Need at least %d probe points for %s" % (n, self.name))
def update_probe_points(self, points, min_points):
self.probe_points = points
self.minimum_points(min_points)
def use_xy_offsets(self, use_offsets):
self.use_offsets = use_offsets
def get_lift_speed(self):
return self.lift_speed
def _move(self, coord, speed):
self.printer.lookup_object('toolhead').manual_move(coord, speed)
def _raise_tool(self, is_first=False):
speed = self.lift_speed
if is_first:
# Use full speed to first probe position
speed = self.speed
self._move([None, None, self.horizontal_move_z], speed)
def _invoke_callback(self, results):
# Flush lookahead queue
toolhead = self.printer.lookup_object('toolhead')
print_time = toolhead.get_last_move_time()
res = self.mcu_probe.query_endstop(print_time)
self.last_state = res
gcmd.respond_info("probe: %s" % (["open", "TRIGGERED"][not not res],))
def get_status(self, eventtime):
return {'name': self.name,
'last_query': self.last_state,
'last_z_result': self.last_z_result}
cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position"
def cmd_PROBE_ACCURACY(self, gcmd):
speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.)
lift_speed = self.get_lift_speed(gcmd)
sample_count = gcmd.get_int("SAMPLES", 10, minval=1)
sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST",
self.sample_retract_dist, above=0.)
toolhead = self.printer.lookup_object('toolhead')
pos = toolhead.get_position()
gcmd.respond_info("PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f"
" (samples=%d retract=%.3f"
" speed=%.1f lift_speed=%.1f)\n"
% (pos[0], pos[1], pos[2],
sample_count, sample_retract_dist,
speed, lift_speed))
# Probe bed sample_count times
self.multi_probe_begin()
positions = []
while len(positions) < sample_count:
# Probe position
pos = self._probe(speed)
positions.append(pos)
# Retract
liftpos = [None, None, pos[2] + sample_retract_dist]
self._move(liftpos, lift_speed)
self.multi_probe_end()
# Calculate maximum, minimum and average values
max_value = max([p[2] for p in positions])
min_value = min([p[2] for p in positions])
range_value = max_value - min_value
avg_value = self._calc_mean(positions)[2]
median = self._calc_median(positions)[2]
# calculate the standard deviation
deviation_sum = 0
for i in range(len(positions)):
deviation_sum += pow(positions[i][2] - avg_value, 2.)
sigma = (deviation_sum / len(positions)) ** 0.5
# Show information
gcmd.respond_info(
"probe accuracy results: maximum %.6f, minimum %.6f, range %.6f, "
"average %.6f, median %.6f, standard deviation %.6f" % (
max_value, min_value, range_value, avg_value, median, sigma))
def probe_calibrate_finalize(self, kin_pos):
toolhead.get_last_move_time()
# Invoke callback
res = self.finalize_callback(self.probe_offsets, results)
return res != "retry"
def _move_next(self, probe_num):
# Move to next XY probe point
nextpos = list(self.probe_points[probe_num])
if self.use_offsets:
nextpos[0] -= self.probe_offsets[0]
nextpos[1] -= self.probe_offsets[1]
self._move(nextpos, self.speed)
def start_probe(self, gcmd):
manual_probe.verify_no_manual_probe(self.printer)
# Lookup objects
probe = self.printer.lookup_object('probe', None)
method = gcmd.get('METHOD', 'automatic').lower()
def_move_z = self.default_horizontal_move_z
self.horizontal_move_z = gcmd.get_float('HORIZONTAL_MOVE_Z',
def_move_z)
if probe is None or method == 'manual':
# Manual probe
self.lift_speed = self.speed
self.probe_offsets = (0., 0., 0.)
self.manual_results = []
self._manual_probe_start()
return
# Perform automatic probing
self.lift_speed = probe.get_probe_params(gcmd)['lift_speed']
self.probe_offsets = probe.get_offsets()
if self.horizontal_move_z < self.probe_offsets[2]:
raise gcmd.error("horizontal_move_z can't be less than"
" probe's z_offset")
probe_session = probe.start_probe_session(gcmd)
probe_num = 0
while 1:
self._raise_tool(not probe_num)
if probe_num >= len(self.probe_points):
results = probe_session.pull_probed_results()
done = self._invoke_callback(results)
if done:
break
# Caller wants a "retry" - restart probing
probe_num = 0
self._move_next(probe_num)
probe_session.run_probe(gcmd)
probe_num += 1
probe_session.end_probe_session()
def _manual_probe_start(self):
self._raise_tool(not self.manual_results)
if len(self.manual_results) >= len(self.probe_points):
done = self._invoke_callback(self.manual_results)
if done:
return
# Caller wants a "retry" - clear results and restart probing
self.manual_results = []
self._move_next(len(self.manual_results))
gcmd = self.gcode.create_gcode_command("", "", {})
manual_probe.ManualProbeHelper(self.printer, gcmd,
self._manual_probe_finalize)
def _manual_probe_finalize(self, kin_pos):
if kin_pos is None:
return
z_offset = self.probe_calibrate_z - kin_pos[2]
self.gcode.respond_info(
"%s: z_offset: %.3f\n"
"The SAVE_CONFIG command will update the printer config file\n"
"with the above and restart the printer." % (self.name, z_offset))
configfile = self.printer.lookup_object('configfile')
configfile.set(self.name, 'z_offset', "%.3f" % (z_offset,))
cmd_PROBE_CALIBRATE_help = "Calibrate the probe's z_offset"
def cmd_PROBE_CALIBRATE(self, gcmd):
manual_probe.verify_no_manual_probe(self.printer)
# Perform initial probe
lift_speed = self.get_lift_speed(gcmd)
curpos = self.run_probe(gcmd)
# Move away from the bed
self.probe_calibrate_z = curpos[2]
curpos[2] += 5.
self._move(curpos, lift_speed)
# Move the nozzle over the probe point
curpos[0] += self.x_offset
curpos[1] += self.y_offset
self._move(curpos, self.speed)
# Start manual probe
manual_probe.ManualProbeHelper(self.printer, gcmd,
self.probe_calibrate_finalize)
def cmd_Z_OFFSET_APPLY_PROBE(self,gcmd):
offset = self.gcode_move.get_status()['homing_origin'].z
configfile = self.printer.lookup_object('configfile')
if offset == 0:
self.gcode.respond_info("Nothing to do: Z Offset is 0")
else:
new_calibrate = self.z_offset - offset
self.gcode.respond_info(
"%s: z_offset: %.3f\n"
"The SAVE_CONFIG command will update the printer config file\n"
"with the above and restart the printer."
% (self.name, new_calibrate))
configfile.set(self.name, 'z_offset', "%.3f" % (new_calibrate,))
cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset"
self.manual_results.append(kin_pos)
self._manual_probe_start()
# Helper to obtain a single probe measurement
def run_single_probe(probe, gcmd):
probe_session = probe.start_probe_session(gcmd)
probe_session.run_probe(gcmd)
pos = probe_session.pull_probed_results()[0]
probe_session.end_probe_session()
return pos
######################################################################
# Handle [probe] config
######################################################################
# Endstop wrapper that enables probe specific features
class ProbeEndstopWrapper:
@ -304,12 +511,7 @@ class ProbeEndstopWrapper:
config, 'deactivate_gcode', '')
# Create an "endstop" object to handle the probe pin
ppins = self.printer.lookup_object('pins')
pin = config.get('pin')
pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True)
mcu = pin_params['chip']
self.mcu_endstop = mcu.setup_pin('endstop', pin_params)
self.printer.register_event_handler('klippy:mcu_identify',
self._handle_mcu_identify)
self.mcu_endstop = ppins.setup_pin('endstop', config.get('pin'))
# Wrappers
self.get_mcu = self.mcu_endstop.get_mcu
self.add_stepper = self.mcu_endstop.add_stepper
@ -319,11 +521,6 @@ class ProbeEndstopWrapper:
self.query_endstop = self.mcu_endstop.query_endstop
# multi probes state
self.multi = 'OFF'
def _handle_mcu_identify(self):
kin = self.printer.lookup_object('toolhead').get_kinematics()
for stepper in kin.get_steppers():
if stepper.is_active_axis('z'):
self.add_stepper(stepper)
def _raise_probe(self):
toolhead = self.printer.lookup_object('toolhead')
start_pos = toolhead.get_position()
@ -361,100 +558,23 @@ class ProbeEndstopWrapper:
def get_position_endstop(self):
return self.position_endstop
# Helper code that can probe a series of points and report the
# position at each point.
class ProbePointsHelper:
def __init__(self, config, finalize_callback, default_points=None):
# Main external probe interface
class PrinterProbe:
def __init__(self, config):
self.printer = config.get_printer()
self.finalize_callback = finalize_callback
self.probe_points = default_points
self.name = config.get_name()
self.gcode = self.printer.lookup_object('gcode')
# Read config settings
if default_points is None or config.get('points', None) is not None:
self.probe_points = config.getlists('points', seps=(',', '\n'),
parser=float, count=2)
def_move_z = config.getfloat('horizontal_move_z', 5.)
self.default_horizontal_move_z = def_move_z
self.speed = config.getfloat('speed', 50., above=0.)
self.use_offsets = False
# Internal probing state
self.lift_speed = self.speed
self.probe_offsets = (0., 0., 0.)
self.results = []
def minimum_points(self,n):
if len(self.probe_points) < n:
raise self.printer.config_error(
"Need at least %d probe points for %s" % (n, self.name))
def update_probe_points(self, points, min_points):
self.probe_points = points
self.minimum_points(min_points)
def use_xy_offsets(self, use_offsets):
self.use_offsets = use_offsets
def get_lift_speed(self):
return self.lift_speed
def _move_next(self):
toolhead = self.printer.lookup_object('toolhead')
# Lift toolhead
speed = self.lift_speed
if not self.results:
# Use full speed to first probe position
speed = self.speed
toolhead.manual_move([None, None, self.horizontal_move_z], speed)
# Check if done probing
if len(self.results) >= len(self.probe_points):
toolhead.get_last_move_time()
res = self.finalize_callback(self.probe_offsets, self.results)
if res != "retry":
return True
self.results = []
# Move to next XY probe point
nextpos = list(self.probe_points[len(self.results)])
if self.use_offsets:
nextpos[0] -= self.probe_offsets[0]
nextpos[1] -= self.probe_offsets[1]
toolhead.manual_move(nextpos, self.speed)
return False
def start_probe(self, gcmd):
manual_probe.verify_no_manual_probe(self.printer)
# Lookup objects
probe = self.printer.lookup_object('probe', None)
method = gcmd.get('METHOD', 'automatic').lower()
self.results = []
def_move_z = self.default_horizontal_move_z
self.horizontal_move_z = gcmd.get_float('HORIZONTAL_MOVE_Z',
def_move_z)
if probe is None or method != 'automatic':
# Manual probe
self.lift_speed = self.speed
self.probe_offsets = (0., 0., 0.)
self._manual_probe_start()
return
# Perform automatic probing
self.lift_speed = probe.get_lift_speed(gcmd)
self.probe_offsets = probe.get_offsets()
if self.horizontal_move_z < self.probe_offsets[2]:
raise gcmd.error("horizontal_move_z can't be less than"
" probe's z_offset")
probe.multi_probe_begin()
while 1:
done = self._move_next()
if done:
break
pos = probe.run_probe(gcmd)
self.results.append(pos)
probe.multi_probe_end()
def _manual_probe_start(self):
done = self._move_next()
if not done:
gcmd = self.gcode.create_gcode_command("", "", {})
manual_probe.ManualProbeHelper(self.printer, gcmd,
self._manual_probe_finalize)
def _manual_probe_finalize(self, kin_pos):
if kin_pos is None:
return
self.results.append(kin_pos)
self._manual_probe_start()
self.mcu_probe = ProbeEndstopWrapper(config)
self.cmd_helper = ProbeCommandHelper(config, self,
self.mcu_probe.query_endstop)
self.probe_offsets = ProbeOffsetsHelper(config)
self.probe_session = ProbeSessionHelper(config, self.mcu_probe)
def get_probe_params(self, gcmd=None):
return self.probe_session.get_probe_params(gcmd)
def get_offsets(self):
return self.probe_offsets.get_offsets()
def get_status(self, eventtime):
return self.cmd_helper.get_status(eventtime)
def start_probe_session(self, gcmd):
return self.probe_session.start_probe_session(gcmd)
def load_config(config):
return PrinterProbe(config, ProbeEndstopWrapper(config))
return PrinterProbe(config)

View file

@ -7,11 +7,14 @@ import logging, math, bisect
import mcu
from . import ldc1612, probe, manual_probe
OUT_OF_RANGE = 99.9
# Tool for calibrating the sensor Z detection and applying that calibration
class EddyCalibration:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name()
self.drift_comp = DummyDriftCompensation()
# Current calibration data
self.cal_freqs = []
self.cal_zpos = []
@ -35,12 +38,14 @@ class EddyCalibration:
self.cal_freqs = [c[0] for c in cal]
self.cal_zpos = [c[1] for c in cal]
def apply_calibration(self, samples):
cur_temp = self.drift_comp.get_temperature()
for i, (samp_time, freq, dummy_z) in enumerate(samples):
pos = bisect.bisect(self.cal_freqs, freq)
adj_freq = self.drift_comp.adjust_freq(freq, cur_temp)
pos = bisect.bisect(self.cal_freqs, adj_freq)
if pos >= len(self.cal_zpos):
zpos = -99.9
zpos = -OUT_OF_RANGE
elif pos == 0:
zpos = 99.9
zpos = OUT_OF_RANGE
else:
# XXX - could further optimize and avoid div by zero
this_freq = self.cal_freqs[pos]
@ -49,8 +54,12 @@ class EddyCalibration:
prev_zpos = self.cal_zpos[pos - 1]
gain = (this_zpos - prev_zpos) / (this_freq - prev_freq)
offset = prev_zpos - prev_freq * gain
zpos = freq * gain + offset
zpos = adj_freq * gain + offset
samples[i] = (samp_time, freq, round(zpos, 6))
def freq_to_height(self, freq):
dummy_sample = [(0., freq, 0.)]
self.apply_calibration(dummy_sample)
return dummy_sample[0][2]
def height_to_freq(self, height):
# XXX - could optimize lookup
rev_zpos = list(reversed(self.cal_zpos))
@ -65,7 +74,8 @@ class EddyCalibration:
prev_zpos = rev_zpos[pos - 1]
gain = (this_freq - prev_freq) / (this_zpos - prev_zpos)
offset = prev_freq - prev_zpos * gain
return height * gain + offset
freq = height * gain + offset
return self.drift_comp.unadjust_freq(freq)
def do_calibration_moves(self, move_speed):
toolhead = self.printer.lookup_object('toolhead')
kin = toolhead.get_kinematics()
@ -80,19 +90,20 @@ class EddyCalibration:
return True
self.printer.lookup_object(self.name).add_client(handle_batch)
toolhead.dwell(1.)
# Move to each 50um position
max_z = 4
samp_dist = 0.050
num_steps = int(max_z / samp_dist + .5) + 1
self.drift_comp.note_z_calibration_start()
# Move to each 40um position
max_z = 4.0
samp_dist = 0.040
req_zpos = [i*samp_dist for i in range(int(max_z / samp_dist) + 1)]
start_pos = toolhead.get_position()
times = []
for i in range(num_steps):
for zpos in req_zpos:
# Move to next position (always descending to reduce backlash)
hop_pos = list(start_pos)
hop_pos[2] += i * samp_dist + 0.500
hop_pos[2] += zpos + 0.500
move(hop_pos, move_speed)
next_pos = list(start_pos)
next_pos[2] += i * samp_dist
next_pos[2] += zpos
move(next_pos, move_speed)
# Note sample timing
start_query_time = toolhead.get_last_move_time() + 0.050
@ -106,6 +117,7 @@ class EddyCalibration:
times.append((start_query_time, end_query_time, kin_pos[2]))
toolhead.dwell(1.0)
toolhead.wait_moves()
self.drift_comp.note_z_calibration_finish()
# Finish data collection
is_finished = True
# Correlate query responses
@ -182,9 +194,116 @@ class EddyCalibration:
# Start manual probe
manual_probe.ManualProbeHelper(self.printer, gcmd,
self.post_manual_probe)
def register_drift_compensation(self, comp):
self.drift_comp = comp
# Helper for implementing PROBE style commands
# Tool to gather samples and convert them to probe positions
class EddyGatherSamples:
def __init__(self, printer, sensor_helper, calibration, z_offset):
self._printer = printer
self._sensor_helper = sensor_helper
self._calibration = calibration
self._z_offset = z_offset
# Results storage
self._samples = []
self._probe_times = []
self._probe_results = []
self._need_stop = False
# Start samples
if not self._calibration.is_calibrated():
raise self._printer.command_error(
"Must calibrate probe_eddy_current first")
sensor_helper.add_client(self._add_measurement)
def _add_measurement(self, msg):
if self._need_stop:
del self._samples[:]
return False
self._samples.append(msg)
self._check_samples()
return True
def finish(self):
self._need_stop = True
def _await_samples(self):
# Make sure enough samples have been collected
reactor = self._printer.get_reactor()
mcu = self._sensor_helper.get_mcu()
while self._probe_times:
start_time, end_time, pos_time, toolhead_pos = self._probe_times[0]
systime = reactor.monotonic()
est_print_time = mcu.estimated_print_time(systime)
if est_print_time > end_time + 1.0:
raise self._printer.command_error(
"probe_eddy_current sensor outage")
reactor.pause(systime + 0.010)
def _pull_freq(self, start_time, end_time):
# Find average sensor frequency between time range
msg_num = discard_msgs = 0
samp_sum = 0.
samp_count = 0
while msg_num < len(self._samples):
msg = self._samples[msg_num]
msg_num += 1
data = msg['data']
if data[0][0] > end_time:
break
if data[-1][0] < start_time:
discard_msgs = msg_num
continue
for time, freq, z in data:
if time >= start_time and time <= end_time:
samp_sum += freq
samp_count += 1
del self._samples[:discard_msgs]
if not samp_count:
# No sensor readings - raise error in pull_probed()
return 0.
return samp_sum / samp_count
def _lookup_toolhead_pos(self, pos_time):
toolhead = self._printer.lookup_object('toolhead')
kin = toolhead.get_kinematics()
kin_spos = {s.get_name(): s.mcu_to_commanded_position(
s.get_past_mcu_position(pos_time))
for s in kin.get_steppers()}
return kin.calc_position(kin_spos)
def _check_samples(self):
while self._samples and self._probe_times:
start_time, end_time, pos_time, toolhead_pos = self._probe_times[0]
if self._samples[-1]['data'][-1][0] < end_time:
break
freq = self._pull_freq(start_time, end_time)
if pos_time is not None:
toolhead_pos = self._lookup_toolhead_pos(pos_time)
sensor_z = None
if freq:
sensor_z = self._calibration.freq_to_height(freq)
self._probe_results.append((sensor_z, toolhead_pos))
self._probe_times.pop(0)
def pull_probed(self):
self._await_samples()
results = []
for sensor_z, toolhead_pos in self._probe_results:
if sensor_z is None:
raise self._printer.command_error(
"Unable to obtain probe_eddy_current sensor readings")
if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE:
raise self._printer.command_error(
"probe_eddy_current sensor not in valid range")
# Callers expect position relative to z_offset, so recalculate
bed_deviation = toolhead_pos[2] - sensor_z
toolhead_pos[2] = self._z_offset + bed_deviation
results.append(toolhead_pos)
del self._probe_results[:]
return results
def note_probe(self, start_time, end_time, toolhead_pos):
self._probe_times.append((start_time, end_time, None, toolhead_pos))
self._check_samples()
def note_probe_and_position(self, start_time, end_time, pos_time):
self._probe_times.append((start_time, end_time, pos_time, None))
self._check_samples()
# Helper for implementing PROBE style commands (descend until trigger)
class EddyEndstopWrapper:
REASON_SENSOR_ERROR = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1
def __init__(self, config, sensor_helper, calibration):
self._printer = config.get_printer()
self._sensor_helper = sensor_helper
@ -192,35 +311,8 @@ class EddyEndstopWrapper:
self._calibration = calibration
self._z_offset = config.getfloat('z_offset', minval=0.)
self._dispatch = mcu.TriggerDispatch(self._mcu)
self._samples = []
self._is_sampling = self._start_from_home = self._need_stop = False
self._trigger_time = 0.
self._printer.register_event_handler('klippy:mcu_identify',
self._handle_mcu_identify)
def _handle_mcu_identify(self):
kin = self._printer.lookup_object('toolhead').get_kinematics()
for stepper in kin.get_steppers():
if stepper.is_active_axis('z'):
self.add_stepper(stepper)
# Measurement gathering
def _start_measurements(self, is_home=False):
self._need_stop = False
if self._is_sampling:
return
self._is_sampling = True
self._is_from_home = is_home
self._sensor_helper.add_client(self._add_measurement)
def _stop_measurements(self, is_home=False):
if not self._is_sampling or (is_home and not self._start_from_home):
return
self._need_stop = True
def _add_measurement(self, msg):
if self._need_stop:
del self._samples[:]
self._is_sampling = self._need_stop = False
return False
self._samples.append(msg)
return True
self._gather = None
# Interface for MCU_endstop
def get_mcu(self):
return self._mcu
@ -231,20 +323,21 @@ class EddyEndstopWrapper:
def home_start(self, print_time, sample_time, sample_count, rest_time,
triggered=True):
self._trigger_time = 0.
self._start_measurements(is_home=True)
trigger_freq = self._calibration.height_to_freq(self._z_offset)
trigger_completion = self._dispatch.start(print_time)
self._sensor_helper.setup_home(
print_time, trigger_freq, self._dispatch.get_oid(),
mcu.MCU_trsync.REASON_ENDSTOP_HIT)
mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.REASON_SENSOR_ERROR)
return trigger_completion
def home_wait(self, home_end_time):
self._dispatch.wait_end(home_end_time)
trigger_time = self._sensor_helper.clear_home()
self._stop_measurements(is_home=True)
res = self._dispatch.stop()
if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT:
return -1.
if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT:
if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT:
raise self._printer.command_error(
"Communication timeout during homing")
raise self._printer.command_error("Eddy current sensor error")
if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT:
return 0.
if self._mcu.is_fileoutput():
@ -260,48 +353,19 @@ class EddyEndstopWrapper:
trig_pos = phoming.probing_move(self, pos, speed)
if not self._trigger_time:
return trig_pos
# Wait for 200ms to elapse since trigger time
reactor = self._printer.get_reactor()
while 1:
systime = reactor.monotonic()
est_print_time = self._mcu.estimated_print_time(systime)
need_delay = self._trigger_time + 0.200 - est_print_time
if need_delay <= 0.:
break
reactor.pause(systime + need_delay)
# Find position since trigger
samples = self._samples
self._samples = []
# Extract samples
start_time = self._trigger_time + 0.050
end_time = start_time + 0.100
samp_sum = 0.
samp_count = 0
for msg in samples:
data = msg['data']
if data[0][0] > end_time:
break
if data[-1][0] < start_time:
continue
for time, freq, z in data:
if time >= start_time and time <= end_time:
samp_sum += z
samp_count += 1
if not samp_count:
raise self._printer.command_error(
"Unable to obtain probe_eddy_current sensor readings")
halt_z = samp_sum / samp_count
# Calculate reported "trigger" position
toolhead = self._printer.lookup_object("toolhead")
new_pos = toolhead.get_position()
new_pos[2] += self._z_offset - halt_z
return new_pos
toolhead_pos = toolhead.get_position()
self._gather.note_probe(start_time, end_time, toolhead_pos)
return self._gather.pull_probed()[0]
def multi_probe_begin(self):
if not self._calibration.is_calibrated():
raise self._printer.command_error(
"Must calibrate probe_eddy_current first")
self._start_measurements()
self._gather = EddyGatherSamples(self._printer, self._sensor_helper,
self._calibration, self._z_offset)
def multi_probe_end(self):
self._stop_measurements()
self._gather.finish()
self._gather = None
def probe_prepare(self, hmove):
pass
def probe_finish(self, hmove):
@ -309,6 +373,46 @@ class EddyEndstopWrapper:
def get_position_endstop(self):
return self._z_offset
# Implementing probing with "METHOD=scan"
class EddyScanningProbe:
def __init__(self, printer, sensor_helper, calibration, z_offset, gcmd):
self._printer = printer
self._sensor_helper = sensor_helper
self._calibration = calibration
self._z_offset = z_offset
self._gather = EddyGatherSamples(printer, sensor_helper,
calibration, z_offset)
self._sample_time_delay = 0.050
self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0)
self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan'
def _rapid_lookahead_cb(self, printtime):
start_time = printtime - self._sample_time / 2
self._gather.note_probe_and_position(
start_time, start_time + self._sample_time, printtime)
def run_probe(self, gcmd):
toolhead = self._printer.lookup_object("toolhead")
if self._is_rapid:
toolhead.register_lookahead_callback(self._rapid_lookahead_cb)
return
printtime = toolhead.get_last_move_time()
toolhead.dwell(self._sample_time_delay + self._sample_time)
start_time = printtime + self._sample_time_delay
self._gather.note_probe_and_position(
start_time, start_time + self._sample_time, start_time)
def pull_probed_results(self):
if self._is_rapid:
# Flush lookahead (so all lookahead callbacks are invoked)
toolhead = self._printer.lookup_object("toolhead")
toolhead.get_last_move_time()
results = self._gather.pull_probed()
# Allow axis_twist_compensation to update results
for epos in results:
self._printer.send_event("probe:update_results", epos)
return results
def end_probe_session(self):
self._gather.finish()
self._gather = None
# Main "printer object"
class PrinterEddyProbe:
def __init__(self, config):
@ -319,11 +423,42 @@ class PrinterEddyProbe:
sensor_type = config.getchoice('sensor_type', {s: s for s in sensors})
self.sensor_helper = sensors[sensor_type](config, self.calibration)
# Probe interface
self.probe = EddyEndstopWrapper(config, self.sensor_helper,
self.calibration)
self.printer.add_object('probe', probe.PrinterProbe(config, self.probe))
self.mcu_probe = EddyEndstopWrapper(config, self.sensor_helper,
self.calibration)
self.cmd_helper = probe.ProbeCommandHelper(
config, self, self.mcu_probe.query_endstop)
self.probe_offsets = probe.ProbeOffsetsHelper(config)
self.probe_session = probe.ProbeSessionHelper(config, self.mcu_probe)
self.printer.add_object('probe', self)
def add_client(self, cb):
self.sensor_helper.add_client(cb)
def get_probe_params(self, gcmd=None):
return self.probe_session.get_probe_params(gcmd)
def get_offsets(self):
return self.probe_offsets.get_offsets()
def get_status(self, eventtime):
return self.cmd_helper.get_status(eventtime)
def start_probe_session(self, gcmd):
method = gcmd.get('METHOD', 'automatic').lower()
if method in ('scan', 'rapid_scan'):
z_offset = self.get_offsets()[2]
return EddyScanningProbe(self.printer, self.sensor_helper,
self.calibration, z_offset, gcmd)
return self.probe_session.start_probe_session(gcmd)
def register_drift_compensation(self, comp):
self.calibration.register_drift_compensation(comp)
class DummyDriftCompensation:
def get_temperature(self):
return 0.
def note_z_calibration_start(self):
pass
def note_z_calibration_finish(self):
pass
def adjust_freq(self, freq, temp=None):
return freq
def unadjust_freq(self, freq, temp=None):
return freq
def load_config_prefix(config):
return PrinterEddyProbe(config)

View file

@ -160,7 +160,7 @@ class Replicape:
printer = config.get_printer()
ppins = printer.lookup_object('pins')
ppins.register_chip('replicape', self)
revisions = {'B3': 'B3'}
revisions = ['B3']
config.getchoice('revision', revisions)
self.host_mcu = mcu.get_printer_mcu(printer, config.get('host_mcu'))
# Setup enable pin

View file

@ -58,13 +58,15 @@ class PrinterServo:
return width * self.width_to_value
cmd_SET_SERVO_help = "Set servo angle"
def cmd_SET_SERVO(self, gcmd):
print_time = self.printer.lookup_object('toolhead').get_last_move_time()
width = gcmd.get_float('WIDTH', None)
if width is not None:
self._set_pwm(print_time, self._get_pwm_from_pulse_width(width))
value = self._get_pwm_from_pulse_width(width)
else:
angle = gcmd.get_float('ANGLE')
self._set_pwm(print_time, self._get_pwm_from_angle(angle))
value = self._get_pwm_from_angle(angle)
toolhead = self.printer.lookup_object('toolhead')
toolhead.register_lookahead_callback((lambda pt:
self._set_pwm(pt, value)))
def load_config_prefix(config):
return PrinterServo(config)

View file

@ -27,6 +27,13 @@ SHT3X_CMD = {
'LOW_REP': [0x24, 0x16],
},
},
'PERIODIC': {
'2HZ': {
'HIGH_REP': [0x22, 0x36],
'MED_REP': [0x22, 0x20],
'LOW_REP': [0x22, 0x2B],
},
},
'OTHER': {
'STATUS': {
'READ': [0xF3, 0x2D],
@ -72,10 +79,12 @@ class SHT3X:
def _init_sht3x(self):
# Device Soft Reset
self.i2c.i2c_write(SHT3X_CMD['OTHER']['SOFTRESET'])
# Wait 2ms after reset
self.reactor.pause(self.reactor.monotonic() + .02)
self.i2c.i2c_write_wait_ack(SHT3X_CMD['OTHER']['BREAK'])
# Break takes ~ 1ms
self.reactor.pause(self.reactor.monotonic() + .0015)
self.i2c.i2c_write_wait_ack(SHT3X_CMD['OTHER']['SOFTRESET'])
# Wait <=1.5ms after reset
self.reactor.pause(self.reactor.monotonic() + .0015)
status = self.i2c.i2c_read(SHT3X_CMD['OTHER']['STATUS']['READ'], 3)
response = bytearray(status['response'])
@ -86,17 +95,17 @@ class SHT3X:
if self._crc8(status) != checksum:
logging.warning("sht3x: Reading status - checksum error!")
# Enable periodic mode
self.i2c.i2c_write_wait_ack(
SHT3X_CMD['PERIODIC']['2HZ']['HIGH_REP']
)
# Wait <=15.5ms for first measurment
self.reactor.pause(self.reactor.monotonic() + .0155)
def _sample_sht3x(self, eventtime):
try:
# Read Temeprature
params = self.i2c.i2c_write(
SHT3X_CMD['MEASURE']['STRETCH_ENABLED']['HIGH_REP']
)
# Wait
self.reactor.pause(self.reactor.monotonic()
+ .20)
params = self.i2c.i2c_read([], 6)
# Read measurment
params = self.i2c.i2c_read(SHT3X_CMD['OTHER']['FETCH'], 6)
response = bytearray(params['response'])
rtemp = response[0] << 8

View file

@ -48,7 +48,7 @@ class ControlPinHelper:
bit_time += bit_step
return bit_time
class SmartEffectorEndstopWrapper:
class SmartEffectorProbe:
def __init__(self, config):
self.printer = config.get_printer()
self.gcode = self.printer.lookup_object('gcode')
@ -64,6 +64,12 @@ class SmartEffectorEndstopWrapper:
self.query_endstop = self.probe_wrapper.query_endstop
self.multi_probe_begin = self.probe_wrapper.multi_probe_begin
self.multi_probe_end = self.probe_wrapper.multi_probe_end
self.get_position_endstop = self.probe_wrapper.get_position_endstop
# Common probe implementation helpers
self.cmd_helper = probe.ProbeCommandHelper(
config, self, self.probe_wrapper.query_endstop)
self.probe_offsets = probe.ProbeOffsetsHelper(config)
self.probe_session = probe.ProbeSessionHelper(config, self)
# SmartEffector control
control_pin = config.get('control_pin', None)
if control_pin:
@ -78,6 +84,14 @@ class SmartEffectorEndstopWrapper:
self.gcode.register_command("SET_SMART_EFFECTOR",
self.cmd_SET_SMART_EFFECTOR,
desc=self.cmd_SET_SMART_EFFECTOR_help)
def get_probe_params(self, gcmd=None):
return self.probe_session.get_probe_params(gcmd)
def get_offsets(self):
return self.probe_offsets.get_offsets()
def get_status(self, eventtime):
return self.cmd_helper.get_status(eventtime)
def start_probe_session(self, gcmd):
return self.probe_session.start_probe_session(gcmd)
def probing_move(self, pos, speed):
phoming = self.printer.lookup_object('homing')
return phoming.probing_move(self, pos, speed)
@ -151,7 +165,6 @@ class SmartEffectorEndstopWrapper:
gcmd.respond_info('SmartEffector sensitivity was reset')
def load_config(config):
smart_effector = SmartEffectorEndstopWrapper(config)
config.get_printer().add_object('probe',
probe.PrinterProbe(config, smart_effector))
smart_effector = SmartEffectorProbe(config)
config.get_printer().add_object('probe', smart_effector)
return smart_effector

View file

@ -1,10 +1,11 @@
# Support for micro-controller chip based temperature sensors
#
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2020-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import mcu
from . import adc_temperature
SAMPLE_TIME = 0.001
SAMPLE_COUNT = 8
@ -31,30 +32,33 @@ class PrinterTemperatureMCU:
self.mcu_adc = ppins.setup_pin('adc',
'%s:ADC_TEMPERATURE' % (mcu_name,))
self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback)
query_adc = config.get_printer().load_object(config, 'query_adc')
query_adc.register_adc(config.get_name(), self.mcu_adc)
self.diag_helper = adc_temperature.HelperTemperatureDiagnostics(
config, self.mcu_adc, self.calc_temp)
# Register callbacks
if self.printer.get_start_args().get('debugoutput') is not None:
self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT,
range_check_count=RANGE_CHECK_COUNT)
self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT)
return
self.printer.register_event_handler("klippy:mcu_identify",
self._mcu_identify)
self.handle_mcu_identify)
# Temperature interface
def setup_callback(self, temperature_callback):
self.temperature_callback = temperature_callback
def get_report_time_delta(self):
return REPORT_TIME
def adc_callback(self, read_time, read_value):
temp = self.base_temperature + read_value * self.slope
self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp)
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
# Internal code
def adc_callback(self, read_time, read_value):
temp = self.base_temperature + read_value * self.slope
self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp)
def calc_temp(self, adc):
return self.base_temperature + adc * self.slope
def calc_adc(self, temp):
return (temp - self.base_temperature) / self.slope
def calc_base(self, temp, adc):
return temp - adc * self.slope
def _mcu_identify(self):
def handle_mcu_identify(self):
# Obtain mcu information
mcu = self.mcu_adc.get_mcu()
self.debug_read_cmd = mcu.lookup_query_command(
@ -89,10 +93,13 @@ class PrinterTemperatureMCU:
self.slope = (self.temp2 - self.temp1) / (self.adc2 - self.adc1)
self.base_temperature = self.calc_base(self.temp1, self.adc1)
# Setup min/max checks
adc_range = [self.calc_adc(t) for t in [self.min_temp, self.max_temp]]
self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT,
minval=min(adc_range), maxval=max(adc_range),
range_check_count=RANGE_CHECK_COUNT)
arange = [self.calc_adc(t) for t in [self.min_temp, self.max_temp]]
min_adc, max_adc = sorted(arange)
self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT,
minval=min_adc, maxval=max_adc,
range_check_count=RANGE_CHECK_COUNT)
self.diag_helper.setup_diag_minmax(self.min_temp, self.max_temp,
min_adc, max_adc)
def config_unknown(self):
raise self.printer.config_error("MCU temperature not supported on %s"
% (self.mcu_type,))

View file

@ -0,0 +1,721 @@
# Probe temperature sensor and drift calibration
#
# Copyright (C) 2024 Eric Callahan <arksine.code@gmail.com>
#
# 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)

View file

@ -278,16 +278,14 @@ class TMCCommandHelper:
raise gcmd.error("Unknown field name '%s'" % (field_name,))
value = gcmd.get_int('VALUE', None)
velocity = gcmd.get_float('VELOCITY', None, minval=0.)
tmc_frequency = self.mcu_tmc.get_tmc_frequency()
if tmc_frequency is None and velocity is not None:
raise gcmd.error("VELOCITY parameter not supported by this driver")
if (value is None) == (velocity is None):
raise gcmd.error("Specify either VALUE or VELOCITY")
if velocity is not None:
step_dist = self.stepper.get_step_dist()
mres = self.fields.get_field("mres")
value = TMCtstepHelper(step_dist, mres, tmc_frequency,
velocity)
if self.mcu_tmc.get_tmc_frequency() is None:
raise gcmd.error(
"VELOCITY parameter not supported by this driver")
value = TMCtstepHelper(self.mcu_tmc, velocity,
pstepper=self.stepper)
reg_val = self.fields.set_field(field_name, value)
print_time = self.printer.lookup_object('toolhead').get_last_move_time()
self.mcu_tmc.set_register(reg_name, reg_val, print_time)
@ -481,7 +479,7 @@ class TMCVirtualPinHelper:
self.diag_pin_field = None
self.mcu_endstop = None
self.en_pwm = False
self.pwmthrs = self.coolthrs = 0
self.pwmthrs = self.coolthrs = self.thigh = 0
# Register virtual_endstop pin
name_parts = config.get_name().split()
ppins = self.printer.lookup_object("pins")
@ -505,8 +503,8 @@ class TMCVirtualPinHelper:
def handle_homing_move_begin(self, hmove):
if self.mcu_endstop not in hmove.get_mcu_endstops():
return
# Enable/disable stealthchop
self.pwmthrs = self.fields.get_field("tpwmthrs")
self.coolthrs = self.fields.get_field("tcoolthrs")
reg = self.fields.lookup_register("en_pwm_mode", None)
if reg is None:
# On "stallguard4" drivers, "stealthchop" must be enabled
@ -520,12 +518,21 @@ class TMCVirtualPinHelper:
self.fields.set_field("en_pwm_mode", 0)
val = self.fields.set_field(self.diag_pin_field, 1)
self.mcu_tmc.set_register("GCONF", val)
# Enable tcoolthrs (if not already)
self.coolthrs = self.fields.get_field("tcoolthrs")
if self.coolthrs == 0:
tc_val = self.fields.set_field("tcoolthrs", 0xfffff)
self.mcu_tmc.set_register("TCOOLTHRS", tc_val)
# Disable thigh
reg = self.fields.lookup_register("thigh", None)
if reg is not None:
self.thigh = self.fields.get_field("thigh")
th_val = self.fields.set_field("thigh", 0)
self.mcu_tmc.set_register(reg, th_val)
def handle_homing_move_end(self, hmove):
if self.mcu_endstop not in hmove.get_mcu_endstops():
return
# Restore stealthchop/spreadcycle
reg = self.fields.lookup_register("en_pwm_mode", None)
if reg is None:
tp_val = self.fields.set_field("tpwmthrs", self.pwmthrs)
@ -535,8 +542,14 @@ class TMCVirtualPinHelper:
self.fields.set_field("en_pwm_mode", self.en_pwm)
val = self.fields.set_field(self.diag_pin_field, 0)
self.mcu_tmc.set_register("GCONF", val)
# Restore tcoolthrs
tc_val = self.fields.set_field("tcoolthrs", self.coolthrs)
self.mcu_tmc.set_register("TCOOLTHRS", tc_val)
# Restore thigh
reg = self.fields.lookup_register("thigh", None)
if reg is not None:
th_val = self.fields.set_field("thigh", self.thigh)
self.mcu_tmc.set_register(reg, th_val)
######################################################################
@ -564,7 +577,7 @@ def TMCWaveTableHelper(config, mcu_tmc):
set_config_field(config, "start_sin", 0)
set_config_field(config, "start_sin90", 247)
# Helper to configure and query the microstep settings
# Helper to configure the microstep settings
def TMCMicrostepHelper(config, mcu_tmc):
fields = mcu_tmc.get_fields()
stepper_name = " ".join(config.get_name().split()[1:])
@ -572,27 +585,31 @@ def TMCMicrostepHelper(config, mcu_tmc):
raise config.error(
"Could not find config section '[%s]' required by tmc driver"
% (stepper_name,))
stepper_config = ms_config = config.getsection(stepper_name)
if (stepper_config.get('microsteps', None, note_valid=False) is None
and config.get('microsteps', None, note_valid=False) is not None):
# Older config format with microsteps in tmc config section
ms_config = config
sconfig = config.getsection(stepper_name)
steps = {256: 0, 128: 1, 64: 2, 32: 3, 16: 4, 8: 5, 4: 6, 2: 7, 1: 8}
mres = ms_config.getchoice('microsteps', steps)
mres = sconfig.getchoice('microsteps', steps)
fields.set_field("mres", mres)
fields.set_field("intpol", config.getboolean("interpolate", True))
# Helper for calculating TSTEP based values from velocity
def TMCtstepHelper(step_dist, mres, tmc_freq, velocity):
if velocity > 0.:
step_dist_256 = step_dist / (1 << mres)
threshold = int(tmc_freq * step_dist_256 / velocity + .5)
return max(0, min(0xfffff, threshold))
else:
def TMCtstepHelper(mcu_tmc, velocity, pstepper=None, config=None):
if velocity <= 0.:
return 0xfffff
if pstepper is not None:
step_dist = pstepper.get_step_dist()
else:
stepper_name = " ".join(config.get_name().split()[1:])
sconfig = config.getsection(stepper_name)
rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig)
step_dist = rotation_dist / steps_per_rotation
mres = mcu_tmc.get_fields().get_field("mres")
step_dist_256 = step_dist / (1 << mres)
tmc_freq = mcu_tmc.get_tmc_frequency()
threshold = int(tmc_freq * step_dist_256 / velocity + .5)
return max(0, min(0xfffff, threshold))
# Helper to configure stealthChop-spreadCycle transition velocity
def TMCStealthchopHelper(config, mcu_tmc, tmc_freq):
def TMCStealthchopHelper(config, mcu_tmc):
fields = mcu_tmc.get_fields()
en_pwm_mode = False
velocity = config.getfloat('stealthchop_threshold', None, minval=0.)
@ -600,13 +617,7 @@ def TMCStealthchopHelper(config, mcu_tmc, tmc_freq):
if velocity is not None:
en_pwm_mode = True
stepper_name = " ".join(config.get_name().split()[1:])
sconfig = config.getsection(stepper_name)
rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig)
step_dist = rotation_dist / steps_per_rotation
mres = fields.get_field("mres")
tpwmthrs = TMCtstepHelper(step_dist, mres, tmc_freq, velocity)
tpwmthrs = TMCtstepHelper(mcu_tmc, velocity, config=config)
fields.set_field("tpwmthrs", tpwmthrs)
reg = fields.lookup_register("en_pwm_mode", None)
@ -615,3 +626,22 @@ def TMCStealthchopHelper(config, mcu_tmc, tmc_freq):
else:
# TMC2208 uses en_spreadCycle
fields.set_field("en_spreadcycle", not en_pwm_mode)
# Helper to configure StallGuard and CoolStep minimum velocity
def TMCVcoolthrsHelper(config, mcu_tmc):
fields = mcu_tmc.get_fields()
velocity = config.getfloat('coolstep_threshold', None, minval=0.)
tcoolthrs = 0
if velocity is not None:
tcoolthrs = TMCtstepHelper(mcu_tmc, velocity, config=config)
fields.set_field("tcoolthrs", tcoolthrs)
# Helper to configure StallGuard and CoolStep maximum velocity and
# SpreadCycle-FullStepping (High velocity) mode threshold.
def TMCVhighHelper(config, mcu_tmc):
fields = mcu_tmc.get_fields()
velocity = config.getfloat('high_velocity_threshold', None, minval=0.)
thigh = 0
if velocity is not None:
thigh = TMCtstepHelper(mcu_tmc, velocity, config=config)
fields.set_field("thigh", thigh)

View file

@ -296,7 +296,9 @@ class TMC2130:
self.get_status = cmdhelper.get_status
# Setup basic register values
tmc.TMCWaveTableHelper(config, self.mcu_tmc)
tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY)
tmc.TMCStealthchopHelper(config, self.mcu_tmc)
tmc.TMCVcoolthrsHelper(config, self.mcu_tmc)
tmc.TMCVhighHelper(config, self.mcu_tmc)
# Allow other registers to be set from the config
set_config_field = self.fields.set_config_field
# CHOPCONF
@ -304,8 +306,16 @@ class TMC2130:
set_config_field(config, "hstrt", 0)
set_config_field(config, "hend", 7)
set_config_field(config, "tbl", 1)
set_config_field(config, "vhighfs", 0)
set_config_field(config, "vhighchm", 0)
# COOLCONF
set_config_field(config, "semin", 0)
set_config_field(config, "seup", 0)
set_config_field(config, "semax", 0)
set_config_field(config, "sedn", 0)
set_config_field(config, "seimin", 0)
set_config_field(config, "sgt", 0)
set_config_field(config, "sfilt", 0)
# IHOLDIRUN
set_config_field(config, "iholddelay", 8)
# PWMCONF

View file

@ -197,7 +197,7 @@ class TMC2208:
self.get_status = cmdhelper.get_status
# Setup basic register values
self.fields.set_field("mstep_reg_select", True)
tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY)
tmc.TMCStealthchopHelper(config, self.mcu_tmc)
# Allow other registers to be set from the config
set_config_field = self.fields.set_config_field
# GCONF

View file

@ -73,7 +73,8 @@ class TMC2209:
self.get_status = cmdhelper.get_status
# Setup basic register values
self.fields.set_field("mstep_reg_select", True)
tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY)
tmc.TMCStealthchopHelper(config, self.mcu_tmc)
tmc.TMCVcoolthrsHelper(config, self.mcu_tmc)
# Allow other registers to be set from the config
set_config_field = self.fields.set_config_field
# GCONF
@ -83,6 +84,12 @@ class TMC2209:
set_config_field(config, "hstrt", 5)
set_config_field(config, "hend", 0)
set_config_field(config, "tbl", 2)
# COOLCONF
set_config_field(config, "semin", 0)
set_config_field(config, "seup", 0)
set_config_field(config, "semax", 0)
set_config_field(config, "sedn", 0)
set_config_field(config, "seimin", 0)
# IHOLDIRUN
set_config_field(config, "iholddelay", 8)
# PWMCONF

View file

@ -95,7 +95,7 @@ Fields["DRV_STATUS"] = {
"s2vsb": 0x01 << 13,
"stealth": 0x01 << 14,
"fsactive": 0x01 << 15,
"csactual": 0x1F << 16,
"cs_actual": 0x1F << 16,
"stallguard": 0x01 << 24,
"ot": 0x01 << 25,
"otpw": 0x01 << 26,
@ -364,7 +364,10 @@ class TMC2240:
# Setup basic register values
tmc.TMCWaveTableHelper(config, self.mcu_tmc)
self.fields.set_config_field(config, "offset_sin90", 0)
tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY)
tmc.TMCStealthchopHelper(config, self.mcu_tmc)
tmc.TMCVcoolthrsHelper(config, self.mcu_tmc)
tmc.TMCVhighHelper(config, self.mcu_tmc)
# Allow other registers to be set from the config
set_config_field = self.fields.set_config_field
# GCONF
set_config_field(config, "multistep_filt", True)

View file

@ -118,7 +118,7 @@ Fields["DRV_STATUS"] = {
"s2vsb": 0x01 << 13,
"stealth": 0x01 << 14,
"fsactive": 0x01 << 15,
"csactual": 0xFF << 16,
"cs_actual": 0x1F << 16,
"stallguard": 0x01 << 24,
"ot": 0x01 << 25,
"otpw": 0x01 << 26,
@ -242,6 +242,9 @@ Fields["TCOOLTHRS"] = {
Fields["TSTEP"] = {
"tstep": 0xfffff << 0
}
Fields["THIGH"] = {
"thigh": 0xfffff << 0
}
SignedFields = ["cur_a", "cur_b", "sgt", "xactual", "vactual", "pwm_scale_auto"]
@ -335,7 +338,10 @@ class TMC5160:
self.get_status = cmdhelper.get_status
# Setup basic register values
tmc.TMCWaveTableHelper(config, self.mcu_tmc)
tmc.TMCStealthchopHelper(config, self.mcu_tmc, TMC_FREQUENCY)
tmc.TMCStealthchopHelper(config, self.mcu_tmc)
tmc.TMCVcoolthrsHelper(config, self.mcu_tmc)
tmc.TMCVhighHelper(config, self.mcu_tmc)
# Allow other registers to be set from the config
set_config_field = self.fields.set_config_field
# GCONF
set_config_field(config, "multistep_filt", True)

View file

@ -33,7 +33,7 @@ class FilamentWidthSensor:
# Start adc
self.ppins = self.printer.lookup_object('pins')
self.mcu_adc = self.ppins.setup_pin('adc', self.pin)
self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback)
# extrude factor updating
self.extrude_factor_update_timer = self.reactor.register_timer(

View file

@ -406,7 +406,7 @@ class GCodeIO:
self._dump_debug()
if self.is_fileinput:
self.printer.request_exit('error_exit')
m112_r = re.compile('^(?:[nN][0-9]+)?\s*[mM]112(?:\s|$)')
m112_r = re.compile(r'^(?:[nN][0-9]+)?\s*[mM]112(?:\s|$)')
def _process_data(self, eventtime):
# Read input, separate by newline, and add to pending_commands
try:

View file

@ -23,7 +23,7 @@ class CartKinematics:
self.dc_module = None
if config.has_section('dual_carriage'):
dc_config = config.getsection('dual_carriage')
dc_axis = dc_config.getchoice('axis', {'x': 'x', 'y': 'y'})
dc_axis = dc_config.getchoice('axis', ['x', 'y'])
self.dual_carriage_axis = {'x': 0, 'y': 1}[dc_axis]
# setup second dual carriage rail
self.rails.append(stepper.LookupMultiRail(dc_config))
@ -52,20 +52,27 @@ class CartKinematics:
def get_steppers(self):
return [s for rail in self.rails for s in rail.get_steppers()]
def calc_position(self, stepper_positions):
return [stepper_positions[rail.get_name()] for rail in self.rails]
rails = self.rails
if self.dc_module:
primary_rail = self.dc_module.get_primary_rail().get_rail()
rails = (rails[:self.dc_module.axis] +
[primary_rail] + rails[self.dc_module.axis+1:])
return [stepper_positions[rail.get_name()] for rail in rails]
def update_limits(self, i, range):
l, h = self.limits[i]
# Only update limits if this axis was already homed,
# otherwise leave in un-homed state.
if l <= h:
self.limits[i] = range
def override_rail(self, i, rail):
self.rails[i] = rail
def set_position(self, newpos, homing_axes):
for i, rail in enumerate(self.rails):
rail.set_position(newpos)
if i in homing_axes:
self.limits[i] = rail.get_range()
for axis in homing_axes:
if self.dc_module and axis == self.dc_module.axis:
rail = self.dc_module.get_primary_rail().get_rail()
else:
rail = self.rails[axis]
self.limits[axis] = rail.get_range()
def note_z_not_homed(self):
# Helper for Safe Z Home
self.limits[2] = (1.0, -1.0)

View file

@ -18,7 +18,7 @@ class ExtruderStepper:
self.stepper = stepper.PrinterStepper(config)
ffi_main, ffi_lib = chelper.get_ffi()
self.sk_extruder = ffi_main.gc(ffi_lib.extruder_stepper_alloc(),
ffi_lib.free)
ffi_lib.extruder_stepper_free)
self.stepper.set_stepper_kinematics(self.sk_extruder)
self.motion_queue = None
# Register commands
@ -71,11 +71,14 @@ class ExtruderStepper:
if not pressure_advance:
new_smooth_time = 0.
toolhead = self.printer.lookup_object("toolhead")
toolhead.note_step_generation_scan_time(new_smooth_time * .5,
old_delay=old_smooth_time * .5)
if new_smooth_time != old_smooth_time:
toolhead.note_step_generation_scan_time(
new_smooth_time * .5, old_delay=old_smooth_time * .5)
ffi_main, ffi_lib = chelper.get_ffi()
espa = ffi_lib.extruder_set_pressure_advance
espa(self.sk_extruder, pressure_advance, new_smooth_time)
toolhead.register_lookahead_callback(
lambda print_time: espa(self.sk_extruder, print_time,
pressure_advance, new_smooth_time))
self.pressure_advance = pressure_advance
self.pressure_advance_smooth_time = smooth_time
cmd_SET_PRESSURE_ADVANCE_help = "Set pressure advance parameters"

View file

@ -27,7 +27,7 @@ class HybridCoreXYKinematics:
if config.has_section('dual_carriage'):
dc_config = config.getsection('dual_carriage')
# dummy for cartesian config users
dc_config.getchoice('axis', {'x': 'x'}, default='x')
dc_config.getchoice('axis', ['x'], default='x')
# setup second dual carriage rail
self.rails.append(stepper.PrinterRail(dc_config))
self.rails[1].get_endstops()[0][0].add_stepper(
@ -57,7 +57,7 @@ class HybridCoreXYKinematics:
pos = [stepper_positions[rail.get_name()] for rail in self.rails]
if (self.dc_module is not None and 'PRIMARY' == \
self.dc_module.get_status()['carriage_1']):
return [pos[0] - pos[1], pos[1], pos[2]]
return [pos[3] - pos[1], pos[1], pos[2]]
else:
return [pos[0] + pos[1], pos[1], pos[2]]
def update_limits(self, i, range):
@ -66,13 +66,15 @@ class HybridCoreXYKinematics:
# otherwise leave in un-homed state.
if l <= h:
self.limits[i] = range
def override_rail(self, i, rail):
self.rails[i] = rail
def set_position(self, newpos, homing_axes):
for i, rail in enumerate(self.rails):
rail.set_position(newpos)
if i in homing_axes:
self.limits[i] = rail.get_range()
for axis in homing_axes:
if self.dc_module and axis == self.dc_module.axis:
rail = self.dc_module.get_primary_rail().get_rail()
else:
rail = self.rails[axis]
self.limits[axis] = rail.get_range()
def note_z_not_homed(self):
# Helper for Safe Z Home
self.limits[2] = (1.0, -1.0)

View file

@ -27,7 +27,7 @@ class HybridCoreXZKinematics:
if config.has_section('dual_carriage'):
dc_config = config.getsection('dual_carriage')
# dummy for cartesian config users
dc_config.getchoice('axis', {'x': 'x'}, default='x')
dc_config.getchoice('axis', ['x'], default='x')
# setup second dual carriage rail
self.rails.append(stepper.PrinterRail(dc_config))
self.rails[2].get_endstops()[0][0].add_stepper(
@ -57,7 +57,7 @@ class HybridCoreXZKinematics:
pos = [stepper_positions[rail.get_name()] for rail in self.rails]
if (self.dc_module is not None and 'PRIMARY' == \
self.dc_module.get_status()['carriage_1']):
return [pos[0] - pos[2], pos[1], pos[2]]
return [pos[3] - pos[2], pos[1], pos[2]]
else:
return [pos[0] + pos[2], pos[1], pos[2]]
def update_limits(self, i, range):
@ -66,13 +66,15 @@ class HybridCoreXZKinematics:
# otherwise leave in un-homed state.
if l <= h:
self.limits[i] = range
def override_rail(self, i, rail):
self.rails[i] = rail
def set_position(self, newpos, homing_axes):
for i, rail in enumerate(self.rails):
rail.set_position(newpos)
if i in homing_axes:
self.limits[i] = rail.get_range()
for axis in homing_axes:
if self.dc_module and axis == self.dc_module.axis:
rail = self.dc_module.get_primary_rail().get_rail()
else:
rail = self.rails[axis]
self.limits[axis] = rail.get_range()
def note_z_not_homed(self):
# Helper for Safe Z Home
self.limits[2] = (1.0, -1.0)

View file

@ -4,7 +4,7 @@
# Copyright (C) 2023 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math
import logging, math
import chelper
INACTIVE = 'INACTIVE'
@ -42,7 +42,12 @@ class DualCarriages:
desc=self.cmd_RESTORE_DUAL_CARRIAGE_STATE_help)
def get_rails(self):
return self.dc
def toggle_active_dc_rail(self, index, override_rail=False):
def get_primary_rail(self):
for rail in self.dc:
if rail.mode == PRIMARY:
return rail
return None
def toggle_active_dc_rail(self, index):
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
pos = toolhead.get_position()
@ -52,15 +57,11 @@ class DualCarriages:
if i != index:
if dc.is_active():
dc.inactivate(pos)
if override_rail:
kin.override_rail(3, dc_rail)
target_dc = self.dc[index]
if target_dc.mode != PRIMARY:
newpos = pos[:self.axis] + [target_dc.get_axis_position(pos)] \
+ pos[self.axis+1:]
target_dc.activate(PRIMARY, newpos, old_position=pos)
if override_rail:
kin.override_rail(self.axis, target_dc.get_rail())
toolhead.set_position(newpos)
kin.update_limits(self.axis, target_dc.get_rail().get_range())
def home(self, homing_state):
@ -72,10 +73,10 @@ class DualCarriages:
# the same direction and the first carriage homes on the second one
enumerated_dcs.reverse()
for i, dc_rail in enumerated_dcs:
self.toggle_active_dc_rail(i, override_rail=True)
self.toggle_active_dc_rail(i)
kin.home_axis(homing_state, self.axis, dc_rail.get_rail())
# Restore the original rails ordering
self.toggle_active_dc_rail(0, override_rail=True)
self.toggle_active_dc_rail(0)
def get_status(self, eventtime=None):
return {('carriage_%d' % (i,)) : dc.mode
for (i, dc) in enumerate(self.dc)}
@ -201,14 +202,31 @@ class DualCarriages:
move_speed = gcmd.get_float('MOVE_SPEED', 0., above=0.)
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
pos = toolhead.get_position()
if gcmd.get_int('MOVE', 1):
homing_speed = 99999999.
cur_pos = []
for i, dc in enumerate(self.dc):
self.toggle_active_dc_rail(i)
saved_pos = saved_state['axes_positions'][i]
toolhead.manual_move(
pos[:self.axis] + [saved_pos] + pos[self.axis+1:],
move_speed or dc.get_rail().homing_speed)
homing_speed = min(homing_speed, dc.get_rail().homing_speed)
cur_pos.append(toolhead.get_position())
move_pos = list(cur_pos[0])
dl = [saved_state['axes_positions'][i] - cur_pos[i][self.axis]
for i in range(2)]
primary_ind = 0 if abs(dl[0]) >= abs(dl[1]) else 1
self.toggle_active_dc_rail(primary_ind)
move_pos[self.axis] = saved_state['axes_positions'][primary_ind]
dc_mode = INACTIVE if min(abs(dl[0]), abs(dl[1])) < 0.000000001 \
else COPY if dl[0] * dl[1] > 0 else MIRROR
if dc_mode != INACTIVE:
self.dc[1-primary_ind].activate(dc_mode, cur_pos[primary_ind])
self.dc[1-primary_ind].override_axis_scaling(
abs(dl[1-primary_ind] / dl[primary_ind]),
cur_pos[primary_ind])
toolhead.manual_move(move_pos, move_speed or homing_speed)
toolhead.flush_step_generation()
# Make sure the scaling coefficients are restored with the mode
self.dc[0].inactivate(move_pos)
self.dc[1].inactivate(move_pos)
for i, dc in enumerate(self.dc):
saved_mode = saved_state['carriage_modes'][i]
self.activate_dc_mode(i, saved_mode)
@ -256,3 +274,8 @@ class DualCarriagesRail:
self.scale = 0.
self.apply_transform()
self.mode = INACTIVE
def override_axis_scaling(self, new_scale, position):
old_axis_position = self.get_axis_position(position)
self.scale = math.copysign(new_scale, self.scale)
self.offset = old_axis_position - position[self.axis] * self.scale
self.apply_transform()

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python2
# Main code for host side printer firmware
#
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2016-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import sys, os, gc, optparse, logging, time, collections, importlib
@ -22,31 +22,6 @@ command to reload the config and restart the host software.
Printer is halted
"""
message_protocol_error1 = """
This is frequently caused by running an older version of the
firmware on the MCU(s). Fix by recompiling and flashing the
firmware.
"""
message_protocol_error2 = """
Once the underlying issue is corrected, use the "RESTART"
command to reload the config and restart the host software.
"""
message_mcu_connect_error = """
Once the underlying issue is corrected, use the
"FIRMWARE_RESTART" command to reset the firmware, reload the
config, and restart the host software.
Error configuring printer
"""
message_shutdown = """
Once the underlying issue is corrected, use the
"FIRMWARE_RESTART" command to reset the firmware, reload the
config, and restart the host software.
Printer is shutdown
"""
class Printer:
config_error = configfile.error
command_error = gcode.CommandError
@ -85,6 +60,13 @@ class Printer:
if (msg != message_ready
and self.start_args.get('debuginput') is not None):
self.request_exit('error_exit')
def update_error_msg(self, oldmsg, newmsg):
if (self.state_message != oldmsg
or self.state_message in (message_ready, message_startup)
or newmsg in (message_ready, message_startup)):
return
self.state_message = newmsg
logging.error(newmsg)
def add_object(self, name, obj):
if name in self.objects:
raise self.config_error(
@ -143,33 +125,6 @@ class Printer:
m.add_printer_objects(config)
# Validate that there are no undefined parameters in the config file
pconfig.check_unused_options(config)
def _build_protocol_error_message(self, e):
host_version = self.start_args['software_version']
msg_update = []
msg_updated = []
for mcu_name, mcu in self.lookup_objects('mcu'):
try:
mcu_version = mcu.get_status()['mcu_version']
except:
logging.exception("Unable to retrieve mcu_version from mcu")
continue
if mcu_version != host_version:
msg_update.append("%s: Current version %s"
% (mcu_name.split()[-1], mcu_version))
else:
msg_updated.append("%s: Current version %s"
% (mcu_name.split()[-1], mcu_version))
if not msg_update:
msg_update.append("<none>")
if not msg_updated:
msg_updated.append("<none>")
msg = ["MCU Protocol error",
message_protocol_error1,
"Your Klipper version is: %s" % (host_version,),
"MCU(s) which should be updated:"]
msg += msg_update + ["Up-to-date MCU(s):"] + msg_updated
msg += [message_protocol_error2, str(e)]
return "\n".join(msg)
def _connect(self, eventtime):
try:
self._read_config()
@ -183,13 +138,17 @@ class Printer:
self._set_state("%s\n%s" % (str(e), message_restart))
return
except msgproto.error as e:
logging.exception("Protocol error")
self._set_state(self._build_protocol_error_message(e))
msg = "Protocol error"
logging.exception(msg)
self._set_state(msg)
self.send_event("klippy:notify_mcu_error", msg, {"error": str(e)})
util.dump_mcu_build()
return
except mcu.error as e:
logging.exception("MCU error during connect")
self._set_state("%s%s" % (str(e), message_mcu_connect_error))
msg = "MCU error during connect"
logging.exception(msg)
self._set_state(msg)
self.send_event("klippy:notify_mcu_error", msg, {"error": str(e)})
util.dump_mcu_build()
return
except Exception as e:
@ -241,12 +200,12 @@ class Printer:
logging.info(info)
if self.bglogger is not None:
self.bglogger.set_rollover_info(name, info)
def invoke_shutdown(self, msg):
def invoke_shutdown(self, msg, details={}):
if self.in_shutdown_state:
return
logging.error("Transition to shutdown state: %s", msg)
self.in_shutdown_state = True
self._set_state("%s%s" % (msg, message_shutdown))
self._set_state(msg)
for cb in self.event_handlers.get("klippy:shutdown", []):
try:
cb()
@ -254,9 +213,10 @@ class Printer:
logging.exception("Exception during shutdown handler")
logging.info("Reactor garbage collection: %s",
self.reactor.get_gc_stats())
def invoke_async_shutdown(self, msg):
self.send_event("klippy:notify_mcu_shutdown", msg, details)
def invoke_async_shutdown(self, msg, details):
self.reactor.register_async_callback(
(lambda e: self.invoke_shutdown(msg)))
(lambda e: self.invoke_shutdown(msg, details)))
def register_event_handler(self, event, callback):
self.event_handlers.setdefault(event, []).append(callback)
def send_event(self, event, *params):

View file

@ -1,6 +1,6 @@
# Interface to Klipper micro-controller code
#
# Copyright (C) 2016-2023 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2016-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import sys, os, zlib, logging, math
@ -87,7 +87,7 @@ class CommandWrapper:
if cmd_queue is None:
cmd_queue = serial.get_default_command_queue()
self._cmd_queue = cmd_queue
self._msgtag = msgparser.lookup_msgtag(msgformat) & 0xffffffff
self._msgtag = msgparser.lookup_msgid(msgformat) & 0xffffffff
def send(self, data=(), minclock=0, reqclock=0):
cmd = self._cmd.encode(data)
self._serial.raw_send(cmd, minclock, reqclock, self._cmd_queue)
@ -104,9 +104,9 @@ class CommandWrapper:
class MCU_trsync:
REASON_ENDSTOP_HIT = 1
REASON_COMMS_TIMEOUT = 2
REASON_HOST_REQUEST = 3
REASON_PAST_END_TIME = 4
REASON_HOST_REQUEST = 2
REASON_PAST_END_TIME = 3
REASON_COMMS_TIMEOUT = 4
def __init__(self, mcu, trdispatch):
self._mcu = mcu
self._trdispatch = trdispatch
@ -180,7 +180,7 @@ class MCU_trsync:
if tc is not None:
self._trigger_completion = None
reason = params['trigger_reason']
is_failure = (reason == self.REASON_COMMS_TIMEOUT)
is_failure = (reason >= self.REASON_COMMS_TIMEOUT)
self._reactor.async_complete(tc, is_failure)
elif self._home_end_clock is not None:
clock = self._mcu.clock32_to_clock64(params['clock'])
@ -279,8 +279,9 @@ class TriggerDispatch:
ffi_main, ffi_lib = chelper.get_ffi()
ffi_lib.trdispatch_stop(self._trdispatch)
res = [trsync.stop() for trsync in self._trsyncs]
if any([r == MCU_trsync.REASON_COMMS_TIMEOUT for r in res]):
return MCU_trsync.REASON_COMMS_TIMEOUT
err_res = [r for r in res if r >= MCU_trsync.REASON_COMMS_TIMEOUT]
if err_res:
return err_res[0]
return res[0]
class MCU_endstop:
@ -334,8 +335,9 @@ class MCU_endstop:
self._dispatch.wait_end(home_end_time)
self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0])
res = self._dispatch.stop()
if res == MCU_trsync.REASON_COMMS_TIMEOUT:
return -1.
if res >= MCU_trsync.REASON_COMMS_TIMEOUT:
cmderr = self._mcu.get_printer().command_error
raise cmderr("Communication timeout during homing")
if res != MCU_trsync.REASON_ENDSTOP_HIT:
return 0.
if self._mcu.is_fileoutput():
@ -494,8 +496,8 @@ class MCU_adc:
self._inv_max_adc = 0.
def get_mcu(self):
return self._mcu
def setup_minmax(self, sample_time, sample_count,
minval=0., maxval=1., range_check_count=0):
def setup_adc_sample(self, sample_time, sample_count,
minval=0., maxval=1., range_check_count=0):
self._sample_time = sample_time
self._sample_count = sample_count
self._min_sample = minval
@ -572,9 +574,8 @@ class MCU:
restart_methods = [None, 'arduino', 'cheetah', 'command', 'rpi_usb']
self._restart_method = 'command'
if self._baud:
rmethods = {m: m for m in restart_methods}
self._restart_method = config.getchoice('restart_method',
rmethods, None)
restart_methods, None)
self._reset_cmd = self._config_reset_cmd = None
self._is_mcu_bridge = False
self._emergency_stop_cmd = None
@ -604,6 +605,7 @@ class MCU:
self._mcu_tick_stddev = 0.
self._mcu_tick_awake = 0.
# Register handlers
printer.load_object(config, "error_mcu")
printer.register_event_handler("klippy:firmware_restart",
self._firmware_restart)
printer.register_event_handler("klippy:mcu_identify",
@ -630,13 +632,13 @@ class MCU:
if clock is not None:
self._shutdown_clock = self.clock32_to_clock64(clock)
self._shutdown_msg = msg = params['static_string_id']
logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, params['#name'],
event_type = params['#name']
self._printer.invoke_async_shutdown(
"MCU shutdown", {"reason": msg, "mcu": self._name,
"event_type": event_type})
logging.info("MCU '%s' %s: %s\n%s\n%s", self._name, event_type,
self._shutdown_msg, self._clocksync.dump_debug(),
self._serial.dump_debug())
prefix = "MCU '%s' shutdown: " % (self._name,)
if params['#name'] == 'is_shutdown':
prefix = "Previous MCU '%s' shutdown: " % (self._name,)
self._printer.invoke_async_shutdown(prefix + msg + error_help(msg))
def _handle_starting(self, params):
if not self._is_shutdown:
self._printer.invoke_async_shutdown("MCU '%s' spontaneous restart"
@ -1007,34 +1009,6 @@ class MCU:
self._get_status_info['last_stats'] = last_stats
return False, '%s: %s' % (self._name, stats)
Common_MCU_errors = {
("Timer too close",): """
This often indicates the host computer is overloaded. Check
for other processes consuming excessive CPU time, high swap
usage, disk errors, overheating, unstable voltage, or
similar system problems on the host computer.""",
("Missed scheduling of next ",): """
This is generally indicative of an intermittent
communication failure between micro-controller and host.""",
("ADC out of range",): """
This generally occurs when a heater temperature exceeds
its configured min_temp or max_temp.""",
("Rescheduled timer in the past", "Stepper too far in past"): """
This generally occurs when the micro-controller has been
requested to step at a rate higher than it is capable of
obtaining.""",
("Command request",): """
This generally occurs in response to an M112 G-Code command
or in response to an internal error in the host software.""",
}
def error_help(msg):
for prefixes, help_msg in Common_MCU_errors.items():
for prefix in prefixes:
if msg.startswith(prefix):
return help_msg
return ""
def add_printer_objects(config):
printer = config.get_printer()
reactor = printer.get_reactor()

View file

@ -1,6 +1,6 @@
# Protocol definitions for firmware communication
#
# Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2016-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import json, zlib, logging
@ -160,8 +160,8 @@ def convert_msg_format(msgformat):
return msgformat
class MessageFormat:
def __init__(self, msgid, msgformat, enumerations={}):
self.msgid = msgid
def __init__(self, msgid_bytes, msgformat, enumerations={}):
self.msgid_bytes = msgid_bytes
self.msgformat = msgformat
self.debugformat = convert_msg_format(msgformat)
self.name = msgformat.split()[0]
@ -169,19 +169,17 @@ class MessageFormat:
self.param_types = [t for name, t in self.param_names]
self.name_to_type = dict(self.param_names)
def encode(self, params):
out = []
out.append(self.msgid)
out = list(self.msgid_bytes)
for i, t in enumerate(self.param_types):
t.encode(out, params[i])
return out
def encode_by_name(self, **params):
out = []
out.append(self.msgid)
out = list(self.msgid_bytes)
for name, t in self.param_names:
t.encode(out, params[name])
return out
def parse(self, s, pos):
pos += 1
pos += len(self.msgid_bytes)
out = {}
for name, t in self.param_names:
v, pos = t.parse(s, pos)
@ -198,13 +196,13 @@ class MessageFormat:
class OutputFormat:
name = '#output'
def __init__(self, msgid, msgformat):
self.msgid = msgid
def __init__(self, msgid_bytes, msgformat):
self.msgid_bytes = msgid_bytes
self.msgformat = msgformat
self.debugformat = convert_msg_format(msgformat)
self.param_types = lookup_output_params(msgformat)
def parse(self, s, pos):
pos += 1
pos += len(self.msgid_bytes)
out = []
for t in self.param_types:
v, pos = t.parse(s, pos)
@ -219,7 +217,7 @@ class OutputFormat:
class UnknownFormat:
name = '#unknown'
def parse(self, s, pos):
msgid = s[pos]
msgid, param_pos = PT_int32().parse(s, pos)
msg = bytes(bytearray(s))
return {'#msgid': msgid, '#msg': msg}, len(s)-MESSAGE_TRAILER_SIZE
def format_params(self, params):
@ -234,7 +232,8 @@ class MessageParser:
self.messages = []
self.messages_by_id = {}
self.messages_by_name = {}
self.msgtag_by_format = {}
self.msgid_by_format = {}
self.msgid_parser = PT_int32()
self.config = {}
self.version = self.build_versions = ""
self.raw_identify_data = ""
@ -266,7 +265,7 @@ class MessageParser:
out = ["seq: %02x" % (msgseq,)]
pos = MESSAGE_HEADER_SIZE
while 1:
msgid = s[pos]
msgid, param_pos = self.msgid_parser.parse(s, pos)
mid = self.messages_by_id.get(msgid, self.unknown)
params, pos = mid.parse(s, pos)
out.append(mid.format_params(params))
@ -283,14 +282,14 @@ class MessageParser:
return "%s %s" % (name, msg)
return str(params)
def parse(self, s):
msgid = s[MESSAGE_HEADER_SIZE]
msgid, param_pos = self.msgid_parser.parse(s, MESSAGE_HEADER_SIZE)
mid = self.messages_by_id.get(msgid, self.unknown)
params, pos = mid.parse(s, MESSAGE_HEADER_SIZE)
if pos != len(s)-MESSAGE_TRAILER_SIZE:
self._error("Extra data at end of message")
params['#name'] = mid.name
return params
def encode(self, seq, cmd):
def encode_msgblock(self, seq, cmd):
msglen = MESSAGE_MIN + len(cmd)
seq = (seq & MESSAGE_SEQ_MASK) | MESSAGE_DEST
out = [msglen, seq] + cmd
@ -317,11 +316,11 @@ class MessageParser:
self._error("Command format mismatch: %s vs %s",
msgformat, mp.msgformat)
return mp
def lookup_msgtag(self, msgformat):
msgtag = self.msgtag_by_format.get(msgformat)
if msgtag is None:
def lookup_msgid(self, msgformat):
msgid = self.msgid_by_format.get(msgformat)
if msgid is None:
self._error("Unknown command: %s", msgformat)
return msgtag
return msgid
def create_command(self, msg):
parts = msg.strip().split()
if not parts:
@ -372,22 +371,22 @@ class MessageParser:
start_value, count = value
for i in range(count):
enums[enum_root + str(start_enum + i)] = start_value + i
def _init_messages(self, messages, command_tags=[], output_tags=[]):
for msgformat, msgtag in messages.items():
def _init_messages(self, messages, command_ids=[], output_ids=[]):
for msgformat, msgid in messages.items():
msgtype = 'response'
if msgtag in command_tags:
if msgid in command_ids:
msgtype = 'command'
elif msgtag in output_tags:
elif msgid in output_ids:
msgtype = 'output'
self.messages.append((msgtag, msgtype, msgformat))
if msgtag < -32 or msgtag > 95:
self._error("Multi-byte msgtag not supported")
self.msgtag_by_format[msgformat] = msgtag
msgid = msgtag & 0x7f
self.messages.append((msgid, msgtype, msgformat))
self.msgid_by_format[msgformat] = msgid
msgid_bytes = []
self.msgid_parser.encode(msgid_bytes, msgid)
if msgtype == 'output':
self.messages_by_id[msgid] = OutputFormat(msgid, msgformat)
self.messages_by_id[msgid] = OutputFormat(msgid_bytes,
msgformat)
else:
msg = MessageFormat(msgid, msgformat, self.enumerations)
msg = MessageFormat(msgid_bytes, msgformat, self.enumerations)
self.messages_by_id[msgid] = msg
self.messages_by_name[msg.name] = msg
def process_identify(self, data, decompress=True):

View file

@ -136,7 +136,7 @@ class SerialReader:
can_filters=filters,
bustype='socketcan')
bus.send(set_id_msg)
except (can.CanError, os.error) as e:
except (can.CanError, os.error, IOError) as e:
logging.warning("%sUnable to open CAN port: %s",
self.warn_prefix, e)
self.reactor.pause(self.reactor.monotonic() + 5.)

View file

@ -265,6 +265,7 @@ def parse_gear_ratio(config, note_valid):
# Obtain "step distance" information from a config section
def parse_step_distance(config, units_in_radians=None, note_valid=False):
# Check rotation_distance and gear_ratio
if units_in_radians is None:
# Caller doesn't know if units are in radians - infer it
rd = config.get('rotation_distance', None, note_valid=False)
@ -276,7 +277,7 @@ def parse_step_distance(config, units_in_radians=None, note_valid=False):
else:
rotation_dist = config.getfloat('rotation_distance', above=0.,
note_valid=note_valid)
# Newer config format with rotation_distance
# Check microsteps and full_steps_per_rotation
microsteps = config.getint('microsteps', minval=1, note_valid=note_valid)
full_steps = config.getint('full_steps_per_rotation', 200, minval=1,
note_valid=note_valid)

View file

@ -251,8 +251,9 @@ class HandleCommandGeneration:
def __init__(self):
self.commands = {}
self.encoders = []
self.msg_to_id = dict(msgproto.DefaultMessages)
self.messages_by_name = { m.split()[0]: m for m in self.msg_to_id }
self.msg_to_encid = dict(msgproto.DefaultMessages)
self.encid_to_msgid = {}
self.messages_by_name = { m.split()[0]: m for m in self.msg_to_encid }
self.all_param_types = {}
self.ctr_dispatch = {
'DECL_COMMAND_FLAGS': self.decl_command,
@ -280,37 +281,47 @@ class HandleCommandGeneration:
def decl_output(self, req):
msg = req.split(None, 1)[1]
self.encoders.append((None, msg))
def convert_encoded_msgid(self, encoded_msgid):
if encoded_msgid >= 0x80:
data = [(encoded_msgid >> 7) | 0x80, encoded_msgid & 0x7f]
else:
data = [encoded_msgid]
return msgproto.PT_int32().parse(data, 0)[0]
def create_message_ids(self):
# Create unique ids for each message type
msgid = max(self.msg_to_id.values())
encoded_msgid = max(self.msg_to_encid.values())
mlist = list(self.commands.keys()) + [m for n, m in self.encoders]
for msgname in mlist:
msg = self.messages_by_name.get(msgname, msgname)
if msg not in self.msg_to_id:
msgid += 1
self.msg_to_id[msg] = msgid
if msgid >= 128:
# The mcu currently assumes all message ids encode to one byte
if msg not in self.msg_to_encid:
encoded_msgid += 1
self.msg_to_encid[msg] = encoded_msgid
if encoded_msgid >= 1<<14:
# The mcu currently assumes all message ids encode to 1 or 2 bytes
error("Too many message ids")
self.encid_to_msgid = {
encoded_msgid: self.convert_encoded_msgid(encoded_msgid)
for encoded_msgid in self.msg_to_encid.values()
}
def update_data_dictionary(self, data):
# Handle message ids over 96 (they are decoded as negative numbers)
msg_to_tag = {msg: msgid if msgid < 96 else msgid - 128
for msg, msgid in self.msg_to_id.items()}
command_tags = [msg_to_tag[msg]
# Convert ids to standard form (use both positive and negative numbers)
msg_to_msgid = {msg: self.encid_to_msgid[encoded_msgid]
for msg, encoded_msgid in self.msg_to_encid.items()}
command_ids = [msg_to_msgid[msg]
for msgname, msg in self.messages_by_name.items()
if msgname in self.commands]
response_ids = [msg_to_msgid[msg]
for msgname, msg in self.messages_by_name.items()
if msgname in self.commands]
response_tags = [msg_to_tag[msg]
for msgname, msg in self.messages_by_name.items()
if msgname not in self.commands]
data['commands'] = { msg: msgtag for msg, msgtag in msg_to_tag.items()
if msgtag in command_tags }
data['responses'] = { msg: msgtag for msg, msgtag in msg_to_tag.items()
if msgtag in response_tags }
output = {msg: msgtag for msg, msgtag in msg_to_tag.items()
if msgtag not in command_tags and msgtag not in response_tags}
if msgname not in self.commands]
data['commands'] = { msg: msgid for msg, msgid in msg_to_msgid.items()
if msgid in command_ids }
data['responses'] = { msg: msgid for msg, msgid in msg_to_msgid.items()
if msgid in response_ids }
output = {msg: msgid for msg, msgid in msg_to_msgid.items()
if msgid not in command_ids and msgid not in response_ids}
if output:
data['output'] = output
def build_parser(self, msgid, msgformat, msgtype):
def build_parser(self, encoded_msgid, msgformat, msgtype):
if msgtype == "output":
param_types = msgproto.lookup_output_params(msgformat)
comment = "Output: " + msgformat
@ -327,17 +338,21 @@ class HandleCommandGeneration:
params = 'command_parameters%d' % (paramid,)
out = """
// %s
.msg_id=%d,
.encoded_msgid=%d, // msgid=%d
.num_params=%d,
.param_types = %s,
""" % (comment, msgid, len(types), params)
""" % (comment, encoded_msgid, self.encid_to_msgid[encoded_msgid],
len(types), params)
if msgtype == 'response':
num_args = (len(types) + types.count('PT_progmem_buffer')
+ types.count('PT_buffer'))
out += " .num_args=%d," % (num_args,)
else:
msgid_size = 1
if encoded_msgid >= 0x80:
msgid_size = 2
max_size = min(msgproto.MESSAGE_MAX,
(msgproto.MESSAGE_MIN + 1
(msgproto.MESSAGE_MIN + msgid_size
+ sum([t.max_length for t in param_types])))
out += " .max_size=%d," % (max_size,)
return out
@ -347,22 +362,23 @@ class HandleCommandGeneration:
encoder_code = []
did_output = {}
for msgname, msg in self.encoders:
msgid = self.msg_to_id[msg]
if msgid in did_output:
encoded_msgid = self.msg_to_encid[msg]
if encoded_msgid in did_output:
continue
did_output[msgid] = True
did_output[encoded_msgid] = True
code = (' if (__builtin_strcmp(str, "%s") == 0)\n'
' return &command_encoder_%s;\n' % (msg, msgid))
' return &command_encoder_%s;\n'
% (msg, encoded_msgid))
if msgname is None:
parsercode = self.build_parser(msgid, msg, 'output')
parsercode = self.build_parser(encoded_msgid, msg, 'output')
output_code.append(code)
else:
parsercode = self.build_parser(msgid, msg, 'command')
parsercode = self.build_parser(encoded_msgid, msg, 'command')
encoder_code.append(code)
encoder_defs.append(
"const struct command_encoder command_encoder_%s PROGMEM = {"
" %s\n};\n" % (
msgid, parsercode))
encoded_msgid, parsercode))
fmt = """
%s
@ -384,21 +400,21 @@ ctr_lookup_output(const char *str)
"".join(encoder_code).strip(),
"".join(output_code).strip())
def generate_commands_code(self):
cmd_by_id = {
self.msg_to_id[self.messages_by_name.get(msgname, msgname)]: cmd
cmd_by_encid = {
self.msg_to_encid[self.messages_by_name.get(msgname, msgname)]: cmd
for msgname, cmd in self.commands.items()
}
max_cmd_msgid = max(cmd_by_id.keys())
max_cmd_encid = max(cmd_by_encid.keys())
index = []
externs = {}
for msgid in range(max_cmd_msgid+1):
if msgid not in cmd_by_id:
for encoded_msgid in range(max_cmd_encid+1):
if encoded_msgid not in cmd_by_encid:
index.append(" {\n},")
continue
funcname, flags, msgname = cmd_by_id[msgid]
funcname, flags, msgname = cmd_by_encid[encoded_msgid]
msg = self.messages_by_name[msgname]
externs[funcname] = 1
parsercode = self.build_parser(msgid, msg, 'response')
parsercode = self.build_parser(encoded_msgid, msg, 'response')
index.append(" {%s\n .flags=%s,\n .func=%s\n}," % (
parsercode, flags, funcname))
index = "".join(index).strip()
@ -411,7 +427,7 @@ const struct command_parser command_index[] PROGMEM = {
%s
};
const uint8_t command_index_size PROGMEM = ARRAY_SIZE(command_index);
const uint16_t command_index_size PROGMEM = ARRAY_SIZE(command_index);
"""
return fmt % (externs, index)
def generate_param_code(self):

View file

@ -35,7 +35,7 @@ if [ ! -f ${PRU_FILE} ]; then
cd ${BUILD_DIR}
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
git clone https://github.com/dinuxbg/gnupru -b 2023.01 --depth 1
git clone https://github.com/dinuxbg/gnupru -b 2024.05 --depth 1
cd gnupru
export PREFIX=${PRU_DIR}
./download-and-prepare.sh 2>&1 | pv -nli 30 > ${BUILD_DIR}/gnupru-build.log

533
scripts/graph_mesh.py Executable file
View file

@ -0,0 +1,533 @@
#!/usr/bin/env python3
# Bed Mesh data plotting and analysis
#
# Copyright (C) 2024 Eric Callahan <arksine.code@gmail.com>
#
# 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="<plot type>", type=str, choices=PLOT_TYPES.keys(),
help="Type of data to graph"
)
plot_parser.add_argument(
"input", metavar="<input>",
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="<input>",
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="<input>",
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()

View file

@ -293,7 +293,7 @@ class HandleStepPhase:
self._pull_block(req_time)
continue
step_pos = step_data[data_pos][1]
return (step_pos - self.mcu_phase_offset) % self.phases
return (step_pos + self.mcu_phase_offset) % self.phases
def _pull_block(self, req_time):
step_data = self.step_data
del step_data[:-1]

View file

@ -31,6 +31,11 @@ BOARD_DEFS = {
'spi_bus': "spi1",
"cs_pin": "PA4"
},
'btt-skr-mini-v3-b0': {
'mcu': "stm32g0b0xx",
'spi_bus': "spi1",
"cs_pin": "PA4"
},
'flyboard-mini': {
'mcu': "stm32f103xe",
'spi_bus': "spi2",
@ -41,6 +46,7 @@ BOARD_DEFS = {
'mcu': "stm32f103xe",
'spi_bus': "spi2",
"cs_pin": "PA15",
"conversion_script": "scripts/update_mks_robin.py",
"firmware_path": "Robin_e3.bin",
"current_firmware_path": "Robin_e3.cur"
},
@ -128,6 +134,16 @@ BOARD_DEFS = {
'mcu': "stm32g0b1xx",
'spi_bus': "spi1",
"cs_pin": "PB8"
},
'chitu-v6': {
'mcu': "stm32f103xe",
'spi_bus': "swspi",
'spi_pins': "PC8,PD2,PC12",
"cs_pin": "PC11",
#'sdio_bus': 'sdio',
"conversion_script": "scripts/update_chitu.py",
"firmware_path": "update.cbd",
'skip_verify': True
}
}
@ -152,6 +168,7 @@ BOARD_ALIASES = {
'btt-skr-mini-e3-v1.2': BOARD_DEFS['btt-skr-mini'],
'btt-skr-mini-e3-v2': BOARD_DEFS['btt-skr-mini'],
'btt-skr-mini-e3-v3': BOARD_DEFS['btt-skr-mini-v3'],
'btt-skr-mini-e3-v3-b0': BOARD_DEFS['btt-skr-mini-v3-b0'],
'btt-skr-mini-mz': BOARD_DEFS['btt-skr-mini'],
'btt-skr-e3-dip': BOARD_DEFS['btt-skr-mini'],
'btt002-v1': BOARD_DEFS['btt-skr-mini'],
@ -176,7 +193,8 @@ BOARD_ALIASES = {
'fysetc-s6-v1.2': BOARD_DEFS['fysetc-spider'],
'fysetc-s6-v2': BOARD_DEFS['fysetc-spider'],
'robin_v3': BOARD_DEFS['monster8'],
'btt-skrat-v1.0': BOARD_DEFS['btt-skrat']
'btt-skrat-v1.0': BOARD_DEFS['btt-skrat'],
'chitu-v6': BOARD_DEFS['chitu-v6']
}
def list_boards():

View file

@ -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
###########################################################

View file

@ -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

View file

@ -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

View file

@ -20,7 +20,7 @@ CFLAGS-$(CONFIG_MACH_SAM4E) += -Ilib/sam4e/include
CFLAGS-$(CONFIG_MACH_SAME70) += -Ilib/same70b/include
CFLAGS += $(CFLAGS-y) -D__$(MCU)__ -mthumb -Ilib/cmsis-core -Ilib/fast-hash
CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs
CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano
CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld
$(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld

View file

@ -14,7 +14,7 @@ CFLAGS-$(CONFIG_MACH_SAME51) += -Ilib/same51/include
CFLAGS-$(CONFIG_MACH_SAMX5) += -mcpu=cortex-m4 -Ilib/same54/include
CFLAGS += $(CFLAGS-y) -D__$(MCU)__ -mthumb -Ilib/cmsis-core -Ilib/fast-hash
CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs
CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano
CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld
$(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld

View file

@ -1,6 +1,6 @@
// Code for parsing incoming commands and encoding outgoing messages
//
// Copyright (C) 2016,2017 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2016-2024 Kevin O'Connor <kevin@koconnor.net>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
@ -69,6 +69,28 @@ parse_int(uint8_t **pp)
return v;
}
// Write an encoded msgid (optimized 2-byte VLQ encoder)
static uint8_t *
encode_msgid(uint8_t *p, uint_fast16_t encoded_msgid)
{
if (encoded_msgid >= 0x80)
*p++ = (encoded_msgid >> 7) | 0x80;
*p++ = encoded_msgid & 0x7f;
return p;
}
// Parse an encoded msgid (optimized 2-byte parser, return as positive number)
uint_fast16_t
command_parse_msgid(uint8_t **pp)
{
uint8_t *p = *pp;
uint_fast16_t encoded_msgid = *p++;
if (encoded_msgid & 0x80)
encoded_msgid = ((encoded_msgid & 0x7f) << 7) | (*p++);
*pp = p;
return encoded_msgid;
}
// Parse an incoming command into 'args'
uint8_t *
command_parsef(uint8_t *p, uint8_t *maxend
@ -119,7 +141,7 @@ command_encodef(uint8_t *buf, const struct command_encoder *ce, va_list args)
uint8_t *maxend = &p[max_size - MESSAGE_MIN];
uint_fast8_t num_params = READP(ce->num_params);
const uint8_t *param_types = READP(ce->param_types);
*p++ = READP(ce->msg_id);
p = encode_msgid(p, READP(ce->encoded_msgid));
while (num_params--) {
if (p > maxend)
goto error;
@ -227,7 +249,7 @@ DECL_SHUTDOWN(sendf_shutdown);
// Find the command handler associated with a command
static const struct command_parser *
command_lookup_parser(uint_fast8_t cmdid)
command_lookup_parser(uint_fast16_t cmdid)
{
if (!cmdid || cmdid >= READP(command_index_size))
shutdown("Invalid command");
@ -309,7 +331,7 @@ command_dispatch(uint8_t *buf, uint_fast8_t msglen)
uint8_t *p = &buf[MESSAGE_HEADER_SIZE];
uint8_t *msgend = &buf[msglen-MESSAGE_TRAILER_SIZE];
while (p < msgend) {
uint_fast8_t cmdid = *p++;
uint_fast16_t cmdid = command_parse_msgid(&p);
const struct command_parser *cp = command_lookup_parser(cmdid);
uint32_t args[READP(cp->num_args)];
p = command_parsef(p, msgend, cp, args);

View file

@ -57,11 +57,13 @@
#define MESSAGE_SYNC 0x7E
struct command_encoder {
uint8_t msg_id, max_size, num_params;
uint16_t encoded_msgid;
uint8_t max_size, num_params;
const uint8_t *param_types;
};
struct command_parser {
uint8_t msg_id, num_args, flags, num_params;
uint16_t encoded_msgid;
uint8_t num_args, flags, num_params;
const uint8_t *param_types;
void (*func)(uint32_t *args);
};
@ -72,6 +74,7 @@ enum {
// command.c
void *command_decode_ptr(uint32_t v);
uint_fast16_t command_parse_msgid(uint8_t **pp);
uint8_t *command_parsef(uint8_t *p, uint8_t *maxend
, const struct command_parser *cp, uint32_t *args);
uint_fast8_t command_encode_and_frame(
@ -86,7 +89,7 @@ int_fast8_t command_find_and_dispatch(uint8_t *buf, uint_fast8_t buf_len
// out/compile_time_request.c (auto generated file)
extern const struct command_parser command_index[];
extern const uint8_t command_index_size;
extern const uint16_t command_index_size;
extern const uint8_t command_identify_data[];
extern const uint32_t command_identify_size;
const struct command_encoder *ctr_lookup_encoder(const char *str);

View file

@ -69,5 +69,8 @@ SECTIONS
// that isn't needed so no need to include them in the binary.
*(.init)
*(.fini)
// Don't include exception tables
*(.ARM.extab)
*(.ARM.exidx)
}
}

View file

@ -7,7 +7,7 @@ dirs-y += src/hc32f460 src/generic lib/hc32f460/driver/src lib/hc32f460/mcu/comm
CFLAGS += -mthumb -mcpu=cortex-m4 -Isrc/hc32f460 -Ilib/hc32f460/driver/inc -Ilib/hc32f460/mcu/common -Ilib/cmsis-core -DHC32F460
CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs
CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano
CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld
$(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld

View file

@ -7,7 +7,7 @@ dirs-y += src/lpc176x src/generic lib/lpc176x/device
CFLAGS += -mthumb -mcpu=cortex-m3 -Ilib/lpc176x/device -Ilib/cmsis-core
CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs
CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano
CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld
$(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld

View file

@ -141,7 +141,7 @@ do_dispatch(uint8_t *buf, uint32_t msglen)
uint8_t *msgend = &buf[msglen-MESSAGE_TRAILER_SIZE];
while (p < msgend) {
// Parse command
uint_fast8_t cmdid = *p++;
uint_fast16_t cmdid = command_parse_msgid(&p);
const struct command_parser *cp = &SHARED_MEM->command_index[cmdid];
if (!cmdid || cmdid >= SHARED_MEM->command_index_size
|| cp->num_args > ARRAY_SIZE(SHARED_MEM->next_command_args)) {

View file

@ -46,7 +46,6 @@ $(OUT)klipper.uf2: $(OUT)klipper.elf $(OUT)lib/rp2040/elf2uf2/elf2uf2
$(Q)$(OUT)lib/rp2040/elf2uf2/elf2uf2 $< $@
rptarget-$(CONFIG_RP2040_HAVE_STAGE2) := $(OUT)klipper.uf2
rplink-$(CONFIG_RP2040_HAVE_STAGE2) := $(OUT)src/rp2040/rp2040_link.ld
stage2-$(CONFIG_RP2040_HAVE_STAGE2) := $(OUT)stage2.o
# rp2040 building when using a bootloader
@ -55,13 +54,13 @@ $(OUT)klipper.bin: $(OUT)klipper.elf
$(Q)$(OBJCOPY) -O binary $< $@
rptarget-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)klipper.bin
rplink-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)src/rp2040/rp2040_link.ld
# Set klipper.elf linker rules
target-y += $(rptarget-y)
CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs -T $(rplink-y)
CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano
CFLAGS_klipper.elf += -T $(OUT)src/rp2040/rp2040_link.ld
OBJS_klipper.elf += $(stage2-y)
$(OUT)klipper.elf: $(stage2-y) $(rplink-y)
$(OUT)klipper.elf: $(stage2-y) $(OUT)src/rp2040/rp2040_link.ld
# Flash rules
lib/rp2040_flash/rp2040_flash:

View file

@ -21,6 +21,16 @@ MEMORY
ram (rwx) : ORIGIN = CONFIG_RAM_START , LENGTH = CONFIG_RAM_SIZE
}
// Force flags for each output section to avoid RWX linker warning
PHDRS
{
text_segment PT_LOAD FLAGS(5); // RX flags
ram_vectortable_segment PT_LOAD FLAGS(6); // RW flags
data_segment PT_LOAD FLAGS(6); // RW flags
bss_segment PT_LOAD FLAGS(6); // RW flags
stack_segment PT_LOAD FLAGS(6); // RW flags
}
SECTIONS
{
.text : {
@ -32,7 +42,7 @@ SECTIONS
KEEP(*(.vector_table))
_text_vectortable_end = .;
*(.text.armcm_boot*)
} > rom
} > rom :text_segment
. = ALIGN(4);
_data_flash = .;
@ -41,7 +51,7 @@ SECTIONS
_ram_vectortable_start = .;
. = . + ( _text_vectortable_end - _text_vectortable_start ) ;
_ram_vectortable_end = .;
} > ram
} > ram :ram_vectortable_segment
.data : AT (_data_flash)
{
@ -53,7 +63,7 @@ SECTIONS
*(.data .data.*);
. = ALIGN(4);
_data_end = .;
} > ram
} > ram :data_segment
.bss (NOLOAD) :
{
@ -63,19 +73,22 @@ SECTIONS
*(COMMON)
. = ALIGN(4);
_bss_end = .;
} > ram
} > ram :bss_segment
_stack_start = CONFIG_RAM_START + CONFIG_RAM_SIZE - CONFIG_STACK_SIZE ;
.stack _stack_start (NOLOAD) :
{
. = . + CONFIG_STACK_SIZE;
_stack_end = .;
} > ram
} > ram :stack_segment
/DISCARD/ : {
// The .init/.fini sections are used by __libc_init_array(), but
// that isn't needed so no need to include them in the binary.
*(.init)
*(.fini)
// Don't include exception tables
*(.ARM.extab)
*(.ARM.exidx)
}
}

163
src/sensor_ads1220.c Normal file
View file

@ -0,0 +1,163 @@
// Support for ADS1220 ADC Chip
//
// Copyright (C) 2024 Gareth Farrington <gareth@waves.ky>
//
// 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 <stdint.h>
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);

View file

@ -4,7 +4,7 @@
struct sensor_bulk {
uint16_t sequence, possible_overflows;
uint8_t data_count;
uint8_t data[52];
uint8_t data[51];
};
void sensor_bulk_reset(struct sensor_bulk *sb);

253
src/sensor_hx71x.c Normal file
View file

@ -0,0 +1,253 @@
// Support for bit-banging commands to HX711 and HX717 ADC chips
//
// Copyright (C) 2024 Gareth Farrington <gareth@waves.ky>
//
// 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 <stdbool.h>
#include <stdint.h>
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);

View file

@ -17,7 +17,7 @@
#include "trsync.h" // trsync_do_trigger
enum {
LDC_PENDING = 1<<0,
LDC_PENDING = 1<<0, LDC_HAVE_INTB = 1<<1,
LH_AWAIT_HOMING = 1<<1, LH_CAN_TRIGGER = 1<<2
};
@ -27,16 +27,24 @@ struct ldc1612 {
struct i2cdev_s *i2c;
uint8_t flags;
struct sensor_bulk sb;
struct gpio_in intb_pin;
// homing
struct trsync *ts;
uint8_t homing_flags;
uint8_t trigger_reason;
uint8_t trigger_reason, error_reason;
uint32_t trigger_threshold;
uint32_t homing_clock;
};
static struct task_wake ldc1612_wake;
// Check if the intb line is "asserted"
static int
check_intb_asserted(struct ldc1612 *ld)
{
return !gpio_in_read(ld->intb_pin);
}
// Query ldc1612 data
static uint_fast8_t
ldc1612_event(struct timer *timer)
@ -44,8 +52,10 @@ ldc1612_event(struct timer *timer)
struct ldc1612 *ld = container_of(timer, struct ldc1612, timer);
if (ld->flags & LDC_PENDING)
ld->sb.possible_overflows++;
ld->flags |= LDC_PENDING;
sched_wake_task(&ldc1612_wake);
if (!(ld->flags & LDC_HAVE_INTB) || check_intb_asserted(ld)) {
ld->flags |= LDC_PENDING;
sched_wake_task(&ldc1612_wake);
}
ld->timer.waketime += ld->rest_ticks;
return SF_RESCHEDULE;
}
@ -60,6 +70,17 @@ command_config_ldc1612(uint32_t *args)
}
DECL_COMMAND(command_config_ldc1612, "config_ldc1612 oid=%c i2c_oid=%c");
void
command_config_ldc1612_with_intb(uint32_t *args)
{
command_config_ldc1612(args);
struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612);
ld->intb_pin = gpio_in_setup(args[2], 1);
ld->flags = LDC_HAVE_INTB;
}
DECL_COMMAND(command_config_ldc1612_with_intb,
"config_ldc1612_with_intb oid=%c i2c_oid=%c intb_pin=%c");
void
command_ldc1612_setup_home(uint32_t *args)
{
@ -74,11 +95,12 @@ command_ldc1612_setup_home(uint32_t *args)
ld->homing_clock = args[1];
ld->ts = trsync_oid_lookup(args[3]);
ld->trigger_reason = args[4];
ld->error_reason = args[5];
ld->homing_flags = LH_AWAIT_HOMING | LH_CAN_TRIGGER;
}
DECL_COMMAND(command_ldc1612_setup_home,
"ldc1612_setup_home oid=%c clock=%u threshold=%u"
" trsync_oid=%c trigger_reason=%c");
" trsync_oid=%c trigger_reason=%c error_reason=%c");
void
command_query_ldc1612_home_state(uint32_t *args)
@ -90,6 +112,32 @@ command_query_ldc1612_home_state(uint32_t *args)
DECL_COMMAND(command_query_ldc1612_home_state,
"query_ldc1612_home_state oid=%c");
// Check if a sample should trigger a homing event
static void
check_home(struct ldc1612 *ld, uint32_t data)
{
uint8_t homing_flags = ld->homing_flags;
if (!(homing_flags & LH_CAN_TRIGGER))
return;
if (data > 0x0fffffff) {
// Sensor reports an issue - cancel homing
ld->homing_flags = 0;
trsync_do_trigger(ld->ts, ld->error_reason);
return;
}
uint32_t time = timer_read_time();
if ((homing_flags & LH_AWAIT_HOMING)
&& timer_is_before(time, ld->homing_clock))
return;
homing_flags &= ~LH_AWAIT_HOMING;
if (data > ld->trigger_threshold) {
homing_flags = 0;
ld->homing_clock = time;
trsync_do_trigger(ld->ts, ld->trigger_reason);
}
ld->homing_flags = homing_flags;
}
// Chip registers
#define REG_DATA0_MSB 0x00
#define REG_DATA0_LSB 0x01
@ -117,14 +165,12 @@ read_reg_status(struct ldc1612 *ld)
static void
ldc1612_query(struct ldc1612 *ld, uint8_t oid)
{
// Clear pending flag
// Check if data available (and clear INTB line)
uint16_t status = read_reg_status(ld);
irq_disable();
ld->flags &= ~LDC_PENDING;
irq_enable();
// Check if data available
uint16_t status = read_reg_status(ld);
if (status != 0x48)
if (!(status & 0x08))
return;
// Read coil0 frequency
@ -134,21 +180,11 @@ ldc1612_query(struct ldc1612 *ld, uint8_t oid)
ld->sb.data_count += BYTES_PER_SAMPLE;
// Check for endstop trigger
uint8_t homing_flags = ld->homing_flags;
if (homing_flags & LH_CAN_TRIGGER) {
uint32_t time = timer_read_time();
if (!(homing_flags & LH_AWAIT_HOMING)
|| !timer_is_before(time, ld->homing_clock)) {
homing_flags &= ~LH_AWAIT_HOMING;
uint32_t data = (d[0] << 24L) | (d[1] << 16L) | (d[2] << 8) | d[3];
if (data > ld->trigger_threshold) {
homing_flags = 0;
ld->homing_clock = time;
trsync_do_trigger(ld->ts, ld->trigger_reason);
}
ld->homing_flags = homing_flags;
}
}
uint32_t data = ((uint32_t)d[0] << 24)
| ((uint32_t)d[1] << 16)
| ((uint32_t)d[2] << 8)
| ((uint32_t)d[3]);
check_home(ld, data);
// Flush local buffer if needed
if (ld->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(ld->sb.data))
@ -161,7 +197,7 @@ command_query_ldc1612(uint32_t *args)
struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612);
sched_del_timer(&ld->timer);
ld->flags = 0;
ld->flags &= ~LDC_PENDING;
if (!args[1])
// End measurements
return;
@ -177,18 +213,29 @@ command_query_ldc1612(uint32_t *args)
DECL_COMMAND(command_query_ldc1612, "query_ldc1612 oid=%c rest_ticks=%u");
void
command_query_ldc1612_status(uint32_t *args)
command_query_status_ldc1612(uint32_t *args)
{
struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612);
if (ld->flags & LDC_HAVE_INTB) {
// Check if a sample is pending in the chip via the intb line
irq_disable();
uint32_t time = timer_read_time();
int p = check_intb_asserted(ld);
irq_enable();
sensor_bulk_status(&ld->sb, args[0], time, 0, p ? BYTES_PER_SAMPLE : 0);
return;
}
// Query sensor to see if a sample is pending
uint32_t time1 = timer_read_time();
uint16_t status = read_reg_status(ld);
uint32_t time2 = timer_read_time();
uint32_t fifo = status == 0x48 ? BYTES_PER_SAMPLE : 0;
uint32_t fifo = status & 0x08 ? BYTES_PER_SAMPLE : 0;
sensor_bulk_status(&ld->sb, args[0], time1, time2-time1, fifo);
}
DECL_COMMAND(command_query_ldc1612_status, "query_ldc1612_status oid=%c");
DECL_COMMAND(command_query_status_ldc1612, "query_status_ldc1612 oid=%c");
void
ldc1612_task(void)

View file

@ -88,6 +88,9 @@ choice
config MACH_STM32G431
bool "STM32G431"
select MACH_STM32G4
config MACH_STM32G474
bool "STM32G474"
select MACH_STM32G4
config MACH_STM32H723
bool "STM32H723"
select MACH_STM32H7
@ -181,6 +184,7 @@ config MCU
default "stm32g0b0xx" if MACH_STM32G0B0
default "stm32g0b1xx" if MACH_STM32G0B1
default "stm32g431xx" if MACH_STM32G431
default "stm32g474xx" if MACH_STM32G474
default "stm32h723xx" if MACH_STM32H723
default "stm32h743xx" if MACH_STM32H743
default "stm32h750xx" if MACH_STM32H750
@ -199,6 +203,7 @@ config CLOCK_FREQ
default 216000000 if MACH_STM32F765
default 64000000 if MACH_STM32G0
default 150000000 if MACH_STM32G431
default 170000000 if MACH_STM32G474
default 400000000 if MACH_STM32H7 # 400Mhz is max Klipper currently supports
default 80000000 if MACH_STM32L412
default 64000000 if MACH_N32G45x && STM32_CLOCK_REF_INTERNAL
@ -206,13 +211,13 @@ config CLOCK_FREQ
config FLASH_SIZE
hex
default 0x4000 if MACH_STM32F031
default 0x8000 if MACH_STM32F042
default 0x8000 if MACH_STM32F031 || MACH_STM32F042
default 0x20000 if MACH_STM32F070 || MACH_STM32F072
default 0x10000 if MACH_STM32F103 || MACH_STM32L412 # Flash size of stm32f103x8 (64KiB)
default 0x40000 if MACH_STM32F2 || MACH_STM32F401 || MACH_STM32H723
default 0x80000 if MACH_STM32F4x5 || MACH_STM32F446
default 0x20000 if MACH_STM32G0 || MACH_STM32G431
default 0x40000 if MACH_STM32G474
default 0x20000 if MACH_STM32H750
default 0x200000 if MACH_STM32H743 || MACH_STM32F765
default 0x20000 if MACH_N32G45x
@ -233,6 +238,7 @@ config RAM_SIZE
default 0x2800 if MACH_STM32F103x6
default 0x5000 if MACH_STM32F103 && !MACH_STM32F103x6 # Ram size of stm32f103x8
default 0x8000 if MACH_STM32G431
default 0x20000 if MACH_STM32G474
default 0xa000 if MACH_STM32L412
default 0x20000 if MACH_STM32F207
default 0x10000 if MACH_STM32F401

View file

@ -31,7 +31,7 @@ CFLAGS-$(CONFIG_MACH_STM32H7) += -mcpu=cortex-m7 -Ilib/stm32h7/include
CFLAGS-$(CONFIG_MACH_STM32L4) += -mcpu=cortex-m4 -Ilib/stm32l4/include
CFLAGS += $(CFLAGS-y) -D$(MCU_UPPER) -mthumb -Ilib/cmsis-core -Ilib/fast-hash
CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs
CFLAGS_klipper.elf += -nostdlib -lgcc -lc_nano
CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld
$(OUT)klipper.elf: $(OUT)src/generic/armcm_link.ld

View file

@ -60,16 +60,23 @@
|| CONFIG_STM32_CANBUS_PB5_PB6 ||CONFIG_STM32_CANBUS_PB12_PB13)
#define SOC_CAN FDCAN1
#define MSG_RAM (((struct fdcan_ram_layout*)SRAMCAN_BASE)->fdcan1)
#if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4
#define CAN_IT0_IRQn FDCAN1_IT0_IRQn
#endif
#else
#define SOC_CAN FDCAN2
#define MSG_RAM (((struct fdcan_ram_layout*)SRAMCAN_BASE)->fdcan2)
#if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4
#define CAN_IT0_IRQn FDCAN2_IT0_IRQn
#endif
#endif
#if CONFIG_MACH_STM32G0
#define CAN_IT0_IRQn TIM16_FDCAN_IT0_IRQn
#define CAN_FUNCTION GPIO_FUNCTION(3) // Alternative function mapping number
#elif CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4
#define CAN_IT0_IRQn FDCAN1_IT0_IRQn
#endif
#if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G4
#define CAN_FUNCTION GPIO_FUNCTION(9) // Alternative function mapping number
#endif

View file

@ -22,6 +22,8 @@ struct i2c_info {
DECL_CONSTANT_STR("BUS_PINS_i2c1_PF1_PF0", "PF1,PF0");
DECL_ENUMERATION("i2c_bus", "i2c1_PB8_PB9", 2);
DECL_CONSTANT_STR("BUS_PINS_i2c1_PB8_PB9", "PB8,PB9");
DECL_ENUMERATION("i2c_bus", "i2c1_PB8_PB7", 3);
DECL_CONSTANT_STR("BUS_PINS_i2c1_PB8_PB7", "PB8,PB7");
// Deprecated "i2c1a" style mappings
DECL_ENUMERATION("i2c_bus", "i2c1", 0);
DECL_CONSTANT_STR("BUS_PINS_i2c1", "PB6,PB7");
@ -93,6 +95,7 @@ static const struct i2c_info i2c_bus[] = {
{ I2C1, GPIO('B', 6), GPIO('B', 7), GPIO_FUNCTION(1) },
{ I2C1, GPIO('F', 1), GPIO('F', 0), GPIO_FUNCTION(1) },
{ I2C1, GPIO('B', 8), GPIO('B', 9), GPIO_FUNCTION(1) },
{ I2C1, GPIO('B', 8), GPIO('B', 7), GPIO_FUNCTION(1) },
#elif CONFIG_MACH_STM32F7
{ I2C1, GPIO('B', 6), GPIO('B', 7), GPIO_FUNCTION(1) },
#elif CONFIG_MACH_STM32G0

View file

@ -22,6 +22,12 @@ lookup_clock_line(uint32_t periph_base)
if (periph_base < APB2PERIPH_BASE) {
uint32_t pos = (periph_base - APB1PERIPH_BASE) / 0x400;
if (pos < 32) {
#if defined(FDCAN2_BASE)
if (periph_base == FDCAN2_BASE)
return (struct cline){.en = &RCC->APB1ENR1,
.rst = &RCC->APB1RSTR1,
.bit = 1 << 25};
#endif
return (struct cline){.en = &RCC->APB1ENR1,
.rst = &RCC->APB1RSTR1,
.bit = 1 << pos};
@ -98,7 +104,7 @@ enable_clock_stm32g4(void)
RCC->CR |= RCC_CR_PLLON;
// Enable 48Mhz USB clock using clock recovery
if (CONFIG_USBSERIAL) {
if (CONFIG_USB) {
RCC->CRRCR |= RCC_CRRCR_HSI48ON;
while (!(RCC->CRRCR & RCC_CRRCR_HSI48RDY))
;

View file

@ -12,6 +12,8 @@
#include "internal.h" // GPIO
#include "sched.h" // sched_shutdown
#define ADC_INVALID_PIN 0xFF
#define ADC_TEMPERATURE_PIN 0xfe
DECL_ENUMERATION("pin", "ADC_TEMPERATURE", ADC_TEMPERATURE_PIN);
@ -24,8 +26,8 @@ DECL_CONSTANT("ADC_MAX", 4095);
static const uint8_t adc_pins[] = {
#if CONFIG_MACH_STM32H7
// ADC1
0, // PA0_C ADC12_INP0
0, // PA1_C ADC12_INP1
ADC_INVALID_PIN, // PA0_C ADC12_INP0
ADC_INVALID_PIN, // PA1_C ADC12_INP1
GPIO('F', 11), // ADC1_INP2
GPIO('A', 6), // ADC12_INP3
GPIO('C', 4), // ADC12_INP4
@ -45,8 +47,8 @@ static const uint8_t adc_pins[] = {
GPIO('A', 4), // ADC12_INP18
GPIO('A', 5), // ADC12_INP19
// ADC2
0, // PA0_C ADC12_INP0
0, // PA1_C ADC12_INP1
ADC_INVALID_PIN, // PA0_C ADC12_INP0
ADC_INVALID_PIN, // PA1_C ADC12_INP1
GPIO('F', 13), // ADC2_INP2
GPIO('A', 6), // ADC12_INP3
GPIO('C', 4), // ADC12_INP4
@ -61,13 +63,13 @@ static const uint8_t adc_pins[] = {
GPIO('C', 3), // ADC12_INP13
GPIO('A', 2), // ADC12_INP14
GPIO('A', 3), // ADC12_INP15
0, // dac_out1
0, // dac_out2
ADC_INVALID_PIN, // dac_out1
ADC_INVALID_PIN, // dac_out2
GPIO('A', 4), // ADC12_INP18
GPIO('A', 5), // ADC12_INP19
// ADC3
0, // PC2_C ADC3_INP0
0, // PC3_C ADC3_INP1
ADC_INVALID_PIN, // PC2_C ADC3_INP0
ADC_INVALID_PIN, // PC3_C ADC3_INP1
GPIO('F', 9) , // ADC3_INP2
GPIO('F', 7), // ADC3_INP3
GPIO('F', 5), // ADC3_INP4
@ -85,14 +87,14 @@ static const uint8_t adc_pins[] = {
GPIO('H', 5), // ADC3_INP16
#if CONFIG_MACH_STM32H723
ADC_TEMPERATURE_PIN,
0,
ADC_INVALID_PIN,
#else
0, // Vbat/4
ADC_INVALID_PIN, // Vbat/4
ADC_TEMPERATURE_PIN,// VSENSE
#endif
0, // VREFINT
ADC_INVALID_PIN, // VREFINT
#elif CONFIG_MACH_STM32G4
0, // [0] vssa
ADC_INVALID_PIN, // [0] vssa
GPIO('A', 0), // [1]
GPIO('A', 1), // [2]
GPIO('A', 2), // [3]
@ -105,14 +107,14 @@ static const uint8_t adc_pins[] = {
GPIO('F', 0), // [10]
GPIO('B', 12), // [11]
GPIO('B', 1), // [12]
0, // [13] opamp
ADC_INVALID_PIN, // [13] opamp
GPIO('B', 11), // [14]
GPIO('B', 0), // [15]
ADC_TEMPERATURE_PIN, // [16] vtemp
0, // [17] vbat/3
0, // [18] vref
0,
0, // [0] vssa ADC 2
ADC_INVALID_PIN, // [17] vbat/3
ADC_INVALID_PIN, // [18] vref
ADC_INVALID_PIN,
ADC_INVALID_PIN, // [0] vssa ADC 2
GPIO('A', 0), // [1]
GPIO('A', 1), // [2]
GPIO('A', 6), // [3]
@ -128,11 +130,11 @@ static const uint8_t adc_pins[] = {
GPIO('A', 5), // [13]
GPIO('B', 11), // [14]
GPIO('B', 15), // [15]
0, // [16] opamp
ADC_INVALID_PIN, // [16] opamp
GPIO('A', 4), // [17]
0, // [18] opamp
ADC_INVALID_PIN, // [18] opamp
#else // stm32l4
0, // vref
ADC_INVALID_PIN, // vref
GPIO('C', 0), // ADC12_IN1 .. 16
GPIO('C', 1),
GPIO('C', 2),
@ -150,7 +152,7 @@ static const uint8_t adc_pins[] = {
GPIO('B', 0),
GPIO('B', 1),
ADC_TEMPERATURE_PIN, // temp
0, // vbat
ADC_INVALID_PIN, // vbat
#endif
};
@ -189,7 +191,11 @@ gpio_adc_setup(uint32_t pin)
if (chan >= 2 * ADCIN_BANK_SIZE) {
chan -= 2 * ADCIN_BANK_SIZE;
adc = ADC3;
#if CONFIG_MACH_STM32G4
adc_common = ADC345_COMMON;
#else
adc_common = ADC3_COMMON;
#endif
} else
#endif
#ifdef ADC2

View file

@ -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))
;

View file

@ -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

View file

@ -1,5 +1,9 @@
# Base config file for STM32F031 boards
# Base config file for STM32F031x4 (16KB) boards with serial on PA14/PA15
CONFIG_MACH_STM32=y
CONFIG_MACH_STM32F031=y
CONFIG_WANT_GPIO_BITBANGING=n
CONFIG_LOW_LEVEL_OPTIONS=y
CONFIG_STM32_SERIAL_USART2_ALT_PA15_PA14=y
CONFIG_STM32_CLOCK_REF_INTERNAL=y
CONFIG_STM32_FLASH_START_0000=y
CONFIG_WANT_DISPLAYS=n
CONFIG_WANT_GPIO_BITBANGING=n

View file

@ -3,3 +3,6 @@ CONFIG_MACH_STM32=y
CONFIG_MACH_STM32F042=y
CONFIG_WANT_SOFTWARE_I2C=n
CONFIG_WANT_LIS2DW=n
CONFIG_WANT_LDC1612=n
CONFIG_WANT_HX71X=n
CONFIG_WANT_ADS1220=n

View file

@ -0,0 +1,3 @@
# Base config file for STM32G474 ARM processor
CONFIG_MACH_STM32=y
CONFIG_MACH_STM32G474=y

View file

@ -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

View file

@ -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

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more