mirror of
https://github.com/Klipper3d/klipper.git
synced 2026-03-09 12:34:43 -06:00
Merge branch 'Klipper3d:master' into msla
This commit is contained in:
commit
982ad9b054
470 changed files with 156373 additions and 13368 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
|
||||
|
|
|
|||
232
config/generic-mellow-fly-e3-v2.cfg
Normal file
232
config/generic-mellow-fly-e3-v2.cfg
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# This file contains common pin mappings for the Mellow Fly-E3-v2.
|
||||
# To use this config, the firmware should be compiled for the
|
||||
# STM32F407 with a "32KiB bootloader".
|
||||
|
||||
# The "make flash" command does not work on the Fly-E3-v2. Instead,
|
||||
# after running "make", copy the generated "out/klipper.bin" file to a
|
||||
# file named "firmware.bin" or "klipper.bin" on an SD card and then restart the Fly-E3-v2
|
||||
# with that SD card.
|
||||
|
||||
# See docs/Config_Reference.md for a description of parameters.
|
||||
|
||||
[mcu]
|
||||
serial: /dev/serial/by-id/usb-Klipper_stm32f407xx_27004A001851323333353137-if00
|
||||
|
||||
[stepper_x]
|
||||
step_pin: PE5
|
||||
dir_pin: PC0
|
||||
enable_pin: !PC1
|
||||
microsteps: 16
|
||||
rotation_distance: 30
|
||||
full_steps_per_rotation: 200
|
||||
endstop_pin: PE7 #X-STOP
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
second_homing_speed: 10
|
||||
homing_retract_dist: 5.0
|
||||
homing_positive_dir: false
|
||||
step_pulse_duration: 0.000004
|
||||
|
||||
[stepper_y]
|
||||
step_pin: PE4
|
||||
dir_pin: !PC13
|
||||
enable_pin: !PC14
|
||||
microsteps: 16
|
||||
rotation_distance: 30
|
||||
full_steps_per_rotation: 200
|
||||
endstop_pin: PE8 #Y-STOP
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 50
|
||||
second_homing_speed: 10
|
||||
homing_retract_dist: 5.0
|
||||
homing_positive_dir: false
|
||||
step_pulse_duration: 0.000004
|
||||
|
||||
[stepper_z]
|
||||
step_pin: PE1
|
||||
dir_pin: !PB7
|
||||
enable_pin: !PE3
|
||||
microsteps: 16
|
||||
rotation_distance: 30
|
||||
full_steps_per_rotation: 200
|
||||
endstop_pin: PE9 #Z-STOP
|
||||
position_min: 0
|
||||
position_endstop: 0
|
||||
position_max: 200
|
||||
homing_speed: 5
|
||||
second_homing_speed: 3
|
||||
homing_retract_dist: 5.0
|
||||
homing_positive_dir: false
|
||||
step_pulse_duration: 0.000004
|
||||
|
||||
[extruder]
|
||||
step_pin: PE2
|
||||
dir_pin: PD5
|
||||
enable_pin: !PD6
|
||||
microsteps: 16
|
||||
rotation_distance: 33.500
|
||||
nozzle_diameter: 0.400
|
||||
filament_diameter: 1.750
|
||||
heater_pin: PC6 #E0
|
||||
|
||||
########################################
|
||||
# Extruder 100K thermistor configuration
|
||||
########################################
|
||||
sensor_type: ATC Semitec 104GT-2
|
||||
sensor_pin: PC4 #T0 TEMP
|
||||
control: pid
|
||||
pid_Kp: 22.2
|
||||
pid_Ki: 1.08
|
||||
pid_Kd: 114
|
||||
min_temp: 0
|
||||
max_temp: 275
|
||||
########################################
|
||||
# Extruder MAX31865 PT100 2 wire config
|
||||
########################################
|
||||
# sensor_type: MAX31865
|
||||
# sensor_pin: PD15 #PT-100
|
||||
# spi_speed: 4000000
|
||||
# spi_software_sclk_pin: PD12
|
||||
# spi_software_mosi_pin: PD11
|
||||
# spi_software_miso_pin: PD13
|
||||
# rtd_nominal_r: 100
|
||||
# rtd_reference_r: 430
|
||||
# rtd_num_of_wires: 2
|
||||
# rtd_use_50Hz_filter: True
|
||||
min_temp: 0
|
||||
max_temp: 300
|
||||
|
||||
#[extruder1]
|
||||
#step_pin: PE0
|
||||
#dir_pin: PD1
|
||||
#enable_pin: !PD3
|
||||
#microsteps: 16
|
||||
#heater_pin: PC7 #E1
|
||||
#sensor_pin: PC5 #T1 TEMP
|
||||
|
||||
########################################
|
||||
# TMC2209 configuration
|
||||
########################################
|
||||
|
||||
[tmc2209 stepper_x]
|
||||
uart_pin: PC15
|
||||
interpolate: False
|
||||
run_current: 0.3
|
||||
sense_resistor: 0.110
|
||||
stealthchop_threshold: 999999
|
||||
|
||||
[tmc2209 stepper_y]
|
||||
uart_pin: PB6
|
||||
interpolate: False
|
||||
run_current: 0.3
|
||||
sense_resistor: 0.110
|
||||
stealthchop_threshold: 999999
|
||||
|
||||
[tmc2209 stepper_z]
|
||||
uart_pin: PD7
|
||||
interpolate: False
|
||||
run_current: 0.4
|
||||
sense_resistor: 0.110
|
||||
stealthchop_threshold: 999999
|
||||
|
||||
[tmc2209 extruder]
|
||||
uart_pin: PD4
|
||||
interpolate: False
|
||||
run_current: 0.27
|
||||
sense_resistor: 0.075
|
||||
stealthchop_threshold: 999999
|
||||
|
||||
#[tmc2209 extruder1]
|
||||
#uart_pin: PD0
|
||||
#interpolate: False
|
||||
#run_current: 0.27
|
||||
#sense_resistor: 0.075
|
||||
#stealthchop_threshold: 999999
|
||||
|
||||
|
||||
#######################################
|
||||
# Heated Bed
|
||||
#######################################
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PB0 #BED
|
||||
sensor_type: Generic 3950
|
||||
sensor_pin: PB1 #B-TEMP
|
||||
max_power: 1.0
|
||||
min_temp: 0
|
||||
max_temp: 120
|
||||
control: pid
|
||||
pid_kp: 58.437
|
||||
pid_ki: 2.347
|
||||
pid_kd: 363.769
|
||||
|
||||
#######################################
|
||||
# LIGHTING
|
||||
#######################################
|
||||
|
||||
#[led Toolhead]
|
||||
#white_pin: PA2 #FAN2
|
||||
#cycle_time: 0.010
|
||||
#initial_white: 0
|
||||
|
||||
#######################################
|
||||
# COOLING
|
||||
#######################################
|
||||
|
||||
[heater_fan hotend_fan]
|
||||
pin: PA1 #FAN1
|
||||
max_power: 1.0
|
||||
kick_start_time: 0.5
|
||||
heater: extruder
|
||||
heater_temp: 50
|
||||
fan_speed: 1.0
|
||||
|
||||
[controller_fan controller_fan]
|
||||
pin: PA0 #FAN0
|
||||
max_power: 1.0
|
||||
kick_start_time: 0.5
|
||||
heater: extruder
|
||||
stepper: stepper_x, stepper_y, stepper_z
|
||||
fan_speed: 1.0
|
||||
idle_timeout: 60
|
||||
|
||||
[fan]
|
||||
pin: PA3 #FAN3
|
||||
max_power: 1.0
|
||||
off_below: 0.2
|
||||
|
||||
[temperature_sensor Mellow_Fly_E3_V2]
|
||||
sensor_type: temperature_mcu
|
||||
min_temp: 5
|
||||
max_temp: 80
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 3000
|
||||
max_z_velocity: 50
|
||||
max_z_accel: 100
|
||||
|
||||
########################################
|
||||
# EXP1 / EXP2 (display) pins
|
||||
########################################
|
||||
[board_pins]
|
||||
aliases:
|
||||
EXP1_1=PD10, EXP1_3=PA8, EXP1_5=PE15, EXP1_7=PA14, EXP1_9=<GND>,
|
||||
EXP1_2=PA9, EXP1_4=PA10, EXP1_6=PE14, EXP1_8=PA13, EXP1_10=<5V>,
|
||||
# EXP2 header
|
||||
EXP2_1=PA6, EXP2_3=PB11, EXP2_5=PB10, EXP2_7=PE13, EXP2_9=<GND>,
|
||||
EXP2_2=PA5, EXP2_4=PA4, EXP2_6=PA7, EXP2_8=<RST>, EXP2_10=<NC>,
|
||||
|
||||
# See the sample-lcd.cfg file for definitions of common LCD displays.
|
||||
|
||||
#######################################
|
||||
# BL-Touch
|
||||
#######################################
|
||||
|
||||
#[bltouch]
|
||||
#sensor_pin: PC2
|
||||
#control_pin: PE6
|
||||
#z_offset: 0
|
||||
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
|
||||
|
|
@ -77,5 +77,14 @@ heater_temp: 50.0
|
|||
pin: toolboard:PA9
|
||||
z_offset: 20
|
||||
|
||||
[samd_sercom sercom_i2c]
|
||||
sercom: sercom1
|
||||
tx_pin: toolboard:PA16
|
||||
clk_pin: toolboard:PA17
|
||||
|
||||
[lis3dh]
|
||||
i2c_mcu: toolboard
|
||||
i2c_bus: sercom1
|
||||
|
||||
[mcu toolboard]
|
||||
canbus_uuid: 4b194673554e
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -24,19 +24,51 @@ try to probe the bed without attaching the probe if you use it.
|
|||
> **Tip:** Make sure the [probe X and Y offsets](Config_Reference.md#probe) are
|
||||
> correctly set as they greatly influence calibration.
|
||||
|
||||
1. After setting up the [axis_twist_compensation] module,
|
||||
perform `AXIS_TWIST_COMPENSATION_CALIBRATE`
|
||||
* The calibration wizard will prompt you to measure the probe Z offset at a few
|
||||
points along the bed
|
||||
* The calibration defaults to 3 points but you can use the option
|
||||
`SAMPLE_COUNT=` to use a different number.
|
||||
2. [Adjust your Z offset](Probe_Calibrate.md#calibrating-probe-z-offset)
|
||||
3. Perform automatic/probe-based bed tramming operations, such as
|
||||
[Screws Tilt Adjust](G-Codes.md#screws_tilt_adjust),
|
||||
[Z Tilt Adjust](G-Codes.md#z_tilt_adjust) etc
|
||||
4. Home all axis, then perform a [Bed Mesh](Bed_Mesh.md) if required
|
||||
5. Perform a test print, followed by any
|
||||
[fine-tuning](Axis_Twist_Compensation.md#fine-tuning) as desired
|
||||
### Basic Usage: X-Axis Calibration
|
||||
1. After setting up the ```[axis_twist_compensation]``` module, run:
|
||||
```
|
||||
AXIS_TWIST_COMPENSATION_CALIBRATE
|
||||
```
|
||||
This command will calibrate the X-axis by default.
|
||||
- The calibration wizard will prompt you to measure the probe Z offset at
|
||||
several points along the bed.
|
||||
- By default, the calibration uses 3 points, but you can specify a different
|
||||
number with the option:
|
||||
``
|
||||
SAMPLE_COUNT=<value>
|
||||
``
|
||||
|
||||
2. **Adjust Your Z Offset:**
|
||||
After completing the calibration, be sure to [adjust your Z offset]
|
||||
(Probe_Calibrate.md#calibrating-probe-z-offset).
|
||||
|
||||
3. **Perform Bed Leveling Operations:**
|
||||
Use probe-based operations as needed, such as:
|
||||
- [Screws Tilt Adjust](G-Codes.md#screws_tilt_adjust)
|
||||
- [Z Tilt Adjust](G-Codes.md#z_tilt_adjust)
|
||||
|
||||
4. **Finalize the Setup:**
|
||||
- Home all axes, and perform a [Bed Mesh](Bed_Mesh.md) if necessary.
|
||||
- Run a test print, followed by any
|
||||
[fine-tuning](Axis_Twist_Compensation.md#fine-tuning)
|
||||
if needed.
|
||||
|
||||
### For Y-Axis Calibration
|
||||
The calibration process for the Y-axis is similar to the X-axis. To calibrate
|
||||
the Y-axis, use:
|
||||
```
|
||||
AXIS_TWIST_COMPENSATION_CALIBRATE AXIS=Y
|
||||
```
|
||||
This will guide you through the same measuring process as for the X-axis.
|
||||
|
||||
### Automatic Calibration for Both Axes
|
||||
To perform automatic calibration for both the X and Y axes without manual
|
||||
intervention, use:
|
||||
```
|
||||
AXIS_TWIST_COMPENSATION_CALIBRATE AUTO=True
|
||||
```
|
||||
In this mode, the calibration process will run for both axes automatically.
|
||||
|
||||
|
||||
> **Tip:** Bed temperature and nozzle temperature and size do not seem to have
|
||||
> an influence to the calibration process.
|
||||
|
|
|
|||
|
|
@ -6,23 +6,64 @@ PRU.
|
|||
## Building an OS image
|
||||
|
||||
Start by installing the
|
||||
[Debian 9.9 2019-08-03 4GB SD IoT](https://beagleboard.org/latest-images)
|
||||
[Debian 11.7 2023-09-02 4GB microSD IoT](https://beagleboard.org/latest-images)
|
||||
image. One may run the image from either a micro-SD card or from
|
||||
builtin eMMC. If using the eMMC, install it to eMMC now by following
|
||||
the instructions from the above link.
|
||||
|
||||
Then ssh into the Beaglebone machine (`ssh debian@beaglebone` --
|
||||
password is `temppwd`) and install Klipper by running the following
|
||||
password is `temppwd`).
|
||||
|
||||
Before start installing Klipper you need to free-up additional space.
|
||||
there are 3 options to do that:
|
||||
1. remove some BeagleBone "Demo" resources
|
||||
2. if you did boot from SD-Card, and it's bigger than 4Gb - you can expand
|
||||
current filesystem to take whole card space
|
||||
3. do option #1 and #2 together.
|
||||
|
||||
To remove some BeagleBone "Demo" resources execute these commands
|
||||
```
|
||||
sudo apt remove bb-node-red-installer
|
||||
sudo apt remove bb-code-server
|
||||
```
|
||||
|
||||
To expand filesystem to full size of your SD-Card execute this command, reboot is not required.
|
||||
```
|
||||
sudo growpart /dev/mmcblk0 1
|
||||
sudo resize2fs /dev/mmcblk0p1
|
||||
```
|
||||
|
||||
|
||||
Install Klipper by running the following
|
||||
commands:
|
||||
|
||||
```
|
||||
git clone https://github.com/Klipper3d/klipper
|
||||
git clone https://github.com/Klipper3d/klipper.git
|
||||
./klipper/scripts/install-beaglebone.sh
|
||||
```
|
||||
|
||||
## Install Octoprint
|
||||
After installing Klipper you need to decide what kind of deployment do you need,
|
||||
but take a note that BeagleBone is 3.3v based hardware and in most cases you can't
|
||||
directly connect pins to 5v or 12v based hardware without conversion boards.
|
||||
|
||||
One may then install Octoprint:
|
||||
As Klipper have multimodule architecture on BeagleBone you can achieve many different use cases,
|
||||
but general ones are following:
|
||||
|
||||
Use case 1: Use BeagleBone only as a host system to run Klipper and additional software
|
||||
like OctoPrint/Fluidd + Moonraker/... and this configuration will be driving
|
||||
external micro-controllers via serial/usb/canbus connections.
|
||||
|
||||
Use case 2: Use BeagleBone with extension board (cape) like CRAMPS board.
|
||||
in this configuration BeagleBone will host Klipper + additional software, and
|
||||
it will drive extension board with BeagleBone PRU cores (2 additional cores 200Mh, 32Bit).
|
||||
|
||||
Use case 3: It's same as "Use case 1" but additionally you want to drive
|
||||
BeagleBone GPIOs with high speed by utilizing PRU cores to offload main CPU.
|
||||
|
||||
|
||||
## Installing Octoprint
|
||||
|
||||
One may then install Octoprint or fully skip this section if desired other software:
|
||||
```
|
||||
git clone https://github.com/foosel/OctoPrint.git
|
||||
cd OctoPrint/
|
||||
|
|
@ -51,25 +92,89 @@ Then start the Octoprint service:
|
|||
```
|
||||
sudo systemctl start octoprint
|
||||
```
|
||||
|
||||
Make sure the OctoPrint web server is accessible - it should be at:
|
||||
Wait 1-2 minutes and make sure the OctoPrint web server is accessible - it should be at:
|
||||
[http://beaglebone:5000/](http://beaglebone:5000/)
|
||||
|
||||
## Building the micro-controller code
|
||||
|
||||
To compile the Klipper micro-controller code, start by configuring it
|
||||
for the "Beaglebone PRU":
|
||||
## Building the BeagleBone PRU micro-controller code (PRU firmware)
|
||||
This section is required for "Use case 2" and "Use case 3" mentioned above,
|
||||
you should skip it for "Use case 1".
|
||||
|
||||
Check that required devices are present
|
||||
|
||||
```
|
||||
sudo beagle-version
|
||||
```
|
||||
You should check that output contains successful "remoteproc" drivers loading and presence of PRU cores,
|
||||
in Kernel 5.10 they should be "remoteproc1" and "remoteproc2" (4a334000.pru, 4a338000.pru)
|
||||
Also check that many GPIOs are loaded they will look like "Allocated GPIO id=0 name='P8_03'"
|
||||
Usually everything is fine and no hardware configuration is required.
|
||||
If something is missing - try to play with "uboot overlays" options or with cape-overlays
|
||||
Just for reference some output of working BeagleBone Black configuration with CRAMPS board:
|
||||
```
|
||||
model:[TI_AM335x_BeagleBone_Black]
|
||||
UBOOT: Booted Device-Tree:[am335x-boneblack-uboot-univ.dts]
|
||||
UBOOT: Loaded Overlay:[BB-ADC-00A0.bb.org-overlays]
|
||||
UBOOT: Loaded Overlay:[BB-BONE-eMMC1-01-00A0.bb.org-overlays]
|
||||
kernel:[5.10.168-ti-r71]
|
||||
/boot/uEnv.txt Settings:
|
||||
uboot_overlay_options:[enable_uboot_overlays=1]
|
||||
uboot_overlay_options:[disable_uboot_overlay_video=0]
|
||||
uboot_overlay_options:[disable_uboot_overlay_audio=1]
|
||||
uboot_overlay_options:[disable_uboot_overlay_wireless=1]
|
||||
uboot_overlay_options:[enable_uboot_cape_universal=1]
|
||||
pkg:[bb-cape-overlays]:[4.14.20210821.0-0~bullseye+20210821]
|
||||
pkg:[bb-customizations]:[1.20230720.1-0~bullseye+20230720]
|
||||
pkg:[bb-usb-gadgets]:[1.20230414.0-0~bullseye+20230414]
|
||||
pkg:[bb-wl18xx-firmware]:[1.20230414.0-0~bullseye+20230414]
|
||||
.............
|
||||
.............
|
||||
|
||||
```
|
||||
|
||||
To compile the Klipper micro-controller code, start by configuring it for the "Beaglebone PRU",
|
||||
for "BeagleBone Black" additionally disable options "Support GPIO Bit-banging devices" and disable "Support LCD devices"
|
||||
inside the "Optional features" because they will not fit in 8Kb PRU firmware memory,
|
||||
then exit and save config:
|
||||
```
|
||||
cd ~/klipper/
|
||||
make menuconfig
|
||||
```
|
||||
|
||||
To build and install the new micro-controller code, run:
|
||||
To build and install the new PRU micro-controller code, run:
|
||||
```
|
||||
sudo service klipper stop
|
||||
make flash
|
||||
sudo service klipper start
|
||||
```
|
||||
After previous commands was executed your PRU firmware should be ready and started
|
||||
to check if everything was fine you can execute following command
|
||||
```
|
||||
dmesg
|
||||
```
|
||||
and compare last messages with sample one which indicate that everything started properly:
|
||||
```
|
||||
[ 71.105499] remoteproc remoteproc1: 4a334000.pru is available
|
||||
[ 71.157155] remoteproc remoteproc2: 4a338000.pru is available
|
||||
[ 73.256287] remoteproc remoteproc1: powering up 4a334000.pru
|
||||
[ 73.279246] remoteproc remoteproc1: Booting fw image am335x-pru0-fw, size 97112
|
||||
[ 73.285807] remoteproc1#vdev0buffer: registered virtio0 (type 7)
|
||||
[ 73.285836] remoteproc remoteproc1: remote processor 4a334000.pru is now up
|
||||
[ 73.286322] remoteproc remoteproc2: powering up 4a338000.pru
|
||||
[ 73.313717] remoteproc remoteproc2: Booting fw image am335x-pru1-fw, size 188560
|
||||
[ 73.313753] remoteproc remoteproc2: header-less resource table
|
||||
[ 73.329964] remoteproc remoteproc2: header-less resource table
|
||||
[ 73.348321] remoteproc remoteproc2: remote processor 4a338000.pru is now up
|
||||
[ 73.443355] virtio_rpmsg_bus virtio0: creating channel rpmsg-pru addr 0x1e
|
||||
[ 73.443727] virtio_rpmsg_bus virtio0: msg received with no recipient
|
||||
[ 73.444352] virtio_rpmsg_bus virtio0: rpmsg host is online
|
||||
[ 73.540993] rpmsg_pru virtio0.rpmsg-pru.-1.30: new rpmsg_pru device: /dev/rpmsg_pru30
|
||||
```
|
||||
take a note about "/dev/rpmsg_pru30" - it's your future serial device for main mcu configuration
|
||||
this device is required to be present, if it's absent - your PRU cores did not start properly.
|
||||
|
||||
## Building and installing Linux host micro-controller code
|
||||
This section is required for "Use case 2" and optional for "Use case 3" mentioned above
|
||||
|
||||
It is also necessary to compile and install the micro-controller code
|
||||
for a Linux host process. Configure it a second time for a "Linux process":
|
||||
|
|
@ -83,12 +188,24 @@ sudo service klipper stop
|
|||
make flash
|
||||
sudo service klipper start
|
||||
```
|
||||
take a note about "/tmp/klipper_host_mcu" - it will be your future serial device for "mcu host"
|
||||
if that file don't exist - refer to "scripts/klipper-mcu.service" file, it was installed by
|
||||
previous commands, and it's responsible for it.
|
||||
|
||||
|
||||
Take a note for "Use case 2" about following: when you will define printer configuration you should always
|
||||
use temperature sensors from "mcu host" because ADCs not present in default "mcu" (PRU cores).
|
||||
Sample configuration of "sensor_pin" for extruder and heated bed are available in "generic-cramps.cfg"
|
||||
You can use any other GPIO directly from "mcu host" by referencing them this way "host:gpiochip1/gpio17"
|
||||
but that should be avoided because it will be creating additional load on main CPU and most probably
|
||||
you can't use them for stepper control.
|
||||
|
||||
|
||||
## Remaining configuration
|
||||
|
||||
Complete the installation by configuring Klipper and Octoprint
|
||||
Complete the installation by configuring Klipper
|
||||
following the instructions in
|
||||
the main [Installation](Installation.md#configuring-klipper) document.
|
||||
the main [Installation](Installation.md#configuring-octoprint-to-use-klipper) document.
|
||||
|
||||
## Printing on the Beaglebone
|
||||
|
||||
|
|
@ -97,4 +214,111 @@ OctoPrint well. Print stalls have been known to occur on complex
|
|||
prints (the printer may move faster than OctoPrint can send movement
|
||||
commands). If this occurs, consider using the "virtual_sdcard" feature
|
||||
(see [Config Reference](Config_Reference.md#virtual_sdcard) for
|
||||
details) to print directly from Klipper.
|
||||
details) to print directly from Klipper
|
||||
and disable any DEBUG or VERBOSE logging options if you did enable them.
|
||||
|
||||
|
||||
## AVR micro-controller code build
|
||||
This environment have everything to build necessary micro-controller code except AVR,
|
||||
AVR packages was removed because of conflict with PRU packages.
|
||||
if you still want to build AVR micro-controller code in this environment you need to remove
|
||||
PRU packages and install AVR packages by executing following commands
|
||||
|
||||
```
|
||||
sudo apt-get remove gcc-pru
|
||||
sudo apt-get install avrdude gcc-avr binutils-avr avr-libc
|
||||
```
|
||||
if you need to restore PRU packages - then remove ARV packages before that
|
||||
```
|
||||
sudo apt-get remove avrdude gcc-avr binutils-avr avr-libc
|
||||
sudo apt-get install gcc-pru
|
||||
```
|
||||
|
||||
|
||||
## Hardware Pin designation
|
||||
BeagleBone is very flexible in terms of pin designation, same pin can be configured for different function
|
||||
but always single function for single pin, same function can be present on different pins.
|
||||
So you can't have multiple functions on single pin or have same function on multiple pins.
|
||||
Example:
|
||||
P9_20 - i2c2_sda/can0_tx/spi1_cs0/gpio0_12/uart1_ctsn
|
||||
P9_19 - i2c2_scl/can0_rx/spi1_cs1/gpio0_13/uart1_rtsn
|
||||
P9_24 - i2c1_scl/can1_rx/gpio0_15/uart1_tx
|
||||
P9_26 - i2c1_sda/can1_tx/gpio0_14/uart1_rx
|
||||
|
||||
Pin designation is defined by using special "overlays" which will be loaded during linux boot
|
||||
they are configured by editing file /boot/uEnv.txt with elevated permissions
|
||||
```
|
||||
sudo editor /boot/uEnv.txt
|
||||
```
|
||||
and defining which functionality to load, for example to enable CAN1 you need to define overlay for it
|
||||
```
|
||||
uboot_overlay_addr4=/lib/firmware/BB-CAN1-00A0.dtbo
|
||||
```
|
||||
This overlay BB-CAN1-00A0.dtbo will reconfigure all required pins for CAN1 and create CAN device in Linux.
|
||||
Any change in overlays will require system reboot to be applied.
|
||||
If you need to understand which pins are involved in some overlay - you can analyze source files in
|
||||
this location: /opt/sources/bb.org-overlays/src/arm/
|
||||
or search info in BeagleBone forums.
|
||||
|
||||
|
||||
## Enabling hardware SPI
|
||||
BeagleBone usually have multiple hardware SPI buses, for example BeagleBone Black can have 2 of them,
|
||||
they can work up to 48Mhz, but usually they are limited to 16Mhz by Kernel Device-tree.
|
||||
By default, in BeagleBone Black some of SPI1 pins are configured for HDMI-Audio output,
|
||||
to fully enable 4-wire SPI1 you need to disable HDMI Audio and enable SPI1
|
||||
To do that edit file /boot/uEnv.txt with elevated permissions
|
||||
```
|
||||
sudo editor /boot/uEnv.txt
|
||||
```
|
||||
uncomment variable
|
||||
```
|
||||
disable_uboot_overlay_audio=1
|
||||
```
|
||||
|
||||
next uncomment variable and define it this way
|
||||
```
|
||||
uboot_overlay_addr4=/lib/firmware/BB-SPIDEV1-00A0.dtbo
|
||||
```
|
||||
Save changes in /boot/uEnv.txt and reboot the board.
|
||||
Now you have SPI1 Enabled, to verify its presence execute command
|
||||
```
|
||||
ls /dev/spidev1.*
|
||||
```
|
||||
Take a note that BeagleBone usually is 3.3v based hardware and to use 5V SPI devices
|
||||
you need to add Level-Shifting chip, for example SN74CBTD3861, SN74LVC1G34 or similar.
|
||||
If you are using CRAMPS board - it already contains Level-Shifting chip and SPI1 pins
|
||||
will become available on P503 port, and they can accept 5v hardware,
|
||||
check CRAMPS board Schematics for pin references.
|
||||
|
||||
## Enabling hardware I2C
|
||||
BeagleBone usually have multiple hardware I2C buses, for example BeagleBone Black can have 3 of them,
|
||||
they support speed up-to 400Kbit Fast mode.
|
||||
By default, in BeagleBone Black there are two of them (i2c-1 and i2c-2) usually both are already configured and
|
||||
present on P9, third ic2-0 usually reserved for internal use.
|
||||
If you are using CRAMPS board then i2c-2 is present on P303 port with 3.3v level,
|
||||
If you want to obtain I2c-1 in CRAMPS board - you can get them on Extruder1.Step, Extruder1.Dir pins,
|
||||
they also are 3.3v based, check CRAMPS board Schematics for pin references.
|
||||
Related overlays, for [Hardware Pin designation](#hardware-pin-designation):
|
||||
I2C1(100Kbit): BB-I2C1-00A0.dtbo
|
||||
I2C1(400Kbit): BB-I2C1-FAST-00A0.dtbo
|
||||
I2C2(100Kbit): BB-I2C2-00A0.dtbo
|
||||
I2C2(400Kbit): BB-I2C2-FAST-00A0.dtbo
|
||||
|
||||
## Enabling hardware UART(Serial)/CAN
|
||||
BeagleBone have up to 6 hardware UART(Serial) buses (up to 3Mbit)
|
||||
and up to 2 hardware CAN(1Mbit) buses.
|
||||
UART1(RX,TX) and CAN1(TX,RX) and I2C2(SDA,SCL) are using same pins - so you need to chose what to use
|
||||
UART1(CTSN,RTSN) and CAN0(TX,RX) and I2C1(SDA,SCL) are using same pins - so you need to chose what to use
|
||||
All UART/CAN related pins are 3.3v based, so you will need to use Transceiver chips/boards like SN74LVC2G241DCUR (for UART),
|
||||
SN65HVD230 (for CAN), TTL-RS485 (for RS-485) or something similar which can convert 3.3v signals to appropriate levels.
|
||||
|
||||
Related overlays, for [Hardware Pin designation](#hardware-pin-designation)
|
||||
CAN0: BB-CAN0-00A0.dtbo
|
||||
CAN1: BB-CAN1-00A0.dtbo
|
||||
UART0: - used for Console
|
||||
UART1(RX,TX): BB-UART1-00A0.dtbo
|
||||
UART1(RTS,CTS): BB-UART1-RTSCTS-00A0.dtbo
|
||||
UART2(RX,TX): BB-UART2-00A0.dtbo
|
||||
UART3(RX,TX): BB-UART3-00A0.dtbo
|
||||
UART4(RS-485): BB-UART4-RS485-00A0.dtbo
|
||||
UART5(RX,TX): BB-UART5-00A0.dtbo
|
||||
|
|
|
|||
272
docs/Bed_Mesh.md
272
docs/Bed_Mesh.md
|
|
@ -269,7 +269,7 @@ printers use an endstop for homing the Z axis and a probe for calibrating the
|
|||
mesh. In this configuration it is possible offset the mesh so that the (X, Y)
|
||||
`reference position` applies zero adjustment. The `reference postion` should
|
||||
be the location on the bed where a
|
||||
[Z_ENDSTOP_CALIBRATE](./Manual_Level#calibrating-a-z-endstop)
|
||||
[Z_ENDSTOP_CALIBRATE](./Manual_Level.md#calibrating-a-z-endstop)
|
||||
paper test is performed. The bed_mesh module provides the
|
||||
`zero_reference_position` option for specifying this coordinate:
|
||||
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -354,6 +354,26 @@ micro-controller.
|
|||
| 1 stepper (200Mhz) | 39 |
|
||||
| 3 stepper (200Mhz) | 181 |
|
||||
|
||||
### SAME70 step rate benchmark
|
||||
|
||||
The following configuration sequence is used on the SAME70:
|
||||
```
|
||||
allocate_oids count=3
|
||||
config_stepper oid=0 step_pin=PC18 dir_pin=PB5 invert_step=-1 step_pulse_ticks=0
|
||||
config_stepper oid=1 step_pin=PC16 dir_pin=PD10 invert_step=-1 step_pulse_ticks=0
|
||||
config_stepper oid=2 step_pin=PC28 dir_pin=PA4 invert_step=-1 step_pulse_ticks=0
|
||||
finalize_config crc=0
|
||||
```
|
||||
|
||||
The test was last run on commit `34e9ea55` with gcc version
|
||||
`arm-none-eabi-gcc (NixOS 10.3-2021.10) 10.3.1` on a SAME70Q20B
|
||||
micro-controller.
|
||||
|
||||
| same70 | ticks |
|
||||
| -------------------- | ----- |
|
||||
| 1 stepper | 45 |
|
||||
| 3 stepper | 190 |
|
||||
|
||||
### AR100 step rate benchmark ###
|
||||
|
||||
The following configuration sequence is used on AR100 CPU (Allwinner A64):
|
||||
|
|
@ -366,7 +386,7 @@ finalize_config crc=0
|
|||
|
||||
```
|
||||
|
||||
The test was last run on commit `08d037c6` with gcc version
|
||||
The test was last run on commit `b7978d37` with gcc version
|
||||
`or1k-linux-musl-gcc (GCC) 9.2.0` on an Allwinner A64-H
|
||||
micro-controller.
|
||||
|
||||
|
|
@ -375,9 +395,9 @@ micro-controller.
|
|||
| 1 stepper | 85 |
|
||||
| 3 stepper | 359 |
|
||||
|
||||
### RP2040 step rate benchmark
|
||||
### RPxxxx step rate benchmark
|
||||
|
||||
The following configuration sequence is used on the RP2040:
|
||||
The following configuration sequence is used on the RP2040 and RP2350:
|
||||
|
||||
```
|
||||
allocate_oids count=3
|
||||
|
|
@ -387,15 +407,26 @@ config_stepper oid=2 step_pin=gpio27 dir_pin=gpio5 invert_step=-1 step_pulse_tic
|
|||
finalize_config crc=0
|
||||
```
|
||||
|
||||
The test was last run on commit `59314d99` with gcc version
|
||||
`arm-none-eabi-gcc (Fedora 10.2.0-4.fc34) 10.2.0` on a Raspberry Pi
|
||||
Pico board.
|
||||
The test was last run on commit `f6718291` with gcc version
|
||||
`arm-none-eabi-gcc (Fedora 14.1.0-1.fc40) 14.1.0` on Raspberry Pi
|
||||
Pico and Pico 2 boards.
|
||||
|
||||
| rp2040 | ticks |
|
||||
| rp2040 (*) | ticks |
|
||||
| -------------------- | ----- |
|
||||
| 1 stepper | 5 |
|
||||
| 3 stepper | 22 |
|
||||
|
||||
| rp2350 | ticks |
|
||||
| -------------------- | ----- |
|
||||
| 1 stepper | 36 |
|
||||
| 3 stepper | 169 |
|
||||
|
||||
(*) Note that the reported rp2040 ticks are relative to a 12Mhz
|
||||
scheduling timer and do not correspond to its 125Mhz internal ARM
|
||||
processing rate. It is expected that 5 scheduling ticks corresponds to
|
||||
~47 ARM core cycles and 22 scheduling ticks corresponds to ~224 ARM
|
||||
core cycles.
|
||||
|
||||
### Linux MCU step rate benchmark
|
||||
|
||||
The following configuration sequence is used on a Raspberry Pi:
|
||||
|
|
@ -456,7 +487,8 @@ hub.
|
|||
| sam4s8c (USB) | 650K | 8d4a5c16 | arm-none-eabi-gcc (Fedora 7.4.0-1.fc30) 7.4.0 |
|
||||
| samd51 (USB) | 864K | 01d2183f | arm-none-eabi-gcc (Fedora 7.4.0-1.fc30) 7.4.0 |
|
||||
| stm32f446 (USB) | 870K | 01d2183f | arm-none-eabi-gcc (Fedora 7.4.0-1.fc30) 7.4.0 |
|
||||
| rp2040 (USB) | 873K | c5667193 | arm-none-eabi-gcc (Fedora 10.2.0-4.fc34) 10.2.0 |
|
||||
| rp2040 (USB) | 885K | f6718291 | arm-none-eabi-gcc (Fedora 14.1.0-1.fc40) 14.1.0 |
|
||||
| rp2350 (USB) | 885K | f6718291 | arm-none-eabi-gcc (Fedora 14.1.0-1.fc40) 14.1.0 |
|
||||
|
||||
## Host Benchmarks
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,37 @@ All dates in this document are approximate.
|
|||
|
||||
## Changes
|
||||
|
||||
20241203: The resonance test has been changed to include slow sweeping
|
||||
moves. This change requires that testing point(s) have some clearance
|
||||
in X/Y plane (+/- 30 mm from the test point should suffice when using
|
||||
the default settings). The new test should generally produce more
|
||||
accurate and reliable test results. However, if required, the previous
|
||||
test behavior can be restored by adding options `sweeping_period: 0` and
|
||||
`accel_per_hz: 75` to the `[resonance_tester]` config section.
|
||||
|
||||
20241201: In some cases Klipper may have ignored leading characters or
|
||||
spaces in a traditional G-Code command. For example, "99M123" may have
|
||||
been interpreted as "M123" and "M 321" may have been interpreted as
|
||||
"M321". Klipper will now report these cases with an "Unknown command"
|
||||
warning.
|
||||
|
||||
20241112: Option `CHIPS=<chip_name>` in `TEST_RESONANCES` and
|
||||
`SHAPER_CALIBRATE` requires specifying the full name(s) of the accel
|
||||
chip(s). For example, `adxl345 rpi` instead of short name - `rpi`.
|
||||
|
||||
20240912: `SET_PIN`, `SET_SERVO`, `SET_FAN_SPEED`, `M106`, and `M107`
|
||||
commands are now collated. Previously, if many updates to the same
|
||||
object were issued faster than the minimum scheduling time (typically
|
||||
100ms) then actual updates could be queued far into the future. Now if
|
||||
many updates are issued in rapid succession then it is possible that
|
||||
only the latest request will be applied. If the previous behavior is
|
||||
requried then consider adding explicit `G4` delay commands between
|
||||
updates.
|
||||
|
||||
20240912: Support for `maximum_mcu_duration` and `static_value`
|
||||
parameters in `[output_pin]` config sections have been removed. These
|
||||
options have been deprecated since 20240123.
|
||||
|
||||
20240415: The `on_error_gcode` parameter in the `[virtual_sdcard]`
|
||||
config section now has a default. If this parameter is not specified
|
||||
it now defaults to `TURN_OFF_HEATERS`. If the previous behavior is
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -1668,8 +1675,9 @@ Support for LIS2DW accelerometers.
|
|||
|
||||
```
|
||||
[lis2dw]
|
||||
cs_pin:
|
||||
# The SPI enable pin for the sensor. This parameter must be provided.
|
||||
#cs_pin:
|
||||
# The SPI enable pin for the sensor. This parameter must be provided
|
||||
# if using SPI.
|
||||
#spi_speed: 5000000
|
||||
# The SPI speed (in hz) to use when communicating with the chip.
|
||||
# The default is 5000000.
|
||||
|
|
@ -1679,6 +1687,46 @@ cs_pin:
|
|||
#spi_software_miso_pin:
|
||||
# See the "common SPI settings" section for a description of the
|
||||
# above parameters.
|
||||
#i2c_address:
|
||||
# Default is 25 (0x19). If SA0 is high, it would be 24 (0x18) instead.
|
||||
#i2c_mcu:
|
||||
#i2c_bus:
|
||||
#i2c_software_scl_pin:
|
||||
#i2c_software_sda_pin:
|
||||
#i2c_speed: 400000
|
||||
# See the "common I2C settings" section for a description of the
|
||||
# above parameters. The default "i2c_speed" is 400000.
|
||||
#axes_map: x, y, z
|
||||
# See the "adxl345" section for information on this parameter.
|
||||
```
|
||||
|
||||
### [lis3dh]
|
||||
|
||||
Support for LIS3DH accelerometers.
|
||||
|
||||
```
|
||||
[lis3dh]
|
||||
#cs_pin:
|
||||
# The SPI enable pin for the sensor. This parameter must be provided
|
||||
# if using SPI.
|
||||
#spi_speed: 5000000
|
||||
# The SPI speed (in hz) to use when communicating with the chip.
|
||||
# The default is 5000000.
|
||||
#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.
|
||||
#i2c_address:
|
||||
# Default is 25 (0x19). If SA0 is high, it would be 24 (0x18) instead.
|
||||
#i2c_mcu:
|
||||
#i2c_bus:
|
||||
#i2c_software_scl_pin:
|
||||
#i2c_software_sda_pin:
|
||||
#i2c_speed: 400000
|
||||
# See the "common I2C settings" section for a description of the
|
||||
# above parameters. The default "i2c_speed" is 400000.
|
||||
#axes_map: x, y, z
|
||||
# See the "adxl345" section for information on this parameter.
|
||||
```
|
||||
|
|
@ -1742,11 +1790,14 @@ section of the measuring resonances guide for more information on
|
|||
# auto-calibration (with 'SHAPER_CALIBRATE' command). By default no
|
||||
# maximum smoothing is specified. Refer to Measuring_Resonances guide
|
||||
# for more details on using this feature.
|
||||
#move_speed: 50
|
||||
# The speed (in mm/s) to move the toolhead to and between test points
|
||||
# during the calibration. The default is 50.
|
||||
#min_freq: 5
|
||||
# Minimum frequency to test for resonances. The default is 5 Hz.
|
||||
#max_freq: 133.33
|
||||
# Maximum frequency to test for resonances. The default is 133.33 Hz.
|
||||
#accel_per_hz: 75
|
||||
#accel_per_hz: 60
|
||||
# This parameter is used to determine which acceleration to use to
|
||||
# test a specific frequency: accel = accel_per_hz * freq. Higher the
|
||||
# value, the higher is the energy of the oscillations. Can be set to
|
||||
|
|
@ -1760,6 +1811,13 @@ section of the measuring resonances guide for more information on
|
|||
# hz_per_sec. Small values make the test slow, and the large values
|
||||
# will decrease the precision of the test. The default value is 1.0
|
||||
# (Hz/sec == sec^-2).
|
||||
#sweeping_accel: 400
|
||||
# An acceleration of slow sweeping moves. The default is 400 mm/sec^2.
|
||||
#sweeping_period: 1.2
|
||||
# A period of slow sweeping moves. Setting this parameter to 0
|
||||
# disables slow sweeping moves. Avoid setting it to a too small
|
||||
# non-zero value in order to not poison the measurements.
|
||||
# The default is 1.2 sec which is a good all-round choice.
|
||||
```
|
||||
|
||||
## Config file helpers
|
||||
|
|
@ -2007,6 +2065,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.
|
||||
|
|
@ -2032,9 +2093,9 @@ sensor_type: ldc1612
|
|||
|
||||
### [axis_twist_compensation]
|
||||
|
||||
A tool to compensate for inaccurate probe readings due to twist in X gantry. See
|
||||
the [Axis Twist Compensation Guide](Axis_Twist_Compensation.md) for more
|
||||
detailed information regarding symptoms, configuration and setup.
|
||||
A tool to compensate for inaccurate probe readings due to twist in X or Y
|
||||
gantry. See the [Axis Twist Compensation Guide](Axis_Twist_Compensation.md)
|
||||
for more detailed information regarding symptoms, configuration and setup.
|
||||
|
||||
```
|
||||
[axis_twist_compensation]
|
||||
|
|
@ -2047,16 +2108,33 @@ detailed information regarding symptoms, configuration and setup.
|
|||
calibrate_start_x: 20
|
||||
# Defines the minimum X coordinate of the calibration
|
||||
# This should be the X coordinate that positions the nozzle at the starting
|
||||
# calibration position. This parameter must be provided.
|
||||
# calibration position.
|
||||
calibrate_end_x: 200
|
||||
# Defines the maximum X coordinate of the calibration
|
||||
# This should be the X coordinate that positions the nozzle at the ending
|
||||
# calibration position. This parameter must be provided.
|
||||
# calibration position.
|
||||
calibrate_y: 112.5
|
||||
# Defines the Y coordinate of the calibration
|
||||
# This should be the Y coordinate that positions the nozzle during the
|
||||
# calibration process. This parameter must be provided and is recommended to
|
||||
# calibration process. This parameter is recommended to
|
||||
# be near the center of the bed
|
||||
|
||||
# For Y-axis twist compensation, specify the following parameters:
|
||||
calibrate_start_y: ...
|
||||
# Defines the minimum Y coordinate of the calibration
|
||||
# This should be the Y coordinate that positions the nozzle at the starting
|
||||
# calibration position for the Y axis. This parameter must be provided if
|
||||
# compensating for Y axis twist.
|
||||
calibrate_end_y: ...
|
||||
# Defines the maximum Y coordinate of the calibration
|
||||
# This should be the Y coordinate that positions the nozzle at the ending
|
||||
# calibration position for the Y axis. This parameter must be provided if
|
||||
# compensating for Y axis twist.
|
||||
calibrate_x: ...
|
||||
# Defines the X coordinate of the calibration for Y axis twist compensation
|
||||
# This should be the X coordinate that positions the nozzle during the
|
||||
# calibration process for Y axis twist compensation. This parameter must be
|
||||
# provided and is recommended to be near the center of the bed.
|
||||
```
|
||||
|
||||
## Additional stepper motors and extruders
|
||||
|
|
@ -2391,6 +2469,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.
|
||||
|
|
@ -2490,9 +2631,9 @@ sensor_pin:
|
|||
# name in the above list.
|
||||
```
|
||||
|
||||
### BMP180/BMP280/BME280/BME680 temperature sensor
|
||||
### BMP180/BMP280/BME280/BMP388/BME680 temperature sensor
|
||||
|
||||
BMP180/BMP280/BME280/BME680 two wire interface (I2C) environmental sensors.
|
||||
BMP180/BMP280/BME280/BMP388/BME680 two wire interface (I2C) environmental sensors.
|
||||
Note that these sensors are not intended for use with extruders and
|
||||
heater beds, but rather for monitoring ambient temperature (C),
|
||||
pressure (hPa), relative humidity and in case of the BME680 gas level.
|
||||
|
|
@ -2503,8 +2644,8 @@ temperature.
|
|||
```
|
||||
sensor_type: BME280
|
||||
#i2c_address:
|
||||
# Default is 118 (0x76). The BMP180 and some BME280 sensors have an address of 119
|
||||
# (0x77).
|
||||
# Default is 118 (0x76). The BMP180, BMP388 and some BME280 sensors
|
||||
# have an address of 119 (0x77).
|
||||
#i2c_mcu:
|
||||
#i2c_bus:
|
||||
#i2c_software_scl_pin:
|
||||
|
|
@ -3317,6 +3458,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 +3500,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 +3609,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 +3635,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 +3774,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 +3907,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
|
||||
|
|
@ -3952,15 +4149,16 @@ Support for a display attached to the micro-controller.
|
|||
[display]
|
||||
lcd_type:
|
||||
# The type of LCD chip in use. This may be "hd44780", "hd44780_spi",
|
||||
# "st7920", "emulated_st7920", "uc1701", "ssd1306", or "sh1106".
|
||||
# "aip31068_spi", "st7920", "emulated_st7920", "uc1701", "ssd1306", or
|
||||
# "sh1106".
|
||||
# See the display sections below for information on each type and
|
||||
# additional parameters they provide. This parameter must be
|
||||
# provided.
|
||||
#display_group:
|
||||
# The name of the display_data group to show on the display. This
|
||||
# controls the content of the screen (see the "display_data" section
|
||||
# for more information). The default is _default_20x4 for hd44780
|
||||
# displays and _default_16x4 for other displays.
|
||||
# for more information). The default is _default_20x4 for hd44780 or
|
||||
# aip31068_spi displays and _default_16x4 for other displays.
|
||||
#menu_timeout:
|
||||
# Timeout for menu. Being inactive this amount of seconds will
|
||||
# trigger menu exit or return to root menu when having autorun
|
||||
|
|
@ -4086,6 +4284,31 @@ spi_software_miso_pin:
|
|||
...
|
||||
```
|
||||
|
||||
#### aip31068_spi display
|
||||
|
||||
Information on configuring an aip31068_spi display - a very similar to hd44780_spi
|
||||
a 20x04 (20 symbols by 4 lines) display with slightly different internal
|
||||
protocol.
|
||||
|
||||
```
|
||||
[display]
|
||||
lcd_type: aip31068_spi
|
||||
latch_pin:
|
||||
spi_software_sclk_pin:
|
||||
spi_software_mosi_pin:
|
||||
spi_software_miso_pin:
|
||||
# The pins connected to the shift register controlling the display.
|
||||
# The spi_software_miso_pin needs to be set to an unused pin of the
|
||||
# printer mainboard as the shift register does not have a MISO pin,
|
||||
# but the software spi implementation requires this pin to be
|
||||
# configured.
|
||||
#line_length:
|
||||
# Set the number of characters per line for an hd44780 type lcd.
|
||||
# Possible values are 20 (default) and 16. The number of lines is
|
||||
# fixed to 4.
|
||||
...
|
||||
```
|
||||
|
||||
#### st7920 display
|
||||
|
||||
Information on configuring st7920 displays (which is used in
|
||||
|
|
@ -4573,6 +4796,112 @@ adc2:
|
|||
# above parameters.
|
||||
```
|
||||
|
||||
## Load Cells
|
||||
|
||||
### [load_cell]
|
||||
Load Cell. Uses an ADC sensor attached to a load cell to create a digital
|
||||
scale.
|
||||
|
||||
```
|
||||
[load_cell]
|
||||
sensor_type:
|
||||
# This must be one of the supported sensor types, see below.
|
||||
```
|
||||
|
||||
#### HX711
|
||||
This is a 24 bit low sample rate chip using "bit-bang" communications. It is
|
||||
suitable for filament scales.
|
||||
```
|
||||
[load_cell]
|
||||
sensor_type: hx711
|
||||
sclk_pin:
|
||||
# The pin connected to the HX711 clock line. This parameter must be provided.
|
||||
dout_pin:
|
||||
# The pin connected to the HX711 data output line. This parameter must be
|
||||
# provided.
|
||||
#gain: A-128
|
||||
# Valid values for gain are: A-128, A-64, B-32. The default is A-128.
|
||||
# 'A' denotes the input channel and the number denotes the gain. Only the 3
|
||||
# listed combinations are supported by the chip. Note that changing the gain
|
||||
# setting also selects the channel being read.
|
||||
#sample_rate: 80
|
||||
# Valid values for sample_rate are 80 or 10. The default value is 80.
|
||||
# This must match the wiring of the chip. The sample rate cannot be changed
|
||||
# in software.
|
||||
```
|
||||
|
||||
#### HX717
|
||||
This is the 4x higher sample rate version of the HX711, suitable for probing.
|
||||
```
|
||||
[load_cell]
|
||||
sensor_type: hx717
|
||||
sclk_pin:
|
||||
# The pin connected to the HX717 clock line. This parameter must be provided.
|
||||
dout_pin:
|
||||
# The pin connected to the HX717 data output line. This parameter must be
|
||||
# provided.
|
||||
#gain: A-128
|
||||
# Valid values for gain are A-128, B-64, A-64, B-8.
|
||||
# 'A' denotes the input channel and the number denotes the gain setting.
|
||||
# Only the 4 listed combinations are supported by the chip. Note that
|
||||
# changing the gain setting also selects the channel being read.
|
||||
#sample_rate: 320
|
||||
# Valid values for sample_rate are: 10, 20, 80, 320. The default is 320.
|
||||
# This must match the wiring of the chip. The sample rate cannot be changed
|
||||
# in software.
|
||||
```
|
||||
|
||||
#### ADS1220
|
||||
The ADS1220 is a 24 bit ADC supporting up to a 2Khz sample rate configurable in
|
||||
software.
|
||||
```
|
||||
[load_cell]
|
||||
sensor_type: ads1220
|
||||
cs_pin:
|
||||
# The pin connected to the ADS1220 chip select line. This parameter must
|
||||
# be provided.
|
||||
#spi_speed: 512000
|
||||
# This chip supports 2 speeds: 256000 or 512000. The faster speed is only
|
||||
# enabled when one of the Turbo sample rates is used. The correct spi_speed
|
||||
# is selected based on the sample rate.
|
||||
#spi_bus:
|
||||
#spi_software_sclk_pin:
|
||||
#spi_software_mosi_pin:
|
||||
#spi_software_miso_pin:
|
||||
# See the "common SPI settings" section for a description of the
|
||||
# above parameters.
|
||||
data_ready_pin:
|
||||
# Pin connected to the ADS1220 data ready line. This parameter must be
|
||||
# provided.
|
||||
#gain: 128
|
||||
# Valid gain values are 128, 64, 32, 16, 8, 4, 2, 1
|
||||
# The default is 128
|
||||
#pga_bypass: False
|
||||
# Disable the internal Programmable Gain Amplifier. If
|
||||
# True the PGA will be disabled for gains 1, 2, and 4. The PGA is always
|
||||
# enabled for gain settings 8 to 128, regardless of the pga_bypass setting.
|
||||
# If AVSS is used as an input pga_bypass is forced to True.
|
||||
# The default is False.
|
||||
#sample_rate: 660
|
||||
# This chip supports two ranges of sample rates, Normal and Turbo. In turbo
|
||||
# mode the chip's internal clock runs twice as fast and the SPI communication
|
||||
# speed is also doubled.
|
||||
# Normal sample rates: 20, 45, 90, 175, 330, 600, 1000
|
||||
# Turbo sample rates: 40, 90, 180, 350, 660, 1200, 2000
|
||||
# The default is 660
|
||||
#input_mux:
|
||||
# Input multiplexer configuration, select a pair of pins to use. The first pin
|
||||
# is the positive, AINP, and the second pin is the negative, AINN. Valid
|
||||
# values are: 'AIN0_AIN1', 'AIN0_AIN2', 'AIN0_AIN3', 'AIN1_AIN2', 'AIN1_AIN3',
|
||||
# 'AIN2_AIN3', 'AIN1_AIN0', 'AIN3_AIN2', 'AIN0_AVSS', 'AIN1_AVSS', 'AIN2_AVSS'
|
||||
# and 'AIN3_AVSS'. If AVSS is used the PGA is bypassed and the pga_bypass
|
||||
# setting will be forced to True.
|
||||
# The default is AIN0_AIN1.
|
||||
#vref:
|
||||
# The selected voltage reference. Valid values are: 'internal', 'REF0', 'REF1'
|
||||
# and 'analog_supply'. Default is 'internal'.
|
||||
```
|
||||
|
||||
## Board specific hardware support
|
||||
|
||||
### [sx1509]
|
||||
|
|
@ -4764,8 +5093,9 @@ serial:
|
|||
### [angle]
|
||||
|
||||
Magnetic hall angle sensor support for reading stepper motor angle
|
||||
shaft measurements using a1333, as5047d, or tle5012b SPI chips. The
|
||||
measurements are available via the [API Server](API_Server.md) and
|
||||
shaft measurements using a1333, as5047d, mt6816, mt6826s,
|
||||
or tle5012b SPI chips.
|
||||
The measurements are available via the [API Server](API_Server.md) and
|
||||
[motion analysis tool](Debugging.md#motion-analysis-and-data-logging).
|
||||
See the [G-Code reference](G-Codes.md#angle) for available commands.
|
||||
|
||||
|
|
@ -4773,7 +5103,7 @@ See the [G-Code reference](G-Codes.md#angle) for available commands.
|
|||
[angle my_angle_sensor]
|
||||
sensor_type:
|
||||
# The type of the magnetic hall sensor chip. Available choices are
|
||||
# "a1333", "as5047d", and "tle5012b". This parameter must be
|
||||
# "a1333", "as5047d", "mt6816", "mt6826s", and "tle5012b". This parameter must be
|
||||
# specified.
|
||||
#sample_period: 0.000400
|
||||
# The query period (in seconds) to use during measurements. The
|
||||
|
|
@ -4836,8 +5166,9 @@ Most Klipper micro-controller implementations only support an
|
|||
micro-controller supports a 400000 speed (*fast mode*, 400kbit/s), but it must be
|
||||
[set in the operating system](RPi_microcontroller.md#optional-enabling-i2c)
|
||||
and the `i2c_speed` parameter is otherwise ignored. The Klipper
|
||||
"RP2040" micro-controller and ATmega AVR family support a rate of 400000
|
||||
via the `i2c_speed` parameter. All other Klipper micro-controllers use a
|
||||
"RP2040" micro-controller and ATmega AVR family and some STM32
|
||||
(F0, G0, G4, L4, F7, H7) support a rate of 400000 via the `i2c_speed` parameter.
|
||||
All other Klipper micro-controllers use a
|
||||
100000 rate and ignore the `i2c_speed` parameter.
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -190,6 +190,8 @@ represent total number of steps per second on the micro-controller.
|
|||
| AR100 | 3529K | 2507K |
|
||||
| STM32F407 | 3652K | 2459K |
|
||||
| STM32F446 | 3913K | 2634K |
|
||||
| RP2350 | 4167K | 2663K |
|
||||
| SAME70 | 6667K | 4737K |
|
||||
| STM32H743 | 9091K | 6061K |
|
||||
|
||||
If unsure of the micro-controller on a particular board, find the
|
||||
|
|
|
|||
106
docs/G-Codes.md
106
docs/G-Codes.md
|
|
@ -127,6 +127,14 @@ use this tool the Python "numpy" package must be installed (see the
|
|||
[measuring resonance document](Measuring_Resonances.md#software-installation)
|
||||
for more information).
|
||||
|
||||
#### ANGLE_CHIP_CALIBRATE
|
||||
`ANGLE_CHIP_CALIBRATE CHIP=<chip_name>`: Perform internal sensor calibration,
|
||||
if implemented (MT6826S/MT6835).
|
||||
|
||||
- **MT68XX**: The motor should be disconnected
|
||||
from any printer carriage before performing calibration.
|
||||
After calibration, the sensor should be reset by disconnecting the power.
|
||||
|
||||
#### ANGLE_DEBUG_READ
|
||||
`ANGLE_DEBUG_READ CHIP=<config_name> REG=<register>`: Queries sensor
|
||||
register "register" (e.g. 44 or 0x2C). Can be useful for debugging
|
||||
|
|
@ -146,9 +154,19 @@ The following commands are available when the
|
|||
section](Config_Reference.md#axis_twist_compensation) is enabled.
|
||||
|
||||
#### AXIS_TWIST_COMPENSATION_CALIBRATE
|
||||
`AXIS_TWIST_COMPENSATION_CALIBRATE [SAMPLE_COUNT=<value>]`: Initiates the X
|
||||
twist calibration wizard. `SAMPLE_COUNT` specifies the number of points along
|
||||
the X axis to calibrate at and defaults to 3.
|
||||
`AXIS_TWIST_COMPENSATION_CALIBRATE [AXIS=<X|Y>] [AUTO=<True|False>]
|
||||
[SAMPLE_COUNT=<value>]`
|
||||
|
||||
Calibrates axis twist compensation by specifying the target axis or
|
||||
enabling automatic calibration.
|
||||
|
||||
- **AXIS:** Define the axis (`X` or `Y`) for which the twist compensation
|
||||
will be calibrated. If not specified, the axis defaults to `'X'`.
|
||||
|
||||
- **AUTO:** Enables automatic calibration mode. When `AUTO=True`, the
|
||||
calibration will run for both the X and Y axes. In this mode, `AXIS`
|
||||
cannot be specified. If both `AXIS` and `AUTO` are provided, an error
|
||||
will be raised.
|
||||
|
||||
### [bed_mesh]
|
||||
|
||||
|
|
@ -527,6 +545,20 @@ enabled.
|
|||
`SET_FAN_SPEED FAN=config_name SPEED=<speed>` This command sets the
|
||||
speed of a fan. "speed" must be between 0.0 and 1.0.
|
||||
|
||||
`SET_FAN_SPEED PIN=config_name TEMPLATE=<template_name>
|
||||
[<param_x>=<literal>]`: If `TEMPLATE` is specified then it assigns a
|
||||
[display_template](Config_Reference.md#display_template) to the given
|
||||
fan. For example, if one defined a `[display_template
|
||||
my_fan_template]` config section then one could assign
|
||||
`TEMPLATE=my_fan_template` here. The display_template should produce a
|
||||
string containing a floating point number with the desired value. The
|
||||
template will be continuously evaluated and the fan will be
|
||||
automatically set to the resulting speed. One may set display_template
|
||||
parameters to use during template evaluation (parameters will be
|
||||
parsed as Python literals). If TEMPLATE is an empty string then this
|
||||
command will clear any previous template assigned to the pin (one can
|
||||
then use `SET_FAN_SPEED` commands to manage the values directly).
|
||||
|
||||
### [filament_switch_sensor]
|
||||
|
||||
The following command is available when a
|
||||
|
|
@ -908,6 +940,20 @@ output `VALUE`. VALUE should be 0 or 1 for "digital" output pins. For
|
|||
PWM pins, set to a value between 0.0 and 1.0, or between 0.0 and
|
||||
`scale` if a scale is configured in the output_pin config section.
|
||||
|
||||
`SET_PIN PIN=config_name TEMPLATE=<template_name> [<param_x>=<literal>]`:
|
||||
If `TEMPLATE` is specified then it assigns a
|
||||
[display_template](Config_Reference.md#display_template) to the given
|
||||
pin. For example, if one defined a `[display_template
|
||||
my_pin_template]` config section then one could assign
|
||||
`TEMPLATE=my_pin_template` here. The display_template should produce a
|
||||
string containing a floating point number with the desired value. The
|
||||
template will be continuously evaluated and the pin will be
|
||||
automatically set to the resulting value. One may set display_template
|
||||
parameters to use during template evaluation (parameters will be
|
||||
parsed as Python literals). If TEMPLATE is an empty string then this
|
||||
command will clear any previous template assigned to the pin (one can
|
||||
then use `SET_PIN` commands to manage the values directly).
|
||||
|
||||
### [palette2]
|
||||
|
||||
The following commands are available when the
|
||||
|
|
@ -1107,20 +1153,19 @@ is enabled (also see the
|
|||
all enabled accelerometer chips.
|
||||
|
||||
#### TEST_RESONANCES
|
||||
`TEST_RESONANCES AXIS=<axis> OUTPUT=<resonances,raw_data>
|
||||
`TEST_RESONANCES AXIS=<axis> [OUTPUT=<resonances,raw_data>]
|
||||
[NAME=<name>] [FREQ_START=<min_freq>] [FREQ_END=<max_freq>]
|
||||
[HZ_PER_SEC=<hz_per_sec>] [CHIPS=<adxl345_chip_name>]
|
||||
[POINT=x,y,z] [INPUT_SHAPING=[<0:1>]]`: Runs the resonance
|
||||
[ACCEL_PER_HZ=<accel_per_hz>] [HZ_PER_SEC=<hz_per_sec>] [CHIPS=<chip_name>]
|
||||
[POINT=x,y,z] [INPUT_SHAPING=<0:1>]`: Runs the resonance
|
||||
test in all configured probe points for the requested "axis" and
|
||||
measures the acceleration using the accelerometer chips configured for
|
||||
the respective axis. "axis" can either be X or Y, or specify an
|
||||
arbitrary direction as `AXIS=dx,dy`, where dx and dy are floating
|
||||
point numbers defining a direction vector (e.g. `AXIS=X`, `AXIS=Y`, or
|
||||
`AXIS=1,-1` to define a diagonal direction). Note that `AXIS=dx,dy`
|
||||
and `AXIS=-dx,-dy` is equivalent. `adxl345_chip_name` can be one or
|
||||
more configured adxl345 chip,delimited with comma, for example
|
||||
`CHIPS="adxl345, adxl345 rpi"`. Note that `adxl345` can be omitted from
|
||||
named adxl345 chips. If POINT is specified it will override the point(s)
|
||||
and `AXIS=-dx,-dy` is equivalent. `chip_name` can be one or
|
||||
more configured accel chips, delimited with comma, for example
|
||||
`CHIPS="adxl345, adxl345 rpi"`. If POINT is specified it will override the point(s)
|
||||
configured in `[resonance_tester]`. If `INPUT_SHAPING=0` or not set(default),
|
||||
disables input shaping for the resonance testing, because
|
||||
it is not valid to run the resonance testing with the input shaper
|
||||
|
|
@ -1137,8 +1182,9 @@ frequency response is calculated (across all probe points) and written into
|
|||
|
||||
#### SHAPER_CALIBRATE
|
||||
`SHAPER_CALIBRATE [AXIS=<axis>] [NAME=<name>] [FREQ_START=<min_freq>]
|
||||
[FREQ_END=<max_freq>] [HZ_PER_SEC=<hz_per_sec>] [CHIPS=<adxl345_chip_name>]
|
||||
[MAX_SMOOTHING=<max_smoothing>]`: Similarly to `TEST_RESONANCES`, runs
|
||||
[FREQ_END=<max_freq>] [ACCEL_PER_HZ=<accel_per_hz>][HZ_PER_SEC=<hz_per_sec>]
|
||||
[CHIPS=<chip_name>] [MAX_SMOOTHING=<max_smoothing>] [INPUT_SHAPING=<0:1>]`:
|
||||
Similarly to `TEST_RESONANCES`, runs
|
||||
the resonance test as configured, and tries to find the optimal
|
||||
parameters for the input shaper for the requested axis (or both X and
|
||||
Y axes if `AXIS` parameter is unset). If `MAX_SMOOTHING` is unset, its
|
||||
|
|
@ -1466,3 +1512,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
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
# Measuring Resonances
|
||||
|
||||
Klipper has built-in support for the ADXL345, MPU-9250 and LIS2DW compatible
|
||||
Klipper has built-in support for the ADXL345, MPU-9250, LIS2DW and LIS3DH compatible
|
||||
accelerometers which can be used to measure resonance frequencies of the printer
|
||||
for different axes, and auto-tune [input shapers](Resonance_Compensation.md) to
|
||||
compensate for resonances. Note that using accelerometers requires some
|
||||
soldering and crimping. The ADXL345/LIS2DW can be connected to the SPI interface
|
||||
soldering and crimping. The ADXL345 can be connected to the SPI interface
|
||||
of a Raspberry Pi or MCU board (it needs to be reasonably fast). The MPU family can
|
||||
be connected to the I2C interface of a Raspberry Pi directly, or to an I2C
|
||||
interface of an MCU board that supports 400kbit/s *fast mode* in Klipper.
|
||||
interface of an MCU board that supports 400kbit/s *fast mode* in Klipper. The
|
||||
LIS2DW and LIS3DH can be connected to either SPI or I2C with the same considerations
|
||||
as above.
|
||||
|
||||
When sourcing accelerometers, be aware that there are a variety of different PCB
|
||||
board designs and different clones of them. If it is going to be connected to a
|
||||
5V printer MCU ensure it has a voltage regulator and level shifters.
|
||||
|
||||
For ADXL345s/LIS2DWs, make sure that the board supports SPI mode (a small number of
|
||||
For ADXL345s, make sure that the board supports SPI mode (a small number of
|
||||
boards appear to be hard-configured for I2C by pulling SDO to GND).
|
||||
|
||||
For MPU-9250/MPU-9255/MPU-6515/MPU-6050/MPU-6500s there are also a variety of
|
||||
board designs and clones with different I2C pull-up resistors which will need
|
||||
supplementing.
|
||||
For MPU-9250/MPU-9255/MPU-6515/MPU-6050/MPU-6500s and LIS2DW/LIS3DH there are also
|
||||
a variety of board designs and clones with different I2C pull-up resistors which
|
||||
will need supplementing.
|
||||
|
||||
## MCUs with Klipper I2C *fast-mode* Support
|
||||
|
||||
|
|
@ -27,6 +29,7 @@ supplementing.
|
|||
| Raspberry Pi | 3B+, Pico | 3A, 3A+, 3B, 4 |
|
||||
| AVR ATmega | ATmega328p | ATmega32u4, ATmega128, ATmega168, ATmega328, ATmega644p, ATmega1280, ATmega1284, ATmega2560 |
|
||||
| AVR AT90 | - | AT90usb646, AT90usb1286 |
|
||||
| SAMD | SAMC21G18 | SAMC21G18, SAMD21G18, SAMD21E18, SAMD21J18, SAMD21E15, SAMD51G19, SAMD51J19, SAMD51N19, SAMD51P20, SAME51J19, SAME51N19, SAME54P20 |
|
||||
|
||||
## Installation instructions
|
||||
|
||||
|
|
@ -212,12 +215,20 @@ sudo apt install python3-numpy python3-matplotlib libatlas-base-dev libopenblas-
|
|||
|
||||
Next, in order to install NumPy in the Klipper environment, run the command:
|
||||
```
|
||||
~/klippy-env/bin/pip install -v numpy
|
||||
~/klippy-env/bin/pip install -v "numpy<1.26"
|
||||
```
|
||||
Note that, depending on the performance of the CPU, it may take *a lot*
|
||||
of time, up to 10-20 minutes. Be patient and wait for the completion of
|
||||
the installation. On some occasions, if the board has too little RAM
|
||||
the installation may fail and you will need to enable swap.
|
||||
the installation may fail and you will need to enable swap. Also note
|
||||
the forced version, due to newer versions of NumPY having requirements
|
||||
that may not be satisfied in some klipper python environments.
|
||||
|
||||
Once installed please check that no errors show from the command:
|
||||
```
|
||||
~/klippy-env/bin/python -c 'import numpy;'
|
||||
```
|
||||
The correct output should simply be a new line.
|
||||
|
||||
#### Configure ADXL345 With RPi
|
||||
|
||||
|
|
@ -305,7 +316,7 @@ you'll also want to modify your `printer.cfg` file to include this:
|
|||
|
||||
Restart Klipper via the `RESTART` command.
|
||||
|
||||
#### Configure LIS2DW series
|
||||
#### Configure LIS2DW series over SPI
|
||||
|
||||
```
|
||||
[mcu lis]
|
||||
|
|
@ -450,7 +461,11 @@ TEST_RESONANCES AXIS=Y
|
|||
```
|
||||
This will generate 2 CSV files (`/tmp/resonances_x_*.csv` and
|
||||
`/tmp/resonances_y_*.csv`). These files can be processed with the stand-alone
|
||||
script on a Raspberry Pi. To do that, run the following commands:
|
||||
script on a Raspberry Pi. This script is intended to be run with a single CSV
|
||||
file for each axis measured, although it can be used with multiple CSV files
|
||||
if you desire to average the results. Averaging results can be useful, for
|
||||
example, if resonance tests were done at multiple test points. Delete the extra
|
||||
CSV files if you do not desire to average them.
|
||||
```
|
||||
~/klipper/scripts/calibrate_shaper.py /tmp/resonances_x_*.csv -o /tmp/shaper_calibrate_x.png
|
||||
~/klipper/scripts/calibrate_shaper.py /tmp/resonances_y_*.csv -o /tmp/shaper_calibrate_y.png
|
||||
|
|
@ -679,6 +694,24 @@ If you are doing a shaper re-calibration and the reported smoothing for the
|
|||
suggested shaper configuration is almost the same as what you got during the
|
||||
previous calibration, this step can be skipped.
|
||||
|
||||
### Unreliable measurements of resonance frequencies
|
||||
|
||||
Sometimes the resonance measurements can produce bogus results, leading to
|
||||
the incorrect suggestions for the input shapers. This can be caused by a
|
||||
variety of reasons, including running fans on the toolhead, incorrect
|
||||
position or non-rigid mounting of the accelerometer, or mechanical problems
|
||||
such as loose belts or binding or bumpy axis. Keep in mind that all fans
|
||||
should be disabled for resonance testing, especially the noisy ones, and
|
||||
that the accelerometer should be rigidly mounted on the corresponding
|
||||
moving part (e.g. on the bed itself for the bed slinger, or on the extruder
|
||||
of the printer itself and not the carriage, and some people get better
|
||||
results by mounting the accelerometer on the nozzle itself). As for
|
||||
mechanical problems, the user should inspect if there is any fault that
|
||||
can be fixed with a moving axis (e.g. linear guide rails cleaned up and
|
||||
lubricated and V-slot wheels tension adjusted correctly). If none of that
|
||||
helps, a user may try the other shapers from the produced list besides the
|
||||
one recommended by default.
|
||||
|
||||
### Testing custom axes
|
||||
|
||||
`TEST_RESONANCES` command supports custom axes. While this is not really
|
||||
|
|
|
|||
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.serial
|
||||
```
|
||||
Then click "Save".
|
||||
|
||||
_In some older setups this address may be `/tmp/printer`_
|
||||
|
||||
|
||||
Enter the Settings tab again and under "Serial Connection" change the "Serial Port"
|
||||
setting to the one added above.
|
||||
|
||||
In the Settings tab, navigate to the "Behavior" sub-tab and select the
|
||||
"Cancel any ongoing prints but stay connected to the printer" option. Click "Save".
|
||||
|
||||
From the main page, under the "Connection" section (at the top left of the page)
|
||||
make sure the "Serial Port" is set to the new additional one added
|
||||
and click "Connect". (If it is not in the available selection then
|
||||
try reloading the page.)
|
||||
|
||||
Once connected, navigate to the "Terminal" tab and type "status" (without the quotes)
|
||||
into the command entry box and click "Send". The terminal window will likely report
|
||||
there is an error opening the config file - that means OctoPrint is successfully
|
||||
communicating with Klipper.
|
||||
|
||||
Please proceed to [Installation.md](Installation.md) and the
|
||||
_Building and flashing the micro-controller_ section
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ serve the 3D printing community better. Follow them on
|
|||
## Sponsors
|
||||
|
||||
[<img src="./img/sponsors/obico-light-horizontal.png" width="200" style="margin:25px" />](https://obico.io/klipper.html?source=klipper_sponsor)
|
||||
[<img src="./img/sponsors/peopoly-logo.png" width="200" style="margin:25px" />](https://peopoly.net)
|
||||
|
||||
## Klipper Developers
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 20 KiB |
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
# Code for reading and writing the Klipper config file
|
||||
#
|
||||
# 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 sys, os, glob, re, time, logging, configparser, io
|
||||
|
||||
error = configparser.Error
|
||||
|
||||
|
||||
######################################################################
|
||||
# Config section parsing helper
|
||||
######################################################################
|
||||
|
||||
class sentinel:
|
||||
pass
|
||||
|
||||
|
|
@ -69,6 +74,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:
|
||||
|
|
@ -132,30 +139,13 @@ class ConfigWrapper:
|
|||
pconfig = self.printer.lookup_object("configfile")
|
||||
pconfig.deprecate(self.section, option, value, msg)
|
||||
|
||||
AUTOSAVE_HEADER = """
|
||||
#*# <---------------------- SAVE_CONFIG ---------------------->
|
||||
#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated.
|
||||
#*#
|
||||
"""
|
||||
|
||||
class PrinterConfig:
|
||||
def __init__(self, printer):
|
||||
self.printer = printer
|
||||
self.autosave = None
|
||||
self.deprecated = {}
|
||||
self.runtime_warnings = []
|
||||
self.deprecate_warnings = []
|
||||
self.status_raw_config = {}
|
||||
self.status_save_pending = {}
|
||||
self.status_settings = {}
|
||||
self.status_warnings = []
|
||||
self.save_config_pending = False
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command("SAVE_CONFIG", self.cmd_SAVE_CONFIG,
|
||||
desc=self.cmd_SAVE_CONFIG_help)
|
||||
def get_printer(self):
|
||||
return self.printer
|
||||
def _read_config_file(self, filename):
|
||||
######################################################################
|
||||
# Config file parsing (with include file support)
|
||||
######################################################################
|
||||
|
||||
class ConfigFileReader:
|
||||
def read_config_file(self, filename):
|
||||
try:
|
||||
f = open(filename, 'r')
|
||||
data = f.read()
|
||||
|
|
@ -165,6 +155,102 @@ class PrinterConfig:
|
|||
logging.exception(msg)
|
||||
raise error(msg)
|
||||
return data.replace('\r\n', '\n')
|
||||
def build_config_string(self, fileconfig):
|
||||
sfile = io.StringIO()
|
||||
fileconfig.write(sfile)
|
||||
return sfile.getvalue().strip()
|
||||
def append_fileconfig(self, fileconfig, data, filename):
|
||||
if not data:
|
||||
return
|
||||
# Strip trailing comments
|
||||
lines = data.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
pos = line.find('#')
|
||||
if pos >= 0:
|
||||
lines[i] = line[:pos]
|
||||
sbuffer = io.StringIO('\n'.join(lines))
|
||||
if sys.version_info.major >= 3:
|
||||
fileconfig.read_file(sbuffer, filename)
|
||||
else:
|
||||
fileconfig.readfp(sbuffer, filename)
|
||||
def _create_fileconfig(self):
|
||||
if sys.version_info.major >= 3:
|
||||
fileconfig = configparser.RawConfigParser(
|
||||
strict=False, inline_comment_prefixes=(';', '#'))
|
||||
else:
|
||||
fileconfig = configparser.RawConfigParser()
|
||||
return fileconfig
|
||||
def build_fileconfig(self, data, filename):
|
||||
fileconfig = self._create_fileconfig()
|
||||
self.append_fileconfig(fileconfig, data, filename)
|
||||
return fileconfig
|
||||
def _resolve_include(self, source_filename, include_spec, fileconfig,
|
||||
visited):
|
||||
dirname = os.path.dirname(source_filename)
|
||||
include_spec = include_spec.strip()
|
||||
include_glob = os.path.join(dirname, include_spec)
|
||||
include_filenames = glob.glob(include_glob)
|
||||
if not include_filenames and not glob.has_magic(include_glob):
|
||||
# Empty set is OK if wildcard but not for direct file reference
|
||||
raise error("Include file '%s' does not exist" % (include_glob,))
|
||||
include_filenames.sort()
|
||||
for include_filename in include_filenames:
|
||||
include_data = self.read_config_file(include_filename)
|
||||
self._parse_config(include_data, include_filename, fileconfig,
|
||||
visited)
|
||||
return include_filenames
|
||||
def _parse_config(self, data, filename, fileconfig, visited):
|
||||
path = os.path.abspath(filename)
|
||||
if path in visited:
|
||||
raise error("Recursive include of config file '%s'" % (filename))
|
||||
visited.add(path)
|
||||
lines = data.split('\n')
|
||||
# Buffer lines between includes and parse as a unit so that overrides
|
||||
# in includes apply linearly as they do within a single file
|
||||
buf = []
|
||||
for line in lines:
|
||||
# Strip trailing comment
|
||||
pos = line.find('#')
|
||||
if pos >= 0:
|
||||
line = line[:pos]
|
||||
# Process include or buffer line
|
||||
mo = configparser.RawConfigParser.SECTCRE.match(line)
|
||||
header = mo and mo.group('header')
|
||||
if header and header.startswith('include '):
|
||||
self.append_fileconfig(fileconfig, '\n'.join(buf), filename)
|
||||
del buf[:]
|
||||
include_spec = header[8:].strip()
|
||||
self._resolve_include(filename, include_spec, fileconfig,
|
||||
visited)
|
||||
else:
|
||||
buf.append(line)
|
||||
self.append_fileconfig(fileconfig, '\n'.join(buf), filename)
|
||||
visited.remove(path)
|
||||
def build_fileconfig_with_includes(self, data, filename):
|
||||
fileconfig = self._create_fileconfig()
|
||||
self._parse_config(data, filename, fileconfig, set())
|
||||
return fileconfig
|
||||
|
||||
|
||||
######################################################################
|
||||
# Config auto save helper
|
||||
######################################################################
|
||||
|
||||
AUTOSAVE_HEADER = """
|
||||
#*# <---------------------- SAVE_CONFIG ---------------------->
|
||||
#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated.
|
||||
#*#
|
||||
"""
|
||||
|
||||
class ConfigAutoSave:
|
||||
def __init__(self, printer):
|
||||
self.printer = printer
|
||||
self.fileconfig = None
|
||||
self.status_save_pending = {}
|
||||
self.save_config_pending = False
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command("SAVE_CONFIG", self.cmd_SAVE_CONFIG,
|
||||
desc=self.cmd_SAVE_CONFIG_help)
|
||||
def _find_autosave_data(self, data):
|
||||
regular_data = data
|
||||
autosave_data = ""
|
||||
|
|
@ -173,7 +259,7 @@ class PrinterConfig:
|
|||
regular_data = data[:pos]
|
||||
autosave_data = data[pos + len(AUTOSAVE_HEADER):].strip()
|
||||
# Check for errors and strip line prefixes
|
||||
if "\n#*# " in regular_data:
|
||||
if "\n#*# " in regular_data or autosave_data.find(AUTOSAVE_HEADER) >= 0:
|
||||
logging.warning("Can't read autosave from config file"
|
||||
" - autosave state corrupted")
|
||||
return data, ""
|
||||
|
|
@ -190,7 +276,7 @@ class PrinterConfig:
|
|||
return regular_data, "\n".join(out)
|
||||
comment_r = re.compile('[#;].*$')
|
||||
value_r = re.compile('[^A-Za-z0-9_].*$')
|
||||
def _strip_duplicates(self, data, config):
|
||||
def _strip_duplicates(self, data, fileconfig):
|
||||
# Comment out fields in 'data' that are defined in 'config'
|
||||
lines = data.split('\n')
|
||||
section = None
|
||||
|
|
@ -208,152 +294,31 @@ class PrinterConfig:
|
|||
section = pruned_line[1:-1].strip()
|
||||
continue
|
||||
field = self.value_r.sub('', pruned_line)
|
||||
if config.fileconfig.has_option(section, field):
|
||||
if fileconfig.has_option(section, field):
|
||||
is_dup_field = True
|
||||
lines[lineno] = '#' + lines[lineno]
|
||||
return "\n".join(lines)
|
||||
def _parse_config_buffer(self, buffer, filename, fileconfig):
|
||||
if not buffer:
|
||||
return
|
||||
data = '\n'.join(buffer)
|
||||
del buffer[:]
|
||||
sbuffer = io.StringIO(data)
|
||||
if sys.version_info.major >= 3:
|
||||
fileconfig.read_file(sbuffer, filename)
|
||||
else:
|
||||
fileconfig.readfp(sbuffer, filename)
|
||||
def _resolve_include(self, source_filename, include_spec, fileconfig,
|
||||
visited):
|
||||
dirname = os.path.dirname(source_filename)
|
||||
include_spec = include_spec.strip()
|
||||
include_glob = os.path.join(dirname, include_spec)
|
||||
include_filenames = glob.glob(include_glob)
|
||||
if not include_filenames and not glob.has_magic(include_glob):
|
||||
# Empty set is OK if wildcard but not for direct file reference
|
||||
raise error("Include file '%s' does not exist" % (include_glob,))
|
||||
include_filenames.sort()
|
||||
for include_filename in include_filenames:
|
||||
include_data = self._read_config_file(include_filename)
|
||||
self._parse_config(include_data, include_filename, fileconfig,
|
||||
visited)
|
||||
return include_filenames
|
||||
def _parse_config(self, data, filename, fileconfig, visited):
|
||||
path = os.path.abspath(filename)
|
||||
if path in visited:
|
||||
raise error("Recursive include of config file '%s'" % (filename))
|
||||
visited.add(path)
|
||||
lines = data.split('\n')
|
||||
# Buffer lines between includes and parse as a unit so that overrides
|
||||
# in includes apply linearly as they do within a single file
|
||||
buffer = []
|
||||
for line in lines:
|
||||
# Strip trailing comment
|
||||
pos = line.find('#')
|
||||
if pos >= 0:
|
||||
line = line[:pos]
|
||||
# Process include or buffer line
|
||||
mo = configparser.RawConfigParser.SECTCRE.match(line)
|
||||
header = mo and mo.group('header')
|
||||
if header and header.startswith('include '):
|
||||
self._parse_config_buffer(buffer, filename, fileconfig)
|
||||
include_spec = header[8:].strip()
|
||||
self._resolve_include(filename, include_spec, fileconfig,
|
||||
visited)
|
||||
else:
|
||||
buffer.append(line)
|
||||
self._parse_config_buffer(buffer, filename, fileconfig)
|
||||
visited.remove(path)
|
||||
def _build_config_wrapper(self, data, filename):
|
||||
if sys.version_info.major >= 3:
|
||||
fileconfig = configparser.RawConfigParser(
|
||||
strict=False, inline_comment_prefixes=(';', '#'))
|
||||
else:
|
||||
fileconfig = configparser.RawConfigParser()
|
||||
self._parse_config(data, filename, fileconfig, set())
|
||||
return ConfigWrapper(self.printer, fileconfig, {}, 'printer')
|
||||
def _build_config_string(self, config):
|
||||
sfile = io.StringIO()
|
||||
config.fileconfig.write(sfile)
|
||||
return sfile.getvalue().strip()
|
||||
def read_config(self, filename):
|
||||
return self._build_config_wrapper(self._read_config_file(filename),
|
||||
filename)
|
||||
def read_main_config(self):
|
||||
def load_main_config(self):
|
||||
filename = self.printer.get_start_args()['config_file']
|
||||
data = self._read_config_file(filename)
|
||||
cfgrdr = ConfigFileReader()
|
||||
data = cfgrdr.read_config_file(filename)
|
||||
regular_data, autosave_data = self._find_autosave_data(data)
|
||||
regular_config = self._build_config_wrapper(regular_data, filename)
|
||||
autosave_data = self._strip_duplicates(autosave_data, regular_config)
|
||||
self.autosave = self._build_config_wrapper(autosave_data, filename)
|
||||
cfg = self._build_config_wrapper(regular_data + autosave_data, filename)
|
||||
return cfg
|
||||
def check_unused_options(self, config):
|
||||
fileconfig = config.fileconfig
|
||||
objects = dict(self.printer.lookup_objects())
|
||||
# Determine all the fields that have been accessed
|
||||
access_tracking = dict(config.access_tracking)
|
||||
for section in self.autosave.fileconfig.sections():
|
||||
for option in self.autosave.fileconfig.options(section):
|
||||
access_tracking[(section.lower(), option.lower())] = 1
|
||||
# Validate that there are no undefined parameters in the config file
|
||||
valid_sections = { s: 1 for s, o in access_tracking }
|
||||
for section_name in fileconfig.sections():
|
||||
section = section_name.lower()
|
||||
if section not in valid_sections and section not in objects:
|
||||
raise error("Section '%s' is not a valid config section"
|
||||
% (section,))
|
||||
for option in fileconfig.options(section_name):
|
||||
option = option.lower()
|
||||
if (section, option) not in access_tracking:
|
||||
raise error("Option '%s' is not valid in section '%s'"
|
||||
% (option, section))
|
||||
# Setup get_status()
|
||||
self._build_status(config)
|
||||
def log_config(self, config):
|
||||
lines = ["===== Config file =====",
|
||||
self._build_config_string(config),
|
||||
"======================="]
|
||||
self.printer.set_rollover_info("config", "\n".join(lines))
|
||||
# Status reporting
|
||||
def runtime_warning(self, msg):
|
||||
logging.warn(msg)
|
||||
res = {'type': 'runtime_warning', 'message': msg}
|
||||
self.runtime_warnings.append(res)
|
||||
self.status_warnings = self.runtime_warnings + self.deprecate_warnings
|
||||
def deprecate(self, section, option, value=None, msg=None):
|
||||
self.deprecated[(section, option, value)] = msg
|
||||
def _build_status(self, config):
|
||||
self.status_raw_config.clear()
|
||||
for section in config.get_prefix_sections(''):
|
||||
self.status_raw_config[section.get_name()] = section_status = {}
|
||||
for option in section.get_prefix_options(''):
|
||||
section_status[option] = section.get(option, note_valid=False)
|
||||
self.status_settings = {}
|
||||
for (section, option), value in config.access_tracking.items():
|
||||
self.status_settings.setdefault(section, {})[option] = value
|
||||
self.deprecate_warnings = []
|
||||
for (section, option, value), msg in self.deprecated.items():
|
||||
if value is None:
|
||||
res = {'type': 'deprecated_option'}
|
||||
else:
|
||||
res = {'type': 'deprecated_value', 'value': value}
|
||||
res['message'] = msg
|
||||
res['section'] = section
|
||||
res['option'] = option
|
||||
self.deprecate_warnings.append(res)
|
||||
self.status_warnings = self.runtime_warnings + self.deprecate_warnings
|
||||
regular_fileconfig = cfgrdr.build_fileconfig_with_includes(
|
||||
regular_data, filename)
|
||||
autosave_data = self._strip_duplicates(autosave_data,
|
||||
regular_fileconfig)
|
||||
self.fileconfig = cfgrdr.build_fileconfig(autosave_data, filename)
|
||||
cfgrdr.append_fileconfig(regular_fileconfig,
|
||||
autosave_data, '*AUTOSAVE*')
|
||||
return regular_fileconfig, self.fileconfig
|
||||
def get_status(self, eventtime):
|
||||
return {'config': self.status_raw_config,
|
||||
'settings': self.status_settings,
|
||||
'warnings': self.status_warnings,
|
||||
'save_config_pending': self.save_config_pending,
|
||||
return {'save_config_pending': self.save_config_pending,
|
||||
'save_config_pending_items': self.status_save_pending}
|
||||
# Autosave functions
|
||||
def set(self, section, option, value):
|
||||
if not self.autosave.fileconfig.has_section(section):
|
||||
self.autosave.fileconfig.add_section(section)
|
||||
if not self.fileconfig.has_section(section):
|
||||
self.fileconfig.add_section(section)
|
||||
svalue = str(value)
|
||||
self.autosave.fileconfig.set(section, option, svalue)
|
||||
self.fileconfig.set(section, option, svalue)
|
||||
pending = dict(self.status_save_pending)
|
||||
if not section in pending or pending[section] is None:
|
||||
pending[section] = {}
|
||||
|
|
@ -364,8 +329,8 @@ class PrinterConfig:
|
|||
self.save_config_pending = True
|
||||
logging.info("save_config: set [%s] %s = %s", section, option, svalue)
|
||||
def remove_section(self, section):
|
||||
if self.autosave.fileconfig.has_section(section):
|
||||
self.autosave.fileconfig.remove_section(section)
|
||||
if self.fileconfig.has_section(section):
|
||||
self.fileconfig.remove_section(section)
|
||||
pending = dict(self.status_save_pending)
|
||||
pending[section] = None
|
||||
self.status_save_pending = pending
|
||||
|
|
@ -376,21 +341,20 @@ class PrinterConfig:
|
|||
del pending[section]
|
||||
self.status_save_pending = pending
|
||||
self.save_config_pending = True
|
||||
def _disallow_include_conflicts(self, regular_data, cfgname, gcode):
|
||||
config = self._build_config_wrapper(regular_data, cfgname)
|
||||
for section in self.autosave.fileconfig.sections():
|
||||
for option in self.autosave.fileconfig.options(section):
|
||||
if config.fileconfig.has_option(section, option):
|
||||
def _disallow_include_conflicts(self, regular_fileconfig):
|
||||
for section in self.fileconfig.sections():
|
||||
for option in self.fileconfig.options(section):
|
||||
if regular_fileconfig.has_option(section, option):
|
||||
msg = ("SAVE_CONFIG section '%s' option '%s' conflicts "
|
||||
"with included value" % (section, option))
|
||||
raise gcode.error(msg)
|
||||
raise self.printer.command_error(msg)
|
||||
cmd_SAVE_CONFIG_help = "Overwrite config file and restart"
|
||||
def cmd_SAVE_CONFIG(self, gcmd):
|
||||
if not self.autosave.fileconfig.sections():
|
||||
if not self.fileconfig.sections():
|
||||
return
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
# Create string containing autosave data
|
||||
autosave_data = self._build_config_string(self.autosave)
|
||||
cfgrdr = ConfigFileReader()
|
||||
autosave_data = cfgrdr.build_config_string(self.fileconfig)
|
||||
lines = [('#*# ' + l).strip()
|
||||
for l in autosave_data.split('\n')]
|
||||
lines.insert(0, "\n" + AUTOSAVE_HEADER.rstrip())
|
||||
|
|
@ -399,16 +363,27 @@ class PrinterConfig:
|
|||
# Read in and validate current config file
|
||||
cfgname = self.printer.get_start_args()['config_file']
|
||||
try:
|
||||
data = self._read_config_file(cfgname)
|
||||
regular_data, old_autosave_data = self._find_autosave_data(data)
|
||||
config = self._build_config_wrapper(regular_data, cfgname)
|
||||
data = cfgrdr.read_config_file(cfgname)
|
||||
except error as e:
|
||||
msg = "Unable to read existing config on SAVE_CONFIG"
|
||||
logging.exception(msg)
|
||||
raise gcmd.error(msg)
|
||||
regular_data, old_autosave_data = self._find_autosave_data(data)
|
||||
regular_data = self._strip_duplicates(regular_data, self.fileconfig)
|
||||
data = regular_data.rstrip() + autosave_data
|
||||
new_regular_data, new_autosave_data = self._find_autosave_data(data)
|
||||
if not new_autosave_data:
|
||||
raise gcmd.error(
|
||||
"Existing config autosave is corrupted."
|
||||
" Can't complete SAVE_CONFIG")
|
||||
try:
|
||||
regular_fileconfig = cfgrdr.build_fileconfig_with_includes(
|
||||
new_regular_data, cfgname)
|
||||
except error as e:
|
||||
msg = "Unable to parse existing config on SAVE_CONFIG"
|
||||
logging.exception(msg)
|
||||
raise gcode.error(msg)
|
||||
regular_data = self._strip_duplicates(regular_data, self.autosave)
|
||||
self._disallow_include_conflicts(regular_data, cfgname, gcode)
|
||||
data = regular_data.rstrip() + autosave_data
|
||||
raise gcmd.error(msg)
|
||||
self._disallow_include_conflicts(regular_fileconfig)
|
||||
# Determine filenames
|
||||
datestr = time.strftime("-%Y%m%d_%H%M%S")
|
||||
backup_name = cfgname + datestr
|
||||
|
|
@ -428,6 +403,135 @@ class PrinterConfig:
|
|||
except:
|
||||
msg = "Unable to write config file during SAVE_CONFIG"
|
||||
logging.exception(msg)
|
||||
raise gcode.error(msg)
|
||||
raise gcmd.error(msg)
|
||||
# Request a restart
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.request_restart('restart')
|
||||
|
||||
|
||||
######################################################################
|
||||
# Config validation (check for undefined options)
|
||||
######################################################################
|
||||
|
||||
class ConfigValidate:
|
||||
def __init__(self, printer):
|
||||
self.printer = printer
|
||||
self.status_settings = {}
|
||||
self.access_tracking = {}
|
||||
self.autosave_options = {}
|
||||
def start_access_tracking(self, autosave_fileconfig):
|
||||
# Note autosave options for use during undefined options check
|
||||
self.autosave_options = {}
|
||||
for section in autosave_fileconfig.sections():
|
||||
for option in autosave_fileconfig.options(section):
|
||||
self.autosave_options[(section.lower(), option.lower())] = 1
|
||||
self.access_tracking = {}
|
||||
return self.access_tracking
|
||||
def check_unused(self, fileconfig):
|
||||
# Don't warn on fields set in autosave segment
|
||||
access_tracking = dict(self.access_tracking)
|
||||
access_tracking.update(self.autosave_options)
|
||||
# Note locally used sections
|
||||
valid_sections = { s: 1 for s, o in self.printer.lookup_objects() }
|
||||
valid_sections.update({ s: 1 for s, o in access_tracking })
|
||||
# Validate that there are no undefined parameters in the config file
|
||||
for section_name in fileconfig.sections():
|
||||
section = section_name.lower()
|
||||
if section not in valid_sections:
|
||||
raise error("Section '%s' is not a valid config section"
|
||||
% (section,))
|
||||
for option in fileconfig.options(section_name):
|
||||
option = option.lower()
|
||||
if (section, option) not in access_tracking:
|
||||
raise error("Option '%s' is not valid in section '%s'"
|
||||
% (option, section))
|
||||
# Setup get_status()
|
||||
self._build_status_settings()
|
||||
# Clear tracking state
|
||||
self.access_tracking.clear()
|
||||
self.autosave_options.clear()
|
||||
def _build_status_settings(self):
|
||||
self.status_settings = {}
|
||||
for (section, option), value in self.access_tracking.items():
|
||||
self.status_settings.setdefault(section, {})[option] = value
|
||||
def get_status(self, eventtime):
|
||||
return {'settings': self.status_settings}
|
||||
|
||||
|
||||
######################################################################
|
||||
# Main printer config tracking
|
||||
######################################################################
|
||||
|
||||
class PrinterConfig:
|
||||
def __init__(self, printer):
|
||||
self.printer = printer
|
||||
self.autosave = ConfigAutoSave(printer)
|
||||
self.validate = ConfigValidate(printer)
|
||||
self.deprecated = {}
|
||||
self.runtime_warnings = []
|
||||
self.deprecate_warnings = []
|
||||
self.status_raw_config = {}
|
||||
self.status_warnings = []
|
||||
def get_printer(self):
|
||||
return self.printer
|
||||
def read_config(self, filename):
|
||||
cfgrdr = ConfigFileReader()
|
||||
data = cfgrdr.read_config_file(filename)
|
||||
fileconfig = cfgrdr.build_fileconfig(data, filename)
|
||||
return ConfigWrapper(self.printer, fileconfig, {}, 'printer')
|
||||
def read_main_config(self):
|
||||
fileconfig, autosave_fileconfig = self.autosave.load_main_config()
|
||||
access_tracking = self.validate.start_access_tracking(
|
||||
autosave_fileconfig)
|
||||
config = ConfigWrapper(self.printer, fileconfig,
|
||||
access_tracking, 'printer')
|
||||
self._build_status_config(config)
|
||||
return config
|
||||
def log_config(self, config):
|
||||
cfgrdr = ConfigFileReader()
|
||||
lines = ["===== Config file =====",
|
||||
cfgrdr.build_config_string(config.fileconfig),
|
||||
"======================="]
|
||||
self.printer.set_rollover_info("config", "\n".join(lines))
|
||||
def check_unused_options(self, config):
|
||||
self.validate.check_unused(config.fileconfig)
|
||||
# Deprecation warnings
|
||||
def runtime_warning(self, msg):
|
||||
logging.warning(msg)
|
||||
res = {'type': 'runtime_warning', 'message': msg}
|
||||
self.runtime_warnings.append(res)
|
||||
self.status_warnings = self.runtime_warnings + self.deprecate_warnings
|
||||
def deprecate(self, section, option, value=None, msg=None):
|
||||
key = (section, option, value)
|
||||
if key in self.deprecated and self.deprecated[key] == msg:
|
||||
return
|
||||
self.deprecated[key] = msg
|
||||
self.deprecate_warnings = []
|
||||
for (section, option, value), msg in self.deprecated.items():
|
||||
if value is None:
|
||||
res = {'type': 'deprecated_option'}
|
||||
else:
|
||||
res = {'type': 'deprecated_value', 'value': value}
|
||||
res['message'] = msg
|
||||
res['section'] = section
|
||||
res['option'] = option
|
||||
self.deprecate_warnings.append(res)
|
||||
self.status_warnings = self.runtime_warnings + self.deprecate_warnings
|
||||
# Status reporting
|
||||
def _build_status_config(self, config):
|
||||
self.status_raw_config = {}
|
||||
for section in config.get_prefix_sections(''):
|
||||
self.status_raw_config[section.get_name()] = section_status = {}
|
||||
for option in section.get_prefix_options(''):
|
||||
section_status[option] = section.get(option, note_valid=False)
|
||||
def get_status(self, eventtime):
|
||||
status = {'config': self.status_raw_config,
|
||||
'warnings': self.status_warnings}
|
||||
status.update(self.autosave.get_status(eventtime))
|
||||
status.update(self.validate.get_status(eventtime))
|
||||
return status
|
||||
# Autosave functions
|
||||
def set(self, section, option, value):
|
||||
self.autosave.set(section, option, value)
|
||||
def remove_section(self, section):
|
||||
self.autosave.remove_section(section)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
||||
######################################################################
|
||||
|
|
|
|||
216
klippy/extras/ads1220.py
Normal file
216
klippy/extras/ads1220.py
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
# 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('sample_rate', self.sps_options,
|
||||
default='660')
|
||||
self.is_turbo = str(self.sps) in self.sps_turbo
|
||||
# Input multiplexer: AINP and AINN
|
||||
mux_options = {'AIN0_AIN1': 0b0000, 'AIN0_AIN2': 0b0001,
|
||||
'AIN0_AIN3': 0b0010, 'AIN1_AIN2': 0b0011,
|
||||
'AIN1_AIN3': 0b0100, 'AIN2_AIN3': 0b0101,
|
||||
'AIN1_AIN0': 0b0110, 'AIN3_AIN2': 0b0111,
|
||||
'AIN0_AVSS': 0b1000, 'AIN1_AVSS': 0b1001,
|
||||
'AIN2_AVSS': 0b1010, 'AIN3_AVSS': 0b1011}
|
||||
self.mux = config.getchoice('input_mux', mux_options,
|
||||
default='AIN0_AIN1')
|
||||
# PGA Bypass
|
||||
self.pga_bypass = config.getboolean('pga_bypass', default=False)
|
||||
# bypass PGA when AVSS is the negative input
|
||||
force_pga_bypass = self.mux >= 0b1000
|
||||
self.pga_bypass = force_pga_bypass or self.pga_bypass
|
||||
# Voltage Reference
|
||||
self.vref_options = {'internal': 0b0, 'REF0': 0b01, 'REF1': 0b10,
|
||||
'analog_supply': 0b11}
|
||||
self.vref = config.getchoice('vref', self.vref_options,
|
||||
default='internal')
|
||||
# check for conflict between REF1 and AIN0/AIN3
|
||||
mux_conflict = [0b0000, 0b0001, 0b0010, 0b0100, 0b0101, 0b0110, 0b0111,
|
||||
0b1000, 0b1011]
|
||||
if self.vref == 0b10 and self.mux in mux_conflict:
|
||||
raise config.error("ADS1220 config error: AIN0/REFP1 and AIN3/REFN1"
|
||||
" cant be used as a voltage reference and"
|
||||
" an input at the same time")
|
||||
# SPI Setup
|
||||
spi_speed = 512000 if self.is_turbo else 256000
|
||||
self.spi = bus.MCU_SPI_from_config(config, 1, default_speed=spi_speed)
|
||||
self.mcu = mcu = self.spi.get_mcu()
|
||||
self.oid = mcu.create_oid()
|
||||
# Data Ready (DRDY) Pin
|
||||
drdy_pin = config.get('data_ready_pin')
|
||||
ppins = printer.lookup_object('pins')
|
||||
drdy_ppin = ppins.lookup_pin(drdy_pin)
|
||||
self.data_ready_pin = drdy_ppin['pin']
|
||||
drdy_pin_mcu = drdy_ppin['chip']
|
||||
if drdy_pin_mcu != self.mcu:
|
||||
raise config.error("ADS1220 config error: SPI communication and"
|
||||
" data_ready_pin must be on the same MCU")
|
||||
# Bulk Sensor Setup
|
||||
self.bulk_queue = bulk_sensor.BulkDataQueue(self.mcu, oid=self.oid)
|
||||
# Clock tracking
|
||||
chip_smooth = self.sps * UPDATE_INTERVAL * 2
|
||||
# Measurement conversion
|
||||
self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, "<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.mux << 4) | (self.gain << 1) | int(self.pga_bypass),
|
||||
(data_rate << 5) | (mode << 3) | (continuous << 2),
|
||||
(self.vref << 6),
|
||||
0x0]
|
||||
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 bytearray(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}
|
||||
|
|
@ -176,9 +176,9 @@ class AccelCommandHelper:
|
|||
self.chip.set_reg(reg, val)
|
||||
|
||||
# Helper to read the axes_map parameter from the config
|
||||
def read_axes_map(config):
|
||||
am = {'x': (0, SCALE_XY), 'y': (1, SCALE_XY), 'z': (2, SCALE_Z),
|
||||
'-x': (0, -SCALE_XY), '-y': (1, -SCALE_XY), '-z': (2, -SCALE_Z)}
|
||||
def read_axes_map(config, scale_x, scale_y, scale_z):
|
||||
am = {'x': (0, scale_x), 'y': (1, scale_y), 'z': (2, scale_z),
|
||||
'-x': (0, -scale_x), '-y': (1, -scale_y), '-z': (2, -scale_z)}
|
||||
axes_map = config.getlist('axes_map', ('x','y','z'), count=3)
|
||||
if any([a not in am for a in axes_map]):
|
||||
raise config.error("Invalid axes_map parameter")
|
||||
|
|
@ -191,7 +191,7 @@ class ADXL345:
|
|||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
AccelCommandHelper(config, self)
|
||||
self.axes_map = read_axes_map(config)
|
||||
self.axes_map = read_axes_map(config, SCALE_XY, SCALE_XY, SCALE_Z)
|
||||
self.data_rate = config.getint('rate', 3200)
|
||||
if self.data_rate not in QUERY_RATES:
|
||||
raise config.error("Invalid rate parameter: %d" % (self.data_rate,))
|
||||
|
|
|
|||
|
|
@ -411,6 +411,196 @@ class HelperTLE5012B:
|
|||
parser=lambda x: int(x, 0))
|
||||
self._write_reg(reg, val)
|
||||
|
||||
class HelperMT6816:
|
||||
SPI_MODE = 3
|
||||
SPI_SPEED = 10000000
|
||||
def __init__(self, config, spi, oid):
|
||||
self.printer = config.get_printer()
|
||||
self.spi = spi
|
||||
self.oid = oid
|
||||
self.mcu = spi.get_mcu()
|
||||
self.mcu.register_config_callback(self._build_config)
|
||||
self.spi_angle_transfer_cmd = None
|
||||
self.is_tcode_absolute = False
|
||||
self.last_temperature = None
|
||||
name = config.get_name().split()[-1]
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
gcode.register_mux_command("ANGLE_DEBUG_READ", "CHIP", name,
|
||||
self.cmd_ANGLE_DEBUG_READ,
|
||||
desc=self.cmd_ANGLE_DEBUG_READ_help)
|
||||
def _build_config(self):
|
||||
cmdqueue = self.spi.get_command_queue()
|
||||
self.spi_angle_transfer_cmd = self.mcu.lookup_query_command(
|
||||
"spi_angle_transfer oid=%c data=%*s",
|
||||
"spi_angle_transfer_response oid=%c clock=%u response=%*s",
|
||||
oid=self.oid, cq=cmdqueue)
|
||||
def _send_spi(self, msg):
|
||||
return self.spi.spi_transfer(msg)
|
||||
def get_static_delay(self):
|
||||
return .000001
|
||||
def _read_reg(self, reg):
|
||||
msg = [reg, 0, 0]
|
||||
params = self._send_spi(msg)
|
||||
resp = bytearray(params['response'])
|
||||
val = (resp[1] << 8) | resp[2]
|
||||
return val
|
||||
def start(self):
|
||||
pass
|
||||
cmd_ANGLE_DEBUG_READ_help = "Query low-level angle sensor register"
|
||||
def cmd_ANGLE_DEBUG_READ(self, gcmd):
|
||||
reg = 0x83
|
||||
val = self._read_reg(reg)
|
||||
gcmd.respond_info("ANGLE REG[0x%02x] = 0x%04x" % (reg, val))
|
||||
angle = val >> 2
|
||||
parity = bin(val >> 1).count("1") % 2
|
||||
gcmd.respond_info("Angle %i ~ %.2f" % (angle, angle * 360 / (1 << 14)))
|
||||
gcmd.respond_info("No Mag: %i" % (val >> 1 & 0x1))
|
||||
gcmd.respond_info("Parity: %i == %i" % (parity, val & 0x1))
|
||||
|
||||
class HelperMT6826S:
|
||||
SPI_MODE = 3
|
||||
SPI_SPEED = 10000000
|
||||
def __init__(self, config, spi, oid):
|
||||
self.printer = config.get_printer()
|
||||
self.stepper_name = config.get('stepper', None)
|
||||
self.spi = spi
|
||||
self.oid = oid
|
||||
self.mcu = spi.get_mcu()
|
||||
self.mcu.register_config_callback(self._build_config)
|
||||
self.spi_angle_transfer_cmd = None
|
||||
self.is_tcode_absolute = False
|
||||
self.last_temperature = None
|
||||
name = config.get_name().split()[-1]
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
gcode.register_mux_command("ANGLE_DEBUG_READ", "CHIP", name,
|
||||
self.cmd_ANGLE_DEBUG_READ,
|
||||
desc=self.cmd_ANGLE_DEBUG_READ_help)
|
||||
gcode.register_mux_command("ANGLE_CHIP_CALIBRATE", "CHIP", name,
|
||||
self.cmd_ANGLE_CHIP_CALIBRATE,
|
||||
desc=self.cmd_ANGLE_CHIP_CALIBRATE_help)
|
||||
self.status_map = {
|
||||
0: "No Calibration",
|
||||
1: "Running Calibration",
|
||||
2: "Calibration Failed",
|
||||
3: "Calibration Successful"
|
||||
}
|
||||
def _build_config(self):
|
||||
cmdqueue = self.spi.get_command_queue()
|
||||
self.spi_angle_transfer_cmd = self.mcu.lookup_query_command(
|
||||
"spi_angle_transfer oid=%c data=%*s",
|
||||
"spi_angle_transfer_response oid=%c clock=%u response=%*s",
|
||||
oid=self.oid, cq=cmdqueue)
|
||||
def _send_spi(self, msg):
|
||||
params = self.spi.spi_transfer(msg)
|
||||
return params
|
||||
def get_static_delay(self):
|
||||
return .00001
|
||||
def _read_reg(self, reg):
|
||||
reg = 0x3000 | reg
|
||||
msg = [reg >> 8, reg & 0xff, 0]
|
||||
params = self._send_spi(msg)
|
||||
resp = bytearray(params['response'])
|
||||
return resp[2]
|
||||
def _write_reg(self, reg, data):
|
||||
reg = 0x6000 | reg
|
||||
msg = [reg >> 8, reg & 0xff, data]
|
||||
self._send_spi(msg)
|
||||
def crc8(self, data):
|
||||
polynomial = 0x07
|
||||
crc = 0x00
|
||||
for byte in data:
|
||||
crc ^= byte
|
||||
for _ in range(8):
|
||||
if crc & 0x80:
|
||||
crc = (crc << 1) ^ polynomial
|
||||
else:
|
||||
crc <<= 1
|
||||
crc &= 0xFF
|
||||
return crc
|
||||
def _read_angle(self, reg):
|
||||
reg = 0x3000 | reg
|
||||
msg = [reg >> 8, reg & 0xff, 0, 0, 0, 0]
|
||||
params = self._send_spi(msg)
|
||||
resp = bytearray(params['response'])
|
||||
angle = (resp[2] << 7) | (resp[3] >> 1)
|
||||
status = resp[4]
|
||||
crc_computed = self.crc8([resp[2], resp[3], resp[4]])
|
||||
crc = resp[5]
|
||||
return angle, status, crc, crc_computed
|
||||
def start(self):
|
||||
val = self._read_reg(0x00d)
|
||||
# Set histeresis to 0.003 degree
|
||||
self._write_reg(0x00d, (val & 0xf8) | 0x5)
|
||||
def get_microsteps(self):
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
sconfig = configfile.get_status(None)['settings']
|
||||
stconfig = sconfig.get(self.stepper_name, {})
|
||||
microsteps = stconfig['microsteps']
|
||||
full_steps = stconfig['full_steps_per_rotation']
|
||||
return microsteps, full_steps
|
||||
cmd_ANGLE_CHIP_CALIBRATE_help = "Run MT6826s calibration sequence"
|
||||
def cmd_ANGLE_CHIP_CALIBRATE(self, gcmd):
|
||||
fmove = self.printer.lookup_object('force_move')
|
||||
mcu_stepper = fmove.lookup_stepper(self.stepper_name)
|
||||
if self.stepper_name is None:
|
||||
gcmd.respond_info("stepper not defined")
|
||||
return
|
||||
|
||||
gcmd.respond_info("MT6826S Run calibration sequence")
|
||||
gcmd.respond_info("Motor will do 18+ rotations -" +
|
||||
" ensure pulley is disconnected")
|
||||
req_freq = self._read_reg(0x00e) >> 4 & 0x7
|
||||
# Minimal calibration speed
|
||||
rpm = (3200 >> req_freq) + 1
|
||||
rps = rpm / 60
|
||||
move = fmove.manual_move
|
||||
# Move stepper several turns (to allow internal sensor calibration)
|
||||
microsteps, full_steps = self.get_microsteps()
|
||||
step_dist = mcu_stepper.get_step_dist()
|
||||
full_step_dist = step_dist * microsteps
|
||||
rotation_dist = full_steps * full_step_dist
|
||||
move(mcu_stepper, 2 * rotation_dist, rps * rotation_dist)
|
||||
self._write_reg(0x155, 0x5e)
|
||||
move(mcu_stepper, 20 * rotation_dist, rps * rotation_dist)
|
||||
val = self._read_reg(0x113)
|
||||
code = val >> 6
|
||||
gcmd.respond_info("Status: %s" % (self.status_map[code]))
|
||||
while code == 1:
|
||||
move(mcu_stepper, 5 * rotation_dist, rps * rotation_dist)
|
||||
val = self._read_reg(0x113)
|
||||
code = val >> 6
|
||||
gcmd.respond_info("Status: %s" % (self.status_map[code]))
|
||||
if code == 2:
|
||||
gcmd.respond_info("Calibration failed")
|
||||
if code == 3:
|
||||
gcmd.respond_info("Calibration success, please poweroff sensor")
|
||||
cmd_ANGLE_DEBUG_READ_help = "Query low-level angle sensor register"
|
||||
def cmd_ANGLE_DEBUG_READ(self, gcmd):
|
||||
reg = gcmd.get("REG", minval=0, maxval=0x155,
|
||||
parser=lambda x: int(x, 0))
|
||||
if reg == 0x003:
|
||||
angle, status, crc1, crc2 = self._read_angle(reg)
|
||||
gcmd.respond_info("ANGLE REG[0x003] = 0x%02x" %
|
||||
(angle >> 7))
|
||||
gcmd.respond_info("ANGLE REG[0x004] = 0x%02x" %
|
||||
((angle << 1) & 0xff))
|
||||
gcmd.respond_info("Angle %i ~ %.2f" % (angle,
|
||||
angle * 360 / (1 << 15)))
|
||||
gcmd.respond_info("Weak Mag: %i" % (status >> 1 & 0x1))
|
||||
gcmd.respond_info("Under Voltage: %i" % (status >> 2 & 0x1))
|
||||
gcmd.respond_info("CRC: 0x%02x == 0x%02x" % (crc1, crc2))
|
||||
elif reg == 0x00e:
|
||||
val = self._read_reg(reg)
|
||||
gcmd.respond_info("GPIO_DS = %i" % (val >> 7))
|
||||
gcmd.respond_info("AUTOCAL_FREQ = %i" % (val >> 4 & 0x7))
|
||||
elif reg == 0x113:
|
||||
val = self._read_reg(reg)
|
||||
gcmd.respond_info("Status: %s" % (self.cal_status[val >> 6]))
|
||||
else:
|
||||
val = self._read_reg(reg)
|
||||
gcmd.respond_info("REG[0x%04x] = 0x%02x" % (reg, val))
|
||||
|
||||
|
||||
BYTES_PER_SAMPLE = 3
|
||||
SAMPLES_PER_BLOCK = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE
|
||||
|
||||
|
|
@ -427,8 +617,11 @@ class Angle:
|
|||
self.start_clock = self.time_shift = self.sample_ticks = 0
|
||||
self.last_sequence = self.last_angle = 0
|
||||
# Sensor type
|
||||
sensors = { "a1333": HelperA1333, "as5047d": HelperAS5047D,
|
||||
"tle5012b": HelperTLE5012B }
|
||||
sensors = { "a1333": HelperA1333,
|
||||
"as5047d": HelperAS5047D,
|
||||
"tle5012b": HelperTLE5012B,
|
||||
"mt6816": HelperMT6816,
|
||||
"mt6826s": HelperMT6826S }
|
||||
sensor_type = config.getchoice('sensor_type', {s: s for s in sensors})
|
||||
sensor_class = sensors[sensor_type]
|
||||
self.spi = bus.MCU_SPI_from_config(config, sensor_class.SPI_MODE,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -23,45 +23,75 @@ class AxisTwistCompensation:
|
|||
self.horizontal_move_z = config.getfloat('horizontal_move_z',
|
||||
DEFAULT_HORIZONTAL_MOVE_Z)
|
||||
self.speed = config.getfloat('speed', DEFAULT_SPEED)
|
||||
self.calibrate_start_x = config.getfloat('calibrate_start_x')
|
||||
self.calibrate_end_x = config.getfloat('calibrate_end_x')
|
||||
self.calibrate_y = config.getfloat('calibrate_y')
|
||||
self.calibrate_start_x = config.getfloat('calibrate_start_x',
|
||||
default=None)
|
||||
self.calibrate_end_x = config.getfloat('calibrate_end_x', default=None)
|
||||
self.calibrate_y = config.getfloat('calibrate_y', default=None)
|
||||
self.z_compensations = config.getlists('z_compensations',
|
||||
default=[], parser=float)
|
||||
self.compensation_start_x = config.getfloat('compensation_start_x',
|
||||
default=None)
|
||||
self.compensation_end_x = config.getfloat('compensation_start_y',
|
||||
self.compensation_end_x = config.getfloat('compensation_end_x',
|
||||
default=None)
|
||||
|
||||
self.m = None
|
||||
self.b = None
|
||||
self.calibrate_start_y = config.getfloat('calibrate_start_y',
|
||||
default=None)
|
||||
self.calibrate_end_y = config.getfloat('calibrate_end_y', default=None)
|
||||
self.calibrate_x = config.getfloat('calibrate_x', default=None)
|
||||
self.compensation_start_y = config.getfloat('compensation_start_y',
|
||||
default=None)
|
||||
self.compensation_end_y = config.getfloat('compensation_end_y',
|
||||
default=None)
|
||||
self.zy_compensations = config.getlists('zy_compensations',
|
||||
default=[], parser=float)
|
||||
|
||||
# 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):
|
||||
if not self.z_compensations:
|
||||
return 0
|
||||
def _update_z_compensation_value(self, pos):
|
||||
if self.z_compensations:
|
||||
pos[2] += self._get_interpolated_z_compensation(
|
||||
pos[0], self.z_compensations,
|
||||
self.compensation_start_x,
|
||||
self.compensation_end_x
|
||||
)
|
||||
|
||||
if self.zy_compensations:
|
||||
pos[2] += self._get_interpolated_z_compensation(
|
||||
pos[1], self.zy_compensations,
|
||||
self.compensation_start_y,
|
||||
self.compensation_end_y
|
||||
)
|
||||
|
||||
def _get_interpolated_z_compensation(
|
||||
self, coord, z_compensations,
|
||||
comp_start,
|
||||
comp_end
|
||||
):
|
||||
|
||||
x_coord = pos[0]
|
||||
z_compensations = self.z_compensations
|
||||
sample_count = len(z_compensations)
|
||||
spacing = ((self.calibrate_end_x - self.calibrate_start_x)
|
||||
spacing = ((comp_end - comp_start)
|
||||
/ (sample_count - 1))
|
||||
interpolate_t = (x_coord - self.calibrate_start_x) / spacing
|
||||
interpolate_t = (coord - comp_start) / 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
|
||||
|
||||
def clear_compensations(self):
|
||||
self.z_compensations = []
|
||||
self.m = None
|
||||
self.b = None
|
||||
|
||||
def clear_compensations(self, axis=None):
|
||||
if axis is None:
|
||||
self.z_compensations = []
|
||||
self.zy_compensations = []
|
||||
elif axis == 'X':
|
||||
self.z_compensations = []
|
||||
elif axis == 'Y':
|
||||
self.zy_compensations = []
|
||||
|
||||
class Calibrater:
|
||||
def __init__(self, compensation, config):
|
||||
|
|
@ -77,10 +107,14 @@ class Calibrater:
|
|||
self._handle_connect)
|
||||
self.speed = compensation.speed
|
||||
self.horizontal_move_z = compensation.horizontal_move_z
|
||||
self.start_point = (compensation.calibrate_start_x,
|
||||
self.x_start_point = (compensation.calibrate_start_x,
|
||||
compensation.calibrate_y)
|
||||
self.end_point = (compensation.calibrate_end_x,
|
||||
self.x_end_point = (compensation.calibrate_end_x,
|
||||
compensation.calibrate_y)
|
||||
self.y_start_point = (compensation.calibrate_x,
|
||||
compensation.calibrate_start_y)
|
||||
self.y_end_point = (compensation.calibrate_x,
|
||||
compensation.calibrate_end_y)
|
||||
self.results = None
|
||||
self.current_point_index = None
|
||||
self.gcmd = None
|
||||
|
|
@ -95,7 +129,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()
|
||||
|
||||
|
|
@ -116,39 +150,246 @@ class Calibrater:
|
|||
def cmd_AXIS_TWIST_COMPENSATION_CALIBRATE(self, gcmd):
|
||||
self.gcmd = gcmd
|
||||
sample_count = gcmd.get_int('SAMPLE_COUNT', DEFAULT_SAMPLE_COUNT)
|
||||
axis = gcmd.get('AXIS', None)
|
||||
auto = gcmd.get('AUTO', False)
|
||||
|
||||
if axis is not None and auto:
|
||||
raise self.gcmd.error(
|
||||
"Cannot use both 'AXIS' and 'AUTO' at the same time."
|
||||
)
|
||||
|
||||
if auto:
|
||||
self._start_autocalibration(sample_count)
|
||||
return
|
||||
|
||||
if axis is None and not auto:
|
||||
axis = 'X'
|
||||
|
||||
# check for valid sample_count
|
||||
if sample_count < 2:
|
||||
raise self.gcmd.error(
|
||||
"SAMPLE_COUNT to probe must be at least 2")
|
||||
|
||||
# calculate the points to put the probe at, returned as a list of tuples
|
||||
nozzle_points = []
|
||||
|
||||
if axis == 'X':
|
||||
|
||||
self.compensation.clear_compensations('X')
|
||||
|
||||
if not all([
|
||||
self.x_start_point[0],
|
||||
self.x_end_point[0],
|
||||
self.x_start_point[1]
|
||||
]):
|
||||
raise self.gcmd.error(
|
||||
"""AXIS_TWIST_COMPENSATION for X axis requires
|
||||
calibrate_start_x, calibrate_end_x and calibrate_y
|
||||
to be defined
|
||||
"""
|
||||
)
|
||||
|
||||
start_point = self.x_start_point
|
||||
end_point = self.x_end_point
|
||||
|
||||
x_axis_range = end_point[0] - start_point[0]
|
||||
interval_dist = x_axis_range / (sample_count - 1)
|
||||
|
||||
for i in range(sample_count):
|
||||
x = start_point[0] + i * interval_dist
|
||||
y = start_point[1]
|
||||
nozzle_points.append((x, y))
|
||||
|
||||
elif axis == 'Y':
|
||||
|
||||
self.compensation.clear_compensations('Y')
|
||||
|
||||
if not all([
|
||||
self.y_start_point[0],
|
||||
self.y_end_point[0],
|
||||
self.y_start_point[1]
|
||||
]):
|
||||
raise self.gcmd.error(
|
||||
"""AXIS_TWIST_COMPENSATION for Y axis requires
|
||||
calibrate_start_y, calibrate_end_y and calibrate_x
|
||||
to be defined
|
||||
"""
|
||||
)
|
||||
|
||||
start_point = self.y_start_point
|
||||
end_point = self.y_end_point
|
||||
|
||||
y_axis_range = end_point[1] - start_point[1]
|
||||
interval_dist = y_axis_range / (sample_count - 1)
|
||||
|
||||
for i in range(sample_count):
|
||||
x = start_point[0]
|
||||
y = start_point[1] + i * interval_dist
|
||||
nozzle_points.append((x, y))
|
||||
|
||||
else:
|
||||
raise self.gcmd.error(
|
||||
"AXIS_TWIST_COMPENSATION_CALIBRATE: "
|
||||
"Invalid axis.")
|
||||
|
||||
probe_points = self._calculate_probe_points(
|
||||
nozzle_points, self.probe_x_offset, self.probe_y_offset)
|
||||
|
||||
# verify no other manual probe is in progress
|
||||
manual_probe.verify_no_manual_probe(self.printer)
|
||||
|
||||
# begin calibration
|
||||
self.current_point_index = 0
|
||||
self.results = []
|
||||
self.current_axis = axis
|
||||
self._calibration(probe_points, nozzle_points, interval_dist)
|
||||
|
||||
def _calculate_corrections(self, coordinates):
|
||||
# Extracting x, y, and z values from coordinates
|
||||
x_coords = [coord[0] for coord in coordinates]
|
||||
y_coords = [coord[1] for coord in coordinates]
|
||||
z_coords = [coord[2] for coord in coordinates]
|
||||
|
||||
# Calculate the desired point (average of all corner points in z)
|
||||
# For a general case, we should extract the unique
|
||||
# combinations of corner points
|
||||
z_corners = [z_coords[i] for i, coord in enumerate(coordinates)
|
||||
if (coord[0] in [x_coords[0], x_coords[-1]])
|
||||
and (coord[1] in [y_coords[0], y_coords[-1]])]
|
||||
z_desired = sum(z_corners) / len(z_corners)
|
||||
|
||||
|
||||
# Calculate average deformation per axis
|
||||
unique_x_coords = sorted(set(x_coords))
|
||||
unique_y_coords = sorted(set(y_coords))
|
||||
|
||||
avg_z_x = []
|
||||
for x in unique_x_coords:
|
||||
indices = [i for i, coord in enumerate(coordinates)
|
||||
if coord[0] == x]
|
||||
avg_z = sum(z_coords[i] for i in indices) / len(indices)
|
||||
avg_z_x.append(avg_z)
|
||||
|
||||
avg_z_y = []
|
||||
for y in unique_y_coords:
|
||||
indices = [i for i, coord in enumerate(coordinates)
|
||||
if coord[1] == y]
|
||||
avg_z = sum(z_coords[i] for i in indices) / len(indices)
|
||||
avg_z_y.append(avg_z)
|
||||
|
||||
# Calculate corrections to reach the desired point
|
||||
x_corrections = [z_desired - avg for avg in avg_z_x]
|
||||
y_corrections = [z_desired - avg for avg in avg_z_y]
|
||||
|
||||
return x_corrections, y_corrections
|
||||
|
||||
def _start_autocalibration(self, sample_count):
|
||||
|
||||
if not all([
|
||||
self.x_start_point[0],
|
||||
self.x_end_point[0],
|
||||
self.y_start_point[0],
|
||||
self.y_end_point[0]
|
||||
]):
|
||||
raise self.gcmd.error(
|
||||
"""AXIS_TWIST_COMPENSATION_AUTOCALIBRATE requires
|
||||
calibrate_start_x, calibrate_end_x, calibrate_start_y
|
||||
and calibrate_end_y to be defined
|
||||
"""
|
||||
)
|
||||
|
||||
# check for valid sample_count
|
||||
if sample_count is None or sample_count < 2:
|
||||
raise self.gcmd.error(
|
||||
"SAMPLE_COUNT to probe must be at least 2")
|
||||
|
||||
# verify no other manual probe is in progress
|
||||
manual_probe.verify_no_manual_probe(self.printer)
|
||||
|
||||
# clear the current config
|
||||
self.compensation.clear_compensations()
|
||||
|
||||
# calculate some values
|
||||
x_range = self.end_point[0] - self.start_point[0]
|
||||
interval_dist = x_range / (sample_count - 1)
|
||||
nozzle_points = self._calculate_nozzle_points(sample_count,
|
||||
interval_dist)
|
||||
probe_points = self._calculate_probe_points(
|
||||
nozzle_points, self.probe_x_offset, self.probe_y_offset)
|
||||
min_x = self.x_start_point[0]
|
||||
max_x = self.x_end_point[0]
|
||||
min_y = self.y_start_point[1]
|
||||
max_y = self.y_end_point[1]
|
||||
|
||||
# verify no other manual probe is in progress
|
||||
ManualProbe.verify_no_manual_probe(self.printer)
|
||||
# calculate x positions
|
||||
interval_x = (max_x - min_x) / (sample_count - 1)
|
||||
xps = [min_x + interval_x * i for i in range(sample_count)]
|
||||
|
||||
# begin calibration
|
||||
self.current_point_index = 0
|
||||
self.results = []
|
||||
self._calibration(probe_points, nozzle_points, interval_dist)
|
||||
# Calculate points array
|
||||
interval_y = (max_y - min_y) / (sample_count - 1)
|
||||
flip = False
|
||||
|
||||
def _calculate_nozzle_points(self, sample_count, interval_dist):
|
||||
# calculate the points to put the probe at, returned as a list of tuples
|
||||
nozzle_points = []
|
||||
points = []
|
||||
for i in range(sample_count):
|
||||
x = self.start_point[0] + i * interval_dist
|
||||
y = self.start_point[1]
|
||||
nozzle_points.append((x, y))
|
||||
return nozzle_points
|
||||
for j in range(sample_count):
|
||||
if(not flip):
|
||||
idx = j
|
||||
else:
|
||||
idx = sample_count -1 - j
|
||||
points.append([xps[i], min_y + interval_y * idx ])
|
||||
flip = not flip
|
||||
|
||||
|
||||
# calculate the points to put the nozzle at, and probe
|
||||
probe_points = []
|
||||
|
||||
for i in range(len(points)):
|
||||
x = points[i][0] - self.probe_x_offset
|
||||
y = points[i][1] - self.probe_y_offset
|
||||
probe_points.append([x, y, self._auto_calibration((x,y))[2]])
|
||||
|
||||
# calculate corrections
|
||||
x_corr, y_corr = self._calculate_corrections(probe_points)
|
||||
|
||||
x_corr_str = ', '.join(["{:.6f}".format(x)
|
||||
for x in x_corr])
|
||||
|
||||
y_corr_str = ', '.join(["{:.6f}".format(x)
|
||||
for x in y_corr])
|
||||
|
||||
# finalize
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
configfile.set(self.configname, 'z_compensations', x_corr_str)
|
||||
configfile.set(self.configname, 'compensation_start_x',
|
||||
self.x_start_point[0])
|
||||
configfile.set(self.configname, 'compensation_end_x',
|
||||
self.x_end_point[0])
|
||||
|
||||
|
||||
configfile.set(self.configname, 'zy_compensations', y_corr_str)
|
||||
configfile.set(self.configname, 'compensation_start_y',
|
||||
self.y_start_point[1])
|
||||
configfile.set(self.configname, 'compensation_end_y',
|
||||
self.y_end_point[1])
|
||||
|
||||
self.gcode.respond_info(
|
||||
"AXIS_TWIST_COMPENSATION state has been saved "
|
||||
"for the current session. The SAVE_CONFIG command will "
|
||||
"update the printer config file and restart the printer.")
|
||||
# output result
|
||||
self.gcmd.respond_info(
|
||||
"AXIS_TWIST_COMPENSATION_AUTOCALIBRATE: Calibration complete: ")
|
||||
self.gcmd.respond_info("\n".join(map(str, [x_corr, y_corr])), log=False)
|
||||
|
||||
def _auto_calibration(self, probe_point):
|
||||
|
||||
# horizontal_move_z (to prevent probe trigger or hitting bed)
|
||||
self._move_helper((None, None, self.horizontal_move_z))
|
||||
|
||||
# move to point to probe
|
||||
self._move_helper((probe_point[0],
|
||||
probe_point[1], None))
|
||||
|
||||
# probe the point
|
||||
pos = probe.run_single_probe(self.probe, self.gcmd)
|
||||
|
||||
# horizontal_move_z (to prevent probe trigger or hitting bed)
|
||||
self._move_helper((None, None, self.horizontal_move_z))
|
||||
|
||||
return pos
|
||||
|
||||
def _calculate_probe_points(self, nozzle_points,
|
||||
probe_x_offset, probe_y_offset):
|
||||
|
|
@ -186,7 +427,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 +437,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))
|
||||
|
|
@ -234,14 +476,31 @@ class Calibrater:
|
|||
configfile = self.printer.lookup_object('configfile')
|
||||
values_as_str = ', '.join(["{:.6f}".format(x)
|
||||
for x in self.results])
|
||||
configfile.set(self.configname, 'z_compensations', values_as_str)
|
||||
configfile.set(self.configname, 'compensation_start_x',
|
||||
self.start_point[0])
|
||||
configfile.set(self.configname, 'compensation_end_x',
|
||||
self.end_point[0])
|
||||
self.compensation.z_compensations = self.results
|
||||
self.compensation.compensation_start_x = self.start_point[0]
|
||||
self.compensation.compensation_end_x = self.end_point[0]
|
||||
|
||||
if(self.current_axis == 'X'):
|
||||
|
||||
configfile.set(self.configname, 'z_compensations', values_as_str)
|
||||
configfile.set(self.configname, 'compensation_start_x',
|
||||
self.x_start_point[0])
|
||||
configfile.set(self.configname, 'compensation_end_x',
|
||||
self.x_end_point[0])
|
||||
|
||||
self.compensation.z_compensations = self.results
|
||||
self.compensation.compensation_start_x = self.x_start_point[0]
|
||||
self.compensation.compensation_end_x = self.x_end_point[0]
|
||||
|
||||
elif(self.current_axis == 'Y'):
|
||||
|
||||
configfile.set(self.configname, 'zy_compensations', values_as_str)
|
||||
configfile.set(self.configname, 'compensation_start_y',
|
||||
self.y_start_point[1])
|
||||
configfile.set(self.configname, 'compensation_end_y',
|
||||
self.y_end_point[1])
|
||||
|
||||
self.compensation.zy_compensations = self.results
|
||||
self.compensation.compensation_start_y = self.y_start_point[1]
|
||||
self.compensation.compensation_end_y = self.y_end_point[1]
|
||||
|
||||
self.gcode.respond_info(
|
||||
"AXIS_TWIST_COMPENSATION state has been saved "
|
||||
"for the current session. The SAVE_CONFIG command will "
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -17,6 +17,29 @@ BME280_REGS = {
|
|||
'HUM_MSB': 0xFD, 'HUM_LSB': 0xFE, 'CAL_1': 0x88, 'CAL_2': 0xE1
|
||||
}
|
||||
|
||||
BMP388_REGS = {
|
||||
"CMD": 0x7E,
|
||||
"STATUS": 0x03,
|
||||
"PWR_CTRL": 0x1B,
|
||||
"OSR": 0x1C,
|
||||
"ORD": 0x1D,
|
||||
"INT_CTRL": 0x19,
|
||||
"CAL_1": 0x31,
|
||||
"TEMP_MSB": 0x09,
|
||||
"TEMP_LSB": 0x08,
|
||||
"TEMP_XLSB": 0x07,
|
||||
"PRESS_MSB": 0x06,
|
||||
"PRESS_LSB": 0x05,
|
||||
"PRESS_XLSB": 0x04,
|
||||
}
|
||||
BMP388_REG_VAL_PRESS_EN = 0x01
|
||||
BMP388_REG_VAL_TEMP_EN = 0x02
|
||||
BMP388_REG_VAL_PRESS_OS_NO = 0b000
|
||||
BMP388_REG_VAL_TEMP_OS_NO = 0b000000
|
||||
BMP388_REG_VAL_ODR_50_HZ = 0x02
|
||||
BMP388_REG_VAL_DRDY_EN = 0b100000
|
||||
BMP388_REG_VAL_NORMAL_MODE = 0x30
|
||||
|
||||
BME680_REGS = {
|
||||
'RESET': 0xE0, 'CTRL_HUM': 0x72, 'CTRL_GAS_1': 0x71, 'CTRL_GAS_0': 0x70,
|
||||
'GAS_WAIT_0': 0x64, 'RES_HEAT_0': 0x5A, 'IDAC_HEAT_0': 0x50,
|
||||
|
|
@ -60,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
|
||||
|
|
@ -68,9 +92,11 @@ MEASURE_DONE = 1 << 5
|
|||
RESET_CHIP_VALUE = 0xB6
|
||||
|
||||
BME_CHIPS = {
|
||||
0x58: 'BMP280', 0x60: 'BME280', 0x61: 'BME680', 0x55: 'BMP180'
|
||||
0x58: 'BMP280', 0x60: 'BME280', 0x61: 'BME680', 0x55: 'BMP180',
|
||||
0x50: 'BMP388'
|
||||
}
|
||||
BME_CHIP_ID_REG = 0xD0
|
||||
BMP3_CHIP_ID_REG = 0x00
|
||||
|
||||
|
||||
def get_twos_complement(val, bit_size):
|
||||
|
|
@ -118,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.
|
||||
|
|
@ -130,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()
|
||||
|
|
@ -163,6 +191,29 @@ class BME280:
|
|||
dig['P9'] = get_signed_short(calib_data_1[22:24])
|
||||
return dig
|
||||
|
||||
def read_calibration_data_bmp388(calib_data_1):
|
||||
dig = {}
|
||||
dig["T1"] = get_unsigned_short(calib_data_1[0:2]) / 0.00390625
|
||||
dig["T2"] = get_unsigned_short(calib_data_1[2:4]) / 1073741824.0
|
||||
dig["T3"] = get_signed_byte(calib_data_1[4]) / 281474976710656.0
|
||||
|
||||
dig["P1"] = get_signed_short(calib_data_1[5:7]) - 16384
|
||||
dig["P1"] /= 1048576.0
|
||||
dig["P2"] = get_signed_short(calib_data_1[7:9]) - 16384
|
||||
dig["P2"] /= 536870912.0
|
||||
dig["P3"] = get_signed_byte(calib_data_1[9]) / 4294967296.0
|
||||
dig["P4"] = get_signed_byte(calib_data_1[10]) / 137438953472.0
|
||||
dig["P5"] = get_unsigned_short(calib_data_1[11:13]) / 0.125
|
||||
dig["P6"] = get_unsigned_short(calib_data_1[13:15]) / 64.0
|
||||
dig["P7"] = get_signed_byte(calib_data_1[15]) / 256.0
|
||||
dig["P8"] = get_signed_byte(calib_data_1[16]) / 32768.0
|
||||
dig["P9"] = get_signed_short(calib_data_1[17:19])
|
||||
dig["P9"] /= 281474976710656.0
|
||||
dig["P10"] = get_signed_byte(calib_data_1[19]) / 281474976710656.0
|
||||
dig["P11"] = get_signed_byte(calib_data_1[20])
|
||||
dig["P11"] /= 36893488147419103232.0
|
||||
return dig
|
||||
|
||||
def read_calibration_data_bme280(calib_data_1, calib_data_2):
|
||||
dig = read_calibration_data_bmp280(calib_data_1)
|
||||
dig['H1'] = calib_data_1[25] & 0xFF
|
||||
|
|
@ -224,7 +275,7 @@ class BME280:
|
|||
dig['MD'] = get_signed_short_msb(calib_data_1[20:22])
|
||||
return dig
|
||||
|
||||
chip_id = self.read_id()
|
||||
chip_id = self.read_id() or self.read_bmp3_id()
|
||||
if chip_id not in BME_CHIPS.keys():
|
||||
logging.info("bme280: Unknown Chip ID received %#x" % chip_id)
|
||||
else:
|
||||
|
|
@ -233,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
|
||||
|
|
@ -245,26 +296,49 @@ 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
|
||||
else:
|
||||
elif self.chip_type == 'BMP388':
|
||||
self.chip_registers = BMP388_REGS
|
||||
self.write_register(
|
||||
"PWR_CTRL",
|
||||
[
|
||||
BMP388_REG_VAL_PRESS_EN
|
||||
| BMP388_REG_VAL_TEMP_EN
|
||||
| BMP388_REG_VAL_NORMAL_MODE
|
||||
],
|
||||
)
|
||||
self.write_register(
|
||||
"OSR", [BMP388_REG_VAL_PRESS_OS_NO | BMP388_REG_VAL_TEMP_OS_NO]
|
||||
)
|
||||
self.write_register("ORD", [BMP388_REG_VAL_ODR_50_HZ])
|
||||
self.write_register("INT_CTRL", [BMP388_REG_VAL_DRDY_EN])
|
||||
|
||||
self.sample_timer = self.reactor.register_timer(self._sample_bmp388)
|
||||
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':
|
||||
cal_1 = self.read_register('CAL_1', 22)
|
||||
elif self.chip_type == 'BMP388':
|
||||
cal_1 = self.read_register('CAL_1', 21)
|
||||
else:
|
||||
cal_1 = self.read_register('CAL_1', 26)
|
||||
cal_2 = self.read_register('CAL_2', 16)
|
||||
|
|
@ -276,22 +350,67 @@ class BME280:
|
|||
self.dig = read_calibration_data_bme680(cal_1, cal_2)
|
||||
elif self.chip_type == 'BMP180':
|
||||
self.dig = read_calibration_data_bmp180(cal_1)
|
||||
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':
|
||||
|
|
@ -318,37 +437,114 @@ class BME280:
|
|||
self._callback(self.mcu.estimated_print_time(measured_time), self.temp)
|
||||
return measured_time + REPORT_TIME
|
||||
|
||||
def _sample_bmp388(self, eventtime):
|
||||
status = self.read_register("STATUS", 1)
|
||||
if status[0] & 0b100000:
|
||||
self.temp = self._sample_bmp388_temp()
|
||||
if self.temp < self.min_temp or self.temp > self.max_temp:
|
||||
self.printer.invoke_shutdown(
|
||||
"BME280 temperature %0.1f outside range of %0.1f:%.01f"
|
||||
% (self.temp, self.min_temp, self.max_temp)
|
||||
)
|
||||
|
||||
if status[0] & 0b010000:
|
||||
self.pressure = self._sample_bmp388_press() / 100.0
|
||||
|
||||
measured_time = self.reactor.monotonic()
|
||||
self._callback(self.mcu.estimated_print_time(measured_time), self.temp)
|
||||
return measured_time + REPORT_TIME
|
||||
|
||||
def _sample_bmp388_temp(self):
|
||||
xlsb = self.read_register("TEMP_XLSB", 1)
|
||||
lsb = self.read_register("TEMP_LSB", 1)
|
||||
msb = self.read_register("TEMP_MSB", 1)
|
||||
adc_T = (msb[0] << 16) + (lsb[0] << 8) + (xlsb[0])
|
||||
|
||||
partial_data1 = adc_T - self.dig["T1"]
|
||||
partial_data2 = self.dig["T2"] * partial_data1
|
||||
|
||||
self.t_fine = partial_data2
|
||||
self.t_fine += (partial_data1 * partial_data1) * self.dig["T3"]
|
||||
|
||||
if self.t_fine < -40.0:
|
||||
self.t_fine = -40.0
|
||||
|
||||
if self.t_fine > 85.0:
|
||||
self.t_fine = 85.0
|
||||
|
||||
return self.t_fine
|
||||
|
||||
def _sample_bmp388_press(self):
|
||||
xlsb = self.read_register("PRESS_XLSB", 1)
|
||||
lsb = self.read_register("PRESS_LSB", 1)
|
||||
msb = self.read_register("PRESS_MSB", 1)
|
||||
adc_P = (msb[0] << 16) + (lsb[0] << 8) + (xlsb[0])
|
||||
|
||||
partial_data1 = self.dig["P6"] * self.t_fine
|
||||
partial_data2 = self.dig["P7"] * (self.t_fine * self.t_fine)
|
||||
partial_data3 = self.dig["P8"]
|
||||
partial_data3 *= self.t_fine * self.t_fine * self.t_fine
|
||||
partial_out1 = self.dig["P5"]
|
||||
partial_out1 += partial_data1 + partial_data2 + partial_data3
|
||||
|
||||
partial_data1 = self.dig["P2"] * self.t_fine
|
||||
partial_data2 = self.dig["P3"] * (self.t_fine * self.t_fine)
|
||||
partial_data3 = self.dig["P4"]
|
||||
partial_data3 *= (self.t_fine * self.t_fine * self.t_fine)
|
||||
partial_out2 = adc_P * (
|
||||
self.dig["P1"] + partial_data1 + partial_data2 + partial_data3
|
||||
)
|
||||
|
||||
partial_data1 = adc_P * adc_P
|
||||
partial_data2 = self.dig["P9"] + (self.dig["P10"] * self.t_fine)
|
||||
partial_data3 = partial_data1 * partial_data2
|
||||
partial_data4 = partial_data3 + adc_P * adc_P * adc_P * self.dig["P11"]
|
||||
|
||||
comp_press = partial_out1 + partial_out2 + partial_data4
|
||||
|
||||
if comp_press < 30000:
|
||||
comp_press = 30000
|
||||
|
||||
if comp_press > 125000:
|
||||
comp_press = 125000
|
||||
|
||||
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
|
||||
|
|
@ -372,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(
|
||||
|
|
@ -500,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])
|
||||
|
|
@ -515,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:
|
||||
|
|
@ -564,18 +764,27 @@ class BME280:
|
|||
params = self.i2c.i2c_read(regs, 1)
|
||||
return bytearray(params['response'])[0]
|
||||
|
||||
def read_bmp3_id(self):
|
||||
# read chip id register
|
||||
regs = [BMP3_CHIP_ID_REG]
|
||||
params = self.i2c.i2c_read(regs, 1)
|
||||
return bytearray(params['response'])[0]
|
||||
|
||||
def read_register(self, reg_name, read_len):
|
||||
# read a single register
|
||||
regs = [self.chip_registers[reg_name]]
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ class MCU_I2C:
|
|||
% (self.oid, speed, addr))
|
||||
self.cmd_queue = self.mcu.alloc_command_queue()
|
||||
self.mcu.register_config_callback(self.build_config)
|
||||
self.i2c_write_cmd = self.i2c_read_cmd = self.i2c_modify_bits_cmd = None
|
||||
self.i2c_write_cmd = self.i2c_read_cmd = None
|
||||
def get_oid(self):
|
||||
return self.oid
|
||||
def get_mcu(self):
|
||||
|
|
@ -180,9 +180,6 @@ class MCU_I2C:
|
|||
"i2c_read oid=%c reg=%*s read_len=%u",
|
||||
"i2c_read_response oid=%c response=%*s", oid=self.oid,
|
||||
cq=self.cmd_queue)
|
||||
self.i2c_modify_bits_cmd = self.mcu.lookup_command(
|
||||
"i2c_modify_bits oid=%c reg=%*s clear_set_bits=%*s",
|
||||
cq=self.cmd_queue)
|
||||
def i2c_write(self, data, minclock=0, reqclock=0):
|
||||
if self.i2c_write_cmd is None:
|
||||
# Send setup message via mcu initialization
|
||||
|
|
@ -192,21 +189,11 @@ 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,
|
||||
minclock=0, reqclock=0):
|
||||
clearset = clear_bits + set_bits
|
||||
if self.i2c_modify_bits_cmd is None:
|
||||
# Send setup message via mcu initialization
|
||||
reg_msg = "".join(["%02x" % (x,) for x in reg])
|
||||
clearset_msg = "".join(["%02x" % (x,) for x in clearset])
|
||||
self.mcu.add_config_cmd(
|
||||
"i2c_modify_bits oid=%d reg=%s clear_set_bits=%s" % (
|
||||
self.oid, reg_msg, clearset_msg), is_init=True)
|
||||
return
|
||||
self.i2c_modify_bits_cmd.send([self.oid, reg, clearset],
|
||||
minclock=minclock, reqclock=reqclock)
|
||||
|
||||
def MCU_I2C_from_config(config, default_addr=None, default_speed=100000):
|
||||
# Load bus parameters
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -62,9 +62,7 @@ class ControllerFan:
|
|||
self.last_on += 1
|
||||
if speed != self.last_speed:
|
||||
self.last_speed = speed
|
||||
curtime = self.printer.get_reactor().monotonic()
|
||||
print_time = self.fan.get_mcu().estimated_print_time(curtime)
|
||||
self.fan.set_speed(print_time + PIN_MIN_TIME, speed)
|
||||
self.fan.set_speed(speed)
|
||||
return eventtime + 1.
|
||||
|
||||
def load_config_prefix(config):
|
||||
|
|
|
|||
209
klippy/extras/display/aip31068_spi.py
Normal file
209
klippy/extras/display/aip31068_spi.py
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# Support for YHCB2004 (20x4 text) LCD displays based on AiP31068 controller
|
||||
#
|
||||
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
|
||||
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
|
||||
# Copyright (C) 2021 Marc-Andre Denis <marcadenis@msn.com>
|
||||
# Copyright (C) 2024 Alexander Bazarov <oss@bazarov.dev>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
# This file is a modified version of hd44780_spi.py, introducing slightly
|
||||
# different protocol as implemented in Marlin FW (based on
|
||||
# https://github.com/red-scorp/LiquidCrystal_AIP31068 ).
|
||||
# In addition, a hack is used to send 8 commands, each 9 bits, at once,
|
||||
# allowing the transmission of a full 9 bytes.
|
||||
# This helps avoid modifying the SW_SPI driver to handle non-8-bit data.
|
||||
|
||||
from .. import bus
|
||||
|
||||
LINE_LENGTH_DEFAULT=20
|
||||
LINE_LENGTH_OPTIONS={16:16, 20:20}
|
||||
|
||||
TextGlyphs = { 'right_arrow': b'\x7e' }
|
||||
|
||||
# Each command is 9 bits long:
|
||||
# 1 bit for RS (Register Select) - 0 for command, 1 for data
|
||||
# 8 bits for the command/data
|
||||
# Command is a bitwise OR of CMND(=opcode) and flg_CMND(=parameters) multiplied
|
||||
# by 1 or 0 as En/Dis flag.
|
||||
# cmd = CMND | flg_CMND.param0*0 | flg_CMND.param1*1
|
||||
# or just by OR with enabled flags:
|
||||
# cmd = CMND | flg_CMND.param1
|
||||
class CMND:
|
||||
CLR = 1 # Clear display
|
||||
HOME = 2 # Return home
|
||||
ENTERY_MODE = 2**2 # Entry mode set
|
||||
DISPLAY = 2**3 # Display on/off control
|
||||
SHIFT = 2**4 # Cursor or display shift
|
||||
FUNCTION = 2**5 # Function set
|
||||
CGRAM = 2**6 # Character Generator RAM
|
||||
DDRAM = 2**7 # Display Data RAM
|
||||
WRITE_RAM = 2**8 # Write to RAM
|
||||
|
||||
# Define flags for all commands:
|
||||
class flg_ENTERY_MODE:
|
||||
INC = 2**1 # Increment
|
||||
SHIFT = 2**0 # Shift display
|
||||
|
||||
class flg_DISPLAY:
|
||||
ON = 2**2 # Display ON
|
||||
CURSOR = 2**1 # Cursor ON
|
||||
BLINK = 2**0 # Blink ON
|
||||
|
||||
class flg_SHIFT:
|
||||
WHOLE_DISPLAY = 2**3 # Shift whole display
|
||||
RIGHT = 2**2 # Shift right
|
||||
|
||||
class flg_FUNCTION:
|
||||
TWO_LINES = 2**3 # 2-line display mode
|
||||
FIVE_BY_ELEVEN = 2**2 # 5x11 dot character font
|
||||
|
||||
class flg_CGRAM:
|
||||
MASK = 0b00111111 # CGRAM address mask
|
||||
|
||||
class flg_DDRAM:
|
||||
MASK = 0b01111111 # DDRAM address mask
|
||||
|
||||
class flg_WRITE_RAM:
|
||||
MASK = 0b11111111 # Write RAM mask
|
||||
|
||||
DISPLAY_INIT_CMNDS= [
|
||||
# CMND.CLR - no need as framebuffer will rewrite all
|
||||
CMND.HOME, # move cursor to home (0x00)
|
||||
CMND.ENTERY_MODE | flg_ENTERY_MODE.INC, # increment cursor and no shift
|
||||
CMND.DISPLAY | flg_DISPLAY.ON, # keep cursor and blinking off
|
||||
CMND.SHIFT | flg_SHIFT.RIGHT, # shift right cursor only
|
||||
CMND.FUNCTION | flg_FUNCTION.TWO_LINES, # 2-line display mode, 5x8 dots
|
||||
]
|
||||
|
||||
class aip31068_spi:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
# spi config
|
||||
self.spi = bus.MCU_SPI_from_config(
|
||||
config, 0x00, pin_option="latch_pin") # (config, mode, cs_name)
|
||||
self.mcu = self.spi.get_mcu()
|
||||
self.icons = {}
|
||||
self.line_length = config.getchoice('line_length', LINE_LENGTH_OPTIONS,
|
||||
LINE_LENGTH_DEFAULT)
|
||||
# each controller's line is 2 lines on the display and hence twice
|
||||
# line length
|
||||
self.text_framebuffers = [bytearray(b' '*2*self.line_length),
|
||||
bytearray(b' '*2*self.line_length)]
|
||||
self.glyph_framebuffer = bytearray(64)
|
||||
# all_framebuffers - list of tuples per buffer type.
|
||||
# Each tuple contains:
|
||||
# 1. the updated framebuffer
|
||||
# 2. a copy of the old framebuffer == data on the display
|
||||
# 3. the command to send to write to this buffer
|
||||
# Then flush() will compare new data with data on the display
|
||||
# and send only the differences to the display controller
|
||||
# and update the old framebuffer with the new data
|
||||
# (immutable tuple is allowed to store mutable bytearray)
|
||||
self.all_framebuffers = [
|
||||
# Text framebuffers
|
||||
(self.text_framebuffers[0], bytearray(b'~'*2*self.line_length),
|
||||
CMND.DDRAM | (flg_DDRAM.MASK & 0x00) ),
|
||||
(self.text_framebuffers[1], bytearray(b'~'*2*self.line_length),
|
||||
CMND.DDRAM | (flg_DDRAM.MASK & 0x40) ),
|
||||
# Glyph framebuffer
|
||||
(self.glyph_framebuffer, bytearray(b'~'*64),
|
||||
CMND.CGRAM | (flg_CGRAM.MASK & 0x00) ) ]
|
||||
@staticmethod
|
||||
def encode(data, width = 9):
|
||||
encoded_bytes = []
|
||||
accumulator = 0 # To accumulate bits
|
||||
acc_bits = 0 # Count of bits in the accumulator
|
||||
for num in data:
|
||||
# check that num will fit in width bits
|
||||
if num >= (1 << width):
|
||||
raise ValueError("Number {} does not fit in {} bits".
|
||||
format(num, width))
|
||||
# Shift the current number into the accumulator from the right
|
||||
accumulator = (accumulator << width) | num
|
||||
acc_bits += width # Update the count of bits in the accumulator
|
||||
# While we have at least 8 bits, form a byte and append it
|
||||
while acc_bits >= 8:
|
||||
acc_bits -= 8 # Decrease bit count by 8
|
||||
# Extract the 8 most significant bits to form a byte
|
||||
byte = (accumulator >> acc_bits) & 0xFF
|
||||
# Remove msb 8 bits from the accumulator
|
||||
accumulator &= (1 << acc_bits) - 1
|
||||
encoded_bytes.append(byte)
|
||||
# Handle any remaining bits by padding them on the right to byte
|
||||
if acc_bits > 0:
|
||||
last_byte = accumulator << (8 - acc_bits)
|
||||
encoded_bytes.append(last_byte)
|
||||
return encoded_bytes
|
||||
def send(self, data, minclock=0):
|
||||
# different commands have different processing time
|
||||
# to avoid timing violation pad with some fast command, e.g. ENTRY_MODE
|
||||
# that has execution time of 39us (for comparison CLR is 1.53ms)
|
||||
pad = CMND.ENTERY_MODE | flg_ENTERY_MODE.INC
|
||||
for i in range(0, len(data), 8):
|
||||
# Take a slice of 8 numbers
|
||||
group = data[i:i+8]
|
||||
# Pad the group if it has fewer than 8 elements
|
||||
if len(group) < 8:
|
||||
group.extend([pad] * (8 - len(group)))
|
||||
self.spi.spi_send(self.encode(group), minclock)
|
||||
def flush(self):
|
||||
# Find all differences in the framebuffers and send them to the chip
|
||||
for new_data, old_data, fb_cmnd in self.all_framebuffers:
|
||||
if new_data == old_data:
|
||||
continue
|
||||
# Find the position of all changed bytes in this framebuffer
|
||||
diffs = [[i, 1] for i, (n, o) in enumerate(zip(new_data, old_data))
|
||||
if n != o]
|
||||
# Batch together changes that are close to each other
|
||||
for i in range(len(diffs)-2, -1, -1):
|
||||
pos, count = diffs[i]
|
||||
nextpos, nextcount = diffs[i+1]
|
||||
if pos + 4 >= nextpos and nextcount < 16:
|
||||
diffs[i][1] = nextcount + (nextpos - pos)
|
||||
del diffs[i+1]
|
||||
# Transmit changes
|
||||
for pos, count in diffs:
|
||||
chip_pos = pos
|
||||
self.send([fb_cmnd + chip_pos])
|
||||
self.send([CMND.WRITE_RAM | byte for byte in
|
||||
new_data[pos:pos+count]])
|
||||
old_data[:] = new_data
|
||||
def init(self):
|
||||
curtime = self.printer.get_reactor().monotonic()
|
||||
print_time = self.mcu.estimated_print_time(curtime)
|
||||
for i, cmds in enumerate(DISPLAY_INIT_CMNDS):
|
||||
minclock = self.mcu.print_time_to_clock(print_time + i * .100)
|
||||
self.send([cmds], minclock=minclock)
|
||||
self.flush()
|
||||
def write_text(self, x, y, data):
|
||||
if x + len(data) > self.line_length:
|
||||
data = data[:self.line_length - min(x, self.line_length)]
|
||||
pos = x + ((y & 0x02) >> 1) * self.line_length
|
||||
self.text_framebuffers[y & 1][pos:pos+len(data)] = data
|
||||
def set_glyphs(self, glyphs):
|
||||
for glyph_name, glyph_data in glyphs.items():
|
||||
data = glyph_data.get('icon5x8')
|
||||
if data is not None:
|
||||
self.icons[glyph_name] = data
|
||||
def write_glyph(self, x, y, glyph_name):
|
||||
data = self.icons.get(glyph_name)
|
||||
if data is not None:
|
||||
slot, bits = data
|
||||
self.write_text(x, y, [slot])
|
||||
self.glyph_framebuffer[slot * 8:(slot + 1) * 8] = bits
|
||||
return 1
|
||||
char = TextGlyphs.get(glyph_name)
|
||||
if char is not None:
|
||||
# Draw character
|
||||
self.write_text(x, y, char)
|
||||
return 1
|
||||
return 0
|
||||
def write_graphics(self, x, y, data):
|
||||
pass # this display supports only hardcoded or 8 user defined glyphs
|
||||
def clear(self):
|
||||
spaces = b' ' * 2*self.line_length
|
||||
self.text_framebuffers[0][:] = spaces
|
||||
self.text_framebuffers[1][:] = spaces
|
||||
def get_dimensions(self):
|
||||
return (self.line_length, 4)
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging, os, ast
|
||||
from . import hd44780, hd44780_spi, st7920, uc1701, menu
|
||||
from . import aip31068_spi, hd44780, hd44780_spi, st7920, uc1701, menu
|
||||
|
||||
# Normal time between each screen redraw
|
||||
REDRAW_TIME = 0.500
|
||||
|
|
@ -17,7 +17,8 @@ LCD_chips = {
|
|||
'st7920': st7920.ST7920, 'emulated_st7920': st7920.EmulatedST7920,
|
||||
'hd44780': hd44780.HD44780, 'uc1701': uc1701.UC1701,
|
||||
'ssd1306': uc1701.SSD1306, 'sh1106': uc1701.SH1106,
|
||||
'hd44780_spi': hd44780_spi.hd44780_spi
|
||||
'hd44780_spi': hd44780_spi.hd44780_spi,
|
||||
'aip31068_spi':aip31068_spi.aip31068_spi
|
||||
}
|
||||
|
||||
# Storage of [display_template my_template] config sections
|
||||
|
|
|
|||
|
|
@ -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(',')
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# Support for "dotstar" leds
|
||||
#
|
||||
# Copyright (C) 2019-2022 Kevin O'Connor <kevin@koconnor.net>
|
||||
# Copyright (C) 2019-2024 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
from . import bus
|
||||
from . import bus, led
|
||||
|
||||
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
|
||||
|
||||
|
|
@ -22,9 +22,8 @@ class PrinterDotstar:
|
|||
self.spi = bus.MCU_SPI(mcu, None, None, 0, 500000, sw_spi_pins)
|
||||
# Initialize color data
|
||||
self.chain_count = config.getint('chain_count', 1, minval=1)
|
||||
pled = printer.load_object(config, "led")
|
||||
self.led_helper = pled.setup_helper(config, self.update_leds,
|
||||
self.chain_count)
|
||||
self.led_helper = led.LEDHelper(config, self.update_leds,
|
||||
self.chain_count)
|
||||
self.prev_data = None
|
||||
# Register commands
|
||||
printer.register_event_handler("klippy:connect", self.handle_connect)
|
||||
|
|
|
|||
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)
|
||||
|
|
@ -1,17 +1,14 @@
|
|||
# Printer cooling fan
|
||||
#
|
||||
# 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.
|
||||
from . import pulse_counter
|
||||
|
||||
FAN_MIN_TIME = 0.100
|
||||
from . import pulse_counter, output_pin
|
||||
|
||||
class Fan:
|
||||
def __init__(self, config, default_shutdown_speed=0.):
|
||||
self.printer = config.get_printer()
|
||||
self.last_fan_value = 0.
|
||||
self.last_fan_time = 0.
|
||||
self.last_fan_value = self.last_req_value = 0.
|
||||
# Read config
|
||||
self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.)
|
||||
self.kick_start_time = config.getfloat('kick_start_time', 0.1,
|
||||
|
|
@ -36,6 +33,10 @@ class Fan:
|
|||
self.enable_pin = ppins.setup_pin('digital_out', enable_pin)
|
||||
self.enable_pin.setup_max_duration(0.)
|
||||
|
||||
# Create gcode request queue
|
||||
self.gcrq = output_pin.GCodeRequestQueue(config, self.mcu_fan.get_mcu(),
|
||||
self._apply_speed)
|
||||
|
||||
# Setup tachometer
|
||||
self.tachometer = FanTachometer(config)
|
||||
|
||||
|
|
@ -45,37 +46,37 @@ class Fan:
|
|||
|
||||
def get_mcu(self):
|
||||
return self.mcu_fan.get_mcu()
|
||||
def set_speed(self, print_time, value):
|
||||
def _apply_speed(self, print_time, value):
|
||||
if value < self.off_below:
|
||||
value = 0.
|
||||
value = max(0., min(self.max_power, value * self.max_power))
|
||||
if value == self.last_fan_value:
|
||||
return
|
||||
print_time = max(self.last_fan_time + FAN_MIN_TIME, print_time)
|
||||
return "discard", 0.
|
||||
if self.enable_pin:
|
||||
if value > 0 and self.last_fan_value == 0:
|
||||
self.enable_pin.set_digital(print_time, 1)
|
||||
elif value == 0 and self.last_fan_value > 0:
|
||||
self.enable_pin.set_digital(print_time, 0)
|
||||
if (value and value < self.max_power and self.kick_start_time
|
||||
if (value and self.kick_start_time
|
||||
and (not self.last_fan_value or value - self.last_fan_value > .5)):
|
||||
# Run fan at full speed for specified kick_start_time
|
||||
self.last_req_value = value
|
||||
self.last_fan_value = self.max_power
|
||||
self.mcu_fan.set_pwm(print_time, self.max_power)
|
||||
print_time += self.kick_start_time
|
||||
return "delay", self.kick_start_time
|
||||
self.last_fan_value = self.last_req_value = value
|
||||
self.mcu_fan.set_pwm(print_time, value)
|
||||
self.last_fan_time = print_time
|
||||
self.last_fan_value = value
|
||||
def set_speed(self, value, print_time=None):
|
||||
self.gcrq.send_async_request(value, print_time)
|
||||
def set_speed_from_command(self, value):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
toolhead.register_lookahead_callback((lambda pt:
|
||||
self.set_speed(pt, value)))
|
||||
self.gcrq.queue_gcode_request(value)
|
||||
def _handle_request_restart(self, print_time):
|
||||
self.set_speed(print_time, 0.)
|
||||
self.set_speed(0., print_time)
|
||||
|
||||
def get_status(self, eventtime):
|
||||
tachometer_status = self.tachometer.get_status(eventtime)
|
||||
return {
|
||||
'speed': self.last_fan_value,
|
||||
'speed': self.last_req_value,
|
||||
'rpm': tachometer_status['rpm'],
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# Support fans that are controlled by gcode
|
||||
#
|
||||
# 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.
|
||||
from . import fan
|
||||
import logging
|
||||
from . import fan, output_pin
|
||||
|
||||
class PrinterFanGeneric:
|
||||
cmd_SET_FAN_SPEED_help = "Sets the speed of a fan"
|
||||
|
|
@ -12,6 +13,9 @@ class PrinterFanGeneric:
|
|||
self.fan = fan.Fan(config, default_shutdown_speed=0.)
|
||||
self.fan_name = config.get_name().split()[-1]
|
||||
|
||||
# Template handling
|
||||
self.template_eval = output_pin.lookup_template_eval(config)
|
||||
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
gcode.register_mux_command("SET_FAN_SPEED", "FAN",
|
||||
self.fan_name,
|
||||
|
|
@ -20,8 +24,21 @@ class PrinterFanGeneric:
|
|||
|
||||
def get_status(self, eventtime):
|
||||
return self.fan.get_status(eventtime)
|
||||
def _template_update(self, text):
|
||||
try:
|
||||
value = float(text)
|
||||
except ValueError as e:
|
||||
logging.exception("fan_generic template render error")
|
||||
self.fan.set_speed(value)
|
||||
def cmd_SET_FAN_SPEED(self, gcmd):
|
||||
speed = gcmd.get_float('SPEED', 0.)
|
||||
speed = gcmd.get_float('SPEED', None, 0.)
|
||||
template = gcmd.get('TEMPLATE', None)
|
||||
if (speed is None) == (template is None):
|
||||
raise gcmd.error("SET_FAN_SPEED must specify SPEED or TEMPLATE")
|
||||
# Check for template setting
|
||||
if template is not None:
|
||||
self.template_eval.set_template(gcmd, self._template_update)
|
||||
return
|
||||
self.fan.set_speed_from_command(speed)
|
||||
|
||||
def load_config_prefix(config):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -33,9 +33,7 @@ class PrinterHeaterFan:
|
|||
speed = self.fan_speed
|
||||
if speed != self.last_speed:
|
||||
self.last_speed = speed
|
||||
curtime = self.printer.get_reactor().monotonic()
|
||||
print_time = self.fan.get_mcu().estimated_print_time(curtime)
|
||||
self.fan.set_speed(print_time + PIN_MIN_TIME, speed)
|
||||
self.fan.set_speed(speed)
|
||||
return eventtime + 1.
|
||||
|
||||
def load_config_prefix(config):
|
||||
|
|
|
|||
|
|
@ -259,7 +259,8 @@ class PrinterHeaters:
|
|||
try:
|
||||
dconfig = pconfig.read_config(filename)
|
||||
except Exception:
|
||||
raise config.config_error("Cannot load config '%s'" % (filename,))
|
||||
logging.exception("Unable to load temperature_sensors.cfg")
|
||||
raise config.error("Cannot load config '%s'" % (filename,))
|
||||
for c in dconfig.get_prefix_sections(''):
|
||||
self.printer.load_object(dconfig, c.get_name())
|
||||
def add_sensor_factory(self, sensor_type, sensor_factory):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Helper code for implementing homing operations
|
||||
#
|
||||
# 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 logging, math
|
||||
|
|
@ -29,10 +29,17 @@ class StepperPosition:
|
|||
self.endstop_name = endstop_name
|
||||
self.stepper_name = stepper.get_name()
|
||||
self.start_pos = stepper.get_mcu_position()
|
||||
self.start_cmd_pos = stepper.mcu_to_commanded_position(self.start_pos)
|
||||
self.halt_pos = self.trig_pos = None
|
||||
def note_home_end(self, trigger_time):
|
||||
self.halt_pos = self.stepper.get_mcu_position()
|
||||
self.trig_pos = self.stepper.get_past_mcu_position(trigger_time)
|
||||
def verify_no_probe_skew(self, haltpos):
|
||||
new_start_pos = self.stepper.get_mcu_position(self.start_cmd_pos)
|
||||
if new_start_pos != self.start_pos:
|
||||
logging.warning(
|
||||
"Stepper '%s' position skew after probe: pos %d now %d",
|
||||
self.stepper.get_name(), self.start_pos, new_start_pos)
|
||||
|
||||
# Implementation of homing/probing moves
|
||||
class HomingMove:
|
||||
|
|
@ -98,11 +105,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
|
||||
|
|
@ -118,6 +128,9 @@ class HomingMove:
|
|||
haltpos = trigpos = self.calc_toolhead_pos(kin_spos, trig_steps)
|
||||
if trig_steps != halt_steps:
|
||||
haltpos = self.calc_toolhead_pos(kin_spos, halt_steps)
|
||||
self.toolhead.set_position(haltpos)
|
||||
for sp in self.stepper_positions:
|
||||
sp.verify_no_probe_skew(haltpos)
|
||||
else:
|
||||
haltpos = trigpos = movepos
|
||||
over_steps = {sp.stepper_name: sp.halt_pos - sp.trig_pos
|
||||
|
|
@ -127,7 +140,7 @@ class HomingMove:
|
|||
halt_kin_spos = {s.get_name(): s.get_commanded_position()
|
||||
for s in kin.get_steppers()}
|
||||
haltpos = self.calc_toolhead_pos(halt_kin_spos, over_steps)
|
||||
self.toolhead.set_position(haltpos)
|
||||
self.toolhead.set_position(haltpos)
|
||||
# Signal homing/probing move complete
|
||||
try:
|
||||
self.printer.send_event("homing:homing_move_end", self)
|
||||
|
|
|
|||
168
klippy/extras/hx71x.py
Normal file
168
klippy/extras/hx71x.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# 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()}
|
||||
|
||||
|
||||
def HX711(config):
|
||||
return HX71xBase(config, "hx711",
|
||||
# HX711 sps options
|
||||
{80: 80, 10: 10}, 80,
|
||||
# HX711 gain/channel options
|
||||
{'A-128': 1, 'B-32': 2, 'A-64': 3}, 'A-128')
|
||||
|
||||
|
||||
def HX717(config):
|
||||
return HX71xBase(config, "hx717",
|
||||
# HX717 sps options
|
||||
{320: 320, 80: 80, 20: 20, 10: 10}, 320,
|
||||
# HX717 gain/channel options
|
||||
{'A-128': 1, 'B-64': 2, 'A-64': 3,
|
||||
'B-8': 4}, 'A-128')
|
||||
|
||||
|
||||
HX71X_SENSOR_TYPES = {
|
||||
"hx711": HX711,
|
||||
"hx717": HX717
|
||||
}
|
||||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
# Support for PWM driven LEDs
|
||||
#
|
||||
# Copyright (C) 2019-2022 Kevin O'Connor <kevin@koconnor.net>
|
||||
# Copyright (C) 2019-2024 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging, ast
|
||||
from .display import display
|
||||
|
||||
# Time between each led template update
|
||||
RENDER_TIME = 0.500
|
||||
import logging
|
||||
from . import output_pin
|
||||
|
||||
# Helper code for common LED initialization and control
|
||||
class LEDHelper:
|
||||
|
|
@ -22,14 +19,22 @@ class LEDHelper:
|
|||
blue = config.getfloat('initial_BLUE', 0., minval=0., maxval=1.)
|
||||
white = config.getfloat('initial_WHITE', 0., minval=0., maxval=1.)
|
||||
self.led_state = [(red, green, blue, white)] * led_count
|
||||
# Support setting an led template
|
||||
self.template_eval = output_pin.lookup_template_eval(config)
|
||||
self.tcallbacks = [(lambda text, s=self, index=i:
|
||||
s._template_update(index, text))
|
||||
for i in range(led_count)]
|
||||
# Register commands
|
||||
name = config.get_name().split()[-1]
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_mux_command("SET_LED", "LED", name, self.cmd_SET_LED,
|
||||
desc=self.cmd_SET_LED_help)
|
||||
def get_led_count(self):
|
||||
return self.led_count
|
||||
def set_color(self, index, color):
|
||||
gcode.register_mux_command("SET_LED_TEMPLATE", "LED", name,
|
||||
self.cmd_SET_LED_TEMPLATE,
|
||||
desc=self.cmd_SET_LED_TEMPLATE_help)
|
||||
def get_status(self, eventtime=None):
|
||||
return {'color_data': self.led_state}
|
||||
def _set_color(self, index, color):
|
||||
if index is None:
|
||||
new_led_state = [color] * self.led_count
|
||||
if self.led_state == new_led_state:
|
||||
|
|
@ -41,7 +46,17 @@ class LEDHelper:
|
|||
new_led_state[index - 1] = color
|
||||
self.led_state = new_led_state
|
||||
self.need_transmit = True
|
||||
def check_transmit(self, print_time):
|
||||
def _template_update(self, index, text):
|
||||
try:
|
||||
parts = [max(0., min(1., float(f)))
|
||||
for f in text.split(',', 4)]
|
||||
except ValueError as e:
|
||||
logging.exception("led template render error")
|
||||
parts = []
|
||||
if len(parts) < 4:
|
||||
parts += [0.] * (4 - len(parts))
|
||||
self._set_color(index, tuple(parts))
|
||||
def _check_transmit(self, print_time=None):
|
||||
if not self.need_transmit:
|
||||
return
|
||||
self.need_transmit = False
|
||||
|
|
@ -62,9 +77,9 @@ class LEDHelper:
|
|||
color = (red, green, blue, white)
|
||||
# Update and transmit data
|
||||
def lookahead_bgfunc(print_time):
|
||||
self.set_color(index, color)
|
||||
self._set_color(index, color)
|
||||
if transmit:
|
||||
self.check_transmit(print_time)
|
||||
self._check_transmit(print_time)
|
||||
if sync:
|
||||
#Sync LED Update with print time and send
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
|
|
@ -72,112 +87,15 @@ class LEDHelper:
|
|||
else:
|
||||
#Send update now (so as not to wake toolhead and reset idle_timeout)
|
||||
lookahead_bgfunc(None)
|
||||
def get_status(self, eventtime=None):
|
||||
return {'color_data': self.led_state}
|
||||
|
||||
# Main LED tracking code
|
||||
class PrinterLED:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.led_helpers = {}
|
||||
self.active_templates = {}
|
||||
self.render_timer = None
|
||||
# Load templates
|
||||
dtemplates = display.lookup_display_templates(config)
|
||||
self.templates = dtemplates.get_display_templates()
|
||||
gcode_macro = self.printer.lookup_object("gcode_macro")
|
||||
self.create_template_context = gcode_macro.create_template_context
|
||||
# Register handlers
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command("SET_LED_TEMPLATE", self.cmd_SET_LED_TEMPLATE,
|
||||
desc=self.cmd_SET_LED_TEMPLATE_help)
|
||||
def setup_helper(self, config, update_func, led_count=1):
|
||||
led_helper = LEDHelper(config, update_func, led_count)
|
||||
name = config.get_name().split()[-1]
|
||||
self.led_helpers[name] = led_helper
|
||||
return led_helper
|
||||
def _activate_timer(self):
|
||||
if self.render_timer is not None or not self.active_templates:
|
||||
return
|
||||
reactor = self.printer.get_reactor()
|
||||
self.render_timer = reactor.register_timer(self._render, reactor.NOW)
|
||||
def _activate_template(self, led_helper, index, template, lparams):
|
||||
key = (led_helper, index)
|
||||
if template is not None:
|
||||
uid = (template,) + tuple(sorted(lparams.items()))
|
||||
self.active_templates[key] = (uid, template, lparams)
|
||||
return
|
||||
if key in self.active_templates:
|
||||
del self.active_templates[key]
|
||||
def _render(self, eventtime):
|
||||
if not self.active_templates:
|
||||
# Nothing to do - unregister timer
|
||||
reactor = self.printer.get_reactor()
|
||||
reactor.register_timer(self.render_timer)
|
||||
self.render_timer = None
|
||||
return reactor.NEVER
|
||||
# Setup gcode_macro template context
|
||||
context = self.create_template_context(eventtime)
|
||||
def render(name, **kwargs):
|
||||
return self.templates[name].render(context, **kwargs)
|
||||
context['render'] = render
|
||||
# Render all templates
|
||||
need_transmit = {}
|
||||
rendered = {}
|
||||
template_info = self.active_templates.items()
|
||||
for (led_helper, index), (uid, template, lparams) in template_info:
|
||||
color = rendered.get(uid)
|
||||
if color is None:
|
||||
try:
|
||||
text = template.render(context, **lparams)
|
||||
parts = [max(0., min(1., float(f)))
|
||||
for f in text.split(',', 4)]
|
||||
except Exception as e:
|
||||
logging.exception("led template render error")
|
||||
parts = []
|
||||
if len(parts) < 4:
|
||||
parts += [0.] * (4 - len(parts))
|
||||
rendered[uid] = color = tuple(parts)
|
||||
need_transmit[led_helper] = 1
|
||||
led_helper.set_color(index, color)
|
||||
context.clear() # Remove circular references for better gc
|
||||
# Transmit pending changes
|
||||
for led_helper in need_transmit.keys():
|
||||
led_helper.check_transmit(None)
|
||||
return eventtime + RENDER_TIME
|
||||
cmd_SET_LED_TEMPLATE_help = "Assign a display_template to an LED"
|
||||
def cmd_SET_LED_TEMPLATE(self, gcmd):
|
||||
led_name = gcmd.get("LED")
|
||||
led_helper = self.led_helpers.get(led_name)
|
||||
if led_helper is None:
|
||||
raise gcmd.error("Unknown LED '%s'" % (led_name,))
|
||||
led_count = led_helper.get_led_count()
|
||||
index = gcmd.get_int("INDEX", None, minval=1, maxval=led_count)
|
||||
template = None
|
||||
lparams = {}
|
||||
tpl_name = gcmd.get("TEMPLATE")
|
||||
if tpl_name:
|
||||
template = self.templates.get(tpl_name)
|
||||
if template is None:
|
||||
raise gcmd.error("Unknown display_template '%s'" % (tpl_name,))
|
||||
tparams = template.get_params()
|
||||
for p, v in gcmd.get_command_parameters().items():
|
||||
if not p.startswith("PARAM_"):
|
||||
continue
|
||||
p = p.lower()
|
||||
if p not in tparams:
|
||||
raise gcmd.error("Invalid display_template parameter: %s"
|
||||
% (p,))
|
||||
try:
|
||||
lparams[p] = ast.literal_eval(v)
|
||||
except ValueError as e:
|
||||
raise gcmd.error("Unable to parse '%s' as a literal" % (v,))
|
||||
index = gcmd.get_int("INDEX", None, minval=1, maxval=self.led_count)
|
||||
set_template = self.template_eval.set_template
|
||||
if index is not None:
|
||||
self._activate_template(led_helper, index, template, lparams)
|
||||
set_template(gcmd, self.tcallbacks[index-1], self._check_transmit)
|
||||
else:
|
||||
for i in range(led_count):
|
||||
self._activate_template(led_helper, i+1, template, lparams)
|
||||
self._activate_timer()
|
||||
for i in range(self.led_count):
|
||||
set_template(gcmd, self.tcallbacks[i], self._check_transmit)
|
||||
|
||||
PIN_MIN_TIME = 0.100
|
||||
MAX_SCHEDULE_TIME = 5.0
|
||||
|
|
@ -205,8 +123,7 @@ class PrinterPWMLED:
|
|||
% (config.get_name(),))
|
||||
self.last_print_time = 0.
|
||||
# Initialize color data
|
||||
pled = printer.load_object(config, "led")
|
||||
self.led_helper = pled.setup_helper(config, self.update_leds, 1)
|
||||
self.led_helper = LEDHelper(config, self.update_leds, 1)
|
||||
self.prev_color = color = self.led_helper.get_status()['color_data'][0]
|
||||
for idx, mcu_pin in self.pins:
|
||||
mcu_pin.setup_start_value(color[idx], 0.)
|
||||
|
|
@ -225,8 +142,5 @@ class PrinterPWMLED:
|
|||
def get_status(self, eventtime=None):
|
||||
return self.led_helper.get_status(eventtime)
|
||||
|
||||
def load_config(config):
|
||||
return PrinterLED(config)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterPWMLED(config)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ REG_LIS2DW_WHO_AM_I_ADDR = 0x0F
|
|||
REG_LIS2DW_CTRL_REG1_ADDR = 0x20
|
||||
REG_LIS2DW_CTRL_REG2_ADDR = 0x21
|
||||
REG_LIS2DW_CTRL_REG3_ADDR = 0x22
|
||||
REG_LIS2DW_CTRL_REG4_ADDR = 0x23
|
||||
REG_LIS2DW_CTRL_REG5_ADDR = 0x24
|
||||
REG_LIS2DW_CTRL_REG6_ADDR = 0x25
|
||||
REG_LIS2DW_STATUS_REG_ADDR = 0x27
|
||||
REG_LIS2DW_OUT_XL_ADDR = 0x28
|
||||
|
|
@ -26,26 +28,57 @@ REG_MOD_READ = 0x80
|
|||
# REG_MOD_MULTI = 0x40
|
||||
|
||||
LIS2DW_DEV_ID = 0x44
|
||||
LIS3DH_DEV_ID = 0x33
|
||||
|
||||
LIS_I2C_ADDR = 0x19
|
||||
|
||||
# Right shift for left justified registers.
|
||||
FREEFALL_ACCEL = 9.80665
|
||||
SCALE = FREEFALL_ACCEL * 1.952 / 4
|
||||
LIS2DW_SCALE = FREEFALL_ACCEL * 1.952 / 4
|
||||
LIS3DH_SCALE = FREEFALL_ACCEL * 3.906 / 16
|
||||
|
||||
BATCH_UPDATES = 0.100
|
||||
|
||||
# "Enums" that should be compatible with all python versions
|
||||
|
||||
LIS2DW_TYPE = 'LIS2DW'
|
||||
LIS3DH_TYPE = 'LIS3DH'
|
||||
|
||||
SPI_SERIAL_TYPE = 'spi'
|
||||
I2C_SERIAL_TYPE = 'i2c'
|
||||
|
||||
# Printer class that controls LIS2DW chip
|
||||
class LIS2DW:
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, lis_type):
|
||||
self.printer = config.get_printer()
|
||||
adxl345.AccelCommandHelper(config, self)
|
||||
self.axes_map = adxl345.read_axes_map(config)
|
||||
self.data_rate = 1600
|
||||
self.lis_type = lis_type
|
||||
if self.lis_type == LIS2DW_TYPE:
|
||||
self.axes_map = adxl345.read_axes_map(config, LIS2DW_SCALE,
|
||||
LIS2DW_SCALE, LIS2DW_SCALE)
|
||||
self.data_rate = 1600
|
||||
else:
|
||||
self.axes_map = adxl345.read_axes_map(config, LIS3DH_SCALE,
|
||||
LIS3DH_SCALE, LIS3DH_SCALE)
|
||||
self.data_rate = 1344
|
||||
# Check for spi or i2c
|
||||
if config.get('cs_pin', None) is not None:
|
||||
self.bus_type = SPI_SERIAL_TYPE
|
||||
else:
|
||||
self.bus_type = I2C_SERIAL_TYPE
|
||||
# Setup mcu sensor_lis2dw bulk query code
|
||||
self.spi = bus.MCU_SPI_from_config(config, 3, default_speed=5000000)
|
||||
self.mcu = mcu = self.spi.get_mcu()
|
||||
if self.bus_type == SPI_SERIAL_TYPE:
|
||||
self.bus = bus.MCU_SPI_from_config(config,
|
||||
3, default_speed=5000000)
|
||||
else:
|
||||
self.bus = bus.MCU_I2C_from_config(config,
|
||||
default_addr=LIS_I2C_ADDR, default_speed=400000)
|
||||
self.mcu = mcu = self.bus.get_mcu()
|
||||
self.oid = oid = mcu.create_oid()
|
||||
self.query_lis2dw_cmd = None
|
||||
mcu.add_config_cmd("config_lis2dw oid=%d spi_oid=%d"
|
||||
% (oid, self.spi.get_oid()))
|
||||
mcu.add_config_cmd("config_lis2dw oid=%d bus_oid=%d bus_oid_type=%s "
|
||||
"lis_chip_type=%s" % (oid, self.bus.get_oid(),
|
||||
self.bus_type, self.lis_type))
|
||||
mcu.add_config_cmd("query_lis2dw oid=%d rest_ticks=0"
|
||||
% (oid,), on_restart=True)
|
||||
mcu.register_config_callback(self._build_config)
|
||||
|
|
@ -63,17 +96,23 @@ class LIS2DW:
|
|||
self.name, {'header': hdr})
|
||||
|
||||
def _build_config(self):
|
||||
cmdqueue = self.spi.get_command_queue()
|
||||
cmdqueue = self.bus.get_command_queue()
|
||||
self.query_lis2dw_cmd = self.mcu.lookup_command(
|
||||
"query_lis2dw oid=%c rest_ticks=%u", cq=cmdqueue)
|
||||
self.ffreader.setup_query_command("query_lis2dw_status oid=%c",
|
||||
oid=self.oid, cq=cmdqueue)
|
||||
def read_reg(self, reg):
|
||||
params = self.spi.spi_transfer([reg | REG_MOD_READ, 0x00])
|
||||
response = bytearray(params['response'])
|
||||
return response[1]
|
||||
if self.bus_type == SPI_SERIAL_TYPE:
|
||||
params = self.bus.spi_transfer([reg | REG_MOD_READ, 0x00])
|
||||
response = bytearray(params['response'])
|
||||
return response[1]
|
||||
params = self.bus.i2c_read([reg], 1)
|
||||
return bytearray(params['response'])[0]
|
||||
def set_reg(self, reg, val, minclock=0):
|
||||
self.spi.spi_send([reg, val & 0xFF], minclock=minclock)
|
||||
if self.bus_type == SPI_SERIAL_TYPE:
|
||||
self.bus.spi_send([reg, val & 0xFF], minclock=minclock)
|
||||
else:
|
||||
self.bus.i2c_write([reg, val & 0xFF], minclock=minclock)
|
||||
stored_val = self.read_reg(reg)
|
||||
if stored_val != val:
|
||||
raise self.printer.command_error(
|
||||
|
|
@ -102,26 +141,48 @@ class LIS2DW:
|
|||
# noise or wrong signal as a correctly initialized device
|
||||
dev_id = self.read_reg(REG_LIS2DW_WHO_AM_I_ADDR)
|
||||
logging.info("lis2dw_dev_id: %x", dev_id)
|
||||
if dev_id != LIS2DW_DEV_ID:
|
||||
raise self.printer.command_error(
|
||||
"Invalid lis2dw id (got %x vs %x).\n"
|
||||
"This is generally indicative of connection problems\n"
|
||||
"(e.g. faulty wiring) or a faulty lis2dw chip."
|
||||
% (dev_id, LIS2DW_DEV_ID))
|
||||
# Setup chip in requested query rate
|
||||
# ODR/2, +-16g, low-pass filter, Low-noise abled
|
||||
self.set_reg(REG_LIS2DW_CTRL_REG6_ADDR, 0x34)
|
||||
# Continuous mode: If the FIFO is full
|
||||
# the new sample overwrites the older sample.
|
||||
self.set_reg(REG_LIS2DW_FIFO_CTRL, 0xC0)
|
||||
# High-Performance / Low-Power mode 1600/200 Hz
|
||||
# High-Performance Mode (14-bit resolution)
|
||||
self.set_reg(REG_LIS2DW_CTRL_REG1_ADDR, 0x94)
|
||||
|
||||
if self.lis_type == LIS2DW_TYPE:
|
||||
if dev_id != LIS2DW_DEV_ID:
|
||||
raise self.printer.command_error(
|
||||
"Invalid lis2dw id (got %x vs %x).\n"
|
||||
"This is generally indicative of connection problems\n"
|
||||
"(e.g. faulty wiring) or a faulty lis2dw chip."
|
||||
% (dev_id, LIS2DW_DEV_ID))
|
||||
# Setup chip in requested query rate
|
||||
# ODR/2, +-16g, low-pass filter, Low-noise abled
|
||||
self.set_reg(REG_LIS2DW_CTRL_REG6_ADDR, 0x34)
|
||||
# Continuous mode: If the FIFO is full
|
||||
# the new sample overwrites the older sample.
|
||||
self.set_reg(REG_LIS2DW_FIFO_CTRL, 0xC0)
|
||||
# High-Performance / Low-Power mode 1600/200 Hz
|
||||
# High-Performance Mode (14-bit resolution)
|
||||
self.set_reg(REG_LIS2DW_CTRL_REG1_ADDR, 0x94)
|
||||
else:
|
||||
if dev_id != LIS3DH_DEV_ID:
|
||||
raise self.printer.command_error(
|
||||
"Invalid lis3dh id (got %x vs %x).\n"
|
||||
"This is generally indicative of connection problems\n"
|
||||
"(e.g. faulty wiring) or a faulty lis3dh chip."
|
||||
% (dev_id, LIS3DH_DEV_ID))
|
||||
# High Resolution / Low Power mode 1344/5376 Hz
|
||||
# High Resolution mode (12-bit resolution)
|
||||
# Enable X Y Z axes
|
||||
self.set_reg(REG_LIS2DW_CTRL_REG1_ADDR, 0x97)
|
||||
# Disable all filtering
|
||||
self.set_reg(REG_LIS2DW_CTRL_REG2_ADDR, 0)
|
||||
# Set +-8g, High Resolution mode
|
||||
self.set_reg(REG_LIS2DW_CTRL_REG4_ADDR, 0x28)
|
||||
# Enable FIFO
|
||||
self.set_reg(REG_LIS2DW_CTRL_REG5_ADDR, 0x40)
|
||||
# Stream mode
|
||||
self.set_reg(REG_LIS2DW_FIFO_CTRL, 0x80)
|
||||
# Start bulk reading
|
||||
rest_ticks = self.mcu.seconds_to_clock(4. / self.data_rate)
|
||||
self.query_lis2dw_cmd.send([self.oid, rest_ticks])
|
||||
self.set_reg(REG_LIS2DW_FIFO_CTRL, 0xC0)
|
||||
if self.lis_type == LIS2DW_TYPE:
|
||||
self.set_reg(REG_LIS2DW_FIFO_CTRL, 0xC0)
|
||||
else:
|
||||
self.set_reg(REG_LIS2DW_FIFO_CTRL, 0x80)
|
||||
logging.info("LIS2DW starting '%s' measurements", self.name)
|
||||
# Initialize clock tracking
|
||||
self.ffreader.note_start()
|
||||
|
|
@ -142,7 +203,7 @@ class LIS2DW:
|
|||
'overflows': self.ffreader.get_last_overflows()}
|
||||
|
||||
def load_config(config):
|
||||
return LIS2DW(config)
|
||||
return LIS2DW(config, LIS2DW_TYPE)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return LIS2DW(config)
|
||||
return LIS2DW(config, LIS2DW_TYPE)
|
||||
|
|
|
|||
12
klippy/extras/lis3dh.py
Normal file
12
klippy/extras/lis3dh.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Support for reading acceleration data from an LIS3DH chip
|
||||
#
|
||||
# Copyright (C) 2024 Luke Vuksta <wulfstawulfsta@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
from . import lis2dw
|
||||
|
||||
def load_config(config):
|
||||
return lis2dw.LIS2DW(config, lis2dw.LIS3DH_TYPE)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return lis2dw.LIS2DW(config, lis2dw.LIS3DH_TYPE)
|
||||
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)
|
||||
|
|
@ -59,7 +59,7 @@ class MPU9250:
|
|||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
adxl345.AccelCommandHelper(config, self)
|
||||
self.axes_map = adxl345.read_axes_map(config)
|
||||
self.axes_map = adxl345.read_axes_map(config, SCALE, SCALE, SCALE)
|
||||
self.data_rate = config.getint('rate', 4000)
|
||||
if self.data_rate not in SAMPLE_RATE_DIVS:
|
||||
raise config.error("Invalid rate parameter: %d" % (self.data_rate,))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging
|
||||
from . import led
|
||||
|
||||
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
|
||||
|
||||
|
|
@ -40,9 +41,7 @@ class PrinterNeoPixel:
|
|||
if len(self.color_map) > MAX_MCU_SIZE:
|
||||
raise config.error("neopixel chain too long")
|
||||
# Initialize color data
|
||||
pled = printer.load_object(config, "led")
|
||||
self.led_helper = pled.setup_helper(config, self.update_leds,
|
||||
chain_count)
|
||||
self.led_helper = led.LEDHelper(config, self.update_leds, chain_count)
|
||||
self.color_data = bytearray(len(self.color_map))
|
||||
self.update_color_data(self.led_helper.get_status()['color_data'])
|
||||
self.old_color_data = bytearray([d ^ 1 for d in self.color_data])
|
||||
|
|
|
|||
|
|
@ -3,9 +3,180 @@
|
|||
# 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, ast
|
||||
from .display import display
|
||||
|
||||
|
||||
######################################################################
|
||||
# G-Code request queuing helper
|
||||
######################################################################
|
||||
|
||||
PIN_MIN_TIME = 0.100
|
||||
RESEND_HOST_TIME = 0.300 + PIN_MIN_TIME
|
||||
|
||||
# Helper code to queue g-code requests
|
||||
class GCodeRequestQueue:
|
||||
def __init__(self, config, mcu, callback):
|
||||
self.printer = printer = config.get_printer()
|
||||
self.mcu = mcu
|
||||
self.callback = callback
|
||||
self.rqueue = []
|
||||
self.next_min_flush_time = 0.
|
||||
self.toolhead = None
|
||||
mcu.register_flush_callback(self._flush_notification)
|
||||
printer.register_event_handler("klippy:connect", self._handle_connect)
|
||||
def _handle_connect(self):
|
||||
self.toolhead = self.printer.lookup_object('toolhead')
|
||||
def _flush_notification(self, print_time, clock):
|
||||
rqueue = self.rqueue
|
||||
while rqueue:
|
||||
next_time = max(rqueue[0][0], self.next_min_flush_time)
|
||||
if next_time > print_time:
|
||||
return
|
||||
# Skip requests that have been overridden with a following request
|
||||
pos = 0
|
||||
while pos + 1 < len(rqueue) and rqueue[pos + 1][0] <= next_time:
|
||||
pos += 1
|
||||
req_pt, req_val = rqueue[pos]
|
||||
# Invoke callback for the request
|
||||
min_wait = 0.
|
||||
ret = self.callback(next_time, req_val)
|
||||
if ret is not None:
|
||||
# Handle special cases
|
||||
action, min_wait = ret
|
||||
if action == "discard":
|
||||
del rqueue[:pos+1]
|
||||
continue
|
||||
if action == "delay":
|
||||
pos -= 1
|
||||
del rqueue[:pos+1]
|
||||
self.next_min_flush_time = next_time + max(min_wait, PIN_MIN_TIME)
|
||||
# Ensure following queue items are flushed
|
||||
self.toolhead.note_mcu_movequeue_activity(self.next_min_flush_time)
|
||||
def _queue_request(self, print_time, value):
|
||||
self.rqueue.append((print_time, value))
|
||||
self.toolhead.note_mcu_movequeue_activity(print_time)
|
||||
def queue_gcode_request(self, value):
|
||||
self.toolhead.register_lookahead_callback(
|
||||
(lambda pt: self._queue_request(pt, value)))
|
||||
def send_async_request(self, value, print_time=None):
|
||||
if print_time is None:
|
||||
systime = self.printer.get_reactor().monotonic()
|
||||
print_time = self.mcu.estimated_print_time(systime + PIN_MIN_TIME)
|
||||
while 1:
|
||||
next_time = max(print_time, self.next_min_flush_time)
|
||||
# Invoke callback for the request
|
||||
action, min_wait = "normal", 0.
|
||||
ret = self.callback(next_time, value)
|
||||
if ret is not None:
|
||||
# Handle special cases
|
||||
action, min_wait = ret
|
||||
if action == "discard":
|
||||
break
|
||||
self.next_min_flush_time = next_time + max(min_wait, PIN_MIN_TIME)
|
||||
if action != "delay":
|
||||
break
|
||||
|
||||
|
||||
######################################################################
|
||||
# Template evaluation helper
|
||||
######################################################################
|
||||
|
||||
# Time between each template update
|
||||
RENDER_TIME = 0.500
|
||||
|
||||
# Main template evaluation code
|
||||
class PrinterTemplateEvaluator:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.active_templates = {}
|
||||
self.render_timer = None
|
||||
# Load templates
|
||||
dtemplates = display.lookup_display_templates(config)
|
||||
self.templates = dtemplates.get_display_templates()
|
||||
gcode_macro = self.printer.load_object(config, "gcode_macro")
|
||||
self.create_template_context = gcode_macro.create_template_context
|
||||
def _activate_timer(self):
|
||||
if self.render_timer is not None or not self.active_templates:
|
||||
return
|
||||
reactor = self.printer.get_reactor()
|
||||
self.render_timer = reactor.register_timer(self._render, reactor.NOW)
|
||||
def _activate_template(self, callback, template, lparams, flush_callback):
|
||||
if template is not None:
|
||||
uid = (template,) + tuple(sorted(lparams.items()))
|
||||
self.active_templates[callback] = (
|
||||
uid, template, lparams, flush_callback)
|
||||
return
|
||||
if callback in self.active_templates:
|
||||
del self.active_templates[callback]
|
||||
def _render(self, eventtime):
|
||||
if not self.active_templates:
|
||||
# Nothing to do - unregister timer
|
||||
reactor = self.printer.get_reactor()
|
||||
reactor.unregister_timer(self.render_timer)
|
||||
self.render_timer = None
|
||||
return reactor.NEVER
|
||||
# Setup gcode_macro template context
|
||||
context = self.create_template_context(eventtime)
|
||||
def render(name, **kwargs):
|
||||
return self.templates[name].render(context, **kwargs)
|
||||
context['render'] = render
|
||||
# Render all templates
|
||||
flush_callbacks = {}
|
||||
rendered = {}
|
||||
template_info = self.active_templates.items()
|
||||
for callback, (uid, template, lparams, flush_callback) in template_info:
|
||||
text = rendered.get(uid)
|
||||
if text is None:
|
||||
try:
|
||||
text = template.render(context, **lparams)
|
||||
except Exception as e:
|
||||
logging.exception("display template render error")
|
||||
text = ""
|
||||
rendered[uid] = text
|
||||
if flush_callback is not None:
|
||||
flush_callbacks[flush_callback] = 1
|
||||
callback(text)
|
||||
context.clear() # Remove circular references for better gc
|
||||
# Invoke optional flush callbacks
|
||||
for flush_callback in flush_callbacks.keys():
|
||||
flush_callback()
|
||||
return eventtime + RENDER_TIME
|
||||
def set_template(self, gcmd, callback, flush_callback=None):
|
||||
template = None
|
||||
lparams = {}
|
||||
tpl_name = gcmd.get("TEMPLATE")
|
||||
if tpl_name:
|
||||
template = self.templates.get(tpl_name)
|
||||
if template is None:
|
||||
raise gcmd.error("Unknown display_template '%s'" % (tpl_name,))
|
||||
tparams = template.get_params()
|
||||
for p, v in gcmd.get_command_parameters().items():
|
||||
if not p.startswith("PARAM_"):
|
||||
continue
|
||||
p = p.lower()
|
||||
if p not in tparams:
|
||||
raise gcmd.error("Invalid display_template parameter: %s"
|
||||
% (p,))
|
||||
try:
|
||||
lparams[p] = ast.literal_eval(v)
|
||||
except ValueError as e:
|
||||
raise gcmd.error("Unable to parse '%s' as a literal" % (v,))
|
||||
self._activate_template(callback, template, lparams, flush_callback)
|
||||
self._activate_timer()
|
||||
|
||||
def lookup_template_eval(config):
|
||||
printer = config.get_printer()
|
||||
te = printer.lookup_object("template_evaluator", None)
|
||||
if te is None:
|
||||
te = PrinterTemplateEvaluator(config)
|
||||
printer.add_object("template_evaluator", te)
|
||||
return te
|
||||
|
||||
|
||||
######################################################################
|
||||
# Main output pin handling
|
||||
######################################################################
|
||||
|
||||
MAX_SCHEDULE_TIME = 5.0
|
||||
|
||||
class PrinterOutputPin:
|
||||
|
|
@ -24,30 +195,18 @@ class PrinterOutputPin:
|
|||
else:
|
||||
self.mcu_pin = ppins.setup_pin('digital_out', config.get('pin'))
|
||||
self.scale = 1.
|
||||
self.last_print_time = 0.
|
||||
# Support mcu checking for maximum duration
|
||||
self.reactor = self.printer.get_reactor()
|
||||
self.resend_timer = None
|
||||
self.resend_interval = 0.
|
||||
max_mcu_duration = config.getfloat('maximum_mcu_duration', 0.,
|
||||
minval=0.500,
|
||||
maxval=MAX_SCHEDULE_TIME)
|
||||
self.mcu_pin.setup_max_duration(max_mcu_duration)
|
||||
if max_mcu_duration:
|
||||
config.deprecate('maximum_mcu_duration')
|
||||
self.resend_interval = max_mcu_duration - RESEND_HOST_TIME
|
||||
self.mcu_pin.setup_max_duration(0.)
|
||||
# Determine start and shutdown values
|
||||
static_value = config.getfloat('static_value', None,
|
||||
minval=0., maxval=self.scale)
|
||||
if static_value is not None:
|
||||
config.deprecate('static_value')
|
||||
self.last_value = self.shutdown_value = static_value / self.scale
|
||||
else:
|
||||
self.last_value = config.getfloat(
|
||||
'value', 0., minval=0., maxval=self.scale) / self.scale
|
||||
self.shutdown_value = config.getfloat(
|
||||
'shutdown_value', 0., minval=0., maxval=self.scale) / self.scale
|
||||
self.last_value = config.getfloat(
|
||||
'value', 0., minval=0., maxval=self.scale) / self.scale
|
||||
self.shutdown_value = config.getfloat(
|
||||
'shutdown_value', 0., minval=0., maxval=self.scale) / self.scale
|
||||
self.mcu_pin.setup_start_value(self.last_value, self.shutdown_value)
|
||||
# Create gcode request queue
|
||||
self.gcrq = GCodeRequestQueue(config, self.mcu_pin.get_mcu(),
|
||||
self._set_pin)
|
||||
# Template handling
|
||||
self.template_eval = lookup_template_eval(config)
|
||||
# Register commands
|
||||
pin_name = config.get_name().split()[1]
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
|
|
@ -56,45 +215,36 @@ class PrinterOutputPin:
|
|||
desc=self.cmd_SET_PIN_help)
|
||||
def get_status(self, eventtime):
|
||||
return {'value': self.last_value}
|
||||
def _set_pin(self, print_time, value, is_resend=False):
|
||||
if value == self.last_value and not is_resend:
|
||||
return
|
||||
print_time = max(print_time, self.last_print_time + PIN_MIN_TIME)
|
||||
def _set_pin(self, print_time, value):
|
||||
if value == self.last_value:
|
||||
return "discard", 0.
|
||||
self.last_value = value
|
||||
if self.is_pwm:
|
||||
self.mcu_pin.set_pwm(print_time, value)
|
||||
else:
|
||||
self.mcu_pin.set_digital(print_time, value)
|
||||
self.last_value = value
|
||||
self.last_print_time = print_time
|
||||
if self.resend_interval and self.resend_timer is None:
|
||||
self.resend_timer = self.reactor.register_timer(
|
||||
self._resend_current_val, self.reactor.NOW)
|
||||
def _template_update(self, text):
|
||||
try:
|
||||
value = float(text)
|
||||
except ValueError as e:
|
||||
logging.exception("output_pin template render error")
|
||||
self.gcrq.send_async_request(value)
|
||||
cmd_SET_PIN_help = "Set the value of an output pin"
|
||||
def cmd_SET_PIN(self, gcmd):
|
||||
value = gcmd.get_float('VALUE', None, minval=0., maxval=self.scale)
|
||||
template = gcmd.get('TEMPLATE', None)
|
||||
if (value is None) == (template is None):
|
||||
raise gcmd.error("SET_PIN command must specify VALUE or TEMPLATE")
|
||||
# Check for template setting
|
||||
if template is not None:
|
||||
self.template_eval.set_template(gcmd, self._template_update)
|
||||
return
|
||||
# Read requested value
|
||||
value = gcmd.get_float('VALUE', minval=0., maxval=self.scale)
|
||||
value /= self.scale
|
||||
if not self.is_pwm and value not in [0., 1.]:
|
||||
raise gcmd.error("Invalid pin value")
|
||||
# Obtain print_time and apply requested settings
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
toolhead.register_lookahead_callback(
|
||||
lambda print_time: self._set_pin(print_time, value))
|
||||
|
||||
def _resend_current_val(self, eventtime):
|
||||
if self.last_value == self.shutdown_value:
|
||||
self.reactor.unregister_timer(self.resend_timer)
|
||||
self.resend_timer = None
|
||||
return self.reactor.NEVER
|
||||
|
||||
systime = self.reactor.monotonic()
|
||||
print_time = self.mcu_pin.get_mcu().estimated_print_time(systime)
|
||||
time_diff = (self.last_print_time + self.resend_interval) - print_time
|
||||
if time_diff > 0.:
|
||||
# Reschedule for resend time
|
||||
return systime + time_diff
|
||||
self._set_pin(print_time + PIN_MIN_TIME, self.last_value, True)
|
||||
return systime + self.resend_interval
|
||||
# Queue requested value
|
||||
self.gcrq.queue_gcode_request(value)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterOutputPin(config)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging
|
||||
from . import bus
|
||||
from . import bus, led
|
||||
|
||||
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
|
||||
|
||||
|
|
@ -16,8 +16,7 @@ class PCA9533:
|
|||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.i2c = bus.MCU_I2C_from_config(config, default_addr=98)
|
||||
pled = self.printer.load_object(config, "led")
|
||||
self.led_helper = pled.setup_helper(config, self.update_leds, 1)
|
||||
self.led_helper = led.LEDHelper(config, self.update_leds, 1)
|
||||
self.i2c.i2c_write([PCA9533_PWM0, 85])
|
||||
self.i2c.i2c_write([PCA9533_PWM1, 170])
|
||||
self.update_leds(self.led_helper.get_status()['color_data'], None)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# Copyright (C) 2022 Ricardo Alcantara <ricardo@vulcanolabs.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
from . import bus, mcp4018
|
||||
from . import bus, led, mcp4018
|
||||
|
||||
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
|
||||
|
||||
|
|
@ -34,8 +34,7 @@ class PCA9632:
|
|||
raise config.error("Invalid color_order '%s'" % (color_order,))
|
||||
self.color_map = ["RGBW".index(c) for c in color_order]
|
||||
self.prev_regs = {}
|
||||
pled = printer.load_object(config, "led")
|
||||
self.led_helper = pled.setup_helper(config, self.update_leds, 1)
|
||||
self.led_helper = led.LEDHelper(config, self.update_leds, 1)
|
||||
printer.register_event_handler("klippy:connect", self.handle_connect)
|
||||
def reg_write(self, reg, val, minclock=0):
|
||||
if self.prev_regs.get(reg) == val:
|
||||
|
|
|
|||
|
|
@ -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,25 +521,20 @@ 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()
|
||||
self.deactivate_gcode.run_gcode_from_command()
|
||||
if toolhead.get_position()[:3] != start_pos[:3]:
|
||||
raise self.printer.command_error(
|
||||
"Toolhead moved during probe activate_gcode script")
|
||||
"Toolhead moved during probe deactivate_gcode script")
|
||||
def _lower_probe(self):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
start_pos = toolhead.get_position()
|
||||
self.activate_gcode.run_gcode_from_command()
|
||||
if toolhead.get_position()[:3] != start_pos[:3]:
|
||||
raise self.printer.command_error(
|
||||
"Toolhead moved during probe deactivate_gcode script")
|
||||
"Toolhead moved during probe activate_gcode script")
|
||||
def multi_probe_begin(self):
|
||||
if self.stow_on_each_sample:
|
||||
return
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -45,40 +45,96 @@ def _parse_axis(gcmd, raw_axis):
|
|||
"Unable to parse axis direction '%s'" % (raw_axis,))
|
||||
return TestAxis(vib_dir=(dir_x, dir_y))
|
||||
|
||||
class VibrationPulseTest:
|
||||
class VibrationPulseTestGenerator:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.min_freq = config.getfloat('min_freq', 5., minval=1.)
|
||||
# Defaults are such that max_freq * accel_per_hz == 10000 (max_accel)
|
||||
self.max_freq = config.getfloat('max_freq', 10000. / 75.,
|
||||
self.max_freq = config.getfloat('max_freq', 135.,
|
||||
minval=self.min_freq, maxval=300.)
|
||||
self.accel_per_hz = config.getfloat('accel_per_hz', 75., above=0.)
|
||||
self.accel_per_hz = config.getfloat('accel_per_hz', 60., above=0.)
|
||||
self.hz_per_sec = config.getfloat('hz_per_sec', 1.,
|
||||
minval=0.1, maxval=2.)
|
||||
|
||||
self.probe_points = config.getlists('probe_points', seps=(',', '\n'),
|
||||
parser=float, count=3)
|
||||
def get_start_test_points(self):
|
||||
return self.probe_points
|
||||
def prepare_test(self, gcmd):
|
||||
self.freq_start = gcmd.get_float("FREQ_START", self.min_freq, minval=1.)
|
||||
self.freq_end = gcmd.get_float("FREQ_END", self.max_freq,
|
||||
minval=self.freq_start, maxval=300.)
|
||||
self.hz_per_sec = gcmd.get_float("HZ_PER_SEC", self.hz_per_sec,
|
||||
above=0., maxval=2.)
|
||||
def run_test(self, axis, gcmd):
|
||||
self.test_accel_per_hz = gcmd.get_float("ACCEL_PER_HZ",
|
||||
self.accel_per_hz, above=0.)
|
||||
self.test_hz_per_sec = gcmd.get_float("HZ_PER_SEC", self.hz_per_sec,
|
||||
above=0., maxval=2.)
|
||||
def gen_test(self):
|
||||
freq = self.freq_start
|
||||
res = []
|
||||
sign = 1.
|
||||
time = 0.
|
||||
while freq <= self.freq_end + 0.000001:
|
||||
t_seg = .25 / freq
|
||||
accel = self.test_accel_per_hz * freq
|
||||
time += t_seg
|
||||
res.append((time, sign * accel, freq))
|
||||
time += t_seg
|
||||
res.append((time, -sign * accel, freq))
|
||||
freq += 2. * t_seg * self.test_hz_per_sec
|
||||
sign = -sign
|
||||
return res
|
||||
def get_max_freq(self):
|
||||
return self.freq_end
|
||||
|
||||
class SweepingVibrationsTestGenerator:
|
||||
def __init__(self, config):
|
||||
self.vibration_generator = VibrationPulseTestGenerator(config)
|
||||
self.sweeping_accel = config.getfloat('sweeping_accel', 400., above=0.)
|
||||
self.sweeping_period = config.getfloat('sweeping_period', 1.2,
|
||||
minval=0.)
|
||||
def prepare_test(self, gcmd):
|
||||
self.vibration_generator.prepare_test(gcmd)
|
||||
self.test_sweeping_accel = gcmd.get_float(
|
||||
"SWEEPING_ACCEL", self.sweeping_accel, above=0.)
|
||||
self.test_sweeping_period = gcmd.get_float(
|
||||
"SWEEPING_PERIOD", self.sweeping_period, minval=0.)
|
||||
def gen_test(self):
|
||||
test_seq = self.vibration_generator.gen_test()
|
||||
accel_fraction = math.sqrt(2.0) * 0.125
|
||||
if self.test_sweeping_period:
|
||||
t_rem = self.test_sweeping_period * accel_fraction
|
||||
sweeping_accel = self.test_sweeping_accel
|
||||
else:
|
||||
t_rem = float('inf')
|
||||
sweeping_accel = 0.
|
||||
res = []
|
||||
last_t = 0.
|
||||
sig = 1.
|
||||
accel_fraction += 0.25
|
||||
for next_t, accel, freq in test_seq:
|
||||
t_seg = next_t - last_t
|
||||
while t_rem <= t_seg:
|
||||
last_t += t_rem
|
||||
res.append((last_t, accel + sweeping_accel * sig, freq))
|
||||
t_seg -= t_rem
|
||||
t_rem = self.test_sweeping_period * accel_fraction
|
||||
accel_fraction = 0.5
|
||||
sig = -sig
|
||||
t_rem -= t_seg
|
||||
res.append((next_t, accel + sweeping_accel * sig, freq))
|
||||
last_t = next_t
|
||||
return res
|
||||
def get_max_freq(self):
|
||||
return self.vibration_generator.get_max_freq()
|
||||
|
||||
class ResonanceTestExecutor:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
def run_test(self, test_seq, axis, gcmd):
|
||||
reactor = self.printer.get_reactor()
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
X, Y, Z, E = toolhead.get_position()
|
||||
sign = 1.
|
||||
freq = self.freq_start
|
||||
# Override maximum acceleration and acceleration to
|
||||
# deceleration based on the maximum test frequency
|
||||
systime = self.printer.get_reactor().monotonic()
|
||||
systime = reactor.monotonic()
|
||||
toolhead_info = toolhead.get_status(systime)
|
||||
old_max_accel = toolhead_info['max_accel']
|
||||
old_minimum_cruise_ratio = toolhead_info['minimum_cruise_ratio']
|
||||
max_accel = self.freq_end * self.accel_per_hz
|
||||
max_accel = max([abs(a) for _, a, _ in test_seq])
|
||||
self.gcode.run_script_from_command(
|
||||
"SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=0"
|
||||
% (max_accel,))
|
||||
|
|
@ -88,24 +144,46 @@ class VibrationPulseTest:
|
|||
gcmd.respond_info("Disabled [input_shaper] for resonance testing")
|
||||
else:
|
||||
input_shaper = None
|
||||
gcmd.respond_info("Testing frequency %.0f Hz" % (freq,))
|
||||
while freq <= self.freq_end + 0.000001:
|
||||
t_seg = .25 / freq
|
||||
accel = self.accel_per_hz * freq
|
||||
max_v = accel * t_seg
|
||||
last_v = last_t = last_accel = last_freq = 0.
|
||||
for next_t, accel, freq in test_seq:
|
||||
t_seg = next_t - last_t
|
||||
toolhead.cmd_M204(self.gcode.create_gcode_command(
|
||||
"M204", "M204", {"S": accel}))
|
||||
L = .5 * accel * t_seg**2
|
||||
dX, dY = axis.get_point(L)
|
||||
nX = X + sign * dX
|
||||
nY = Y + sign * dY
|
||||
toolhead.move([nX, nY, Z, E], max_v)
|
||||
toolhead.move([X, Y, Z, E], max_v)
|
||||
sign = -sign
|
||||
old_freq = freq
|
||||
freq += 2. * t_seg * self.hz_per_sec
|
||||
if math.floor(freq) > math.floor(old_freq):
|
||||
"M204", "M204", {"S": abs(accel)}))
|
||||
v = last_v + accel * t_seg
|
||||
abs_v = abs(v)
|
||||
if abs_v < 0.000001:
|
||||
v = abs_v = 0.
|
||||
abs_last_v = abs(last_v)
|
||||
v2 = v * v
|
||||
last_v2 = last_v * last_v
|
||||
half_inv_accel = .5 / accel
|
||||
d = (v2 - last_v2) * half_inv_accel
|
||||
dX, dY = axis.get_point(d)
|
||||
nX = X + dX
|
||||
nY = Y + dY
|
||||
toolhead.limit_next_junction_speed(abs_last_v)
|
||||
if v * last_v < 0:
|
||||
# The move first goes to a complete stop, then changes direction
|
||||
d_decel = -last_v2 * half_inv_accel
|
||||
decel_X, decel_Y = axis.get_point(d_decel)
|
||||
toolhead.move([X + decel_X, Y + decel_Y, Z, E], abs_last_v)
|
||||
toolhead.move([nX, nY, Z, E], abs_v)
|
||||
else:
|
||||
toolhead.move([nX, nY, Z, E], max(abs_v, abs_last_v))
|
||||
if math.floor(freq) > math.floor(last_freq):
|
||||
gcmd.respond_info("Testing frequency %.0f Hz" % (freq,))
|
||||
reactor.pause(reactor.monotonic() + 0.01)
|
||||
X, Y = nX, nY
|
||||
last_t = next_t
|
||||
last_v = v
|
||||
last_accel = accel
|
||||
last_freq = freq
|
||||
if last_v:
|
||||
d_decel = -.5 * last_v2 / old_max_accel
|
||||
decel_X, decel_Y = axis.get_point(d_decel)
|
||||
toolhead.cmd_M204(self.gcode.create_gcode_command(
|
||||
"M204", "M204", {"S": old_max_accel}))
|
||||
toolhead.move([X + decel_X, Y + decel_Y, Z, E], abs(last_v))
|
||||
# Restore the original acceleration values
|
||||
self.gcode.run_script_from_command(
|
||||
"SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=%.3f"
|
||||
|
|
@ -114,14 +192,13 @@ class VibrationPulseTest:
|
|||
if input_shaper is not None:
|
||||
input_shaper.enable_shaping()
|
||||
gcmd.respond_info("Re-enabled [input_shaper]")
|
||||
def get_max_freq(self):
|
||||
return self.freq_end
|
||||
|
||||
class ResonanceTester:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.move_speed = config.getfloat('move_speed', 50., above=0.)
|
||||
self.test = VibrationPulseTest(config)
|
||||
self.generator = SweepingVibrationsTestGenerator(config)
|
||||
self.executor = ResonanceTestExecutor(config)
|
||||
if not config.get('accel_chip_x', None):
|
||||
self.accel_chip_names = [('xy', config.get('accel_chip').strip())]
|
||||
else:
|
||||
|
|
@ -131,6 +208,8 @@ class ResonanceTester:
|
|||
if self.accel_chip_names[0][1] == self.accel_chip_names[1][1]:
|
||||
self.accel_chip_names = [('xy', self.accel_chip_names[0][1])]
|
||||
self.max_smoothing = config.getfloat('max_smoothing', None, minval=0.05)
|
||||
self.probe_points = config.getlists('probe_points', seps=(',', '\n'),
|
||||
parser=float, count=3)
|
||||
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.gcode.register_command("MEASURE_AXES_NOISE",
|
||||
|
|
@ -154,12 +233,9 @@ class ResonanceTester:
|
|||
toolhead = self.printer.lookup_object('toolhead')
|
||||
calibration_data = {axis: None for axis in axes}
|
||||
|
||||
self.test.prepare_test(gcmd)
|
||||
self.generator.prepare_test(gcmd)
|
||||
|
||||
if test_point is not None:
|
||||
test_points = [test_point]
|
||||
else:
|
||||
test_points = self.test.get_start_test_points()
|
||||
test_points = [test_point] if test_point else self.probe_points
|
||||
|
||||
for point in test_points:
|
||||
toolhead.manual_move(point, self.move_speed)
|
||||
|
|
@ -184,7 +260,8 @@ class ResonanceTester:
|
|||
raw_values.append((axis, aclient, chip.name))
|
||||
|
||||
# Generate moves
|
||||
self.test.run_test(axis, gcmd)
|
||||
test_seq = self.generator.gen_test()
|
||||
self.executor.run_test(test_seq, axis, gcmd)
|
||||
for chip_axis, aclient, chip_name in raw_values:
|
||||
aclient.finish_measurements()
|
||||
if raw_name_suffix is not None:
|
||||
|
|
@ -212,15 +289,11 @@ class ResonanceTester:
|
|||
def _parse_chips(self, accel_chips):
|
||||
parsed_chips = []
|
||||
for chip_name in accel_chips.split(','):
|
||||
if "adxl345" in chip_name:
|
||||
chip_lookup_name = chip_name.strip()
|
||||
else:
|
||||
chip_lookup_name = "adxl345 " + chip_name.strip();
|
||||
chip = self.printer.lookup_object(chip_lookup_name)
|
||||
chip = self.printer.lookup_object(chip_name.strip())
|
||||
parsed_chips.append(chip)
|
||||
return parsed_chips
|
||||
def _get_max_calibration_freq(self):
|
||||
return 1.5 * self.test.get_max_freq()
|
||||
return 1.5 * self.generator.get_max_freq()
|
||||
cmd_TEST_RESONANCES_help = ("Runs the resonance test for a specifed axis")
|
||||
def cmd_TEST_RESONANCES(self, gcmd):
|
||||
# Parse parameters
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# Support for servos
|
||||
#
|
||||
# Copyright (C) 2017-2020 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.
|
||||
from . import output_pin
|
||||
|
||||
SERVO_SIGNAL_PERIOD = 0.020
|
||||
PIN_MIN_TIME = 0.100
|
||||
|
||||
class PrinterServo:
|
||||
def __init__(self, config):
|
||||
|
|
@ -18,7 +18,7 @@ class PrinterServo:
|
|||
self.max_angle = config.getfloat('maximum_servo_angle', 180.)
|
||||
self.angle_to_width = (self.max_width - self.min_width) / self.max_angle
|
||||
self.width_to_value = 1. / SERVO_SIGNAL_PERIOD
|
||||
self.last_value = self.last_value_time = 0.
|
||||
self.last_value = 0.
|
||||
initial_pwm = 0.
|
||||
iangle = config.getfloat('initial_angle', None, minval=0., maxval=360.)
|
||||
if iangle is not None:
|
||||
|
|
@ -33,6 +33,9 @@ class PrinterServo:
|
|||
self.mcu_servo.setup_max_duration(0.)
|
||||
self.mcu_servo.setup_cycle_time(SERVO_SIGNAL_PERIOD)
|
||||
self.mcu_servo.setup_start_value(initial_pwm, 0.)
|
||||
# Create gcode request queue
|
||||
self.gcrq = output_pin.GCodeRequestQueue(
|
||||
config, self.mcu_servo.get_mcu(), self._set_pwm)
|
||||
# Register commands
|
||||
servo_name = config.get_name().split()[1]
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
|
|
@ -43,11 +46,9 @@ class PrinterServo:
|
|||
return {'value': self.last_value}
|
||||
def _set_pwm(self, print_time, value):
|
||||
if value == self.last_value:
|
||||
return
|
||||
print_time = max(print_time, self.last_value_time + PIN_MIN_TIME)
|
||||
self.mcu_servo.set_pwm(print_time, value)
|
||||
return "discard", 0.
|
||||
self.last_value = value
|
||||
self.last_value_time = print_time
|
||||
self.mcu_servo.set_pwm(print_time, value)
|
||||
def _get_pwm_from_angle(self, angle):
|
||||
angle = max(0., min(self.max_angle, angle))
|
||||
width = self.min_width + angle * self.angle_to_width
|
||||
|
|
@ -58,13 +59,13 @@ 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)
|
||||
self.gcrq.queue_gcode_request(value)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterServo(config)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ class CalibrationData:
|
|||
# Avoid division by zero errors
|
||||
psd /= self.freq_bins + .1
|
||||
# Remove low-frequency noise
|
||||
psd[self.freq_bins < MIN_FREQ] = 0.
|
||||
low_freqs = self.freq_bins < 2. * MIN_FREQ
|
||||
psd[low_freqs] *= self.numpy.exp(
|
||||
-(2. * MIN_FREQ / (self.freq_bins[low_freqs] + .1))**2 + 1.)
|
||||
def get_psd(self, axis='all'):
|
||||
return self._psd_map[axis]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -38,19 +38,17 @@ class SX1509(object):
|
|||
REG_INPUT_DISABLE : 0, REG_ANALOG_DRIVER_ENABLE : 0}
|
||||
self.reg_i_on_dict = {reg : 0 for reg in REG_I_ON}
|
||||
def _build_config(self):
|
||||
# Reset the chip
|
||||
# Reset the chip, Default RegClock/RegMisc 0x0
|
||||
self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % (
|
||||
self._oid, REG_RESET, 0x12))
|
||||
self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % (
|
||||
self._oid, REG_RESET, 0x34))
|
||||
# Enable Oscillator
|
||||
self._mcu.add_config_cmd("i2c_modify_bits oid=%d reg=%02x"
|
||||
" clear_set_bits=%02x%02x" % (
|
||||
self._oid, REG_CLOCK, 0, (1 << 6)))
|
||||
self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % (
|
||||
self._oid, REG_CLOCK, (1 << 6)))
|
||||
# Setup Clock Divider
|
||||
self._mcu.add_config_cmd("i2c_modify_bits oid=%d reg=%02x"
|
||||
" clear_set_bits=%02x%02x" % (
|
||||
self._oid, REG_MISC, 0, (1 << 4)))
|
||||
self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%02x" % (
|
||||
self._oid, REG_MISC, (1 << 4)))
|
||||
# Transfer all regs with their initial cached state
|
||||
for _reg, _data in self.reg_dict.items():
|
||||
self._mcu.add_config_cmd("i2c_write oid=%d data=%02x%04x" % (
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class TemperatureFan:
|
|||
self.cmd_SET_TEMPERATURE_FAN_TARGET,
|
||||
desc=self.cmd_SET_TEMPERATURE_FAN_TARGET_help)
|
||||
|
||||
def set_speed(self, read_time, value):
|
||||
def set_tf_speed(self, read_time, value):
|
||||
if value <= 0.:
|
||||
value = 0.
|
||||
elif value < self.min_speed:
|
||||
|
|
@ -60,7 +60,7 @@ class TemperatureFan:
|
|||
speed_time = read_time + self.speed_delay
|
||||
self.next_speed_time = speed_time + 0.75 * MAX_FAN_TIME
|
||||
self.last_speed_value = value
|
||||
self.fan.set_speed(speed_time, value)
|
||||
self.fan.set_speed(value, speed_time)
|
||||
def temperature_callback(self, read_time, temp):
|
||||
self.last_temp = temp
|
||||
self.control.temperature_callback(read_time, temp)
|
||||
|
|
@ -128,10 +128,10 @@ class ControlBangBang:
|
|||
and temp <= target_temp-self.max_delta):
|
||||
self.heating = True
|
||||
if self.heating:
|
||||
self.temperature_fan.set_speed(read_time, 0.)
|
||||
self.temperature_fan.set_tf_speed(read_time, 0.)
|
||||
else:
|
||||
self.temperature_fan.set_speed(read_time,
|
||||
self.temperature_fan.get_max_speed())
|
||||
self.temperature_fan.set_tf_speed(
|
||||
read_time, self.temperature_fan.get_max_speed())
|
||||
|
||||
######################################################################
|
||||
# Proportional Integral Derivative (PID) control algo
|
||||
|
|
@ -171,7 +171,7 @@ class ControlPID:
|
|||
# Calculate output
|
||||
co = self.Kp*temp_err + self.Ki*temp_integ - self.Kd*temp_deriv
|
||||
bounded_co = max(0., min(self.temperature_fan.get_max_speed(), co))
|
||||
self.temperature_fan.set_speed(
|
||||
self.temperature_fan.set_tf_speed(
|
||||
read_time, max(self.temperature_fan.get_min_speed(),
|
||||
self.temperature_fan.get_max_speed() - bounded_co))
|
||||
# Store state for next measurement
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -62,7 +66,7 @@ class PrinterTemperatureMCU:
|
|||
self.mcu_type = mcu.get_constants().get("MCU", "")
|
||||
# Run MCU specific configuration
|
||||
cfg_funcs = [
|
||||
('rp2040', self.config_rp2040),
|
||||
('rp2', self.config_rp2040),
|
||||
('sam3', self.config_sam3), ('sam4', self.config_sam4),
|
||||
('same70', self.config_same70), ('samd21', self.config_samd21),
|
||||
('samd51', self.config_samd51), ('same5', self.config_samd51),
|
||||
|
|
@ -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,
|
||||
|
|
@ -348,7 +348,7 @@ class TMC2240:
|
|||
if config.get("uart_pin", None) is not None:
|
||||
# use UART for communication
|
||||
self.mcu_tmc = tmc_uart.MCU_TMC_uart(config, Registers, self.fields,
|
||||
3, TMC_FREQUENCY)
|
||||
7, TMC_FREQUENCY)
|
||||
else:
|
||||
# Use SPI bus for communication
|
||||
self.mcu_tmc = tmc2130.MCU_TMC_SPI(config, Registers, self.fields,
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Parse gcode commands
|
||||
#
|
||||
# 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 os, re, logging, collections, shlex
|
||||
|
|
@ -28,19 +28,18 @@ class GCodeCommand:
|
|||
return self._params
|
||||
def get_raw_command_parameters(self):
|
||||
command = self._command
|
||||
if command.startswith("M117 ") or command.startswith("M118 "):
|
||||
command = command[:4]
|
||||
rawparams = self._commandline
|
||||
urawparams = rawparams.upper()
|
||||
if not urawparams.startswith(command):
|
||||
rawparams = rawparams[urawparams.find(command):]
|
||||
end = rawparams.rfind('*')
|
||||
if end >= 0:
|
||||
rawparams = rawparams[:end]
|
||||
rawparams = rawparams[len(command):]
|
||||
if rawparams.startswith(' '):
|
||||
rawparams = rawparams[1:]
|
||||
return rawparams
|
||||
origline = self._commandline
|
||||
param_start = len(command)
|
||||
param_end = len(origline)
|
||||
if origline[:param_start].upper() != command:
|
||||
# Skip any gcode line-number and ignore any trailing checksum
|
||||
param_start += origline.upper().find(command)
|
||||
end = origline.rfind('*')
|
||||
if end >= 0 and origline[end+1:].isdigit():
|
||||
param_end = end
|
||||
if origline[param_start:param_start+1].isspace():
|
||||
param_start += 1
|
||||
return origline[param_start:param_end]
|
||||
def ack(self, msg=None):
|
||||
if not self._need_ack:
|
||||
return False
|
||||
|
|
@ -133,6 +132,10 @@ class GCodeDispatch:
|
|||
raise self.printer.config_error(
|
||||
"gcode command %s already registered" % (cmd,))
|
||||
if not self.is_traditional_gcode(cmd):
|
||||
if (cmd.upper() != cmd or not cmd.replace('_', 'A').isalnum()
|
||||
or cmd[0].isdigit() or cmd[1:2].isdigit()):
|
||||
raise self.printer.config_error(
|
||||
"Can't register '%s' as it is an invalid name" % (cmd,))
|
||||
origfunc = func
|
||||
func = lambda params: origfunc(self._get_extended_params(params))
|
||||
self.ready_gcode_handlers[cmd] = func
|
||||
|
|
@ -184,7 +187,7 @@ class GCodeDispatch:
|
|||
self._build_status_commands()
|
||||
self._respond_state("Ready")
|
||||
# Parse input into commands
|
||||
args_r = re.compile('([A-Z_]+|[A-Z*/])')
|
||||
args_r = re.compile('([A-Z_]+|[A-Z*])')
|
||||
def _process_commands(self, commands, need_ack=True):
|
||||
for line in commands:
|
||||
# Ignore comments and leading/trailing spaces
|
||||
|
|
@ -194,16 +197,14 @@ class GCodeDispatch:
|
|||
line = line[:cpos]
|
||||
# Break line into parts and determine command
|
||||
parts = self.args_r.split(line.upper())
|
||||
numparts = len(parts)
|
||||
cmd = ""
|
||||
if numparts >= 3 and parts[1] != 'N':
|
||||
cmd = parts[1] + parts[2].strip()
|
||||
elif numparts >= 5 and parts[1] == 'N':
|
||||
if ''.join(parts[:2]) == 'N':
|
||||
# Skip line number at start of command
|
||||
cmd = parts[3] + parts[4].strip()
|
||||
cmd = ''.join(parts[3:5]).strip()
|
||||
else:
|
||||
cmd = ''.join(parts[:3]).strip()
|
||||
# Build gcode "params" dictionary
|
||||
params = { parts[i]: parts[i+1].strip()
|
||||
for i in range(1, numparts, 2) }
|
||||
for i in range(1, len(parts), 2) }
|
||||
gcmd = GCodeCommand(self, cmd, origline, params, need_ack)
|
||||
# Invoke handler for command
|
||||
handler = self.gcode_handlers.get(cmd, self.cmd_default)
|
||||
|
|
@ -251,26 +252,22 @@ class GCodeDispatch:
|
|||
def _respond_state(self, state):
|
||||
self.respond_info("Klipper state: %s" % (state,), log=False)
|
||||
# Parameter parsing helpers
|
||||
extended_r = re.compile(
|
||||
r'^\s*(?:N[0-9]+\s*)?'
|
||||
r'(?P<cmd>[a-zA-Z_][a-zA-Z0-9_]+)(?:\s+|$)'
|
||||
r'(?P<args>[^#*;]*?)'
|
||||
r'\s*(?:[#*;].*)?$')
|
||||
def _get_extended_params(self, gcmd):
|
||||
m = self.extended_r.match(gcmd.get_commandline())
|
||||
if m is None:
|
||||
raise self.error("Malformed command '%s'"
|
||||
% (gcmd.get_commandline(),))
|
||||
eargs = m.group('args')
|
||||
rawparams = gcmd.get_raw_command_parameters()
|
||||
# Extract args while allowing shell style quoting
|
||||
s = shlex.shlex(rawparams, posix=True)
|
||||
s.whitespace_split = True
|
||||
s.commenters = '#;'
|
||||
try:
|
||||
eparams = [earg.split('=', 1) for earg in shlex.split(eargs)]
|
||||
eparams = [earg.split('=', 1) for earg in s]
|
||||
eparams = { k.upper(): v for k, v in eparams }
|
||||
gcmd._params.clear()
|
||||
gcmd._params.update(eparams)
|
||||
return gcmd
|
||||
except ValueError as e:
|
||||
raise self.error("Malformed command '%s'"
|
||||
% (gcmd.get_commandline(),))
|
||||
# Update gcmd with new parameters
|
||||
gcmd._params.clear()
|
||||
gcmd._params.update(eparams)
|
||||
return gcmd
|
||||
# G-Code special command handlers
|
||||
def cmd_default(self, gcmd):
|
||||
cmd = gcmd.get_command()
|
||||
|
|
@ -289,12 +286,15 @@ class GCodeDispatch:
|
|||
if cmdline:
|
||||
logging.debug(cmdline)
|
||||
return
|
||||
if cmd.startswith("M117 ") or cmd.startswith("M118 "):
|
||||
if ' ' in cmd:
|
||||
# Handle M117/M118 gcode with numeric and special characters
|
||||
handler = self.gcode_handlers.get(cmd[:4], None)
|
||||
if handler is not None:
|
||||
handler(gcmd)
|
||||
return
|
||||
realcmd = cmd.split()[0]
|
||||
if realcmd in ["M117", "M118", "M23"]:
|
||||
handler = self.gcode_handlers.get(realcmd, None)
|
||||
if handler is not None:
|
||||
gcmd._command = realcmd
|
||||
handler(gcmd)
|
||||
return
|
||||
elif cmd in ['M140', 'M104'] and not gcmd.get_float('S', 0.):
|
||||
# Don't warn about requests to turn off heaters when not present
|
||||
return
|
||||
|
|
@ -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"
|
||||
|
|
@ -830,9 +832,10 @@ class MCU:
|
|||
systime = self._reactor.monotonic()
|
||||
get_clock = self._clocksync.get_clock
|
||||
calc_freq = get_clock(systime + 1) - get_clock(systime)
|
||||
freq_diff = abs(mcu_freq - calc_freq)
|
||||
mcu_freq_mhz = int(mcu_freq / 1000000. + 0.5)
|
||||
calc_freq_mhz = int(calc_freq / 1000000. + 0.5)
|
||||
if mcu_freq_mhz != calc_freq_mhz:
|
||||
if freq_diff > mcu_freq*0.01 and mcu_freq_mhz != calc_freq_mhz:
|
||||
pconfig = self._printer.lookup_object('configfile')
|
||||
msg = ("MCU '%s' configured for %dMhz but running at %dMhz!"
|
||||
% (self._name, mcu_freq_mhz, calc_freq_mhz))
|
||||
|
|
@ -1007,34 +1010,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,15 +316,15 @@ 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:
|
||||
return ""
|
||||
return []
|
||||
msgname = parts[0]
|
||||
mp = self.messages_by_name.get(msgname)
|
||||
if mp is None:
|
||||
|
|
@ -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.)
|
||||
|
|
@ -166,7 +166,8 @@ class SerialReader:
|
|||
try:
|
||||
fd = os.open(filename, os.O_RDWR | os.O_NOCTTY)
|
||||
except OSError as e:
|
||||
logging.warn("%sUnable to open port: %s", self.warn_prefix, e)
|
||||
logging.warning("%sUnable to open port: %s",
|
||||
self.warn_prefix, e)
|
||||
self.reactor.pause(self.reactor.monotonic() + 5.)
|
||||
continue
|
||||
serial_dev = os.fdopen(fd, 'rb+', 0)
|
||||
|
|
@ -187,7 +188,7 @@ class SerialReader:
|
|||
serial_dev.rts = rts
|
||||
serial_dev.open()
|
||||
except (OSError, IOError, serial.SerialException) as e:
|
||||
logging.warn("%sUnable to open serial port: %s",
|
||||
logging.warning("%sUnable to open serial port: %s",
|
||||
self.warn_prefix, e)
|
||||
self.reactor.pause(self.reactor.monotonic() + 5.)
|
||||
continue
|
||||
|
|
@ -291,13 +292,13 @@ class SerialReader:
|
|||
logging.debug("%sUnknown message %d (len %d) while identifying",
|
||||
self.warn_prefix, params['#msgid'], len(params['#msg']))
|
||||
def handle_unknown(self, params):
|
||||
logging.warn("%sUnknown message type %d: %s",
|
||||
logging.warning("%sUnknown message type %d: %s",
|
||||
self.warn_prefix, params['#msgid'], repr(params['#msg']))
|
||||
def handle_output(self, params):
|
||||
logging.info("%s%s: %s", self.warn_prefix,
|
||||
params['#name'], params['#msg'])
|
||||
def handle_default(self, params):
|
||||
logging.warn("%sgot %s", self.warn_prefix, params)
|
||||
logging.warning("%sgot %s", self.warn_prefix, params)
|
||||
|
||||
# Class to send a query command and return the received response
|
||||
class SerialRetryCommand:
|
||||
|
|
|
|||
|
|
@ -138,8 +138,10 @@ class MCU_stepper:
|
|||
def get_commanded_position(self):
|
||||
ffi_main, ffi_lib = chelper.get_ffi()
|
||||
return ffi_lib.itersolve_get_commanded_pos(self._stepper_kinematics)
|
||||
def get_mcu_position(self):
|
||||
mcu_pos_dist = self.get_commanded_position() + self._mcu_position_offset
|
||||
def get_mcu_position(self, cmd_pos=None):
|
||||
if cmd_pos is None:
|
||||
cmd_pos = self.get_commanded_position()
|
||||
mcu_pos_dist = cmd_pos + self._mcu_position_offset
|
||||
mcu_pos = mcu_pos_dist / self._step_dist
|
||||
if mcu_pos >= 0.:
|
||||
return int(mcu_pos + 0.5)
|
||||
|
|
@ -265,6 +267,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 +279,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)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class Move:
|
|||
self.delta_v2 = 2.0 * move_d * self.accel
|
||||
self.max_smoothed_v2 = 0.
|
||||
self.smooth_delta_v2 = 2.0 * move_d * toolhead.max_accel_to_decel
|
||||
self.next_junction_v2 = 999999999.9
|
||||
def limit_speed(self, speed, accel):
|
||||
speed2 = speed**2
|
||||
if speed2 < self.max_cruise_v2:
|
||||
|
|
@ -55,6 +56,8 @@ class Move:
|
|||
self.accel = min(self.accel, accel)
|
||||
self.delta_v2 = 2.0 * self.move_d * self.accel
|
||||
self.smooth_delta_v2 = min(self.smooth_delta_v2, self.delta_v2)
|
||||
def limit_next_junction_speed(self, speed):
|
||||
self.next_junction_v2 = min(self.next_junction_v2, speed**2)
|
||||
def move_error(self, msg="Move out of range"):
|
||||
ep = self.end_pos
|
||||
m = "%s: %.3f %.3f %.3f [%.3f]" % (msg, ep[0], ep[1], ep[2], ep[3])
|
||||
|
|
@ -64,32 +67,33 @@ class Move:
|
|||
return
|
||||
# Allow extruder to calculate its maximum junction
|
||||
extruder_v2 = self.toolhead.extruder.calc_junction(prev_move, self)
|
||||
max_start_v2 = min(extruder_v2, self.max_cruise_v2,
|
||||
prev_move.max_cruise_v2, prev_move.next_junction_v2,
|
||||
prev_move.max_start_v2 + prev_move.delta_v2)
|
||||
# Find max velocity using "approximated centripetal velocity"
|
||||
axes_r = self.axes_r
|
||||
prev_axes_r = prev_move.axes_r
|
||||
junction_cos_theta = -(axes_r[0] * prev_axes_r[0]
|
||||
+ axes_r[1] * prev_axes_r[1]
|
||||
+ axes_r[2] * prev_axes_r[2])
|
||||
if junction_cos_theta > 0.999999:
|
||||
return
|
||||
junction_cos_theta = max(junction_cos_theta, -0.999999)
|
||||
sin_theta_d2 = math.sqrt(0.5*(1.0-junction_cos_theta))
|
||||
R_jd = sin_theta_d2 / (1. - sin_theta_d2)
|
||||
# Approximated circle must contact moves no further away than mid-move
|
||||
tan_theta_d2 = sin_theta_d2 / math.sqrt(0.5*(1.0+junction_cos_theta))
|
||||
move_centripetal_v2 = .5 * self.move_d * tan_theta_d2 * self.accel
|
||||
prev_move_centripetal_v2 = (.5 * prev_move.move_d * tan_theta_d2
|
||||
* prev_move.accel)
|
||||
sin_theta_d2 = math.sqrt(max(0.5*(1.0-junction_cos_theta), 0.))
|
||||
cos_theta_d2 = math.sqrt(max(0.5*(1.0+junction_cos_theta), 0.))
|
||||
one_minus_sin_theta_d2 = 1. - sin_theta_d2
|
||||
if one_minus_sin_theta_d2 > 0. and cos_theta_d2 > 0.:
|
||||
R_jd = sin_theta_d2 / one_minus_sin_theta_d2
|
||||
move_jd_v2 = R_jd * self.junction_deviation * self.accel
|
||||
pmove_jd_v2 = R_jd * prev_move.junction_deviation * prev_move.accel
|
||||
# Approximated circle must contact moves no further than mid-move
|
||||
# centripetal_v2 = .5 * self.move_d * self.accel * tan_theta_d2
|
||||
quarter_tan_theta_d2 = .25 * sin_theta_d2 / cos_theta_d2
|
||||
move_centripetal_v2 = self.delta_v2 * quarter_tan_theta_d2
|
||||
pmove_centripetal_v2 = prev_move.delta_v2 * quarter_tan_theta_d2
|
||||
max_start_v2 = min(max_start_v2, move_jd_v2, pmove_jd_v2,
|
||||
move_centripetal_v2, pmove_centripetal_v2)
|
||||
# Apply limits
|
||||
self.max_start_v2 = min(
|
||||
R_jd * self.junction_deviation * self.accel,
|
||||
R_jd * prev_move.junction_deviation * prev_move.accel,
|
||||
move_centripetal_v2, prev_move_centripetal_v2,
|
||||
extruder_v2, self.max_cruise_v2, prev_move.max_cruise_v2,
|
||||
prev_move.max_start_v2 + prev_move.delta_v2)
|
||||
self.max_start_v2 = max_start_v2
|
||||
self.max_smoothed_v2 = min(
|
||||
self.max_start_v2
|
||||
, prev_move.max_smoothed_v2 + prev_move.smooth_delta_v2)
|
||||
max_start_v2, prev_move.max_smoothed_v2 + prev_move.smooth_delta_v2)
|
||||
def set_junction(self, start_v2, cruise_v2, end_v2):
|
||||
# Determine accel, cruise, and decel portions of the move distance
|
||||
half_inv_accel = .5 / self.accel
|
||||
|
|
@ -481,6 +485,10 @@ class ToolHead:
|
|||
self.commanded_pos[:] = newpos
|
||||
self.kin.set_position(newpos, homing_axes)
|
||||
self.printer.send_event("toolhead:set_position")
|
||||
def limit_next_junction_speed(self, speed):
|
||||
last_move = self.lookahead.get_last()
|
||||
if last_move is not None:
|
||||
last_move.limit_next_junction_speed(speed)
|
||||
def move(self, newpos, speed):
|
||||
move = Move(self, self.commanded_pos, newpos, speed)
|
||||
if not move.move_d:
|
||||
|
|
|
|||
19
lib/README
19
lib/README
|
|
@ -105,16 +105,23 @@ The stm32h7 directory contains code from:
|
|||
version v1.9.0 (ccb11556044540590ca6e45056e6b65cdca2deb2). Contents
|
||||
taken from the Drivers/CMSIS/Device/ST/STM32H7xx/ directory.
|
||||
|
||||
The rp2040 directory contains code from the pico sdk:
|
||||
The pico-sdk directory contains code from the pico sdk:
|
||||
https://github.com/raspberrypi/pico-sdk.git
|
||||
version 1.2.0 (bfcbefafc5d2a210551a4d9d80b4303d4ae0adf7). It has been
|
||||
version 2.0.0 (efe2103f9b28458a1615ff096054479743ade236). It has been
|
||||
modified so that it can build outside of the pico sdk. See
|
||||
rp2040.patch for the modifications.
|
||||
pico-sdk.patch for the modifications.
|
||||
|
||||
The elf2uf2 directory contains code from the pico sdk:
|
||||
https://github.com/raspberrypi/pico-sdk.git
|
||||
version 1.2.0 (bfcbefafc5d2a210551a4d9d80b4303d4ae0adf7). Contents
|
||||
taken from the tools/elf2uf2/ directory.
|
||||
|
||||
The rp2040_flash directory contains a light-weight bootsel flash tool.
|
||||
It uses C part of the the `picoboot_connection` directory found in:
|
||||
https://github.com/raspberrypi/picotool.git
|
||||
version v1.1.0 (55fd880c3dc029b961fc1a0967a6cfdc0af02721).
|
||||
version 2.0.0 (8a9af99ab10b20b1c6afb30cd9384e562a6647f9). Note that
|
||||
Makefile and main.c are locally developed files (the remaining files
|
||||
are from the picotool repo).
|
||||
|
||||
The hub-ctrl directory contains code from:
|
||||
https://github.com/codazoda/hub-ctrl.c/
|
||||
|
|
@ -132,7 +139,7 @@ details. See changes.diff for the modifications.
|
|||
|
||||
The pru_rpmsg directory contains code from:
|
||||
https://github.com/dinuxbg/pru-gcc-examples
|
||||
revision 425a42d82006cf0aa24be27b483d2f6a41607489. The code is taken
|
||||
revision e2bd170d4d61b3e642da65e0f0d487e10872fe22. The code is taken
|
||||
from the repo's hc-sr04-range-sensor directory. It has been modified
|
||||
so that the IEP definitions compile correctly. See pru_rpmsg.patch for
|
||||
the modifications.
|
||||
|
|
@ -167,7 +174,7 @@ used to upload firmware to devices flashed with the CanBoot bootloader.
|
|||
|
||||
The can2040 directory contains code from:
|
||||
https://github.com/KevinOConnor/can2040
|
||||
version v1.6.0 (af3d21e5d61b8408c63fbdfb0aceb21d69d91693)
|
||||
commit 13321ce2bc046e059a47def70f977a579a984462.
|
||||
|
||||
The Huada HC32F460 directory contains code from:
|
||||
https://www.hdsc.com.cn/Category83-1490
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
#include <stdint.h> // uint32_t
|
||||
#include <string.h> // memset
|
||||
#include "RP2040.h" // hw_set_bits
|
||||
#include "can2040.h" // can2040_setup
|
||||
#include "cmsis_gcc.h" // __DMB
|
||||
#include "hardware/regs/dreq.h" // DREQ_PIO0_RX1
|
||||
#include "hardware/structs/dma.h" // dma_hw
|
||||
#include "hardware/structs/iobank0.h" // iobank0_hw
|
||||
|
|
@ -128,7 +128,7 @@ static void
|
|||
pio_sync_setup(struct can2040 *cd)
|
||||
{
|
||||
pio_hw_t *pio_hw = cd->pio_hw;
|
||||
struct pio_sm_hw *sm = &pio_hw->sm[0];
|
||||
pio_sm_hw_t *sm = &pio_hw->sm[0];
|
||||
sm->execctrl = (
|
||||
cd->gpio_rx << PIO_SM0_EXECCTRL_JMP_PIN_LSB
|
||||
| (can2040_offset_sync_end - 1) << PIO_SM0_EXECCTRL_WRAP_TOP_LSB
|
||||
|
|
@ -148,7 +148,7 @@ static void
|
|||
pio_rx_setup(struct can2040 *cd)
|
||||
{
|
||||
pio_hw_t *pio_hw = cd->pio_hw;
|
||||
struct pio_sm_hw *sm = &pio_hw->sm[1];
|
||||
pio_sm_hw_t *sm = &pio_hw->sm[1];
|
||||
sm->execctrl = (
|
||||
(can2040_offset_shared_rx_end - 1) << PIO_SM0_EXECCTRL_WRAP_TOP_LSB
|
||||
| can2040_offset_shared_rx_read << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB);
|
||||
|
|
@ -165,7 +165,7 @@ static void
|
|||
pio_match_setup(struct can2040 *cd)
|
||||
{
|
||||
pio_hw_t *pio_hw = cd->pio_hw;
|
||||
struct pio_sm_hw *sm = &pio_hw->sm[2];
|
||||
pio_sm_hw_t *sm = &pio_hw->sm[2];
|
||||
sm->execctrl = (
|
||||
(can2040_offset_match_end - 1) << PIO_SM0_EXECCTRL_WRAP_TOP_LSB
|
||||
| can2040_offset_shared_rx_read << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB);
|
||||
|
|
@ -173,7 +173,7 @@ pio_match_setup(struct can2040 *cd)
|
|||
sm->shiftctrl = 0;
|
||||
sm->instr = 0xe040; // set y, 0
|
||||
sm->instr = 0xa0e2; // mov osr, y
|
||||
sm->instr = 0xa02a, // mov x, !y
|
||||
sm->instr = 0xa02a; // mov x, !y
|
||||
sm->instr = can2040_offset_match_load_next; // jmp match_load_next
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ static void
|
|||
pio_tx_setup(struct can2040 *cd)
|
||||
{
|
||||
pio_hw_t *pio_hw = cd->pio_hw;
|
||||
struct pio_sm_hw *sm = &pio_hw->sm[3];
|
||||
pio_sm_hw_t *sm = &pio_hw->sm[3];
|
||||
sm->execctrl = (
|
||||
cd->gpio_rx << PIO_SM0_EXECCTRL_JMP_PIN_LSB
|
||||
| can2040_offset_tx_conflict << PIO_SM0_EXECCTRL_WRAP_TOP_LSB
|
||||
|
|
@ -255,7 +255,7 @@ pio_tx_reset(struct can2040 *cd)
|
|||
| (0x08 << PIO_CTRL_SM_RESTART_LSB));
|
||||
pio_hw->irq = (SI_MATCHED | SI_ACKDONE) >> 8; // clear PIO irq flags
|
||||
// Clear tx fifo
|
||||
struct pio_sm_hw *sm = &pio_hw->sm[3];
|
||||
pio_sm_hw_t *sm = &pio_hw->sm[3];
|
||||
sm->shiftctrl = 0;
|
||||
sm->shiftctrl = (PIO_SM0_SHIFTCTRL_FJOIN_TX_BITS
|
||||
| PIO_SM0_SHIFTCTRL_AUTOPULL_BITS);
|
||||
|
|
@ -271,7 +271,7 @@ pio_tx_send(struct can2040 *cd, uint32_t *data, uint32_t count)
|
|||
uint32_t i;
|
||||
for (i=0; i<count; i++)
|
||||
pio_hw->txf[3] = data[i];
|
||||
struct pio_sm_hw *sm = &pio_hw->sm[3];
|
||||
pio_sm_hw_t *sm = &pio_hw->sm[3];
|
||||
sm->instr = 0xe001; // set pins, 1
|
||||
sm->instr = 0x6021; // out x, 1
|
||||
sm->instr = can2040_offset_tx_write_pin; // jmp tx_write_pin
|
||||
|
|
@ -287,7 +287,7 @@ pio_tx_inject_ack(struct can2040 *cd, uint32_t match_key)
|
|||
pio_tx_reset(cd);
|
||||
pio_hw->instr_mem[can2040_offset_tx_got_recessive] = 0xc023; // irq wait 3
|
||||
pio_hw->txf[3] = 0x7fffffff;
|
||||
struct pio_sm_hw *sm = &pio_hw->sm[3];
|
||||
pio_sm_hw_t *sm = &pio_hw->sm[3];
|
||||
sm->instr = 0xe001; // set pins, 1
|
||||
sm->instr = 0x6021; // out x, 1
|
||||
sm->instr = can2040_offset_tx_write_pin; // jmp tx_write_pin
|
||||
|
|
|
|||
3264
lib/cmsis-core/core_cm33.h
Normal file
3264
lib/cmsis-core/core_cm33.h
Normal file
File diff suppressed because it is too large
Load diff
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