mirror of
https://github.com/Klipper3d/klipper.git
synced 2026-01-08 15:57:49 -07:00
Merge branch 'master' into nm_vsdlist_sort
This commit is contained in:
commit
12eb7fb1f8
103 changed files with 5715 additions and 1246 deletions
|
|
@ -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
|
||||
|
|
|
|||
126
config/printer-artillery-genius-pro-2022.cfg
Normal file
126
config/printer-artillery-genius-pro-2022.cfg
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
138
config/printer-tronxy-crux1-2022.cfg
Normal file
138
config/printer-tronxy-crux1-2022.cfg
Normal 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
|
||||
|
|
@ -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`.
|
||||
|
|
|
|||
270
docs/Bed_Mesh.md
270
docs/Bed_Mesh.md
|
|
@ -421,12 +421,75 @@ have undesirable results when attempting print moves **outside** of the probed a
|
|||
full bed mesh has a variance greater than 1 layer height, caution must be taken when using
|
||||
adaptive bed meshes and attempting print moves outside of the meshed area.
|
||||
|
||||
## Surface Scans
|
||||
|
||||
Some probes, such as the [Eddy Current Probe](./Eddy_Probe.md), are capable of
|
||||
"scanning" the surface of the bed. That is, these probes can sample a mesh
|
||||
without lifting the tool between samples. To activate scanning mode, the
|
||||
`METHOD=scan` or `METHOD=rapid_scan` probe parameter should be passed in the
|
||||
`BED_MESH_CALIBRATE` gcode command.
|
||||
|
||||
### Scan Height
|
||||
|
||||
The scan height is set by the `horizontal_move_z` option in `[bed_mesh]`. In
|
||||
addition it can be supplied with the `BED_MESH_CALIBRATE` gcode command via the
|
||||
`HORIZONTAL_MOVE_Z` parameter.
|
||||
|
||||
The scan height must be sufficiently low to avoid scanning errors. Typically
|
||||
a height of 2mm (ie: `HORIZONTAL_MOVE_Z=2`) should work well, presuming that the
|
||||
probe is mounted correctly.
|
||||
|
||||
It should be noted that if the probe is more than 4mm above the surface then the
|
||||
results will be invalid. Thus, scanning is not possible on beds with severe
|
||||
surface deviation or beds with extreme tilt that hasn't been corrected.
|
||||
|
||||
### Rapid (Continuous) Scanning
|
||||
|
||||
When performing a `rapid_scan` one should keep in mind that the results will
|
||||
have some amount of error. This error should be low enough to be useful on
|
||||
large print areas with reasonably thick layer heights. Some probes may be
|
||||
more prone to error than others.
|
||||
|
||||
It is not recommended that rapid mode be used to scan a "dense" mesh. Some of
|
||||
the error introduced during a rapid scan may be gaussian noise from the sensor,
|
||||
and a dense mesh will reflect this noise (ie: there will be peaks and valleys).
|
||||
|
||||
Bed Mesh will attempt to optimize the travel path to provide the best possible
|
||||
result based on the configuration. This includes avoiding faulty regions
|
||||
when collecting samples and "overshooting" the mesh when changing direction.
|
||||
This overshoot improves sampling at the edges of a mesh, however it requires
|
||||
that the mesh be configured in a way that allows the tool to travel outside
|
||||
of the mesh.
|
||||
|
||||
```
|
||||
[bed_mesh]
|
||||
speed: 120
|
||||
horizontal_move_z: 5
|
||||
mesh_min: 35, 6
|
||||
mesh_max: 240, 198
|
||||
probe_count: 5
|
||||
scan_overshoot: 8
|
||||
```
|
||||
|
||||
- `scan_overshoot`
|
||||
_Default Value: 0 (disabled)_\
|
||||
The maximum amount of travel (in mm) available outside of the mesh.
|
||||
For rectangular beds this applies to travel on the X axis, and for round beds
|
||||
it applies to the entire radius. The tool must be able to travel the amount
|
||||
specified outside of the mesh. This value is used to optimize the travel
|
||||
path when performing a "rapid scan". The minimum value that may be specified
|
||||
is 1. The default is no overshoot.
|
||||
|
||||
If no scan overshoot is configured then travel path optimization will not
|
||||
be applied to changes in direction.
|
||||
|
||||
## Bed Mesh Gcodes
|
||||
|
||||
### Calibration
|
||||
|
||||
`BED_MESH_CALIBRATE PROFILE=<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`
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
79
docs/OctoPrint.md
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = """
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
187
klippy/extras/ads1220.py
Normal 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}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
133
klippy/extras/error_mcu.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
170
klippy/extras/hx71x.py
Normal 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
|
||||
}
|
||||
|
|
@ -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])
|
||||
|
|
|
|||
30
klippy/extras/load_cell.py
Normal file
30
klippy/extras/load_cell.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,))
|
||||
|
|
|
|||
721
klippy/extras/temperature_probe.py
Normal file
721
klippy/extras/temperature_probe.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
533
scripts/graph_mesh.py
Executable 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()
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
###########################################################
|
||||
|
|
|
|||
17
src/Kconfig
17
src/Kconfig
|
|
@ -108,6 +108,14 @@ config WANT_LDC1612
|
|||
bool
|
||||
depends on HAVE_GPIO_I2C
|
||||
default y
|
||||
config WANT_HX71X
|
||||
bool
|
||||
depends on WANT_GPIO_BITBANGING
|
||||
default y
|
||||
config WANT_ADS1220
|
||||
bool
|
||||
depends on HAVE_GPIO_SPI
|
||||
default y
|
||||
config WANT_SOFTWARE_I2C
|
||||
bool
|
||||
depends on HAVE_GPIO && HAVE_GPIO_I2C
|
||||
|
|
@ -118,7 +126,8 @@ config WANT_SOFTWARE_SPI
|
|||
default y
|
||||
config NEED_SENSOR_BULK
|
||||
bool
|
||||
depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612
|
||||
depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612 || WANT_HX71X \
|
||||
|| WANT_ADS1220
|
||||
default y
|
||||
menu "Optional features (to reduce code size)"
|
||||
depends on HAVE_LIMITED_CODE_SIZE
|
||||
|
|
@ -137,6 +146,12 @@ config WANT_LIS2DW
|
|||
config WANT_LDC1612
|
||||
bool "Support ldc1612 eddy current sensor"
|
||||
depends on HAVE_GPIO_I2C
|
||||
config WANT_HX71X
|
||||
bool "Support HX711 and HX717 ADC chips"
|
||||
depends on WANT_GPIO_BITBANGING
|
||||
config WANT_ADS1220
|
||||
bool "Support ADS 1220 ADC chip"
|
||||
depends on HAVE_GPIO_SPI
|
||||
config WANT_SOFTWARE_I2C
|
||||
bool "Support software based I2C \"bit-banging\""
|
||||
depends on HAVE_GPIO && HAVE_GPIO_I2C
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
163
src/sensor_ads1220.c
Normal 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);
|
||||
|
|
@ -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
253
src/sensor_hx71x.c
Normal 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);
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
3
test/configs/stm32g474.config
Normal file
3
test/configs/stm32g474.config
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Base config file for STM32G474 ARM processor
|
||||
CONFIG_MACH_STM32=y
|
||||
CONFIG_MACH_STM32G474=y
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue