This commit is contained in:
abcbca 2025-02-07 23:48:52 +08:00
commit 331be3ae76
505 changed files with 157795 additions and 13521 deletions

View file

@ -21,7 +21,7 @@ jobs:
run: ./scripts/ci-build.sh 2>&1
- name: Upload micro-controller data dictionaries
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: data-dict
path: ci_build/dict

View file

@ -4,15 +4,14 @@ Welcome to the Klipper project!
https://www.klipper3d.org/
Klipper is a 3d-Printer firmware. It combines the power of a general
purpose computer with one or more micro-controllers. See the
The Klipper firmware controls 3d-Printers. It combines the power of a
general purpose computer with one or more micro-controllers. See the
[features document](https://www.klipper3d.org/Features.html) for more
information on why you should use Klipper.
information on why you should use the Klipper software.
To begin using Klipper start by
[installing](https://www.klipper3d.org/Installation.html) it.
Start by [installing Klipper software](https://www.klipper3d.org/Installation.html).
Klipper is Free Software. See the [license](COPYING) or read the
[documentation](https://www.klipper3d.org/Overview.html). We depend on
the generous support from our
Klipper software is Free Software. See the [license](COPYING) or read
the [documentation](https://www.klipper3d.org/Overview.html). We
depend on the generous support from our
[sponsors](https://www.klipper3d.org/Sponsors.html).

View file

@ -85,11 +85,10 @@ uart_pin: PC11
tx_pin: PC10
uart_address: 3
run_current: 0.650
stealthchop_threshold: 999999
[heater_bed]
heater_pin: PC9
sensor_type: ATC Semitec 104GT-2
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PC4
control: pid
pid_Kp: 54.027

View file

@ -95,4 +95,4 @@ max_z_accel: 100
aliases:
EXP1_1=PC6,EXP1_3=PB10,EXP1_5=PB14,EXP1_7=PB12,EXP1_9=<GND>,
EXP1_2=PB2,EXP1_4=PB11,EXP1_6=PB13,EXP1_8=PB15,EXP1_10=<5V>,
PROBE_IN=PB0,PROBE_OUT=PB1,FIL_RUNOUT=PC6
PROBE_IN=PB0,PROBE_OUT=PB1,FIL_RUNOUT=PA4

View 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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,256 @@
# This file contains common pin mappings for the Geeetech GT2560 v4.0 and v4.1b
# boards. These boards use a firmware compiled for the AVR atmega2560.
# For default Geeetech A10/A20 (1 extruder),
# A10M/A20M (mixing 2 in 1 out),
# A10T/A20T (mixing 3 in 1 out) printers
# Installation: https://www.klipper3d.org/Installation.html
# Always read for first start: https://www.klipper3d.org/Config_checks.html
[mcu]
# Might need to be changed: https://www.klipper3d.org/Installation.html
serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0
[printer]
kinematics: cartesian
max_velocity: 200
max_accel: 1500
max_z_velocity: 20
max_z_accel: 500
# # uncomment for BLTouch/3DTouch
# [bltouch]
# sensor_pin: PC7 # there is an external pull up so no need in ^
# control_pin: PB5
# speed: 3.0
# samples: 2
# x_offset: -42.0
# y_offset: -1.0
# z_offset: 1.0 # during calibration this line is commented out and new record added at the end of file
[safe_z_home]
home_xy_position: 100, 100 # Change coordinates to the center of your print bed
speed: 50
z_hop: 10 # Move up 10mm
z_hop_speed: 5
[stepper_x]
enable_pin: !PC2
dir_pin: !PG2
step_pin: PC0
microsteps: 16
rotation_distance: 40
endstop_pin: !PA2 # there are external pull ups
position_endstop: 0
position_max: 220 # for A10/M/T / change to 250 for A20/M/T
homing_speed: 40
[stepper_y]
enable_pin: !PA7
dir_pin: !PC4
step_pin: PC6
microsteps: 16
rotation_distance: 40
endstop_pin: !PA6 # there are external pull ups
position_endstop: 0
position_max: 220 # for A10/M/T / change to 250 for A20/M/T
homing_speed: 40
[stepper_z]
enable_pin: !PA5
dir_pin: PA1
step_pin: PA3
microsteps: 16
rotation_distance: 8
#endstop_pin: probe:z_virtual_endstop # uncomment for BLTouch/3DTouch
endstop_pin: !PC7 # comment for BLTouch/3DTouch
position_endstop: 0 # comment for BLTouch/3DTouch
position_max: 230 # for A10/M/T / change to 250 for A20/M/T
position_min: -5
homing_speed: 20
[extruder]
enable_pin: !PB6
dir_pin: PL5
step_pin: PL3
microsteps: 16
rotation_distance: 8 # Needs to be optimized: https://www.klipper3d.org/Rotation_Distance.html#calibrating-rotation_distance-on-extruders
nozzle_diameter: 0.4
filament_diameter: 1.750
heater_pin: PB4
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PK3
min_temp: 0
max_temp: 250
max_extrude_only_distance: 200.0
# Parameters for stock hotend on A10M
# Please recalibrate according to https://www.klipper3d.org/Config_checks.html#calibrate-pid-settings
control: pid
pid_kp: 54.722
pid_ki: 4.800
pid_kd: 155.958
[extruder_stepper extruder_1]
extruder:
enable_pin: !PL1
dir_pin: PL2
step_pin: PL0
microsteps: 16
rotation_distance: 8 # Needs to be optimized: https://www.klipper3d.org/Rotation_Distance.html#calibrating-rotation_distance-on-extruders
[extruder_stepper extruder_2]
extruder:
enable_pin: !PG0
dir_pin: PL4
step_pin: PL6
microsteps: 16
rotation_distance: 8 # Needs to be optimized: https://www.klipper3d.org/Rotation_Distance.html#calibrating-rotation_distance-on-extruders
[heater_bed]
heater_pin: PG5
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PK2
min_temp: 0
max_temp: 120
# Parameters for `SuperPlate` on A10M
# Please recalibrate according to https://www.klipper3d.org/Config_checks.html#calibrate-pid-settings
control: pid
pid_kp: 70.936
pid_ki: 1.785
pid_kd: 704.924
[fan]
pin: PH6
cycle_time: 0.150
kick_start_time: 0.300
# # for GT2560V4.0 with 20pin flat cable toward the display
# [display]
# lcd_type: hd44780
# hd44780_protocol_init: True
# rs_pin: PD1
# e_pin: PH0
# d4_pin: PH1
# d5_pin: PD0
# d6_pin: PE3
# d7_pin: PC1
# encoder_pins: ^PG1, ^PL7
# click_pin: ^!PD2
# for GT2560V4.1B with 12pin flat cable toward the display YHCB2004-06 ver3.0
# the aip31068_spi driver was added to Klipper on 2024-12-02, commit aecb29d2
[display]
lcd_type: aip31068_spi
latch_pin: PE3
spi_software_sclk_pin: PD0
spi_software_mosi_pin: PC1
spi_software_miso_pin: PH7 # any unused pin
encoder_pins: ^PH0, ^PH1
click_pin: ^!PD2
[filament_switch_sensor sensor_e0]
switch_pin: !PK4
[filament_switch_sensor sensor_e1]
switch_pin: !PK5
[filament_switch_sensor sensor_e2]
# switch_pin: !PE2 # for GT2560V4.0
switch_pin: !PF0 # for GT2560V4.1B
# to enable M118 echo command
[respond]
# Specific macros for mixing colors.
# Add in slicer new filament color and in filament start G-Code add desired mixing factor:
# M163 S0 P50 ; set extruder 0 to 50%
# M163 S1 P40 ; set extruder 1 to 40%
# M163 S2 P10 ; set extruder 2 to 10%
# M164 ; commit the mix factors
[gcode_macro M163]
description: M163 [P<factor>] [S<index>] Set a single mix factor (in proportion to the sum total of all mix factors). The mix must be committed to a virtual tool by M164 before it takes effect.
gcode:
{% if 'P' in params %}
{% set s = params.S|default(0)| int %}
{% if s == 0 %}
SET_GCODE_VARIABLE MACRO=M164 VARIABLE=e0_parts VALUE={params.P|default(0)|float}
M118 Set Mixing factor for extruder 0 to {params.P|default(0)|float}
{% elif s == 1 %}
SET_GCODE_VARIABLE MACRO=M164 VARIABLE=e1_parts VALUE={params.P|default(0)|float}
M118 Set Mixing factor for extruder 1 to {params.P|default(0)|float}
{% elif s == 2 %}
SET_GCODE_VARIABLE MACRO=M164 VARIABLE=e2_parts VALUE={params.P|default(0)|float}
M118 Set Mixing factor for extruder 2 to {params.P|default(0)|float}
{% endif %}
{% else %}
M118 No Mixing factor set, missing value for P
{% endif %}
M118 {e0_parts} {e1_parts} {e2_parts}
[gcode_macro M164]
description: Applies the set mixing factors to the extruders
# default values:
variable_e0_parts : 100
variable_e1_parts : 0
variable_e2_parts : 0
gcode:
# normalize the parts to sum of 1
{% set e0 = e0_parts / (e0_parts + e1_parts + e2_parts) | float %}
{% set e1 = e1_parts / (e0_parts + e1_parts + e2_parts) | float %}
{% set e2 = e2_parts / (e0_parts + e1_parts + e2_parts) | float %}
M118 scaled rot-dist_e0 { printer.configfile.settings.extruder.rotation_distance / (e0 + 0.000001) | float }
M118 scaled rot-dist_e1 { printer.configfile.settings['extruder_stepper extruder_1'].rotation_distance / (e1 + 0.000001) | float }
M118 scaled rot-dist_e2 { printer.configfile.settings['extruder_stepper extruder_2'].rotation_distance / (e2 + 0.000001) |float }
# activate stepper percentages
SYNC_EXTRUDER_MOTION EXTRUDER=extruder MOTION_QUEUE=extruder
SYNC_EXTRUDER_MOTION EXTRUDER=extruder_1 MOTION_QUEUE=extruder
SYNC_EXTRUDER_MOTION EXTRUDER=extruder_2 MOTION_QUEUE=extruder
SET_EXTRUDER_ROTATION_DISTANCE EXTRUDER=extruder DISTANCE={ printer.configfile.settings.extruder.rotation_distance / (e0+0.000001)|float }
SET_EXTRUDER_ROTATION_DISTANCE EXTRUDER=extruder_1 DISTANCE={ printer.configfile.settings['extruder_stepper extruder_1'].rotation_distance / (e1+0.000001)|float }
SET_EXTRUDER_ROTATION_DISTANCE EXTRUDER=extruder_2 DISTANCE={ printer.configfile.settings['extruder_stepper extruder_2'].rotation_distance / (e2+0.000001)|float }
M118 Mixing factors {e0} {e1} {e2} are activated
# In PrusaSlicer:
# - you can add as many extruders as mixing ratios you want
# - in Printer Settings -> Custom G-code -> Tool change G-code add:
# TOOL_CHANGE EXTRUDER={next_extruder}
# - in this config file add:
# [gcode_macro TOOL_CHANGE]
# description: Tool change macro with mix ratio setup for 11 extruders
# variable_extruder: 0
# gcode:
# {% set extruder = params.EXTRUDER|default(0)| int %}
# {% if extruder == 0 %}
# M163 S0 P100
# M163 S1 P0
# M163 S2 P0
# M164
# M118 Switching to Extruder 0
# {% elif extruder == 1 %}
# M163 S0 P90
# M163 S1 P10
# M163 S2P0
# M164
# M118 Switching to Extruder 1
# {% elif extruder == 2 %}
# # and so on ...
# {% else %}
# M118 Unknown extruder number: {extruder}
# {% endif %}
# In OrcaSlicer:
# you can add as many filaments as mixing ratios you want
# in Material settings -> Advanced -> Filament start G-code add desired mixing ratio:
# ; filament start gcode
# M163 S0 P100 ; set extruder 0
# M163 S1 P0 ; set extruder 1
# M163 S2 P0 ; set extruder 2
# M164 ; commit the mix factors
# For gradient over Z axis:
# In `Printer -> Custom G-code -> After layer change G-code` add:
# M163 S0 P{ layer_num * 100 / total_layer_count } ; Gradient 0-100
# M163 S1 P{(total_layer_count-layer_num) * 100 / total_layer_count} ; Gradient 100-0
# M164 ; commit the mix factors

View file

@ -0,0 +1,138 @@
# Klipper configuration for the TronXY Crux1 printer
# CXY-V10.1-220921 mainboard, GD32F4XX or STM32F446 MCU
#
# =======================
# BUILD AND FLASH OPTIONS
# =======================
#
# MCU-architecture: STMicroelectronics
# Processor model: STM32F446
# Bootloader offset: 64KiB
# Comms interface: Serial on USART1 PA10/PA9
#
# Build the firmware with these options
# Rename the resulting klipper.bin into fmw_tronxy.bin
# Put the file into a directory called "update" on a FAT32 formatted SD card.
# Turn off the printer, plug in the SD card and turn the printer back on
# Flashing will start automatically and progress will be indicated on the LCD
# Once the flashing is completed the display will get stuck on the white Tronxy logo bootscreen
# The LCD display will NOT work anymore after flashing Klipper onto this printer
[mcu]
serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0
restart_method: command
[printer]
kinematics: cartesian
max_velocity: 250
max_accel: 1500
square_corner_velocity: 5
max_z_velocity: 15
max_z_accel: 100
[controller_fan drivers_fan]
pin: PD7
[pwm_cycle_time BEEPER_pin]
pin: PA8
value: 0
shutdown_value: 0
cycle_time: 0.001
[safe_z_home]
home_xy_position: 0, 0
speed: 100
z_hop: 10
z_hop_speed: 5
[stepper_x]
step_pin: PE5
dir_pin: PF1
enable_pin: !PF0
microsteps: 16
rotation_distance: 20
endstop_pin: ^!PC15
position_endstop: -1
position_min: -1
position_max: 180
homing_speed: 100
homing_retract_dist: 10
second_homing_speed: 25
[stepper_y]
step_pin: PF9
dir_pin: !PF3
enable_pin: !PF5
microsteps: 16
rotation_distance: 20
endstop_pin: ^!PC14
position_endstop: -3
position_min: -3
position_max: 180
homing_retract_dist: 10
homing_speed: 100
second_homing_speed: 25
[stepper_z]
step_pin: PA6
dir_pin: !PF15
enable_pin: !PA5
microsteps: 16
rotation_distance: 4
endstop_pin: ^!PC13
position_endstop: 0
position_max: 180
position_min: 0
[extruder]
step_pin: PB1
dir_pin: PF13
enable_pin: !PF14
microsteps: 16
rotation_distance: 16.75
nozzle_diameter: 0.400
filament_diameter: 1.750
heater_pin: PG7
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PC3
control: pid
pid_kp: 22.2
pid_ki: 1.08
pid_kd: 114.00
min_temp: 0
max_temp: 250
min_extrude_temp: 170
max_extrude_only_distance: 450
[heater_fan hotend_fan]
heater: extruder
heater_temp: 50.0
pin: PG9
[fan]
pin: PG0
[filament_switch_sensor filament_sensor]
pause_on_runout: True
switch_pin: ^!PE6
[heater_bed]
heater_pin: PE2
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PC2
min_temp: 0
max_temp: 130
control: pid
pid_kp: 10.00
pid_ki: 0.023
pid_kd: 305.4
[bed_screws]
screw1: 17.5, 11
screw1_name: front_left
screw2: 162.5, 11
screw2_name: front_right
screw3: 162.5, 162.5
screw3_name: back_right
screw4: 17.5, 162.5
screw4_name: back_left

View file

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

View file

@ -364,6 +364,38 @@ and might later produce asynchronous messages such as:
The "header" field in the initial query response is used to describe
the fields found in later "data" responses.
### hx71x/dump_hx71x
This endpoint is used to subscribe to raw HX711 and HX717 ADC data.
Obtaining these low-level ADC updates may be useful for diagnostic
and debugging purposes. Using this endpoint may increase Klipper's
system load.
A request may look like:
`{"id": 123, "method":"hx71x/dump_hx71x",
"params": {"sensor": "load_cell", "response_template": {}}}`
and might return:
`{"id": 123,"result":{"header":["time","counts","value"]}}`
and might later produce asynchronous messages such as:
`{"params":{"data":[[3292.432935, 562534, 0.067059278],
[3292.4394937, 5625322, 0.670590639]]}}`
### ads1220/dump_ads1220
This endpoint is used to subscribe to raw ADS1220 ADC data.
Obtaining these low-level ADC updates may be useful for diagnostic
and debugging purposes. Using this endpoint may increase Klipper's
system load.
A request may look like:
`{"id": 123, "method":"ads1220/dump_ads1220",
"params": {"sensor": "load_cell", "response_template": {}}}`
and might return:
`{"id": 123,"result":{"header":["time","counts","value"]}}`
and might later produce asynchronous messages such as:
`{"params":{"data":[[3292.432935, 562534, 0.067059278],
[3292.4394937, 5625322, 0.670590639]]}}`
### pause_resume/cancel
This endpoint is similar to running the "PRINT_CANCEL" G-Code command.
@ -401,3 +433,130 @@ might return:
As with the "gcode/script" endpoint, this endpoint only completes
after any pending G-Code commands complete.
### bed_mesh/dump_mesh
Dumps the configuration and state for the current mesh and all
saved profiles.
For example:
`{"id": 123, "method": "bed_mesh/dump_mesh"}`
might return:
```
{
"current_mesh": {
"name": "eddy-scan-test",
"probed_matrix": [...],
"mesh_matrix": [...],
"mesh_params": {
"x_count": 9,
"y_count": 9,
"mesh_x_pps": 2,
"mesh_y_pps": 2,
"algo": "bicubic",
"tension": 0.5,
"min_x": 20,
"max_x": 330,
"min_y": 30,
"max_y": 320
}
},
"profiles": {
"default": {
"points": [...],
"mesh_params": {
"min_x": 20,
"max_x": 330,
"min_y": 30,
"max_y": 320,
"x_count": 9,
"y_count": 9,
"mesh_x_pps": 2,
"mesh_y_pps": 2,
"algo": "bicubic",
"tension": 0.5
}
},
"eddy-scan-test": {
"points": [...],
"mesh_params": {
"x_count": 9,
"y_count": 9,
"mesh_x_pps": 2,
"mesh_y_pps": 2,
"algo": "bicubic",
"tension": 0.5,
"min_x": 20,
"max_x": 330,
"min_y": 30,
"max_y": 320
}
},
"eddy-rapid-test": {
"points": [...],
"mesh_params": {
"x_count": 9,
"y_count": 9,
"mesh_x_pps": 2,
"mesh_y_pps": 2,
"algo": "bicubic",
"tension": 0.5,
"min_x": 20,
"max_x": 330,
"min_y": 30,
"max_y": 320
}
}
},
"calibration": {
"points": [...],
"config": {
"x_count": 9,
"y_count": 9,
"mesh_x_pps": 2,
"mesh_y_pps": 2,
"algo": "bicubic",
"tension": 0.5,
"mesh_min": [
20,
30
],
"mesh_max": [
330,
320
],
"origin": null,
"radius": null
},
"probe_path": [...],
"rapid_path": [...]
},
"probe_offsets": [
0,
25,
0.5
],
"axis_minimum": [
0,
0,
-5,
0
],
"axis_maximum": [
351,
358,
330,
0
]
}
```
The `dump_mesh` endpoint takes one optional parameter, `mesh_args`.
This parameter must be an object, where the keys and values are
parameters available to [BED_MESH_CALIBRATE](#bed_mesh_calibrate).
This will update the mesh configuration and probe points using the
supplied parameters prior to returning the result. It is recommended
to omit mesh parameters unless it is desired to visualize the probe points
and/or travel path before performing `BED_MESH_CALIBRATE`.

View file

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

View file

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

View file

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

View file

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

View file

@ -359,10 +359,10 @@ Useful steps:
be efficient as it is typically only called during homing and
probing operations.
5. Other methods. Implement the `check_move()`, `get_status()`,
`get_steppers()`, `home()`, and `set_position()` methods. These
functions are typically used to provide kinematic specific checks.
However, at the start of development one can use boiler-plate code
here.
`get_steppers()`, `home()`, `clear_homing_state()`, and `set_position()`
methods. These functions are typically used to provide kinematic
specific checks. However, at the start of development one can use
boiler-plate code here.
6. Implement test cases. Create a g-code file with a series of moves
that can test important cases for the given kinematics. Follow the
[debugging documentation](Debugging.md) to convert this g-code file

View file

@ -8,6 +8,41 @@ All dates in this document are approximate.
## Changes
20250131: Option `VARIABLE=<name>` in `SAVE_VARIABLE` requires lowercase
value. For example, `extruder` instead of mixedcase `Extruder` or
uppercase `EXTRUDER`. Using any uppercase letter will raise an error.
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

View file

@ -998,6 +998,13 @@ Visual Examples:
#adaptive_margin:
# An optional margin (in mm) to be added around the bed area used by
# the defined print objects when generating an adaptive mesh.
#scan_overshoot:
# The maximum amount of travel (in mm) available outside of the mesh.
# For rectangular beds this applies to travel on the X axis, and for round beds
# it applies to the entire radius. The tool must be able to travel the amount
# specified outside of the mesh. This value is used to optimize the travel
# path when performing a "rapid scan". The minimum value that may be specified
# is 1. The default is no overshoot.
```
### [bed_tilt]
@ -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
@ -3661,6 +3846,7 @@ run_current:
#driver_SEIMIN: 0
#driver_SFILT: 0
#driver_SG4_ANGLE_OFFSET: 1
#driver_SLOPE_CONTROL: 0
# Set the given register during the configuration of the TMC2240
# chip. This may be used to set custom motor parameters. The
# defaults for each parameter are next to the parameter name in the
@ -3722,6 +3908,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 +4150,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 +4285,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
@ -4520,6 +4744,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]
@ -4606,6 +4936,50 @@ vssa_pin:
# noise. The default is 2 seconds.
```
### [ads1x1x]
ADS1013, ADS1014, ADS1015, ADS1113, ADS1114 and ADS1115 are I2C based Analog to
Digital Converters that can be used for temperature sensors. They provide 4
analog input pins either as single line or as differential input.
Note: Use caution if using this sensor to control heaters. The heater min_temp
and max_temp are only verified in the host and only if the host is running and
operating normally. (ADC inputs directly connected to the micro-controller
verify min_temp and max_temp within the micro-controller and do not require a
working connection to the host.)
```
[ads1x1x my_ads1x1x]
chip: ADS1115
#pga: 4.096V
# Default value is 4.096V. The maximum voltage range used for the input. This
# scales all values read from the ADC. Options are: 6.144V, 4.096V, 2.048V,
# 1.024V, 0.512V, 0.256V
#adc_voltage: 3.3
# The suppy voltage for the device. This allows additional software scaling
# for all values read from the ADC.
i2c_mcu: host
i2c_bus: i2c.1
#address_pin: GND
# Default value is GND. There can be up to four addressed devices depending
# upon wiring of the device. Check the datasheet for details. The i2c_address
# can be specified directly instead of using the address_pin.
```
The chip provides pins that can be used on other sensors.
```
sensor_type: ...
# Can be any thermistor or adc_temperature.
sensor_pin: my_ads1x1x:AIN0
# A combination of the name of the ads1x1x chip and the pin. Possible
# pin values are AIN0, AIN1, AIN2 and AIN3 for single ended lines and
# DIFF01, DIFF03, DIFF13 and DIFF23 for differential between their
# correspoding lines. For example
# DIFF03 measures the differential between line 0 and 3. Only specific
# combinations for the differentials are allowed.
```
### [replicape]
Replicape support - see the [beaglebone guide](Beaglebone.md) and the
@ -4711,8 +5085,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.
@ -4720,7 +5095,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
@ -4783,8 +5158,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.
```

View file

@ -132,3 +132,10 @@ There are several
you have questions on the code then you can also ask in the
[Klipper Discourse Forum](#discourse-forum) or on the
[Klipper Discord Chat](#discord-chat).
## Professional Services
![](img/klipper-logo-small.png)
Custom software development, software support, and solutions:
[https://ko-fi.com/koconnor](https://ko-fi.com/koconnor)

View file

@ -54,3 +54,93 @@ result in changes in reported Z height. Changes in either the bed
surface temperature or sensor hardware temperature can skew the
results. It is important that calibration and probing is only done
when the printer is at a stable temperature.
## Thermal Drift Calibration
As with all inductive probes, eddy current probes are subject to
significant thermal drift. If the eddy probe has a temperature
sensor on the coil it is possible to configure a `[temperature_probe]`
to report coil temperature and enable software drift compensation. To
link a temperature probe to an eddy current probe the
`[temperature_probe]` section must share a name with the
`[probe_eddy_current]` section. For example:
```
[probe_eddy_current my_probe]
# eddy probe configuration...
[temperature_probe my_probe]
# temperature probe configuration...
```
See the [configuration reference](Config_Reference.md#temperature_probe)
for further details on how to configure a `temperature_probe`. It is
advised to configure the `calibration_position`,
`calibration_extruder_temp`, `extruder_heating_z`, and
`calibration_bed_temp` options, as doing so will automate some of the
steps outlined below. If the printer to be calibrated is enclosed, it
is strongly recommended to set the `max_validation_temp` option to a value
between 100 and 120.
Eddy probe manufacturers may offer a stock drift calibration that can be
manually added to `drift_calibration` option of the `[probe_eddy_current]`
section. If they do not, or if the stock calibration does not perform well on
your system, the `temperature_probe` module offers a manual calibration
procedure via the `TEMPERATURE_PROBE_CALIBRATE` gcode command.
Prior to performing calibration the user should have an idea of what the
maximum attainable temperature probe coil temperature is. This temperature
should be used to set the `TARGET` parameter of the
`TEMPERATURE_PROBE_CALIBRATE` command. The goal is to calibrate across the
widest temperature range possible, thus its desirable to start with the printer
cold and finish with the coil at the maximum temperature it can reach.
Once a `[temperature_probe]` is configured, the following steps may be taken
to perform thermal drift calibration:
- The probe must be calibrated using `PROBE_EDDY_CURRENT_CALIBRATE`
when a `[temperature_probe]` is configured and linked. This captures
the temperature during calibration which is necessary to perform
thermal drift compensation.
- Make sure the nozzle is free of debris and filament.
- The bed, nozzle, and probe coil should be cold prior to calibration.
- The following steps are required if the `calibration_position`,
`calibration_extruder_temp`, and `extruder_heating_z` options in
`[temperature_probe]` are **NOT** configured:
- Move the tool to the center of the bed. Z should be 30mm+ above the bed.
- Heat the extruder to a temperature above the maximum safe bed temperature.
150-170C should be sufficient for most configurations. The purpose of
heating the extruder is to avoid nozzle expansion during calibration.
- When the extruder temperature has settled, move the Z axis down to about 1mm
above the bed.
- Start drift calibration. If the probe's name is `my_probe` and the maximum
probe temperature we can achieve is 80C, the appropriate gcode command is
`TEMPERATURE_PROBE_CALIBRATE PROBE=my_probe TARGET=80`. If configured, the
tool will move to the X,Y coordinate specified by the `calibration_position`
and the Z value specified by `extruder_heating_z`. After heating the extruder
to the specified temperature the tool will move to the Z value specified
by the`calibration_position`.
- The procedure will request a manual probe. Perform the manual probe with
the paper test and `ACCEPT`. The calibration procedure will take the first
set of samples with the probe then park the probe in the heating position.
- If the `calibration_bed_temp` is **NOT** configured turn on the bed heat
to the maximum safe temperature. Otherwise this step will be performed
automatically.
- By default the calibration procedure will request a manual probe every
2C between samples until the `TARGET` is reached. The temperature delta
between samples can be customized by setting the `STEP` parameter in
`TEMPERATURE_PROBE_CALIBRATE`. Care should be taken when setting a custom
`STEP` value, a value too high may request too few samples resulting in
a poor calibration.
- The following additional gcode commands are available during drift
calibration:
- `TEMPERATURE_PROBE_NEXT` may be used to force a new sample before the step
delta has been reached.
- `TEMPERATURE_PROBE_COMPLETE` may be used to complete calibration before the
`TARGET` has been reached.
- `ABORT` may be used to end calibration and discard results.
- When calibration is finished use `SAVE_CONFIG` to store the drift
calibration.
As one may conclude, the calibration process outlined above is more challenging
and time consuming than most other procedures. It may require practice and several attempts to achieve an optimal calibration.

View file

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

View file

@ -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]
@ -476,6 +494,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
@ -553,15 +585,18 @@ state; issue a G28 afterwards to reset the kinematics. This command is
intended for low-level diagnostics and debugging.
#### SET_KINEMATIC_POSITION
`SET_KINEMATIC_POSITION [X=<value>] [Y=<value>] [Z=<value>]`: Force
the low-level kinematic code to believe the toolhead is at the given
cartesian position. This is a diagnostic and debugging command; use
SET_GCODE_OFFSET and/or G92 for regular axis transformations. If an
axis is not specified then it will default to the position that the
head was last commanded to. Setting an incorrect or invalid position
may lead to internal software errors. This command may invalidate
future boundary checks; issue a G28 afterwards to reset the
kinematics.
`SET_KINEMATIC_POSITION [X=<value>] [Y=<value>] [Z=<value>]
[CLEAR=<[X][Y][Z]>]`: Force the low-level kinematic code to believe the
toolhead is at the given cartesian position. This is a diagnostic and
debugging command; use SET_GCODE_OFFSET and/or G92 for regular axis
transformations. If an axis is not specified then it will default to the
position that the head was last commanded to. Setting an incorrect or
invalid position may lead to internal software errors. Use the CLEAR
parameter to forget the homing state for the given axes. Note that CLEAR
will not override the previous functionality; if an axis is not specified
to CLEAR it will have its kinematic position set as per above. This
command may invalidate future boundary checks; issue a G28 afterwards to
reset the kinematics.
### [gcode]
@ -857,6 +892,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
@ -1021,6 +1070,21 @@ CYCLE_TIME parameter is not stored between SET_PIN commands (any
SET_PIN command without an explicit CYCLE_TIME parameter will use the
`cycle_time` specified in the pwm_cycle_time config section).
### [quad_gantry_level]
The following commands are available when the
[quad_gantry_level config section](Config_Reference.md#quad_gantry_level)
is enabled.
#### QUAD_GANTRY_LEVEL
`QUAD_GANTRY_LEVEL [RETRIES=<value>] [RETRY_TOLERANCE=<value>]
[HORIZONTAL_MOVE_Z=<value>] [<probe_parameter>=<value>]`: This 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 `RETRIES`, `RETRY_TOLERANCE`, and `HORIZONTAL_MOVE_Z` values
override those options specified in the config file.
### [query_adc]
The query_adc module is automatically loaded.
@ -1056,20 +1120,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
@ -1086,8 +1149,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
@ -1137,8 +1201,9 @@ has been enabled.
#### SAVE_VARIABLE
`SAVE_VARIABLE VARIABLE=<name> VALUE=<value>`: Saves the variable to
disk so that it can be used across restarts. All stored variables are
loaded into the `printer.save_variables.variables` dict at startup and
disk so that it can be used across restarts. The VARIABLE must be lowercase.
All stored variables are loaded into the
`printer.save_variables.variables` dict at startup and
can be used in gcode macros. The provided VALUE is parsed as a Python
literal.
@ -1410,8 +1475,46 @@ The following commands are available when the
[z_tilt config section](Config_Reference.md#z_tilt) is enabled.
#### Z_TILT_ADJUST
`Z_TILT_ADJUST [HORIZONTAL_MOVE_Z=<value>] [<probe_parameter>=<value>]`: This
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.
`Z_TILT_ADJUST [RETRIES=<value>] [RETRY_TOLERANCE=<value>]
[HORIZONTAL_MOVE_Z=<value>] [<probe_parameter>=<value>]`: This 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 `RETRIES`, `RETRY_TOLERANCE`, and `HORIZONTAL_MOVE_Z` values
override those options specified in the config file.
### [temperature_probe]
The following commands are available when a
[temperature_probe config section](Config_Reference.md#temperature_probe)
is enabled.
#### TEMPERATURE_PROBE_CALIBRATE
`TEMPERATURE_PROBE_CALIBRATE [PROBE=<probe name>] [TARGET=<value>] [STEP=<value>]`:
Initiates probe drift calibration for eddy current based probes. The `TARGET`
is a target temperature for the last sample. When the temperature recorded
during a sample exceeds the `TARGET` calibration will complete. The `STEP`
parameter sets temperature delta (in C) between samples. After a sample has
been taken, this delta is used to schedule a call to `TEMPERATURE_PROBE_NEXT`.
The default `STEP` is 2.
#### TEMPERATURE_PROBE_NEXT
`TEMPERATURE_PROBE_NEXT`: After calibration has started this command is run to
take the next sample. It is automatically scheduled to run when the delta
specified by `STEP` has been reached, however its also possible to manually run
this command to force a new sample. This command is only available during
calibration.
#### TEMPERATURE_PROBE_COMPLETE:
`TEMPERATURE_PROBE_COMPLETE`: Can be used to end calibration and save the
current result before the `TARGET` temperature is reached. This command
is only available during calibration.
#### ABORT
`ABORT`: Aborts the calibration process, discarding the current results.
This command is only available during drift calibration.
### TEMPERATURE_PROBE_ENABLE
`TEMPERATURE_PROBE_ENABLE ENABLE=[0|1]`: Sets temperature drift
compensation on or off. If ENABLE is set to 0, drift compensation
will be disabled, if set to 1 it is enabled.

View file

@ -1,15 +1,20 @@
# Installation
These instructions assume the software will run on a Raspberry Pi
computer in conjunction with OctoPrint. It is recommended that a
Raspberry Pi 2 (or later) be used as the host machine (see the
These instructions assume the software will run on a linux based host
running a Klipper compatible front end. It is recommended that a
SBC(Small Board Computer) such as a Raspberry Pi or Debian based Linux
device be used as the host machine (see the
[FAQ](FAQ.md#can-i-run-klipper-on-something-other-than-a-raspberry-pi-3)
for other machines).
for other options).
For the purposes of these instructions host relates to the Linux device and
mcu relates to the printboard. SBC relates to the term Small Board Computer
such as the Raspberry Pi.
## Obtain a Klipper Configuration File
Most Klipper settings are determined by a "printer configuration file"
that will be stored on the Raspberry Pi. An appropriate configuration
printer.cfg, that will be stored on the host. An appropriate configuration
file can often be found by looking in the Klipper
[config directory](../config/) for a file starting with a "printer-"
prefix that corresponds to the target printer. The Klipper
@ -35,38 +40,51 @@ printer configuration file, then start with the closest example
[config file](../config/) and use the Klipper
[config reference](Config_Reference.md) for further information.
## Prepping an OS image
## Interacting with Klipper
Start by installing [OctoPi](https://github.com/guysoft/OctoPi) on the
Raspberry Pi computer. Use OctoPi v0.17.0 or later - see the
[OctoPi releases](https://github.com/guysoft/OctoPi/releases) for
release information. One should verify that OctoPi boots and that the
OctoPrint web server works. After connecting to the OctoPrint web
page, follow the prompt to upgrade OctoPrint to v1.4.2 or later.
Klipper is a 3d printer firmware, so it needs some way for the user to
interact with it.
After installing OctoPi and upgrading OctoPrint, it will be necessary
to ssh into the target machine to run a handful of system commands. If
using a Linux or MacOS desktop, then the "ssh" software should already
be installed on the desktop. There are free ssh clients available for
other desktops (eg,
[PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/)). Use the
ssh utility to connect to the Raspberry Pi (`ssh pi@octopi` -- password
is "raspberry") and run the following commands:
Currently the best choices are front ends that retrieve information through
the [Moonraker web API](https://moonraker.readthedocs.io/) and there is also
the option to use [Octoprint](https://octoprint.org/) to control Klipper.
```
git clone https://github.com/Klipper3d/klipper
./klipper/scripts/install-octopi.sh
```
The choice is up to the user on what to use, but the underlying Klipper is the
same in all cases. We encourage users to research the options available and
make an informed decision.
The above will download Klipper, install some system dependencies,
setup Klipper to run at system startup, and start the Klipper host
software. It will require an internet connection and it may take a few
minutes to complete.
## Obtaining an OS image for SBC's
There are many ways to obtain an OS image for Klipper for SBC use, most depend on
what front end you wish to use. Some manafactures of these SBC boards also provide
their own Klipper-centric images.
The two main Moonraker based front ends are [Fluidd](https://docs.fluidd.xyz/)
and [Mainsail](https://docs.mainsail.xyz/), the latter of which has a premade install
image ["MainsailOS"](https://docs-os.mainsail.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

View file

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

@ -0,0 +1,79 @@
# OctoPrint for Klipper
Klipper has a few options for its front ends, Octoprint was the first
and original front end for Klipper. This document will give
a brief overview of installing with this option.
## Install with OctoPi
Start by installing [OctoPi](https://github.com/guysoft/OctoPi) on the
Raspberry Pi computer. Use OctoPi v0.17.0 or later - see the
[OctoPi releases](https://github.com/guysoft/OctoPi/releases) for
release information.
One should verify that OctoPi boots and that the
OctoPrint web server works. After connecting to the OctoPrint web
page, follow the prompt to upgrade OctoPrint if needed.
After installing OctoPi and upgrading OctoPrint, it will be necessary
to ssh into the target machine to run a handful of system commands.
Start by running these commands on your host device:
__If you do not have git installed, please do so with:__
```
sudo apt install git
```
then proceed:
```
cd ~
git clone https://github.com/Klipper3d/klipper
./klipper/scripts/install-octopi.sh
```
The above will download Klipper, install the needed system dependencies,
setup Klipper to run at system startup, and start the Klipper host
software. It will require an internet connection and it may take a few
minutes to complete.
## Installing with KIAUH
KIAUH can be used to install OctoPrint on a variety of Linux based systems
that run a form of Debian. More information can be found
at https://github.com/dw-0/kiauh
## Configuring OctoPrint to use Klipper
The OctoPrint web server needs to be configured to communicate with the Klipper
host software. Using a web browser, login to the OctoPrint web page and then
configure the following items:
Navigate to the Settings tab (the wrench icon at the top of the page).
Under "Serial Connection" in "Additional serial ports" add:
```
~/printer_data/comms/klippy.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

View file

@ -17,6 +17,7 @@ communication with the Klipper developers.
## Installation and Configuration
- [Installation](Installation.md): Guide to installing Klipper.
- [Octoprint](OctoPrint.md): Guide to installing Octoprint with Klipper.
- [Config Reference](Config_Reference.md): Description of config
parameters.
- [Rotation Distance](Rotation_Distance.md): Calculating the

View file

@ -22,7 +22,7 @@ Use a slicer to generate g-code for the large hollow square found in
[docs/prints/square_tower.stl](prints/square_tower.stl). Use a high
speed (eg, 100mm/s), zero infill, and a coarse layer height (the layer
height should be around 75% of the nozzle diameter). Make sure any
"dynamic acceleration control" is disabled in the slicer.
"dynamic acceleration control" and "scarf joint" seams are disabled in the slicer.
Prepare for the test by issuing the following G-Code command:
```

View file

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

View file

@ -39,6 +39,27 @@ the following strings: "adjust", "fine".
- `current_screw`: The index for the current screw being adjusted.
- `accepted_screws`: The number of accepted screws.
## canbus_stats
The following information is available in the `canbus_stats
some_mcu_name` object (this object is automatically available if an
mcu is configured to use canbus):
- `rx_error`: The number of receive errors detected by the
micro-controller canbus hardware.
- `tx_error`: The number of transmit errors detected by the
micro-controller canbus hardware.
- `tx_retries`: The number of transmit attempts that were retried due
to bus contention or errors.
- `bus_state`: The status of the interface (typically "active" for a
bus in normal operation, "warn" for a bus with recent errors,
"passive" for a bus that will no longer transmit canbus error
frames, or "off" for a bus that will no longer transmit or receive
messages).
Note that only the rp2XXX micro-controllers report a non-zero
`tx_retries` field and the rp2XXX micro-controllers always report
`tx_error` as zero and `bus_state` as "active".
## configfile
The following information is available in the `configfile` object

View file

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

View file

@ -71,7 +71,7 @@ extra:
# https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-analytics/#site-search-tracking
analytics:
provider: google
property: UA-138371409-1
property: G-VEN1PGNQL4
# Language Selection
alternate:
- name: English
@ -88,7 +88,9 @@ nav:
- Config_Changes.md
- Contact.md
- Installation and Configuration:
- Installation.md
- Installation:
- Installation.md
- OctoPrint.md
- Configuration Reference:
- Config_Reference.md
- Rotation_Distance.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

View file

@ -6,13 +6,16 @@ title: Welcome
![](img/klipper-logo.png){ .center-image }
Klipper is a 3d-Printer firmware. It combines the power of a general
purpose computer with one or more micro-controllers. See the
[features](Features.md) document for more information on why you
should use Klipper.
The Klipper firmware controls 3d-Printers. It combines the power of a
general purpose computer with one or more micro-controllers. See the
[features document](https://www.klipper3d.org/Features.html) for more
information on why you should use the Klipper software.
To begin using Klipper start by [installing](Installation.md) it.
Start by [installing Klipper software](https://www.klipper3d.org/Installation.html).
Klipper is Free Software. Read the [documentation](Overview.md) or
view [the Klipper code on github](https://github.com/Klipper3d/klipper).
We depend on the generous support from our [sponsors](Sponsors.md).
Klipper software is Free Software. Read the
[documentation](https://www.klipper3d.org/Overview.html), see the
[license](COPYING), or
[download](https://github.com/Klipper3d/Klipper) the software. We
depend on the generous support from our
[sponsors](https://www.klipper3d.org/Sponsors.html).

View file

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

View file

@ -9,9 +9,15 @@
#include <string.h> // memset
#include "compiler.h" // __visible
#include "itersolve.h" // struct stepper_kinematics
#include "list.h" // list_node
#include "pyhelper.h" // errorf
#include "trapq.h" // move_get_distance
struct pa_params {
double pressure_advance, active_print_time;
struct list_node node;
};
// Without pressure advance, the extruder stepper position is:
// extruder_position(t) = nominal_position(t)
// When pressure advance is enabled, additional filament is pushed
@ -52,17 +58,25 @@ extruder_integrate_time(double base, double start_v, double half_accel
// Calculate the definitive integral of extruder for a given move
static double
pa_move_integrate(struct move *m, double pressure_advance
pa_move_integrate(struct move *m, struct list_head *pa_list
, double base, double start, double end, double time_offset)
{
if (start < 0.)
start = 0.;
if (end > m->move_t)
end = m->move_t;
// Calculate base position and velocity with pressure advance
// Determine pressure_advance value
int can_pressure_advance = m->axes_r.y != 0.;
if (!can_pressure_advance)
pressure_advance = 0.;
double pressure_advance = 0.;
if (can_pressure_advance) {
struct pa_params *pa = list_last_entry(pa_list, struct pa_params, node);
while (unlikely(pa->active_print_time > m->print_time) &&
!list_is_first(&pa->node, pa_list)) {
pa = list_prev_entry(pa, node);
}
pressure_advance = pa->pressure_advance;
}
// Calculate base position and velocity with pressure advance
base += pressure_advance * m->start_v;
double start_v = m->start_v + pressure_advance * 2. * m->half_accel;
// Calculate definitive integral
@ -75,20 +89,20 @@ pa_move_integrate(struct move *m, double pressure_advance
// Calculate the definitive integral of the extruder over a range of moves
static double
pa_range_integrate(struct move *m, double move_time
, double pressure_advance, double hst)
, struct list_head *pa_list, double hst)
{
// Calculate integral for the current move
double res = 0., start = move_time - hst, end = move_time + hst;
double start_base = m->start_pos.x;
res += pa_move_integrate(m, pressure_advance, 0., start, move_time, start);
res -= pa_move_integrate(m, pressure_advance, 0., move_time, end, end);
res += pa_move_integrate(m, pa_list, 0., start, move_time, start);
res -= pa_move_integrate(m, pa_list, 0., move_time, end, end);
// Integrate over previous moves
struct move *prev = m;
while (unlikely(start < 0.)) {
prev = list_prev_entry(prev, node);
start += prev->move_t;
double base = prev->start_pos.x - start_base;
res += pa_move_integrate(prev, pressure_advance, base, start
res += pa_move_integrate(prev, pa_list, base, start
, prev->move_t, start);
}
// Integrate over future moves
@ -96,14 +110,15 @@ pa_range_integrate(struct move *m, double move_time
end -= m->move_t;
m = list_next_entry(m, node);
double base = m->start_pos.x - start_base;
res -= pa_move_integrate(m, pressure_advance, base, 0., end, end);
res -= pa_move_integrate(m, pa_list, base, 0., end, end);
}
return res;
}
struct extruder_stepper {
struct stepper_kinematics sk;
double pressure_advance, half_smooth_time, inv_half_smooth_time2;
struct list_head pa_list;
double half_smooth_time, inv_half_smooth_time2;
};
static double
@ -116,22 +131,45 @@ extruder_calc_position(struct stepper_kinematics *sk, struct move *m
// Pressure advance not enabled
return m->start_pos.x + move_get_distance(m, move_time);
// Apply pressure advance and average over smooth_time
double area = pa_range_integrate(m, move_time, es->pressure_advance, hst);
double area = pa_range_integrate(m, move_time, &es->pa_list, hst);
return m->start_pos.x + area * es->inv_half_smooth_time2;
}
void __visible
extruder_set_pressure_advance(struct stepper_kinematics *sk
extruder_set_pressure_advance(struct stepper_kinematics *sk, double print_time
, double pressure_advance, double smooth_time)
{
struct extruder_stepper *es = container_of(sk, struct extruder_stepper, sk);
double hst = smooth_time * .5;
double hst = smooth_time * .5, old_hst = es->half_smooth_time;
es->half_smooth_time = hst;
es->sk.gen_steps_pre_active = es->sk.gen_steps_post_active = hst;
// Cleanup old pressure advance parameters
double cleanup_time = sk->last_flush_time - (old_hst > hst ? old_hst : hst);
struct pa_params *first_pa = list_first_entry(
&es->pa_list, struct pa_params, node);
while (!list_is_last(&first_pa->node, &es->pa_list)) {
struct pa_params *next_pa = list_next_entry(first_pa, node);
if (next_pa->active_print_time >= cleanup_time) break;
list_del(&first_pa->node);
first_pa = next_pa;
}
if (! hst)
return;
es->inv_half_smooth_time2 = 1. / (hst * hst);
es->pressure_advance = pressure_advance;
if (list_last_entry(&es->pa_list, struct pa_params, node)->pressure_advance
== pressure_advance) {
// Retain old pa_params
return;
}
// Add new pressure advance parameters
struct pa_params *pa = malloc(sizeof(*pa));
memset(pa, 0, sizeof(*pa));
pa->pressure_advance = pressure_advance;
pa->active_print_time = print_time;
list_add_tail(&pa->node, &es->pa_list);
}
struct stepper_kinematics * __visible
@ -141,5 +179,22 @@ extruder_stepper_alloc(void)
memset(es, 0, sizeof(*es));
es->sk.calc_position_cb = extruder_calc_position;
es->sk.active_flags = AF_X;
list_init(&es->pa_list);
struct pa_params *pa = malloc(sizeof(*pa));
memset(pa, 0, sizeof(*pa));
list_add_tail(&pa->node, &es->pa_list);
return &es->sk;
}
void __visible
extruder_stepper_free(struct stepper_kinematics *sk)
{
struct extruder_stepper *es = container_of(sk, struct extruder_stepper, sk);
while (!list_empty(&es->pa_list)) {
struct pa_params *pa = list_first_entry(
&es->pa_list, struct pa_params, node);
list_del(&pa->node);
free(pa);
}
free(sk);
}

View file

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

View file

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

View file

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

216
klippy/extras/ads1220.py Normal file
View 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}

393
klippy/extras/ads1x1x.py Normal file
View file

@ -0,0 +1,393 @@
# Support for I2C based ADS1013, ADS1014, ADS1015, ADS1113, ADS1114 and ADS1115
#
# Copyright (C) 2024 Konstantin Koch <korsarnek@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import pins
from . import bus
# Supported chip types
ADS1X1X_CHIP_TYPE = {
'ADS1013': 3,
'ADS1014': 4,
'ADS1015': 5,
'ADS1113': 13,
'ADS1114': 14,
'ADS1115': 15
}
def isADS101X(chip):
return (chip == ADS1X1X_CHIP_TYPE['ADS1013'] \
or chip == ADS1X1X_CHIP_TYPE['ADS1014'] \
or chip == ADS1X1X_CHIP_TYPE['ADS1015'])
def isADS111X(chip):
return (chip == ADS1X1X_CHIP_TYPE['ADS1113'] \
or chip == ADS1X1X_CHIP_TYPE['ADS1114'] \
or chip == ADS1X1X_CHIP_TYPE['ADS1115'])
# Address is defined by how the address pin is wired
ADS1X1X_CHIP_ADDR = {
'GND': 0x48,
'VCC': 0x49,
'SDA': 0x4a,
'SCL': 0x4b
}
# Chip "pointer" registers
ADS1X1X_REG_POINTER_MASK = 0x03
ADS1X1X_REG_POINTER = {
'CONVERSION': 0x00,
'CONFIG': 0x01,
'LO_THRESH': 0x02,
'HI_THRESH': 0x03
}
# Config register masks
ADS1X1X_REG_CONFIG = {
'OS_MASK': 0x8000,
'MULTIPLEXER_MASK': 0x7000,
'PGA_MASK': 0x0E00,
'MODE_MASK': 0x0100,
'DATA_RATE_MASK': 0x00E0,
'COMPARATOR_MODE_MASK': 0x0010,
'COMPARATOR_POLARITY_MASK': 0x0008,
# Determines if ALERT/RDY pin latches once asserted
'COMPARATOR_LATCHING_MASK': 0x0004,
'COMPARATOR_QUEUE_MASK': 0x0003
}
#
# The following enums are to be used with the configuration functions.
#
ADS1X1X_OS = {
'OS_IDLE': 0x8000, # Device is not performing a conversion
'OS_SINGLE': 0x8000 # Single-conversion
}
ADS1X1X_MUX = {
'DIFF01': 0x0000, # Differential P = AIN0, N = AIN1 0
'DIFF03': 0x1000, # Differential P = AIN0, N = AIN3 4096
'DIFF13': 0x2000, # Differential P = AIN1, N = AIN3 8192
'DIFF23': 0x3000, # Differential P = AIN2, N = AIN3 12288
'AIN0': 0x4000, # Single-ended (ADS1015: AIN0 16384)
'AIN1': 0x5000, # Single-ended (ADS1015: AIN1 20480)
'AIN2': 0x6000, # Single-ended (ADS1015: AIN2 24576)
'AIN3': 0x7000 # Single-ended (ADS1015: AIN3 28672)
}
ADS1X1X_PGA = {
'6.144V': 0x0000, # +/-6.144V range = Gain 2/3
'4.096V': 0x0200, # +/-4.096V range = Gain 1
'2.048V': 0x0400, # +/-2.048V range = Gain 2
'1.024V': 0x0600, # +/-1.024V range = Gain 4
'0.512V': 0x0800, # +/-0.512V range = Gain 8
'0.256V': 0x0A00 # +/-0.256V range = Gain 16
}
ADS1X1X_PGA_VALUE = {
0x0000: 6.144,
0x0200: 4.096,
0x0400: 2.048,
0x0600: 1.024,
0x0800: 0.512,
0x0A00: 0.256,
}
ADS111X_RESOLUTION = 32767.0
ADS111X_PGA_SCALAR = {
0x0000: 6.144 / ADS111X_RESOLUTION, # +/-6.144V range = Gain 2/3
0x0200: 4.096 / ADS111X_RESOLUTION, # +/-4.096V range = Gain 1
0x0400: 2.048 / ADS111X_RESOLUTION, # +/-2.048V range = Gain 2
0x0600: 1.024 / ADS111X_RESOLUTION, # +/-1.024V range = Gain 4
0x0800: 0.512 / ADS111X_RESOLUTION, # +/-0.512V range = Gain 8
0x0A00: 0.256 / ADS111X_RESOLUTION # +/-0.256V range = Gain 16
}
ADS101X_RESOLUTION = 2047.0
ADS101X_PGA_SCALAR = {
0x0000: 6.144 / ADS101X_RESOLUTION, # +/-6.144V range = Gain 2/3
0x0200: 4.096 / ADS101X_RESOLUTION, # +/-4.096V range = Gain 1
0x0400: 2.048 / ADS101X_RESOLUTION, # +/-2.048V range = Gain 2
0x0600: 1.024 / ADS101X_RESOLUTION, # +/-1.024V range = Gain 4
0x0800: 0.512 / ADS101X_RESOLUTION, # +/-0.512V range = Gain 8
0x0A00: 0.256 / ADS101X_RESOLUTION # +/-0.256V range = Gain 16
}
ADS1X1X_MODE = {
'continuous': 0x0000, # Continuous conversion mode
'single': 0x0100 # Power-down single-shot mode
}
# Lesser samples per second means it takes and averages more samples before
# returning a result.
ADS101X_SAMPLES_PER_SECOND = {
'128': 0x0000, # 128 samples per second
'250': 0x0020, # 250 samples per second
'490': 0x0040, # 490 samples per second
'920': 0x0060, # 920 samples per second
'1600': 0x0080, # 1600 samples per second
'2400': 0x00a0, # 2400 samples per second
'3300': 0x00c0, # 3300 samples per second
}
ADS111X_SAMPLES_PER_SECOND = {
'8': 0x0000, # 8 samples per second
'16': 0x0020, # 16 samples per second
'32': 0x0040, # 32 samples per second
'64': 0x0060, # 64 samples per second
'128': 0x0080, # 128 samples per second
'250': 0x00a0, # 250 samples per second
'475': 0x00c0, # 475 samples per second
'860': 0x00e0 # 860 samples per second
}
ADS1X1X_COMPARATOR_MODE = {
'TRADITIONAL': 0x0000, # Traditional comparator with hysteresis
'WINDOW': 0x0010 # Window comparator
}
ADS1X1X_COMPARATOR_POLARITY = {
'ACTIVE_LO': 0x0000, # ALERT/RDY pin is low when active
'ACTIVE_HI': 0x0008 # ALERT/RDY pin is high when active
}
ADS1X1X_COMPARATOR_LATCHING = {
'NON_LATCHING': 0x0000, # Non-latching comparator
'LATCHING': 0x0004 # Latching comparator
}
ADS1X1X_COMPARATOR_QUEUE = {
'QUEUE_1': 0x0000, # Assert ALERT/RDY after one conversions
'QUEUE_2': 0x0001, # Assert ALERT/RDY after two conversions
'QUEUE_4': 0x0002, # Assert ALERT/RDY after four conversions
'QUEUE_NONE': 0x0003 # Disable the comparator and put ALERT/RDY
# in high state
}
ADS1X1_OPERATIONS = {
'SET_MUX': 0,
'READ_CONVERSION': 1
}
class ADS1X1X_chip:
def __init__(self, config):
self._printer = config.get_printer()
self._reactor = self._printer.get_reactor()
self.name = config.get_name().split()[-1]
self.chip = config.getchoice('chip', ADS1X1X_CHIP_TYPE)
address = ADS1X1X_CHIP_ADDR['GND']
# If none is specified, i2c_address can be used for a specific address
if config.get('address_pin', None) is not None:
address = config.getchoice('address_pin', ADS1X1X_CHIP_ADDR)
self._ppins = self._printer.lookup_object("pins")
self._ppins.register_chip(self.name, self)
self.pga = config.getchoice('pga', ADS1X1X_PGA, '4.096V')
self.adc_voltage = config.getfloat('adc_voltage', above=0., default=3.3)
# Comparators are not implemented, they would only be useful if the
# alert pin is used, which we haven't made configurable.
# But that wouldn't be useful for a normal temperature sensor anyway.
self.comp_mode = ADS1X1X_COMPARATOR_MODE['TRADITIONAL']
self.comp_polarity = ADS1X1X_COMPARATOR_POLARITY['ACTIVE_LO']
self.comp_latching = ADS1X1X_COMPARATOR_LATCHING['NON_LATCHING']
self.comp_queue = ADS1X1X_COMPARATOR_QUEUE['QUEUE_NONE']
self._i2c = bus.MCU_I2C_from_config(config, address)
self.mcu = self._i2c.get_mcu()
self._printer.add_object("ads1x1x " + self.name, self)
self._printer.register_event_handler("klippy:connect", \
self._handle_connect)
self._pins = {}
self._mutex = self._reactor.mutex()
def setup_pin(self, pin_type, pin_params):
pin = pin_params['pin']
if pin_type == 'adc':
if (pin not in ADS1X1X_MUX):
raise pins.error('ADS1x1x pin %s is not valid' % \
pin_params['pin'])
config = 0
config |= (ADS1X1X_OS['OS_SINGLE'] & \
ADS1X1X_REG_CONFIG['OS_MASK'])
config |= (ADS1X1X_MUX[pin_params['pin']] & \
ADS1X1X_REG_CONFIG['MULTIPLEXER_MASK'])
config |= (self.pga & ADS1X1X_REG_CONFIG['PGA_MASK'])
# Have to use single mode, because in continuous, it never reaches
# idle state, which we use to determine if the sampling is done.
config |= (ADS1X1X_MODE['single'] & \
ADS1X1X_REG_CONFIG['MODE_MASK'])
# lowest sample rate per default, until report time has been set in
# setup_adc_sample
config |= (self.comp_mode \
& ADS1X1X_REG_CONFIG['COMPARATOR_MODE_MASK'])
config |= (self.comp_polarity \
& ADS1X1X_REG_CONFIG['COMPARATOR_POLARITY_MASK'])
config |= (self.comp_latching \
& ADS1X1X_REG_CONFIG['COMPARATOR_LATCHING_MASK'])
config |= (self.comp_queue \
& ADS1X1X_REG_CONFIG['COMPARATOR_QUEUE_MASK'])
pin_obj = ADS1X1X_pin(self, config)
if pin in self._pins:
raise pins.error(
'pin %s for chip %s is used multiple times' \
% (pin, self.name))
self._pins[pin] = pin_obj
return pin_obj
raise pins.error('Wrong pin or incompatible type: %s with type %s! ' % (
pin, pin_type))
def _handle_connect(self):
try:
# Init all devices on bus for this kind of device
self._i2c.i2c_write([0x06, 0x00, 0x00])
except Exception:
logging.exception("ADS1X1X: error while resetting device")
def is_ready(self):
config = self._read_register(ADS1X1X_REG_POINTER['CONFIG'])
return bool((config & ADS1X1X_REG_CONFIG['OS_MASK']) == \
ADS1X1X_OS['OS_IDLE'])
def calculate_sample_rate(self):
pin_count = len(self._pins)
lowest_report_time = 1
for pin in self._pins.values():
lowest_report_time = min(lowest_report_time, pin.report_time)
sample_rate = 1 / lowest_report_time * pin_count
samples_per_second = ADS111X_SAMPLES_PER_SECOND
if isADS101X(self.chip):
samples_per_second = ADS101X_SAMPLES_PER_SECOND
# make sure the samples list is sorted correctly by number.
samples_per_second = sorted(samples_per_second.items(), \
key=lambda t: int(t[0]))
for rate, bits in samples_per_second:
rate_number = int(rate)
if sample_rate <= rate_number:
return (rate_number, bits)
logging.warning(
"ADS1X1X: requested sample rate %s is higher than supported by %s."\
% (sample_rate, self.name))
return (rate_number, bits)
def handle_report_time_update(self):
(sample_rate, sample_rate_bits) = self.calculate_sample_rate()
for pin in self._pins.values():
pin.config = (pin.config & ~ADS1X1X_REG_CONFIG['DATA_RATE_MASK']) \
| (sample_rate_bits & ADS1X1X_REG_CONFIG['DATA_RATE_MASK'])
self.delay = 1 / float(sample_rate)
def sample(self, pin):
with self._mutex:
try:
self._write_register(ADS1X1X_REG_POINTER['CONFIG'], pin.config)
self._reactor.pause(self._reactor.monotonic() + self.delay)
start_time = self._reactor.monotonic()
while not self.is_ready():
self._reactor.pause(self._reactor.monotonic() + 0.001)
# if we waited twice the expected time, mark this an error
if start_time + self.delay < self._reactor.monotonic():
logging.warning("ADS1X1X: timeout during sampling")
return None
return self._read_register(ADS1X1X_REG_POINTER['CONVERSION'])
except Exception as e:
logging.exception("ADS1X1X: error while sampling: %s" % str(e))
return None
def _read_register(self, reg):
# read a single register
params = self._i2c.i2c_read([reg], 2)
buff = bytearray(params['response'])
return (buff[0]<<8 | buff[1])
def _write_register(self, reg, data):
data = [
(reg & 0xFF), # Control register
((data>>8) & 0xFF), # High byte
(data & 0xFF), # Lo byte
]
self._i2c.i2c_write(data)
class ADS1X1X_pin:
def __init__(self, chip, config):
self.mcu = chip.mcu
self.chip = chip
self.config = config
self.invalid_count = 0
self.chip._printer.register_event_handler("klippy:connect", \
self._handle_connect)
def _handle_connect(self):
self._reactor = self.chip._printer.get_reactor()
self._sample_timer = \
self._reactor.register_timer(self._process_sample, \
self._reactor.NOW)
def _process_sample(self, eventtime):
sample = self.chip.sample(self)
if sample is not None:
# The sample is encoded in the top 12 or full 16 bits
# Value's meaning is defined by ADS1X1X_REG_CONFIG['PGA_MASK']
if isADS101X(self.chip.chip):
sample >>= 4
target_value = sample / ADS101X_RESOLUTION
else:
target_value = sample / ADS111X_RESOLUTION
# Thermistors expect a value between 0 and 1 to work. If we use a
# PGA with 4.096V but supply only 3.3V, the reference voltage for
# voltage divider is only 3.3V, not 4.096V. So we remap the range
# from what the PGA allows as range to end up between 0 and 1 for
# the thermistor logic to work as expected.
target_value = target_value * (ADS1X1X_PGA_VALUE[self.chip.pga] / \
self.chip.adc_voltage)
if target_value > self.maxval or target_value < self.minval:
self.invalid_count = self.invalid_count + 1
logging.warning("ADS1X1X: temperature outside range")
self.check_invalid()
else:
self.invalid_count = 0
# Publish result
measured_time = self._reactor.monotonic()
self.callback(self.chip.mcu.estimated_print_time(measured_time),
target_value)
else:
self.invalid_count = self.invalid_count + 1
self.check_invalid()
return eventtime + self.report_time
def check_invalid(self):
if self.invalid_count > self.range_check_count:
self.chip._printer.invoke_shutdown(
"ADS1X1X temperature check failed")
def get_mcu(self):
return self.mcu
def setup_adc_callback(self, report_time, callback):
self.report_time = report_time
self.callback = callback
self.chip.handle_report_time_update()
def setup_adc_sample(self, sample_time, sample_count,
minval=0., maxval=1., range_check_count=0):
self.minval = minval
self.maxval = maxval
self.range_check_count = range_check_count
def load_config_prefix(config):
return ADS1X1X_chip(config)

View file

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

View file

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

View file

@ -5,7 +5,7 @@
# This file may be distributed under the terms of the GNU GPLv3 license.
import math
from . import manual_probe as ManualProbe, bed_mesh as BedMesh
from . import manual_probe, bed_mesh, probe
DEFAULT_SAMPLE_COUNT = 3
@ -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 "

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,80 @@
# Report canbus connection status
#
# Copyright (C) 2025 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
class PrinterCANBusStats:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.name = config.get_name().split()[-1]
self.mcu = None
self.get_canbus_status_cmd = None
self.status = {'rx_error': None, 'tx_error': None, 'tx_retries': None,
'bus_state': None}
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
self.printer.register_event_handler("klippy:shutdown",
self.handle_shutdown)
def handle_shutdown(self):
status = self.status.copy()
if status['bus_state'] is not None:
# Clear bus_state on shutdown to note that the values may be stale
status['bus_state'] = 'unknown'
self.status = status
def handle_connect(self):
# Lookup mcu
mcu_name = self.name
if mcu_name != 'mcu':
mcu_name = 'mcu ' + mcu_name
self.mcu = self.printer.lookup_object(mcu_name)
# Lookup status query command
if self.mcu.try_lookup_command("get_canbus_status") is None:
return
self.get_canbus_status_cmd = self.mcu.lookup_query_command(
"get_canbus_status",
"canbus_status rx_error=%u tx_error=%u tx_retries=%u"
" canbus_bus_state=%u")
# Register usb_canbus_state message handling (for usb to canbus bridge)
self.mcu.register_response(self.handle_usb_canbus_state,
"usb_canbus_state")
# Register periodic query timer
self.reactor.register_timer(self.query_event, self.reactor.NOW)
def handle_usb_canbus_state(self, params):
discard = params['discard']
if discard:
logging.warning("USB CANBUS bridge '%s' is discarding!"
% (self.name,))
else:
logging.warning("USB CANBUS bridge '%s' is no longer discarding."
% (self.name,))
def query_event(self, eventtime):
prev_rx = self.status['rx_error']
prev_tx = self.status['tx_error']
prev_retries = self.status['tx_retries']
if prev_rx is None:
prev_rx = prev_tx = prev_retries = 0
params = self.get_canbus_status_cmd.send()
rx = prev_rx + ((params['rx_error'] - prev_rx) & 0xffffffff)
tx = prev_tx + ((params['tx_error'] - prev_tx) & 0xffffffff)
retries = prev_retries + ((params['tx_retries'] - prev_retries)
& 0xffffffff)
state = params['canbus_bus_state']
self.status = {'rx_error': rx, 'tx_error': tx, 'tx_retries': retries,
'bus_state': state}
return self.reactor.monotonic() + 1.
def stats(self, eventtime):
status = self.status
if status['rx_error'] is None:
return (False, '')
return (False, 'canstat_%s: bus_state=%s rx_error=%d'
' tx_error=%d tx_retries=%d'
% (self.name, status['bus_state'], status['rx_error'],
status['tx_error'], status['tx_retries']))
def get_status(self, eventtime):
return self.status
def load_config_prefix(config):
return PrinterCANBusStats(config)

View file

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

View 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)

View file

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

View file

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

View file

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

View file

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

View file

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

@ -0,0 +1,133 @@
# More verbose information on micro-controller errors
#
# Copyright (C) 2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
message_shutdown = """
Once the underlying issue is corrected, use the
"FIRMWARE_RESTART" command to reset the firmware, reload the
config, and restart the host software.
Printer is shutdown
"""
message_protocol_error1 = """
This is frequently caused by running an older version of the
firmware on the MCU(s). Fix by recompiling and flashing the
firmware.
"""
message_protocol_error2 = """
Once the underlying issue is corrected, use the "RESTART"
command to reload the config and restart the host software.
"""
message_mcu_connect_error = """
Once the underlying issue is corrected, use the
"FIRMWARE_RESTART" command to reset the firmware, reload the
config, and restart the host software.
Error configuring printer
"""
Common_MCU_errors = {
("Timer too close",): """
This often indicates the host computer is overloaded. Check
for other processes consuming excessive CPU time, high swap
usage, disk errors, overheating, unstable voltage, or
similar system problems on the host computer.""",
("Missed scheduling of next ",): """
This is generally indicative of an intermittent
communication failure between micro-controller and host.""",
("ADC out of range",): """
This generally occurs when a heater temperature exceeds
its configured min_temp or max_temp.""",
("Rescheduled timer in the past", "Stepper too far in past"): """
This generally occurs when the micro-controller has been
requested to step at a rate higher than it is capable of
obtaining.""",
("Command request",): """
This generally occurs in response to an M112 G-Code command
or in response to an internal error in the host software.""",
}
def error_hint(msg):
for prefixes, help_msg in Common_MCU_errors.items():
for prefix in prefixes:
if msg.startswith(prefix):
return help_msg
return ""
class PrinterMCUError:
def __init__(self, config):
self.printer = config.get_printer()
self.clarify_callbacks = {}
self.printer.register_event_handler("klippy:notify_mcu_shutdown",
self._handle_notify_mcu_shutdown)
self.printer.register_event_handler("klippy:notify_mcu_error",
self._handle_notify_mcu_error)
def add_clarify(self, msg, callback):
self.clarify_callbacks.setdefault(msg, []).append(callback)
def _check_mcu_shutdown(self, msg, details):
mcu_name = details['mcu']
mcu_msg = details['reason']
event_type = details['event_type']
prefix = "MCU '%s' shutdown: " % (mcu_name,)
if event_type == 'is_shutdown':
prefix = "Previous MCU '%s' shutdown: " % (mcu_name,)
# Lookup generic hint
hint = error_hint(mcu_msg)
# Add per instance help
clarify = [cb(msg, details)
for cb in self.clarify_callbacks.get(mcu_msg, [])]
clarify = [cm for cm in clarify if cm is not None]
clarify_msg = ""
if clarify:
clarify_msg = "\n".join(["", ""] + clarify + [""])
# Update error message
newmsg = "%s%s%s%s%s" % (prefix, mcu_msg, clarify_msg,
hint, message_shutdown)
self.printer.update_error_msg(msg, newmsg)
def _handle_notify_mcu_shutdown(self, msg, details):
if msg == "MCU shutdown":
self._check_mcu_shutdown(msg, details)
else:
self.printer.update_error_msg(msg, "%s%s" % (msg, message_shutdown))
def _check_protocol_error(self, msg, details):
host_version = self.printer.start_args['software_version']
msg_update = []
msg_updated = []
for mcu_name, mcu in self.printer.lookup_objects('mcu'):
try:
mcu_version = mcu.get_status()['mcu_version']
except:
logging.exception("Unable to retrieve mcu_version from mcu")
continue
if mcu_version != host_version:
msg_update.append("%s: Current version %s"
% (mcu_name.split()[-1], mcu_version))
else:
msg_updated.append("%s: Current version %s"
% (mcu_name.split()[-1], mcu_version))
if not msg_update:
msg_update.append("<none>")
if not msg_updated:
msg_updated.append("<none>")
newmsg = ["MCU Protocol error",
message_protocol_error1,
"Your Klipper version is: %s" % (host_version,),
"MCU(s) which should be updated:"]
newmsg += msg_update + ["Up-to-date MCU(s):"] + msg_updated
newmsg += [message_protocol_error2, details['error']]
self.printer.update_error_msg(msg, "\n".join(newmsg))
def _check_mcu_connect_error(self, msg, details):
newmsg = "%s%s" % (details['error'], message_mcu_connect_error)
self.printer.update_error_msg(msg, newmsg)
def _handle_notify_mcu_error(self, msg, details):
if msg == "Protocol error":
self._check_protocol_error(msg, details)
elif msg == "MCU error during connect":
self._check_mcu_connect_error(msg, details)
def load_config(config):
return PrinterMCUError(config)

View file

@ -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'],
}

View file

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

View file

@ -131,8 +131,12 @@ class ForceMove:
x = gcmd.get_float('X', curpos[0])
y = gcmd.get_float('Y', curpos[1])
z = gcmd.get_float('Z', curpos[2])
logging.info("SET_KINEMATIC_POSITION pos=%.3f,%.3f,%.3f", x, y, z)
toolhead.set_position([x, y, z, curpos[3]], homing_axes=(0, 1, 2))
clear = gcmd.get('CLEAR', '').lower()
clear_axes = "".join([a for a in "xyz" if a in clear])
logging.info("SET_KINEMATIC_POSITION pos=%.3f,%.3f,%.3f clear=%s",
x, y, z, clear_axes)
toolhead.set_position([x, y, z, curpos[3]], homing_axes="xyz")
toolhead.get_kinematics().clear_homing_state(clear_axes)
def load_config(config):
return ForceMove(config)

View file

@ -0,0 +1,31 @@
# Garbage collection optimizations
#
# Copyright (C) 2025 Branden Cash <ammmze@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import gc
import logging
class GarbageCollection:
def __init__(self, config):
self.printer = config.get_printer()
# feature check ... freeze/unfreeze is only available in python 3.7+
can_freeze = hasattr(gc, 'freeze') and hasattr(gc, 'unfreeze')
if can_freeze:
self.printer.register_event_handler("klippy:ready",
self._handle_ready)
self.printer.register_event_handler("klippy:disconnect",
self._handle_disconnect)
def _handle_ready(self):
logging.debug("Running full garbage collection and freezing")
for n in range(3):
gc.collect(n)
gc.freeze()
def _handle_disconnect(self):
logging.debug("Unfreezing garbage collection")
gc.unfreeze()
def load_config(config):
return GarbageCollection(config)

View file

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

View file

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

View file

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

View file

@ -14,6 +14,7 @@ KELVIN_TO_CELSIUS = -273.15
MAX_HEAT_TIME = 5.0
AMBIENT_TEMP = 25.
PID_PARAM_BASE = 255.
MAX_MAINTHREAD_TIME = 5.0
class Heater:
def __init__(self, config, sensor):
@ -37,7 +38,7 @@ class Heater:
self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.)
self.smooth_time = config.getfloat('smooth_time', 1., above=0.)
self.inv_smooth_time = 1. / self.smooth_time
self.is_shutdown = False
self.verify_mainthread_time = -999.
self.lock = threading.Lock()
self.last_temp = self.smoothed_temp = self.target_temp = 0.
self.last_temp_time = 0.
@ -66,7 +67,7 @@ class Heater:
self.printer.register_event_handler("klippy:shutdown",
self._handle_shutdown)
def set_pwm(self, read_time, value):
if self.target_temp <= 0. or self.is_shutdown:
if self.target_temp <= 0. or read_time > self.verify_mainthread_time:
value = 0.
if ((read_time < self.next_pwm_time or not self.last_pwm_value)
and abs(value - self.last_pwm_value) < 0.05):
@ -91,7 +92,7 @@ class Heater:
self.can_extrude = (self.smoothed_temp >= self.min_extrude_temp)
#logging.debug("temp: %.3f %f = %f", read_time, temp)
def _handle_shutdown(self):
self.is_shutdown = True
self.verify_mainthread_time = -999.
# External commands
def get_name(self):
return self.name
@ -129,6 +130,9 @@ class Heater:
target_temp = max(self.min_temp, min(self.max_temp, target_temp))
self.target_temp = target_temp
def stats(self, eventtime):
est_print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime)
if not self.printer.is_shutdown():
self.verify_mainthread_time = est_print_time + MAX_MAINTHREAD_TIME
with self.lock:
target_temp = self.target_temp
last_temp = self.last_temp
@ -259,7 +263,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):

View file

@ -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)
@ -174,7 +187,8 @@ class Homing:
# Notify of upcoming homing operation
self.printer.send_event("homing:home_rails_begin", self, rails)
# Alter kinematics class to think printer is at forcepos
homing_axes = [axis for axis in range(3) if forcepos[axis] is not None]
force_axes = [axis for axis in range(3) if forcepos[axis] is not None]
homing_axes = "".join(["xyz"[i] for i in force_axes])
startpos = self._fill_coord(forcepos)
homepos = self._fill_coord(movepos)
self.toolhead.set_position(startpos, homing_axes=homing_axes)
@ -218,7 +232,7 @@ class Homing:
+ self.adjust_pos.get(s.get_name(), 0.))
for s in kin.get_steppers()}
newpos = kin.calc_position(kin_spos)
for axis in homing_axes:
for axis in force_axes:
homepos[axis] = newpos[axis]
self.toolhead.set_position(homepos)

View file

@ -46,11 +46,11 @@ class HomingOverride:
# Calculate forced position (if configured)
toolhead = self.printer.lookup_object('toolhead')
pos = toolhead.get_position()
homing_axes = []
homing_axes = ""
for axis, loc in enumerate(self.start_pos):
if loc is not None:
pos[axis] = loc
homing_axes.append(axis)
homing_axes += "xyz"[axis]
toolhead.set_position(pos, homing_axes=homing_axes)
# Perform homing
context = self.template.create_template_context()

168
klippy/extras/hx71x.py Normal file
View 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
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,30 @@
# Load Cell Implementation
#
# Copyright (C) 2024 Gareth Farrington <gareth@waves.ky>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import hx71x
from . import ads1220
# Printer class that controls a load cell
class LoadCell:
def __init__(self, config, sensor):
self.printer = printer = config.get_printer()
self.sensor = sensor # must implement BulkAdcSensor
def _on_sample(self, msg):
return True
def get_sensor(self):
return self.sensor
def load_config(config):
# Sensor types
sensors = {}
sensors.update(hx71x.HX71X_SENSOR_TYPES)
sensors.update(ads1220.ADS1220_SENSOR_TYPE)
sensor_class = config.getchoice('sensor_type', sensors)
return LoadCell(config, sensor_class(config))
def load_config_prefix(config):
return load_config(config)

View file

@ -109,7 +109,7 @@ class ManualStepper:
self.sync_print_time()
def get_position(self):
return [self.rail.get_commanded_position(), 0., 0., 0.]
def set_position(self, newpos, homing_axes=()):
def set_position(self, newpos, homing_axes=""):
self.do_set_position(newpos[0])
def get_last_move_time(self):
self.sync_print_time()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -37,11 +37,10 @@ class SafeZHoming:
if 'z' not in kin_status['homed_axes']:
# Always perform the z_hop if the Z axis is not homed
pos[2] = 0
toolhead.set_position(pos, homing_axes=[2])
toolhead.set_position(pos, homing_axes="z")
toolhead.manual_move([None, None, self.z_hop],
self.z_hop_speed)
if hasattr(toolhead.get_kinematics(), "note_z_not_homed"):
toolhead.get_kinematics().note_z_not_homed()
toolhead.get_kinematics().clear_homing_state("z")
elif pos[2] < self.z_hop:
# If the Z axis is homed, and below z_hop, lift it to z_hop
toolhead.manual_move([None, None, self.z_hop],

View file

@ -36,6 +36,8 @@ class SaveVariables:
cmd_SAVE_VARIABLE_help = "Save arbitrary variables to disk"
def cmd_SAVE_VARIABLE(self, gcmd):
varname = gcmd.get('VARIABLE')
if (varname.lower() != varname):
raise gcmd.error("VARIABLE must not contain upper case")
value = gcmd.get('VALUE')
try:
value = ast.literal_eval(value)

View file

@ -12,7 +12,7 @@ class ScrewsTiltAdjust:
self.config = config
self.printer = config.get_printer()
self.screws = []
self.results = []
self.results = {}
self.max_diff = None
self.max_diff_error = False
# Read config

View file

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

View file

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

View file

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

View file

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

View file

@ -94,6 +94,7 @@ class PrinterStepperEnable:
print_time = toolhead.get_last_move_time()
for el in self.enable_lines.values():
el.motor_disable(print_time)
toolhead.get_kinematics().clear_homing_state("xyz")
self.printer.send_event("stepper_enable:motor_off", print_time)
toolhead.dwell(DISABLE_STALL_TIME)
def motor_debug_enable(self, stepper, enable):

View file

@ -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" % (

View file

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

View file

@ -1,10 +1,11 @@
# Support for micro-controller chip based temperature sensors
#
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2020-2024 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import mcu
from . import adc_temperature
SAMPLE_TIME = 0.001
SAMPLE_COUNT = 8
@ -31,30 +32,33 @@ class PrinterTemperatureMCU:
self.mcu_adc = ppins.setup_pin('adc',
'%s:ADC_TEMPERATURE' % (mcu_name,))
self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback)
query_adc = config.get_printer().load_object(config, 'query_adc')
query_adc.register_adc(config.get_name(), self.mcu_adc)
self.diag_helper = adc_temperature.HelperTemperatureDiagnostics(
config, self.mcu_adc, self.calc_temp)
# Register callbacks
if self.printer.get_start_args().get('debugoutput') is not None:
self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT,
range_check_count=RANGE_CHECK_COUNT)
self.mcu_adc.setup_adc_sample(SAMPLE_TIME, SAMPLE_COUNT)
return
self.printer.register_event_handler("klippy:mcu_identify",
self._mcu_identify)
self.handle_mcu_identify)
# Temperature interface
def setup_callback(self, temperature_callback):
self.temperature_callback = temperature_callback
def get_report_time_delta(self):
return REPORT_TIME
def adc_callback(self, read_time, read_value):
temp = self.base_temperature + read_value * self.slope
self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp)
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
# Internal code
def adc_callback(self, read_time, read_value):
temp = self.base_temperature + read_value * self.slope
self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp)
def calc_temp(self, adc):
return self.base_temperature + adc * self.slope
def calc_adc(self, temp):
return (temp - self.base_temperature) / self.slope
def calc_base(self, temp, adc):
return temp - adc * self.slope
def _mcu_identify(self):
def handle_mcu_identify(self):
# Obtain mcu information
mcu = self.mcu_adc.get_mcu()
self.debug_read_cmd = mcu.lookup_query_command(
@ -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,))

View file

@ -0,0 +1,721 @@
# Probe temperature sensor and drift calibration
#
# Copyright (C) 2024 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import manual_probe
KELVIN_TO_CELSIUS = -273.15
######################################################################
# Polynomial Helper Classes and Functions
######################################################################
def calc_determinant(matrix):
m = matrix
aei = m[0][0] * m[1][1] * m[2][2]
bfg = m[1][0] * m[2][1] * m[0][2]
cdh = m[2][0] * m[0][1] * m[1][2]
ceg = m[2][0] * m[1][1] * m[0][2]
bdi = m[1][0] * m[0][1] * m[2][2]
afh = m[0][0] * m[2][1] * m[1][2]
return aei + bfg + cdh - ceg - bdi - afh
class Polynomial2d:
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def __call__(self, xval):
return self.c * xval * xval + self.b * xval + self.a
def get_coefs(self):
return (self.a, self.b, self.c)
def __str__(self):
return "%f, %f, %f" % (self.a, self.b, self.c)
def __repr__(self):
parts = ["y(x) ="]
deg = 2
for i, coef in enumerate((self.c, self.b, self.a)):
if round(coef, 8) == int(coef):
coef = int(coef)
if abs(coef) < 1e-10:
continue
cur_deg = deg - i
x_str = "x^%d" % (cur_deg,) if cur_deg > 1 else "x" * cur_deg
if len(parts) == 1:
parts.append("%f%s" % (coef, x_str))
else:
sym = "-" if coef < 0 else "+"
parts.append("%s %f%s" % (sym, abs(coef), x_str))
return " ".join(parts)
@classmethod
def fit(cls, coords):
xlist = [c[0] for c in coords]
ylist = [c[1] for c in coords]
count = len(coords)
sum_x = sum(xlist)
sum_y = sum(ylist)
sum_x2 = sum([x**2 for x in xlist])
sum_x3 = sum([x**3 for x in xlist])
sum_x4 = sum([x**4 for x in xlist])
sum_xy = sum([x * y for x, y in coords])
sum_x2y = sum([y*x**2 for x, y in coords])
vector_b = [sum_y, sum_xy, sum_x2y]
m = [
[count, sum_x, sum_x2],
[sum_x, sum_x2, sum_x3],
[sum_x2, sum_x3, sum_x4]
]
m0 = [vector_b, m[1], m[2]]
m1 = [m[0], vector_b, m[2]]
m2 = [m[0], m[1], vector_b]
det_m = calc_determinant(m)
a0 = calc_determinant(m0) / det_m
a1 = calc_determinant(m1) / det_m
a2 = calc_determinant(m2) / det_m
return cls(a0, a1, a2)
class TemperatureProbe:
def __init__(self, config):
self.name = config.get_name()
self.printer = config.get_printer()
self.gcode = self.printer.lookup_object("gcode")
self.speed = config.getfloat("speed", None, above=0.)
self.horizontal_move_z = config.getfloat(
"horizontal_move_z", 2., above=0.
)
self.resting_z = config.getfloat("resting_z", .4, above=0.)
self.cal_pos = config.getfloatlist(
"calibration_position", None, count=3
)
self.cal_bed_temp = config.getfloat(
"calibration_bed_temp", None, above=50.
)
self.cal_extruder_temp = config.getfloat(
"calibration_extruder_temp", None, above=50.
)
self.cal_extruder_z = config.getfloat(
"extruder_heating_z", 50., above=0.
)
# Setup temperature sensor
smooth_time = config.getfloat("smooth_time", 2., above=0.)
self.inv_smooth_time = 1. / smooth_time
self.min_temp = config.getfloat(
"min_temp", KELVIN_TO_CELSIUS, minval=KELVIN_TO_CELSIUS
)
self.max_temp = config.getfloat(
"max_temp", 99999999.9, above=self.min_temp
)
pheaters = self.printer.load_object(config, "heaters")
self.sensor = pheaters.setup_sensor(config)
self.sensor.setup_minmax(self.min_temp, self.max_temp)
self.sensor.setup_callback(self._temp_callback)
pheaters.register_sensor(config, self)
self.last_temp_read_time = 0.
self.last_measurement = (0., 99999999., 0.,)
# Calibration State
self.cal_helper = None
self.next_auto_temp = 99999999.
self.target_temp = 0
self.expected_count = 0
self.sample_count = 0
self.in_calibration = False
self.step = 2.
self.last_zero_pos = None
self.total_expansion = 0
self.start_pos = []
# Register GCode Commands
pname = self.name.split(maxsplit=1)[-1]
self.gcode.register_mux_command(
"TEMPERATURE_PROBE_CALIBRATE", "PROBE", pname,
self.cmd_TEMPERATURE_PROBE_CALIBRATE,
desc=self.cmd_TEMPERATURE_PROBE_CALIBRATE_help
)
self.gcode.register_mux_command(
"TEMPERATURE_PROBE_ENABLE", "PROBE", pname,
self.cmd_TEMPERATURE_PROBE_ENABLE,
desc=self.cmd_TEMPERATURE_PROBE_ENABLE_help
)
# Register Drift Compensation Helper with probe
full_probe_name = "probe_eddy_current %s" % (pname,)
if config.has_section(full_probe_name):
pprobe = self.printer.load_object(config, full_probe_name)
self.cal_helper = EddyDriftCompensation(config, self)
pprobe.register_drift_compensation(self.cal_helper)
logging.info(
"%s: registered drift compensation with probe [%s]"
% (self.name, full_probe_name)
)
else:
logging.info(
"%s: No probe named %s configured, thermal drift compensation "
"disabled." % (self.name, pname)
)
def _temp_callback(self, read_time, temp):
smoothed_temp, measured_min, measured_max = self.last_measurement
time_diff = read_time - self.last_temp_read_time
self.last_temp_read_time = read_time
temp_diff = temp - smoothed_temp
adj_time = min(time_diff * self.inv_smooth_time, 1.)
smoothed_temp += temp_diff * adj_time
measured_min = min(measured_min, smoothed_temp)
measured_max = max(measured_max, smoothed_temp)
self.last_measurement = (smoothed_temp, measured_min, measured_max)
if self.in_calibration and smoothed_temp >= self.next_auto_temp:
self.printer.get_reactor().register_async_callback(
self._check_kick_next
)
def _check_kick_next(self, eventtime):
smoothed_temp = self.last_measurement[0]
if self.in_calibration and smoothed_temp >= self.next_auto_temp:
self.next_auto_temp = 99999999.
self.gcode.run_script("TEMPERATURE_PROBE_NEXT")
def get_temp(self, eventtime=None):
return self.last_measurement[0], self.target_temp
def _collect_sample(self, kin_pos, tool_zero_z):
probe = self._get_probe()
x_offset, y_offset, _ = probe.get_offsets()
speeds = self._get_speeds()
lift_speed, _, move_speed = speeds
toolhead = self.printer.lookup_object("toolhead")
cur_pos = toolhead.get_position()
# Move to probe to sample collection position
cur_pos[2] += self.horizontal_move_z
toolhead.manual_move(cur_pos, lift_speed)
cur_pos[0] -= x_offset
cur_pos[1] -= y_offset
toolhead.manual_move(cur_pos, move_speed)
return self.cal_helper.collect_sample(kin_pos, tool_zero_z, speeds)
def _prepare_next_sample(self, last_temp, tool_zero_z):
# Register our own abort command now that the manual
# probe has finished and unregistered
self.gcode.register_command(
"ABORT", self.cmd_TEMPERATURE_PROBE_ABORT,
desc=self.cmd_TEMPERATURE_PROBE_ABORT_help
)
probe_speed = self._get_speeds()[1]
# Move tool down to the resting position
toolhead = self.printer.lookup_object("toolhead")
cur_pos = toolhead.get_position()
cur_pos[2] = tool_zero_z + self.resting_z
toolhead.manual_move(cur_pos, probe_speed)
cnt, exp_cnt = self.sample_count, self.expected_count
self.next_auto_temp = last_temp + self.step
self.gcode.respond_info(
"%s: collected sample %d/%d at temp %.2fC, next sample scheduled "
"at temp %.2fC"
% (self.name, cnt, exp_cnt, last_temp, self.next_auto_temp)
)
def _manual_probe_finalize(self, kin_pos):
if kin_pos is None:
# Calibration aborted
self._finalize_drift_cal(False)
return
if self.last_zero_pos is not None:
z_diff = self.last_zero_pos[2] - kin_pos[2]
self.total_expansion += z_diff
logging.info(
"Estimated Total Thermal Expansion: %.6f"
% (self.total_expansion,)
)
self.last_zero_pos = kin_pos
toolhead = self.printer.lookup_object("toolhead")
tool_zero_z = toolhead.get_position()[2]
try:
last_temp = self._collect_sample(kin_pos, tool_zero_z)
except Exception:
self._finalize_drift_cal(False)
raise
self.sample_count += 1
if last_temp >= self.target_temp:
# Calibration Done
self._finalize_drift_cal(True)
else:
try:
self._prepare_next_sample(last_temp, tool_zero_z)
if self.sample_count == 1:
self._set_bed_temp(self.cal_bed_temp)
except Exception:
self._finalize_drift_cal(False)
raise
def _finalize_drift_cal(self, success, msg=None):
self.next_auto_temp = 99999999.
self.target_temp = 0
self.expected_count = 0
self.sample_count = 0
self.step = 2.
self.in_calibration = False
self.last_zero_pos = None
self.total_expansion = 0
self.start_pos = []
# Unregister Temporary Commands
self.gcode.register_command("ABORT", None)
self.gcode.register_command("TEMPERATURE_PROBE_NEXT", None)
self.gcode.register_command("TEMPERATURE_PROBE_COMPLETE", None)
# Turn off heaters
self._set_extruder_temp(0)
self._set_bed_temp(0)
try:
self.cal_helper.finish_calibration(success)
except self.gcode.error as e:
success = False
msg = str(e)
if not success:
msg = msg or "%s: calibration aborted" % (self.name,)
self.gcode.respond_info(msg)
def _get_probe(self):
probe = self.printer.lookup_object("probe")
if probe is None:
raise self.gcode.error("No probe configured")
return probe
def _set_extruder_temp(self, temp, wait=False):
if self.cal_extruder_temp is None:
# Extruder temperature not configured
return
toolhead = self.printer.lookup_object("toolhead")
extr_name = toolhead.get_extruder().get_name()
self.gcode.run_script_from_command(
"SET_HEATER_TEMPERATURE HEATER=%s TARGET=%f"
% (extr_name, temp)
)
if wait:
self.gcode.run_script_from_command(
"TEMPERATURE_WAIT SENSOR=%s MINIMUM=%f"
% (extr_name, temp)
)
def _set_bed_temp(self, temp):
if self.cal_bed_temp is None:
# Bed temperature not configured
return
self.gcode.run_script_from_command(
"SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=%f"
% (temp,)
)
def _check_homed(self):
toolhead = self.printer.lookup_object("toolhead")
reactor = self.printer.get_reactor()
status = toolhead.get_status(reactor.monotonic())
h_axes = status["homed_axes"]
for axis in "xyz":
if axis not in h_axes:
raise self.gcode.error(
"Printer must be homed before calibration"
)
def _move_to_start(self):
toolhead = self.printer.lookup_object("toolhead")
cur_pos = toolhead.get_position()
move_speed = self._get_speeds()[2]
if self.cal_pos is not None:
if self.cal_extruder_temp is not None:
# Move to extruder heating z position
cur_pos[2] = self.cal_extruder_z
toolhead.manual_move(cur_pos, move_speed)
toolhead.manual_move(self.cal_pos[:2], move_speed)
self._set_extruder_temp(self.cal_extruder_temp, True)
toolhead.manual_move(self.cal_pos, move_speed)
elif self.cal_extruder_temp is not None:
cur_pos[2] = self.cal_extruder_z
toolhead.manual_move(cur_pos, move_speed)
self._set_extruder_temp(self.cal_extruder_temp, True)
def _get_speeds(self):
pparams = self._get_probe().get_probe_params()
probe_speed = pparams["probe_speed"]
lift_speed = pparams["lift_speed"]
move_speed = self.speed or max(probe_speed, lift_speed)
return lift_speed, probe_speed, move_speed
cmd_TEMPERATURE_PROBE_CALIBRATE_help = (
"Calibrate probe temperature drift compensation"
)
def cmd_TEMPERATURE_PROBE_CALIBRATE(self, gcmd):
if self.cal_helper is None:
raise gcmd.error(
"No calibration helper registered for [%s]"
% (self.name,)
)
self._check_homed()
probe = self._get_probe()
probe_name = probe.get_status(None)["name"]
short_name = probe_name.split(maxsplit=1)[-1]
if short_name != self.name.split(maxsplit=1)[-1]:
raise self.gcode.error(
"[%s] not linked to registered probe [%s]."
% (self.name, probe_name)
)
manual_probe.verify_no_manual_probe(self.printer)
if self.in_calibration:
raise gcmd.error(
"Already in probe drift calibration. Use "
"TEMPERATURE_PROBE_COMPLETE or ABORT to exit."
)
cur_temp = self.last_measurement[0]
target_temp = gcmd.get_float("TARGET", above=cur_temp)
step = gcmd.get_float("STEP", 2., minval=1.0)
expected_count = int(
(target_temp - cur_temp) / step + .5
)
if expected_count < 3:
raise gcmd.error(
"Invalid STEP and/or TARGET parameters resulted "
"in too few expected samples: %d"
% (expected_count,)
)
try:
self.gcode.register_command(
"TEMPERATURE_PROBE_NEXT", self.cmd_TEMPERATURE_PROBE_NEXT,
desc=self.cmd_TEMPERATURE_PROBE_NEXT_help
)
self.gcode.register_command(
"TEMPERATURE_PROBE_COMPLETE",
self.cmd_TEMPERATURE_PROBE_COMPLETE,
desc=self.cmd_TEMPERATURE_PROBE_NEXT_help
)
except self.printer.config_error:
raise gcmd.error(
"Auxiliary Probe Drift Commands already registered. Use "
"TEMPERATURE_PROBE_COMPLETE or ABORT to exit."
)
self.in_calibration = True
self.cal_helper.start_calibration()
self.target_temp = target_temp
self.step = step
self.sample_count = 0
self.expected_count = expected_count
# If configured move to heating position and turn on extruder
try:
self._move_to_start()
except self.printer.command_error:
self._finalize_drift_cal(False, "Error during initial move")
raise
# Caputure start position and begin initial probe
toolhead = self.printer.lookup_object("toolhead")
self.start_pos = toolhead.get_position()[:2]
manual_probe.ManualProbeHelper(
self.printer, gcmd, self._manual_probe_finalize
)
cmd_TEMPERATURE_PROBE_NEXT_help = "Sample next probe drift temperature"
def cmd_TEMPERATURE_PROBE_NEXT(self, gcmd):
manual_probe.verify_no_manual_probe(self.printer)
self.next_auto_temp = 99999999.
toolhead = self.printer.lookup_object("toolhead")
# Lift and Move to nozzle back to start position
curpos = toolhead.get_position()
start_z = curpos[2]
lift_speed, probe_speed, move_speed = self._get_speeds()
# Move nozzle to the manual probing position
curpos[2] += self.horizontal_move_z
toolhead.manual_move(curpos, lift_speed)
curpos[0] = self.start_pos[0]
curpos[1] = self.start_pos[1]
toolhead.manual_move(curpos, move_speed)
curpos[2] = start_z
toolhead.manual_move(curpos, probe_speed)
self.gcode.register_command("ABORT", None)
manual_probe.ManualProbeHelper(
self.printer, gcmd, self._manual_probe_finalize
)
cmd_TEMPERATURE_PROBE_COMPLETE_help = "Finish Probe Drift Calibration"
def cmd_TEMPERATURE_PROBE_COMPLETE(self, gcmd):
manual_probe.verify_no_manual_probe(self.printer)
self._finalize_drift_cal(self.sample_count >= 3)
cmd_TEMPERATURE_PROBE_ABORT_help = "Abort Probe Drift Calibration"
def cmd_TEMPERATURE_PROBE_ABORT(self, gcmd):
self._finalize_drift_cal(False)
cmd_TEMPERATURE_PROBE_ENABLE_help = (
"Set adjustment factor applied to drift correction"
)
def cmd_TEMPERATURE_PROBE_ENABLE(self, gcmd):
if self.cal_helper is not None:
self.cal_helper.set_enabled(gcmd)
def is_in_calibration(self):
return self.in_calibration
def get_status(self, eventtime=None):
smoothed_temp, measured_min, measured_max = self.last_measurement
dcomp_enabled = False
if self.cal_helper is not None:
dcomp_enabled = self.cal_helper.is_enabled()
return {
"temperature": smoothed_temp,
"measured_min_temp": round(measured_min, 2),
"measured_max_temp": round(measured_max, 2),
"in_calibration": self.in_calibration,
"estimated_expansion": self.total_expansion,
"compensation_enabled": dcomp_enabled
}
def stats(self, eventtime):
return False, '%s: temp=%.1f' % (self.name, self.last_measurement[0])
#####################################################################
#
# Eddy Current Probe Drift Compensation Helper
#
#####################################################################
DRIFT_SAMPLE_COUNT = 9
class EddyDriftCompensation:
def __init__(self, config, sensor):
self.printer = config.get_printer()
self.temp_sensor = sensor
self.name = config.get_name()
self.cal_temp = config.getfloat("calibration_temp", 0.)
self.drift_calibration = None
self.calibration_samples = None
self.max_valid_temp = config.getfloat("max_validation_temp", 60.)
self.dc_min_temp = config.getfloat("drift_calibration_min_temp", 0.)
dc = config.getlists(
"drift_calibration", None, seps=(',', '\n'), parser=float
)
self.min_freq = 999999999999.
if dc is not None:
for coefs in dc:
if len(coefs) != 3:
raise config.error(
"Invalid polynomial in drift calibration"
)
self.drift_calibration = [Polynomial2d(*coefs) for coefs in dc]
cal = self.drift_calibration
start_temp, end_temp = self.dc_min_temp, self.max_valid_temp
self._check_calibration(cal, start_temp, end_temp, config.error)
low_poly = self.drift_calibration[-1]
self.min_freq = min([low_poly(temp) for temp in range(121)])
cal_str = "\n".join([repr(p) for p in cal])
logging.info(
"%s: loaded temperature drift calibration. Min Temp: %.2f,"
" Min Freq: %.6f\n%s"
% (self.name, self.dc_min_temp, self.min_freq, cal_str)
)
else:
logging.info(
"%s: No drift calibration configured, disabling temperature "
"drift compensation"
% (self.name,)
)
self.enabled = has_dc = self.drift_calibration is not None
if self.cal_temp < 1e-6 and has_dc:
self.enabled = False
logging.info(
"%s: No temperature saved for eddy probe calibration, "
"disabling temperature drift compensation."
% (self.name,)
)
def is_enabled(self):
return self.enabled
def set_enabled(self, gcmd):
enabled = gcmd.get_int("ENABLE")
if enabled:
if self.drift_calibration is None:
raise gcmd.error(
"No drift calibration configured, cannot enable "
"temperature drift compensation"
)
if self.cal_temp < 1e-6:
raise gcmd.error(
"Z Calibration temperature not configured, cannot enable "
"temperature drift compensation"
)
self.enabled = enabled
def note_z_calibration_start(self):
self.cal_temp = self.get_temperature()
def note_z_calibration_finish(self):
self.cal_temp = (self.cal_temp + self.get_temperature()) / 2.0
configfile = self.printer.lookup_object('configfile')
configfile.set(self.name, "calibration_temp", "%.6f " % (self.cal_temp))
gcode = self.printer.lookup_object("gcode")
gcode.respond_info(
"%s: Z Calibration Temperature set to %.2f. "
"The SAVE_CONFIG command will update the printer config "
"file and restart the printer."
% (self.name, self.cal_temp)
)
def collect_sample(self, kin_pos, tool_zero_z, speeds):
if self.calibration_samples is None:
self.calibration_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)]
move_times = []
temps = [0. for _ in range(DRIFT_SAMPLE_COUNT)]
probe_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)]
toolhead = self.printer.lookup_object("toolhead")
cur_pos = toolhead.get_position()
lift_speed, probe_speed, _ = speeds
def _on_bulk_data_recd(msg):
if move_times:
idx, start_time, end_time = move_times[0]
cur_temp = self.get_temperature()
for sample in msg["data"]:
ptime = sample[0]
while ptime > end_time:
move_times.pop(0)
if not move_times:
return idx >= DRIFT_SAMPLE_COUNT - 1
idx, start_time, end_time = move_times[0]
if ptime < start_time:
continue
temps[idx] = cur_temp
probe_samples[idx].append(sample)
return True
sect_name = "probe_eddy_current " + self.name.split(maxsplit=1)[-1]
self.printer.lookup_object(sect_name).add_client(_on_bulk_data_recd)
for i in range(DRIFT_SAMPLE_COUNT):
if i == 0:
# Move down to first sample location
cur_pos[2] = tool_zero_z + .05
else:
# Sample each .5mm in z
cur_pos[2] += 1.
toolhead.manual_move(cur_pos, lift_speed)
cur_pos[2] -= .5
toolhead.manual_move(cur_pos, probe_speed)
start = toolhead.get_last_move_time() + .05
end = start + .1
move_times.append((i, start, end))
toolhead.dwell(.2)
toolhead.wait_moves()
# Wait for sample collection to finish
reactor = self.printer.get_reactor()
evttime = reactor.monotonic()
while move_times:
evttime = reactor.pause(evttime + .1)
sample_temp = sum(temps) / len(temps)
for i, data in enumerate(probe_samples):
freqs = [d[1] for d in data]
zvals = [d[2] for d in data]
avg_freq = sum(freqs) / len(freqs)
avg_z = sum(zvals) / len(zvals)
kin_z = i * .5 + .05 + kin_pos[2]
logging.info(
"Probe Values at Temp %.2fC, Z %.4fmm: Avg Freq = %.6f, "
"Avg Measured Z = %.6f"
% (sample_temp, kin_z, avg_freq, avg_z)
)
self.calibration_samples[i].append((sample_temp, avg_freq))
return sample_temp
def start_calibration(self):
self.enabled = False
self.calibration_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)]
def finish_calibration(self, success):
cal_samples = self.calibration_samples
self.calibration_samples = None
if not success:
return
gcode = self.printer.lookup_object("gcode")
if len(cal_samples) < 3:
raise gcode.error(
"calbration error, not enough samples"
)
min_temp, _ = cal_samples[0][0]
max_temp, _ = cal_samples[-1][0]
polynomials = []
for i, coords in enumerate(cal_samples):
height = .05 + i * .5
poly = Polynomial2d.fit(coords)
polynomials.append(poly)
logging.info("Polynomial at Z=%.2f: %s" % (height, repr(poly)))
end_vld_temp = max(self.max_valid_temp, max_temp)
self._check_calibration(polynomials, min_temp, end_vld_temp)
coef_cfg = "\n" + "\n".join([str(p) for p in polynomials])
configfile = self.printer.lookup_object('configfile')
configfile.set(self.name, "drift_calibration", coef_cfg)
configfile.set(self.name, "drift_calibration_min_temp", min_temp)
gcode.respond_info(
"%s: generated %d 2D polynomials\n"
"The SAVE_CONFIG command will update the printer config "
"file and restart the printer."
% (self.name, len(polynomials))
)
def _check_calibration(self, calibration, start_temp, end_temp, error=None):
error = error or self.printer.command_error
start = int(start_temp)
end = int(end_temp) + 1
for temp in range(start, end, 1):
last_freq = calibration[0](temp)
for i, poly in enumerate(calibration[1:]):
next_freq = poly(temp)
if next_freq >= last_freq:
# invalid polynomial
raise error(
"%s: invalid calibration detected, curve at index "
"%d overlaps previous curve at temp %dC."
% (self.name, i + 1, temp)
)
last_freq = next_freq
def adjust_freq(self, freq, origin_temp=None):
# Adjusts frequency from current temperature toward
# destination temperature
if not self.enabled or freq < self.min_freq:
return freq
if origin_temp is None:
origin_temp = self.get_temperature()
return self._calc_freq(freq, origin_temp, self.cal_temp)
def unadjust_freq(self, freq, dest_temp=None):
# Given a frequency and its orignal sampled temp, find the
# offset frequency based on the current temp
if not self.enabled or freq < self.min_freq:
return freq
if dest_temp is None:
dest_temp = self.get_temperature()
return self._calc_freq(freq, self.cal_temp, dest_temp)
def _calc_freq(self, freq, origin_temp, dest_temp):
high_freq = low_freq = None
dc = self.drift_calibration
for pos, poly in enumerate(dc):
high_freq = low_freq
low_freq = poly(origin_temp)
if freq >= low_freq:
if high_freq is None:
# Freqency above max calibration value
err = poly(dest_temp) - low_freq
return freq + err
t = min(1., max(0., (freq - low_freq) / (high_freq - low_freq)))
low_tgt_freq = poly(dest_temp)
high_tgt_freq = dc[pos-1](dest_temp)
return (1 - t) * low_tgt_freq + t * high_tgt_freq
# Frequency below minimum, no correction
return freq
def get_temperature(self):
return self.temp_sensor.get_temp()[0]
def load_config_prefix(config):
return TemperatureProbe(config)

View file

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

View file

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

View file

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

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