Merge branch 'master' into feature/adxl355-upstream

This commit is contained in:
entemomoh2 2026-03-03 16:15:38 +01:00 committed by GitHub
commit f9e8191e9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
292 changed files with 10589 additions and 5405 deletions

View file

@ -26,7 +26,7 @@ jobs:
- name: Install dependencies
run: pip install -r docs/_klipper3d/mkdocs-requirements.txt
- name: Build MkDocs Pages
run: docs/_klipper3d/build-translations.sh
run: docs/_klipper3d/build-website.sh
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4.4.3
with:

View file

@ -7,36 +7,39 @@
# See docs/Config_Reference.md for a description of parameters.
[carriage x]
[carriage carriage_x]
axis: x
position_endstop: 0
position_max: 300
homing_speed: 50
endstop_pin: ^PE5
[carriage y]
[carriage carriage_y]
axis: y
position_endstop: 0
position_max: 200
homing_speed: 50
endstop_pin: ^PJ1
[extra_carriage y1]
primary_carriage: y
[extra_carriage carriage_y1]
primary_carriage: carriage_y
endstop_pin: ^PB6
[carriage z]
[carriage carriage_z]
axis: z
position_endstop: 0.5
position_max: 100
endstop_pin: ^PD3
[dual_carriage u]
primary_carriage: x
[dual_carriage carriage_u]
primary_carriage: carriage_x
position_endstop: 300
position_max: 300
homing_speed: 50
endstop_pin: ^PE4
[stepper my_stepper_x]
carriages: x+y
carriages: carriage_x+carriage_y
step_pin: PF0
dir_pin: PF1
enable_pin: !PD7
@ -44,7 +47,7 @@ microsteps: 16
rotation_distance: 40
[stepper my_stepper_u]
carriages: u-y1
carriages: carriage_u-carriage_y1
step_pin: PH1
dir_pin: PH0
enable_pin: !PA1
@ -52,7 +55,7 @@ microsteps: 16
rotation_distance: 40
[stepper my_stepper_y0]
carriages: y
carriages: carriage_y
step_pin: PF6
dir_pin: !PF7
enable_pin: !PF2
@ -60,7 +63,7 @@ microsteps: 16
rotation_distance: 40
[stepper my_stepper_y1]
carriages: y1
carriages: carriage_y1
step_pin: PE3
dir_pin: !PH6
enable_pin: !PG5
@ -68,7 +71,7 @@ microsteps: 16
rotation_distance: 40
[stepper my_stepper_z0]
carriages: z
carriages: carriage_z
step_pin: PL3
dir_pin: PL1
enable_pin: !PK0
@ -76,7 +79,7 @@ microsteps: 16
rotation_distance: 8
[stepper my_stepper_z1]
carriages: z
carriages: carriage_z
step_pin: PG1
dir_pin: PG0
enable_pin: !PH3

View file

@ -133,44 +133,34 @@ max_z_accel: 100
#[tmc2130 stepper_x]
#cs_pin: PB8
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10
##diag1_pin: PF3
#run_current: 0.800
#stealthchop_threshold: 999999
#[tmc2130 stepper_y]
#cs_pin: PC9
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10
##diag1_pin: PF4
#run_current: 0.800
#stealthchop_threshold: 999999
#[tmc2130 stepper_z]
#cs_pin: PD0
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10
##diag1_pin: PF5
#run_current: 0.650
#stealthchop_threshold: 999999
#[tmc2130 extruder]
#cs_pin: PD1
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10
#run_current: 0.800
#stealthchop_threshold: 999999
#[tmc2130 extruder1]
#cs_pin: PB5
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10
#run_current: 0.800
#stealthchop_threshold: 999999
@ -195,6 +185,4 @@ aliases:
#[adxl345]
#cs_pin: PC15
#spi_software_miso_pin: PC11
#spi_software_mosi_pin: PC12
#spi_software_sclk_pin: PC10
#spi_bus: spi3_PC11_PC12_PC10

View file

@ -39,7 +39,7 @@ position_max: 270
# Motor4
# The M8P only has 4 heater outputs which leaves an extra stepper
# This can be used for a second Z stepper, dual_carriage, extruder co-stepper,
# or other accesory such as an MMU
# or other accessory such as an MMU
#[stepper_]
#step_pin: PD3
#dir_pin: PD2

View file

@ -40,7 +40,7 @@ position_max: 270
# Motor4
# The M8P only has 4 heater outputs which leaves an extra stepper
# This can be used for a second Z stepper, dual_carriage, extruder co-stepper,
# or other accesory such as an MMU
# or other accessory such as an MMU
#[stepper_]
#step_pin: PD3
#dir_pin: PD2

View file

@ -43,7 +43,7 @@ position_max: 200
# Motor-4
# The Octopus only has 4 heater outputs which leaves an extra stepper
# This can be used for a second Z stepper, dual_carriage, extruder co-stepper,
# or other accesory such as an MMU
# or other accessory such as an MMU
#[stepper_]
#step_pin: PB8
#dir_pin: PB9

View file

@ -52,7 +52,7 @@ position_max: 200
# Driver3
# The Octopus only has 4 heater outputs which leaves an extra stepper
# This can be used for a second Z stepper, dual_carriage, extruder co-stepper,
# or other accesory such as an MMU
# or other accessory such as an MMU
#[stepper_]
#step_pin: PG4
#dir_pin: PC1

View file

@ -89,32 +89,32 @@ max_z_velocity: 5
max_z_accel: 100
[mcp4018 x_axis_pot]
scl_pin: PJ5
sda_pin: PF3
i2c_software_scl_pin: PJ5
i2c_software_sda_pin: PF3
wiper: 0.50
scale: 0.773
[mcp4018 y_axis_pot]
scl_pin: PJ5
sda_pin: PF7
i2c_software_scl_pin: PJ5
i2c_software_sda_pin: PF7
wiper: 0.50
scale: 0.773
[mcp4018 z_axis_pot]
scl_pin: PJ5
sda_pin: PK3
i2c_software_scl_pin: PJ5
i2c_software_sda_pin: PK3
wiper: 0.50
scale: 0.773
[mcp4018 a_axis_pot]
scl_pin: PJ5
sda_pin: PA5
i2c_software_scl_pin: PJ5
i2c_software_sda_pin: PA5
wiper: 0.50
scale: 0.773
[mcp4018 b_axis_pot]
scl_pin: PJ5
sda_pin: PJ6
i2c_software_scl_pin: PJ5
i2c_software_sda_pin: PJ6
wiper: 0.50
scale: 0.773

View file

@ -19,7 +19,7 @@
# FSR switch (z endstop) location [homing_override] section
# FSR switch (z endstop) offset for Z0 [stepper_z] section
# Probe points [quad_gantry_level] section
# Min & Max gantry corner postions [quad_gantry_level] section
# Min & Max gantry corner positions [quad_gantry_level] section
# PID tune [extruder] and [heater_bed] sections
# Fine tune E steps [extruder] section

View file

@ -20,7 +20,7 @@
# FSR switch (z endstop) location [homing_override] section
# FSR switch (z endstop) offset for Z0 [stepper_z] section
# Probe points [quad_gantry_level] section
# Min & Max gantry corner postions [quad_gantry_level] section
# Min & Max gantry corner positions [quad_gantry_level] section
# PID tune [extruder] and [heater_bed] sections
# Fine tune E steps [extruder] section

View file

@ -17,7 +17,7 @@ endstop_pin: ^PE4
homing_speed: 60
# The next parameter needs to be adjusted for
# your printer. You may want to start with 280
# and meassure the distance from nozzle to bed.
# and measure the distance from nozzle to bed.
# This value then needs to be added.
position_endstop: 273.0
arm_length: 229.4

View file

@ -43,7 +43,7 @@ position_max: 400
#Uncomment if you have a BL-Touch:
#position_min: -4
#endstop_pin: probe:z_virtual_endstop
#and comment the follwing lines:
#and comment the following lines:
position_endstop: 0.0
endstop_pin: ^PD3 #ar18

View file

@ -81,7 +81,7 @@ pin: PA0
kick_start_time: 0.5
# Hotend fan
# set fan runnig when extruder temperature is over 60
# set fan running when extruder temperature is over 60
[heater_fan heatbreak_fan]
pin: PC0
heater:extruder

View file

@ -1,7 +1,7 @@
# This file contains pin mappings for the stock 2020 Creality Ender 5
# Pro with the 32-bit Creality 4.2.2 board. To use this config, during
# "make menuconfig" select the STM32F103 with a "28KiB bootloader" and
# with "Use USB for communication" disabled.
# communication interface set to "Serial (on USART1 PA10/PA9)".
# If you prefer a direct serial connection, in "make menuconfig"
# select "Enable extra low-level configuration options" and select the

View file

@ -127,32 +127,32 @@ max_z_velocity: 5
max_z_accel: 100
[mcp4018 x_axis_pot]
scl_pin: PJ5
sda_pin: PF3
i2c_software_scl_pin: PJ5
i2c_software_sda_pin: PF3
wiper: 118
scale: 127
[mcp4018 y_axis_pot]
scl_pin: PJ5
sda_pin: PF7
i2c_software_scl_pin: PJ5
i2c_software_sda_pin: PF7
wiper: 118
scale: 127
[mcp4018 z_axis_pot]
scl_pin: PJ5
sda_pin: PK3
i2c_software_scl_pin: PJ5
i2c_software_sda_pin: PK3
wiper: 40
scale: 127
[mcp4018 a_axis_pot]
scl_pin: PJ5
sda_pin: PA5
i2c_software_scl_pin: PJ5
i2c_software_sda_pin: PA5
wiper: 118
scale: 127
[mcp4018 b_axis_pot]
scl_pin: PJ5
sda_pin: PJ6
i2c_software_scl_pin: PJ5
i2c_software_sda_pin: PJ6
wiper: 118
scale: 127

View file

@ -195,7 +195,7 @@ samples_tolerance: 0.200
samples_tolerance_retries: 2
[bed_tilt]
# Enable bed tilt measurments using the probe we defined above
# Enable bed tilt measurements using the probe we defined above
# Probe points using X0 Y0 offsets @ 0.01mm/step
points: -2, -6
156, -6

View file

@ -183,7 +183,7 @@ samples: 2
samples_tolerance: 0.100
[bed_tilt]
#Enable bed tilt measurments using the probe we defined above
#Enable bed tilt measurements using the probe we defined above
#Probe points using X0 Y0 offsets @ 0.01mm/step
points: -3, -6
282, -6

View file

@ -37,7 +37,7 @@ microsteps: 16
rotation_distance: 4
# Required if not using probe for the virtual endstop
# endstop_pin: ^PD3
# position_endstop: 250 # Will need ajustment
# position_endstop: 250 # Will need adjustment
endstop_pin: probe:z_virtual_endstop
homing_speed: 10.0
position_max: 250

View file

@ -1,4 +1,4 @@
# This file constains the pin mappings for the SeeMeCNC Rostock Max
# This file contains the pin mappings for the SeeMeCNC Rostock Max
# (version 2) delta printer from 2015. To use this config, the
# firmware should be compiled for the AVR atmega2560.

View file

@ -11,9 +11,7 @@ serial: /dev/serial/by-id/usb-Klipper_Klipper_firmware_12345-if00
[adxl345]
cs_pin: EBBCan:PB12
spi_software_sclk_pin: EBBCan:PB10
spi_software_mosi_pin: EBBCan:PB11
spi_software_miso_pin: EBBCan:PB2
spi_bus: spi2_PB2_PB11_PB10
axes_map: x,y,z
[extruder]

View file

@ -11,9 +11,7 @@ serial: /dev/serial/by-id/usb-Klipper_Klipper_firmware_12345-if00
[adxl345]
cs_pin: EBBCan:PB12
spi_software_sclk_pin: EBBCan:PB10
spi_software_mosi_pin: EBBCan:PB11
spi_software_miso_pin: EBBCan:PB2
spi_bus: spi2_PB2_PB11_PB10
axes_map: x,y,z
[extruder]

View file

@ -15,9 +15,7 @@ sensor_pin: EBBCan:PA2
[adxl345]
cs_pin: EBBCan:PB12
spi_software_sclk_pin: EBBCan:PB10
spi_software_mosi_pin: EBBCan:PB11
spi_software_miso_pin: EBBCan:PB2
spi_bus: spi2_PB2_PB11_PB10
axes_map: x,y,z
[extruder]

View file

@ -0,0 +1,52 @@
# This file contains common pin mappings for the Cartographer board V3
# To use this config, the firmware should be compiled for the
# STM32F042 with "24 MHz crystal" and
# "USB (on PA9/PA10)" or "CAN bus (on PA9/PA10)".
# CAN bus requires PA1 GPIO pin to be set at micro-controller start-up
# The "carto" micro-controller will be used to control
# the components on the board.
# See docs/Config_Reference.md for a description of parameters.
[mcu carto]
serial: /dev/serial/by-id/usb-Klipper_stm32f042x6_29000380114330394D363620-if00
#canbus_uuid: 92cf532ef122
#[adxl345 carto]
#cs_pin: carto:PA3
#spi_bus: spi1_PA6_PA7_PA5
#axes_map: x,y,z
[thermistor 50k]
temperature1: 25
resistance1: 50000
temperature2: 50
resistance2: 17940
temperature3: 100
resistance3: 3090
[temperature_probe carto]
pullup_resistor: 10000
sensor_type: 50k
sensor_pin: carto:PA4
min_temp: 0
max_temp: 125
[led carto_led]
white_pin: carto:PB5
initial_WHITE: 0.03
[output_pin _LDC1612_en]
pin: carto:PA15
value: 0 # enable
[static_pwm_clock ldc1612_clk_in]
pin: carto:PB4
frequency: 24000000
[probe_eddy_current carto]
sensor_type: ldc1612
frequency: 24000000
i2c_address: 42
i2c_mcu: carto
i2c_bus: i2c1_PB6_PB7

View file

@ -3,28 +3,30 @@
# See docs/Config_Reference.md for a description of parameters.
[carriage x]
[carriage carriage_x]
axis: x
position_endstop: 0
position_max: 300
homing_speed: 50
endstop_pin: ^PE5
[carriage y]
[carriage carriage_y]
axis: y
position_endstop: 0
position_max: 200
homing_speed: 50
endstop_pin: ^PJ1
[dual_carriage u]
primary_carriage: x
[dual_carriage carriage_u]
primary_carriage: carriage_x
safe_distance: 70
position_endstop: 300
position_max: 300
homing_speed: 50
endstop_pin: ^PE4
[dual_carriage v]
primary_carriage: y
[dual_carriage carriage_v]
primary_carriage: carriage_y
safe_distance: 50
position_endstop: 200
position_max: 200
@ -32,7 +34,7 @@ homing_speed: 50
endstop_pin: ^PD4
[stepper a]
carriages: x+y
carriages: carriage_x+carriage_y
step_pin: PF0
dir_pin: PF1
enable_pin: !PD7
@ -40,7 +42,7 @@ microsteps: 16
rotation_distance: 40
[stepper b]
carriages: u-v
carriages: carriage_u-carriage_v
step_pin: PH1
dir_pin: PH0
enable_pin: !PA1
@ -48,7 +50,7 @@ microsteps: 16
rotation_distance: 40
[stepper c]
carriages: x-y
carriages: carriage_x-carriage_y
step_pin: PF6
dir_pin: !PF7
enable_pin: !PF2
@ -56,7 +58,7 @@ microsteps: 16
rotation_distance: 40
[stepper d]
carriages: u+v
carriages: carriage_u+carriage_v
step_pin: PE3
dir_pin: !PH6
enable_pin: !PG5
@ -83,8 +85,8 @@ max_temp: 250
[gcode_macro PARK_extruder]
gcode:
SET_DUAL_CARRIAGE CARRIAGE=x
SET_DUAL_CARRIAGE CARRIAGE=y
SET_DUAL_CARRIAGE CARRIAGE=carriage_x
SET_DUAL_CARRIAGE CARRIAGE=carriage_y
G90
G1 X0 Y0
@ -92,8 +94,8 @@ gcode:
gcode:
PARK_{printer.toolhead.extruder}
ACTIVATE_EXTRUDER EXTRUDER=extruder
SET_DUAL_CARRIAGE CARRIAGE=x
SET_DUAL_CARRIAGE CARRIAGE=y
SET_DUAL_CARRIAGE CARRIAGE=carriage_x
SET_DUAL_CARRIAGE CARRIAGE=carriage_y
[extruder1]
step_pin: PC1
@ -115,8 +117,8 @@ max_temp: 250
[gcode_macro PARK_extruder1]
gcode:
SET_DUAL_CARRIAGE CARRIAGE=u
SET_DUAL_CARRIAGE CARRIAGE=v
SET_DUAL_CARRIAGE CARRIAGE=carriage_u
SET_DUAL_CARRIAGE CARRIAGE=carriage_v
G90
G1 X300 Y200
@ -124,37 +126,59 @@ gcode:
gcode:
PARK_{printer.toolhead.extruder}
ACTIVATE_EXTRUDER EXTRUDER=extruder1
SET_DUAL_CARRIAGE CARRIAGE=u
SET_DUAL_CARRIAGE CARRIAGE=v
SET_DUAL_CARRIAGE CARRIAGE=carriage_u
SET_DUAL_CARRIAGE CARRIAGE=carriage_v
# A helper script to activate copy mode
[gcode_macro ACTIVATE_COPY_MODE]
gcode:
SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=y MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=carriage_x MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=carriage_y MODE=PRIMARY
G1 X0 Y0
ACTIVATE_EXTRUDER EXTRUDER=extruder
SET_DUAL_CARRIAGE CARRIAGE=u MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=v MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=PRIMARY
G1 X150 Y100
SET_DUAL_CARRIAGE CARRIAGE=u MODE=COPY
SET_DUAL_CARRIAGE CARRIAGE=v MODE=COPY
SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=COPY
SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=COPY
SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder
# A helper script to activate mirror mode
[gcode_macro ACTIVATE_MIRROR_MODE]
gcode:
SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=y MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=carriage_x MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=carriage_y MODE=PRIMARY
G1 X0 Y0
ACTIVATE_EXTRUDER EXTRUDER=extruder
SET_DUAL_CARRIAGE CARRIAGE=u MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=v MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=PRIMARY
SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=PRIMARY
G1 X300 Y100
SET_DUAL_CARRIAGE CARRIAGE=u MODE=MIRROR
SET_DUAL_CARRIAGE CARRIAGE=v MODE=COPY
SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=MIRROR
SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=COPY
SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder
[gcode_macro _DISABLE_AND_PARK_EXTRUDER]
gcode:
SAVE_GCODE_STATE NAME=disable_extruder
SAVE_DUAL_CARRIAGE_STATE NAME=disable_extruder
SET_HEATER_TEMPERATURE HEATER={params.EXTRUDER} TARGET=0
SYNC_EXTRUDER_MOTION EXTRUDER={params.EXTRUDER} MOTION_QUEUE=
PARK_{params.EXTRUDER}
RESTORE_DUAL_CARRIAGE_STATE NAME=disable_extruder MOVE=0
RESTORE_GCODE_STATE NAME=disable_extruder MOVE=0
[gcode_macro DISABLE_T0]
gcode:
SET_DUAL_CARRIAGE CARRIAGE=carriage_x MODE=INACTIVE
SET_DUAL_CARRIAGE CARRIAGE=carriage_y MODE=INACTIVE
_DISABLE_AND_PARK_EXTRUDER EXTRUDER=extruder
[gcode_macro DISABLE_T1]
gcode:
SET_DUAL_CARRIAGE CARRIAGE=carriage_u MODE=INACTIVE
SET_DUAL_CARRIAGE CARRIAGE=carriage_v MODE=INACTIVE
_DISABLE_AND_PARK_EXTRUDER EXTRUDER=extruder
[printer]
kinematics: generic_cartesian
max_velocity: 300
@ -169,9 +193,9 @@ max_z_accel: 100
#[delayed_gcode init_shaper]
#initial_duration: 0.1
#gcode:
# SET_DUAL_CARRIAGE CARRIAGE=u
# SET_DUAL_CARRIAGE CARRIAGE=v
# SET_INPUT_SHAPER SHAPER_TYPE_X=<dual_carriage_x_shaper> SHAPER_FREQ_X=<dual_carriage_x_freq> SHAPER_TYPE_Y=<dual_carriage_y_shaper> SHAPER_FREQ_Y=<dual_carriage_y_freq>
# SET_DUAL_CARRIAGE CARRIAGE=x MODE=PRIMARY
# SET_DUAL_CARRIAGE CARRIAGE=y MODE=PRIMARY
# SET_INPUT_SHAPER SHAPER_TYPE_X=<primary_carriage_x_shaper> SHAPER_FREQ_X=<primary_carriage_x_freq> SHAPER_TYPE_Y=<primary_carriage_y_shaper> SHAPER_FREQ_Y=<primary_carriage_y_freq>
# SET_DUAL_CARRIAGE CARRIAGE=carriage_u
# SET_DUAL_CARRIAGE CARRIAGE=carriage_v
# SET_INPUT_SHAPER SHAPER_TYPE_X=<carriage_u_shaper> SHAPER_FREQ_X=<carriage_u_freq> SHAPER_TYPE_Y=<carriage_v_shaper> SHAPER_FREQ_Y=<carriage_v_freq>
# SET_DUAL_CARRIAGE CARRIAGE=carriage_x MODE=PRIMARY
# SET_DUAL_CARRIAGE CARRIAGE=carriage_y MODE=PRIMARY
# SET_INPUT_SHAPER SHAPER_TYPE_X=<carriage_x_shaper> SHAPER_FREQ_X=<carriage_x_freq> SHAPER_TYPE_Y=<carriage_y_shaper> SHAPER_FREQ_Y=<carriage_y_freq>

View file

@ -6,7 +6,7 @@
# Communication interface of "CAN bus (on PA25/PA24)"
# To flash the board use a debugger, or use a raspberry pi and follow
# the instructions at docs/Bootloaders.md fot the SAMC21. You may
# the instructions at docs/Bootloaders.md for the SAMC21. You may
# supply power to the 1LC by connecting the 3.3v rail on the Pi to the
# 5v input of the SWD header on the 1LC.

View file

@ -96,7 +96,7 @@ switch_pin: !P1.28 # P1.28 for X-max
# variable_pause_z : z lift when MMU2S need intervention and the printer is paused
# variable_min_temp_extruder : minimal required heater temperature to load/unload filament from the extruder gear to the nozzle
# variable_extruder_eject_temp : heater temperature used to eject filament during home if the filament is already loaded
# variable_enable_5in1 : pass from MMU2S standart (0) to MMU2S-5in1 mode with splitter
# variable_enable_5in1 : pass from MMU2S standard (0) to MMU2S-5in1 mode with splitter
#
################################
[gcode_macro VAR_MMU2S]
@ -394,7 +394,7 @@ gcode:
{% endif %}
{% endif %}
# Retry unload, try correct misalignement of bondtech gear
# Retry unload, try correct misalignment of bondtech gear
[gcode_macro RETRY_UNLOAD_FILAMENT_IN_EXTRUDER]
gcode:
{% if printer["filament_switch_sensor ir_sensor"].filament_detected == True %}
@ -444,7 +444,7 @@ gcode:
{% endif %}
{% endif %}
# Ramming process for standart PLA, code extracted from slic3r gcode
# Ramming process for standard PLA, code extracted from slic3r gcode
[gcode_macro RAMMING_SLICER]
gcode:
G91
@ -506,7 +506,7 @@ gcode:
{% if printer["gcode_macro SELECT_TOOL"].tool_selected|int != -1 %}
M118 Loading filament to PINDA ...
MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0
MANUAL_STEPPER STEPPER=gear_stepper MOVE={printer["gcode_macro VAR_MMU2S"].pinda_load_length} STOP_ON_ENDSTOP=2
MANUAL_STEPPER STEPPER=gear_stepper MOVE={printer["gcode_macro VAR_MMU2S"].pinda_load_length} STOP_ON_ENDSTOP=try_home
MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0
MANUAL_STEPPER STEPPER=gear_stepper MOVE=10
IS_FILAMENT_IN_PINDA
@ -579,7 +579,7 @@ gcode:
M118 Unloading filament from extruder to PINDA ...
MANUAL_STEPPER STEPPER=gear_stepper SET_POSITION=0
{% if printer["gcode_macro VAR_MMU2S"].enable_5in1 == 0 %}
MANUAL_STEPPER STEPPER=gear_stepper MOVE=-{printer["gcode_macro VAR_MMU2S"].bowden_unload_length} SPEED=120 ACCEL=80 STOP_ON_ENDSTOP=-2
MANUAL_STEPPER STEPPER=gear_stepper MOVE=-{printer["gcode_macro VAR_MMU2S"].bowden_unload_length} SPEED=120 ACCEL=80 STOP_ON_ENDSTOP=try_inverted_home
IS_FILAMENT_STUCK_IN_PINDA
{% else %}
MANUAL_STEPPER STEPPER=gear_stepper MOVE=-{printer["gcode_macro VAR_MMU2S"].bowden_unload_length} SPEED=120 ACCEL=80
@ -792,7 +792,7 @@ gcode:
{% if printer["gcode_macro VAR_MMU2S"].enable_5in1 == 0 %}
M118 Homing selector
MANUAL_STEPPER STEPPER=selector_stepper SET_POSITION=0
MANUAL_STEPPER STEPPER=selector_stepper MOVE=-76 STOP_ON_ENDSTOP=1
MANUAL_STEPPER STEPPER=selector_stepper MOVE=-76 STOP_ON_ENDSTOP=home
MANUAL_STEPPER STEPPER=selector_stepper SET_POSITION=0
{% endif %}
MANUAL_STEPPER STEPPER=idler_stepper MOVE=0

View file

@ -267,7 +267,7 @@ by heat or interference. This can make calculating the probe's z-offset
challenging, particularly at different bed temperatures. As such, some
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
`reference position` applies zero adjustment. The `reference position` should
be the location on the bed where a
[Z_ENDSTOP_CALIBRATE](./Manual_Level.md#calibrating-a-z-endstop)
paper test is performed. The bed_mesh module provides the
@ -292,33 +292,6 @@ probe_count: 5, 3
z-offset. Note that this coordinate must NOT be in a location specified as
a `faulty_region` if a probe is necessary.
#### The deprecated relative_reference_index
Existing configurations using the `relative_reference_index` option must be
updated to use the `zero_reference_position`. The response to the
[BED_MESH_OUTPUT PGP=1](#output) gcode command will include the (X, Y)
coordinate associated with the index; this position may be used as the value for
the `zero_reference_position`. The output will look similar to the following:
```
// bed_mesh: generated points
// Index | Tool Adjusted | Probe
// 0 | (1.0, 1.0) | (24.0, 6.0)
// 1 | (36.7, 1.0) | (59.7, 6.0)
// 2 | (72.3, 1.0) | (95.3, 6.0)
// 3 | (108.0, 1.0) | (131.0, 6.0)
... (additional generated points)
// bed_mesh: relative_reference_index 24 is (131.5, 108.0)
```
_Note: The above output is also printed in `klippy.log` during initialization._
Using the example above we see that the `relative_reference_index` is
printed along with its coordinate. Thus the `zero_reference_position`
is `131.5, 108`.
### Faulty Regions
It is possible for some areas of a bed to report inaccurate results when

View file

@ -194,7 +194,7 @@ Alternatively, one can use a
When using OpenOCD with the SAMC21, extra steps must be taken to first
put the chip into Cold Plugging mode if the board makes use of the
SWD pins for other purposes. If using OpenOCD on a Rasberry Pi, this
SWD pins for other purposes. If using OpenOCD on a Raspberry Pi, this
can be done by running the following commands before invoking OpenOCD.
```
SWCLK=25

View file

@ -323,7 +323,7 @@ a month without updates.
Once the requirements are met, you need to:
1. update klipper-tranlations repository
1. update klipper-translations repository
[active_translations](https://github.com/Klipper3d/klipper-translations/blob/translations/active_translations)
2. Optional: add a manual-index.md file in klipper-translations repository's
`docs\locals\<lang>` folder to replace the language specific index.md (generated

View file

@ -102,20 +102,35 @@ some functionality in C code.
Initial execution starts in **klippy/klippy.py**. This reads the
command-line arguments, opens the printer config file, instantiates
the main printer objects, and starts the serial connection. The main
execution of G-code commands is in the process_commands() method in
execution of G-code commands is in the _process_commands() method in
**klippy/gcode.py**. This code translates the G-code commands into
printer object calls, which frequently translate the actions to
commands to be executed on the micro-controller (as declared via the
DECL_COMMAND macro in the micro-controller code).
There are four threads in the Klippy host code. The main thread
handles incoming gcode commands. A second thread (which resides
entirely in the **klippy/chelper/serialqueue.c** C code) handles
low-level IO with the serial port. The third thread is used to process
response messages from the micro-controller in the Python code (see
**klippy/serialhdl.py**). The fourth thread writes debug messages to
the log (see **klippy/queuelogger.py**) so that the other threads
never block on log writes.
There are several threads in the Klipper host code:
* There is a Python "main thread" that handles incoming G-Code
commands and is the starting point for most actions. This thread
runs the [reactor](https://en.wikipedia.org/wiki/Reactor_pattern)
(**klippy/reactor.py**) and most high-level actions originate from
IO and timer event callbacks from that reactor.
* A thread for writing messages to the log so that the other threads
do not block on log writes. This thread resides in the
**klippy/queuelogger.py** code and its multi-threaded nature is not
exposed to the main Python thread.
* A thread per micro-controller that performs the low-level reading
and writing of messages to that micro-controller. It resides in the
**klippy/chelper/serialqueue.c** C code and its multi-threaded
nature is not exposed to the Python code.
* A thread per micro-controller for processing messages received from
that micro-controller in the Python code. This thread is created in
**klippy/serialhdl.py**. Care must be taken in Python callbacks
invoked from this thread as this thread may directly interact with
the main Python thread.
* A thread per stepper motor that calculates the timing of stepper
motor step pulses and compresses those times. This thread resides in
the **klippy/chelper/steppersync.c** C code and its multi-threaded
nature is not exposed to the Python code.
## Code flow of a move command
@ -136,9 +151,10 @@ provides further information on the mechanics of moves.
* The ToolHead class (in toolhead.py) handles "look-ahead" and tracks
the timing of printing actions. The main codepath for a move is:
`ToolHead.move() -> LookAheadQueue.add_move() ->
LookAheadQueue.flush() -> Move.set_junction() ->
ToolHead._process_moves()`.
`ToolHead.move() -> LookAheadQueue.add_move()`, then
`ToolHead.move() -> ToolHead._process_lookahead() ->
LookAheadQueue.flush() -> Move.set_junction()`, and then
`ToolHead._process_lookahead() -> trapq_append()`.
* ToolHead.move() creates a Move() object with the parameters of the
move (in cartesian space and in units of seconds and millimeters).
* The kinematics class is given the opportunity to audit each move
@ -157,39 +173,47 @@ provides further information on the mechanics of moves.
phase, followed by a constant deceleration phase. Every move
contains these three phases in this order, but some phases may be of
zero duration.
* When ToolHead._process_moves() is called, everything about the
* When ToolHead._process_lookahead() resumes, everything about the
move is known - its start location, its end location, its
acceleration, its start/cruising/end velocity, and distance traveled
during acceleration/cruising/deceleration. All the information is
stored in the Move() class and is in cartesian space in units of
millimeters and seconds.
* The moves are then placed on a "trapezoid motion queue" via
trapq_append() (in klippy/chelper/trapq.c). The trapq stores all the
information in the Move() class in a C struct accessible to the host
C code.
* Note that the extruder is handled in its own kinematic class:
`ToolHead._process_lookahead() -> PrinterExtruder.process_move()`.
Since the Move() class specifies the exact movement time and since
step pulses are sent to the micro-controller with specific timing,
stepper movements produced by the extruder class will be in sync
with head movement even though the code is kept separate.
* For efficiency reasons, stepper motion is generated in the C code in
a thread per stepper motor. The threads are notified when steps
should be generated by the motion_queuing module
(klippy/extras/motion_queuing.py):
`PrinterMotionQueuing._flush_handler() ->
PrinterMotionQueuing._advance_move_time() ->
steppersyncmgr_gen_steps() -> se_start_gen_steps()`.
* Klipper uses an
[iterative solver](https://en.wikipedia.org/wiki/Root-finding_algorithm)
to generate the step times for each stepper. For efficiency reasons,
the stepper pulse times are generated in C code. The moves are first
placed on a "trapezoid motion queue": `ToolHead._process_moves() ->
trapq_append()` (in klippy/chelper/trapq.c). The step times are then
generated: `ToolHead._process_moves() ->
ToolHead._advance_move_time() -> ToolHead._advance_flush_time() ->
MCU_Stepper.generate_steps() -> itersolve_generate_steps() ->
itersolve_gen_steps_range()` (in klippy/chelper/itersolve.c). The
goal of the iterative solver is to find step times given a function
that calculates a stepper position from a time. This is done by
repeatedly "guessing" various times until the stepper position
formula returns the desired position of the next step on the
stepper. The feedback produced from each guess is used to improve
future guesses so that the process rapidly converges to the desired
time. The kinematic stepper position formulas are located in the
klippy/chelper/ directory (eg, kin_cart.c, kin_corexy.c,
kin_delta.c, kin_extruder.c).
* Note that the extruder is handled in its own kinematic class:
`ToolHead._process_moves() -> PrinterExtruder.move()`. Since
the Move() class specifies the exact movement time and since step
pulses are sent to the micro-controller with specific timing,
stepper movements produced by the extruder class will be in sync
with head movement even though the code is kept separate.
to generate the step times for each stepper. The step times are
generated from the background thread (klippy/chelper/steppersync.c):
`se_background_thread() -> se_generate_steps() ->
itersolve_generate_steps() -> itersolve_gen_steps_range()` (in
klippy/chelper/itersolve.c). The goal of the iterative solver is to
find step times given a function that calculates a stepper position
from a time. This is done by repeatedly "guessing" various times
until the stepper position formula returns the desired position of
the next step on the stepper. The feedback produced from each guess
is used to improve future guesses so that the process rapidly
converges to the desired time. The kinematic stepper position
formulas are located in the klippy/chelper/ directory (eg,
kin_cart.c, kin_corexy.c, kin_delta.c, kin_extruder.c).
* After the iterative solver calculates the step times they are added
to an array: `itersolve_gen_steps_range() -> stepcompress_append()`
@ -206,7 +230,7 @@ provides further information on the mechanics of moves.
commands that correspond to the list of stepper step times built in
the previous stage. These "queue_step" commands are then queued,
prioritized, and sent to the micro-controller (via
stepcompress.c:steppersync and serialqueue.c:serialqueue).
steppersync.c:steppersync and serialqueue.c:serialqueue).
* Processing of the queue_step commands on the micro-controller starts
in src/command.c which parses the command and calls

View file

@ -8,6 +8,81 @@ All dates in this document are approximate.
## Changes
20260214: The `MANUAL_STEPPER` G-Code command `STOP_ON_ENDSTOP`
parameter has changed. See the
[MANUAL_STEPPER](G-Codes.md#manual_stepper) documentation for
details. Using the previous integer values (-2, -1, 1, 2) is
deprecated and support will be removed in the near future.
20260207: The low-level i2c behavior of sx1509 and uc1701 devices has
changed. Previously an i2c error would result in a shutdown, and now
i2c errors when communicating with these devices will only generate
warnings in the log file.
20260109: The status value `{printer.probe.last_z_result}` is
deprecated; it will be removed in the near future. Use
`{printer.probe.last_probe_position}` instead, and note that this new
value already has the probe's configured xyz offsets applied.
20260109: The g-code console text output from the `PROBE`,
`PROBE_ACCURACY`, and similar commands has changed. Now Z heights are
reported relative to the nominal bed Z position instead of relative to
the probe's configured `z_offset`. Similarly, intermediate probe x and
y console reports will also have the probe's configured `x_offset` and
`y_offset` applied.
20260109: The `[screws_tilt_adjust]` module now reports the status
variable `{printer.screws_tilt_adjust.result.screw1.z}` with the
probe's `z_offset` applied. That is, one would previously need to
subtract the probe's configured `z_offset` to find the absolute Z
deviation at the given screw location and now one must not apply the
`z_offset`.
20251122: An option `axis` has been added to `[carriage <name>]`
sections for `generic_cartesian` kinematics, allowing arbitrary names
for primary carriages. Users are encouraged to explicitly specify
`axis` option now.
20251106: The status fields `{printer.toolhead.position}`,
`{printer.gcode_move.position}`,
`{printer.gcode_move.gcode_position}`, and
`{printer.motion_report.live_position}` are changing. These
coordinates used to always contain four components, but now may
contain additional components. The ordering and number of components
may change at run-time - see the
[status reference](Status_Reference.md#accessing-coordinates) for
important details. Accessing any of these coordinates in macros using
the ".e" accessor is deprecated - use something like
`{printer.toolhead.position[printer.gcode_move.axis_map.E]}` as an
alternative.
20251106: The status fields `{printer.gcode_move.homing_origin}`,
`{printer.toolhead.axis_min}`, and `{printer.toolhead.axis_max}`
currently contain four components where the fourth component is always
zero. This behavior is deprecated. In the future these coordinates may
contain only three components. For additional information see the
[status reference](Status_Reference.md#accessing-coordinates).
20251010: During normal printing the command processing will now
attempt to stay one second ahead of printer movement (reduced from two
seconds previously).
20251003: Support for the undocumented `max_stepper_error` option in
the `[printer]` config section has been removed.
20250916: The definitions of EI, 2HUMP_EI, and 3HUMP_EI input shapers
were updated. For best performance it is recommended to recalibrate
input shapers, especially if some of these shapers are currently used.
20250811: Support for the `max_accel_to_decel` parameter in the
`[printer]` config section has been removed and support for the
`ACCEL_TO_DECEL` parameter in the `SET_VELOCITY_LIMIT` command has
been removed. These capabilities were deprecated on 20240313.
20250721: The `[pca9632]` and `[mcp4018]` modules no longer accept the
`scl_pin` and `sda_pin` options. Use `i2c_software_scl_pin` and
`i2c_software_sda_pin` instead.
20250428: The maximum `cycle_time` for pwm `[output_pin]`,
`[pwm_cycle_time]`, `[pwm_tool]`, and similar config sections is now 3
seconds (reduced from 5 seconds). The `maximum_mcu_duration` in
@ -58,7 +133,7 @@ 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
required then consider adding explicit `G4` delay commands between
updates.
20240912: Support for `maximum_mcu_duration` and `static_value`
@ -131,7 +206,7 @@ carriage are exported as `printer.dual_carriage.carriage_0` and
`printer.dual_carriage.carriage_1`.
20230619: The `relative_reference_index` option has been deprecated
and superceded by the `zero_reference_position` option. Refer to the
and superseded by the `zero_reference_position` option. Refer to the
[Bed Mesh Documentation](./Bed_Mesh.md#the-deprecated-relative_reference_index)
for details on how to update the configuration. With this deprecation
the `RELATIVE_REFERENCE_INDEX` is no longer available as a parameter
@ -365,7 +440,7 @@ endstop phases by running the ENDSTOP_PHASE_CALIBRATE command.
`gear_ratio` for their rotary steppers, and they may no longer specify
a `step_distance` parameter. See the
[config reference](Config_Reference.md#stepper) for the format of the
new gear_ratio paramter.
new gear_ratio parameter.
20201213: It is not valid to specify a Z "position_endstop" when using
"probe:z_virtual_endstop". An error will now be raised if a Z

View file

@ -126,8 +126,6 @@ max_accel:
# decelerate to zero at each corner. The value specified here may be
# changed at runtime using the SET_VELOCITY_LIMIT command. The
# default is 5mm/s.
#max_accel_to_decel:
# This parameter is deprecated and should no longer be used.
```
### [stepper]
@ -740,16 +738,17 @@ max_velocity:
max_accel:
#minimum_cruise_ratio:
#square_corner_velocity:
#max_accel_to_decel:
#max_z_velocity:
#max_z_accel:
```
Then a user must define the following three carriages: `[carriage x]`,
`[carriage y]`, and `[carriage z]`, e.g.
Then a user must define three primary carriages for X, Y, and Z axes, e.g.:
```
[carriage x]
[carriage carriage_x]
axis:
# Axis of a carriage, either x, y, or z. This parameter must be provided,
# unless a carriage name is x, y, or z itself.
endstop_pin:
# Endstop switch detection pin. If this endstop pin is on a
# different mcu than the stepper motor(s) moving this carriage,
@ -791,7 +790,8 @@ for instance
carriages:
# A string describing the carriages the stepper moves. All defined
# carriages can be specified here, as well as their linear combinations,
# e.g. x, x+y, y-0.5*z, x-z, etc. This parameter must be provided.
# e.g. carriage_x, carriage_x+carriage_y, carriage_y-0.5*carriage_z,
# carriage_x-carriage_z, etc. This parameter must be provided.
step_pin:
dir_pin:
enable_pin:
@ -803,28 +803,29 @@ microsteps:
```
See [stepper](#stepper) section for more information on the regular
stepper parameters. The `carriages` parameter defines how the stepper
affects the motion of the carriages. For example, `x+y` indicates that
the motion of the stepper in the positive direction by the distance `d`
moves the carriages `x` and `y` by the same distance `d` in the positive
direction, while `x-0.5*y` means the motion of the stepper in the positive
direction by the distance `d` moves the carriage `x` by the distance `d`
in the positive direction, but the carriage `y` will travel distance `d/2`
in the negative direction.
affects the motion of the carriages. For example, `carriage_x+carriage_y`
indicates that the motion of the stepper in the positive direction by the
distance `d` moves the carriages `carriage_x` and `carriage_y` by the same
distance `d` in the positive direction, while `carriage_x-0.5*carriage_y`
means the motion of the stepper in the positive direction by the distance
`d` moves the carriage `carriage_x` by the distance `d` in the positive
direction, but the carriage `carriage_y` will travel distance `d/2` in
the negative direction.
More than a single stepper motor can be defined to drive the same axis
or belt. For example, on a CoreXY AWD setups two motors driving the same
belt can be defined as
```
[carriage x]
[carriage carriage_x]
endstop_pin: ...
...
[carriage y]
[carriage carriage_y]
endstop_pin: ...
...
[stepper a0]
carriages: x-y
carriages: carriage_x-carriage_y
step_pin: ...
dir_pin: ...
enable_pin: ...
@ -832,7 +833,7 @@ rotation_distance: ...
...
[stepper a1]
carriages: x-y
carriages: carriage_x-carriage_y
step_pin: ...
dir_pin: ...
enable_pin: ...
@ -845,7 +846,7 @@ sharing the same `carriages` and corresponding endstops.
There are situations when a user wants to have more than one endstop
per axis. Examples of such configurations include Y axis driven by
two independent stepper motors with belts attached to both ends of the
X beam, with effectively two carriages on Y axis each having an
X gantry, with effectively two carriages on Y axis each having an
independent endstop, and multi-stepper Z axis with each stepper having
its own endstop (not to be confused with the configurations with
multiple Z motors but only a single endstop). These configurations
@ -863,12 +864,12 @@ endstop_pin:
and the corresponding stepper motors, for example:
```
[extra_carriage y1]
primary_carriage: y
[extra_carriage carriage_y1]
primary_carriage: carriage_y
endstop_pin: ...
[stepper sy1]
carriages: y1
carriages: carriage_y1
...
```
Notably, an `[extra_carriage]` does not define parameters such as
@ -1783,17 +1784,22 @@ the [command reference](G-Codes.md#input_shaper).
# input shapers, this parameter can be set from different
# considerations. The default value is 0, which disables input
# shaping for Y axis.
#shaper_freq_z: 0
# A frequency (in Hz) of the input shaper for Z axis. The default
# value is 0, which disables input shaping for Z axis.
#shaper_type: mzv
# A type of the input shaper to use for both X and Y axes. Supported
# A type of the input shaper to use for all axes. Supported
# shapers are zv, mzv, zvd, ei, 2hump_ei, and 3hump_ei. The default
# is mzv input shaper.
#shaper_type_x:
#shaper_type_y:
# If shaper_type is not set, these two parameters can be used to
# configure different input shapers for X and Y axes. The same
#shaper_type_z:
# If shaper_type is not set, these parameters can be used to
# configure different input shapers for X, Y, and Z axes. The same
# values are supported as for shaper_type parameter.
#damping_ratio_x: 0.1
#damping_ratio_y: 0.1
#damping_ratio_z: 0.1
# Damping ratios of vibrations of X and Y axes used by input shapers
# to improve vibration suppression. Default value is 0.1 which is a
# good all-round value for most printers. In most circumstances this
@ -1946,6 +1952,39 @@ Support for LIS3DH accelerometers.
# See the "adxl345" section for information on this parameter.
```
### [bmi160]
BMI160 accelerometer. This sensor can be queried via I2C or SPI bus.
```
[bmi160]
#i2c_address:
# Default is 105 (0x69). If SA0 is tied to GND, use 104 (0x68).
# Only used for I2C.
#i2c_mcu:
#i2c_bus:
#i2c_speed:
# See the "common I2C settings" section for a description of the
# above parameters. Only used for I2C.
#cs_pin:
#spi_speed:
#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. Only used for SPI.
#axes_map: x, y, z
# See the "adxl345" section for information on this parameter.
```
**Important:** Many BMI160 modules use ambiguous pin labels. For SPI:
- Use **SCL** for clock (not SCX)
- Use **SDA** for MOSI (not SDX)
- Use **SA0** for MISO
- Use **CS** for chip select
The pins labeled SCX/SDX are for the auxiliary magnetometer bus.
### [mpu9250]
Support for MPU-9250, MPU-9255, MPU-6515, MPU-6050, and MPU-6500
@ -2001,6 +2040,10 @@ section of the measuring resonances guide for more information on
# and on the toolhead (for X axis). These parameters have the same
# format as 'accel_chip' parameter. Only 'accel_chip' or these two
# parameters must be provided.
#accel_chip_z:
# A name of the accelerometer chip to use for measurements of Z axis.
# This parameter has the same format as 'accel_chip'. The default is
# not to configure an accelerometer for Z axis.
#max_smoothing:
# Maximum input shaper smoothing to allow for each axis during shaper
# auto-calibration (with 'SHAPER_CALIBRATE' command). By default no
@ -2011,16 +2054,20 @@ section of the measuring resonances guide for more information on
# 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.
#max_freq: 135
# Maximum frequency to test for resonances. The default is 135 Hz.
#max_freq_z: 100
# Maximum frequency to test Z axis for resonances. The default is 100 Hz.
#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
# a lower than the default value if the resonances get too strong on
# the printer. However, lower values make measurements of
# high-frequency resonances less precise. The default value is 75
# (mm/sec).
# the printer. However, lower values make measurements of high-frequency
# resonances less precise. The default value is 60 (mm/sec).
#accel_per_hz_z: 15
# This parameter has the same meaning as accel_per_hz, but applies to
# Z axis specifically. The default is 15 (mm/sec).
#hz_per_sec: 1
# Determines the speed of the test. When testing all frequencies in
# range [min_freq, max_freq], each second the frequency increases by
@ -2029,6 +2076,8 @@ section of the measuring resonances guide for more information on
# (Hz/sec == sec^-2).
#sweeping_accel: 400
# An acceleration of slow sweeping moves. The default is 400 mm/sec^2.
#sweeping_accel_z: 50
# Same as sweeping_accel above, but for Z axis. The default is 50 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
@ -2308,6 +2357,16 @@ sensor_type: ldc1612
#samples_tolerance:
#samples_tolerance_retries:
# See the "probe" section for information on these parameters.
#tap_threshold:
# Noise cutoff/stop trigger threshold (in Hz). Specify this value to
# enable support for "METHOD=tap" probe commands. See Eddy_Probe.md
# for more information. Larger values make the tap detection less
# sensitive. That is, larger values make it less likely the toolhead
# will incorrectly stop early due to noise, while increasing the
# risk of the toolhead not correctly stopping when it first contacts
# the bed. If this value is specified then one may override its
# value at run-time using the "TAP_THRESHOLD" parameter on probe
# commands. The default is to not enable support for "tap" probing.
```
### [axis_twist_compensation]
@ -2467,10 +2526,16 @@ Please note that in this case the `[dual_carriage]` configuration deviates
from the configuration described above:
```
[dual_carriage my_dc_carriage]
primary_carriage:
# Defines the matching primary carriage of this dual carriage and
# the corresponding IDEX axis. Valid choices are x, y, z.
# This parameter must be provided.
#primary_carriage:
# Defines the matching carriage on the same gantry as this dual carriage and
# the corresponding dual axis. Must match a name of a defined `[carriage]` or
# another independent `[dual_carriage]`. If not set, which is a default,
# defines a dual carriage independent of a `[carriage]` with the same axis
# as this one (e.g. on a different gantry).
#axis:
# Axis of a carriage, either x or y. If 'primary_carriage' is defined, then
# this parameter defaults to the 'axis' parameter of that primary carriage,
# otherwise this parameter must be defined.
#safe_distance:
# The minimum distance (in mm) to enforce between the dual and the primary
# carriages. If a G-Code command is executed that will bring the carriages
@ -2479,7 +2544,8 @@ primary_carriage:
# position_min and position_max for the dual and primary carriages. If set
# to 0 (or safe_distance is unset and position_min and position_max are
# identical for the primary and dual carriages), the carriages proximity
# checks will be disabled.
# checks will be disabled. Only valid for a dual_carriage with a defined
# 'primary_carriage'.
endstop_pin:
#position_min:
position_endstop:
@ -2497,18 +2563,18 @@ on the regular `carriage` parameters.
Then a user must define one or more stepper motors moving the dual carriage
(and other carriages as appropriate), for instance
```
[carriage x]
[carriage carriage_x]
...
[carriage y]
[carriage carriage_y]
...
[dual_carriage u]
primary_carriage: x
[dual_carriage carriage_u]
primary_carriage: carriage_x
...
[stepper dc_stepper]
carriages: u-y
carriages: carriage_u-carriage_y
...
```
@ -2524,14 +2590,14 @@ example above:
[delayed_gcode init_shaper]
initial_duration: 0.1
gcode:
SET_DUAL_CARRIAGE CARRIAGE=u
SET_INPUT_SHAPER SHAPER_TYPE_X=<dual_carriage_x_shaper> SHAPER_FREQ_X=<dual_carriage_x_freq> SHAPER_TYPE_Y=<y_shaper> SHAPER_FREQ_Y=<y_freq>
SET_DUAL_CARRIAGE CARRIAGE=x
SET_INPUT_SHAPER SHAPER_TYPE_X=<primary_carriage_x_shaper> SHAPER_FREQ_X=<primary_carriage_x_freq> SHAPER_TYPE_Y=<y_shaper> SHAPER_FREQ_Y=<y_freq>
SET_DUAL_CARRIAGE CARRIAGE=carriage_u
SET_INPUT_SHAPER SHAPER_TYPE_X=<carriage_u_shaper> SHAPER_FREQ_X=<carriage_x_freq> SHAPER_TYPE_Y=<carriage_y_shaper> SHAPER_FREQ_Y=<carriage_y_freq>
SET_DUAL_CARRIAGE CARRIAGE=carriage_x
SET_INPUT_SHAPER SHAPER_TYPE_X=<carriage_x_shaper> SHAPER_FREQ_X=<carriage_x_freq> SHAPER_TYPE_Y=<carriage_y_shaper> SHAPER_FREQ_Y=<carriage_y_freq>
```
Note that `SHAPER_TYPE_Y` and `SHAPER_FREQ_Y` must be the same in both
commands in this case, since the same motors drive Y axis when either
of the `x` and `u` carriages are active.
of the `carriage_x` and `carriage_u` carriages are active.
It is worth noting that `generic_cartesian` kinematic can support two
dual carriages for X and Y axes. For reference, see for instance a
@ -2960,7 +3026,7 @@ sensor_type: BME280
### AHT10/AHT20/AHT21 temperature sensor
AHT10/AHT20/AHT21 two wire interface (I2C) environmental sensors.
AHT10/AHT15/AHT20/AHT21/AHT30 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) and
relative humidity. See
@ -2968,8 +3034,9 @@ relative humidity. See
that may be used to report humidity in addition to temperature.
```
sensor_type: AHT10
# Also use AHT10 for AHT20 and AHT21 sensors.
sensor_type: AHT1X
# Must be "AHT1X" , "AHT2X", "AHT3X"
# Some AHT20 sensors can use "AHT1X"
#i2c_address:
# Default is 56 (0x38). Some AHT10 sensors give the option to use
# 57 (0x39) by moving a resistor.
@ -3507,11 +3574,6 @@ PCA9632 LED support. The PCA9632 is used on the FlashForge Dreamer.
#i2c_speed:
# See the "common I2C settings" section for a description of the
# above parameters.
#scl_pin:
#sda_pin:
# Alternatively, if the pca9632 is not connected to a hardware I2C
# bus, then one may specify the "clock" (scl_pin) and "data"
# (sda_pin) pins. The default is to use hardware I2C.
#color_order: RGBW
# Set the pixel order of the LED (using a string containing the
# letters R, G, B, W). The default is RGBW.
@ -3635,6 +3697,20 @@ pin:
# These options are deprecated and should no longer be specified.
```
### [static_pwm_clock]
Static configurable output pin (one may define any number of
sections with an "static_pwm_clock" prefix).
Pins configured here will be set up as clock output pins.
Generally used to provide clock input to other hardware on the board.
```
[static_pwm_clock my_pin]
pin:
# The pin to configure as an output. This parameter must be provided.
#frequency: 100
# Target output frequency.
```
### [pwm_tool]
Pulse width modulation digital output pins capable of high speed
@ -4427,16 +4503,21 @@ prefix).
### [mcp4018]
Statically configured MCP4018 digipot connected via two gpio "bit
banging" pins (one may define any number of sections with an "mcp4018"
prefix).
Statically configured MCP4018 digipot connected via i2c (one may
define any number of sections with an "mcp4018" prefix).
```
[mcp4018 my_digipot]
scl_pin:
# The SCL "clock" pin. This parameter must be provided.
sda_pin:
# The SDA "data" pin. This parameter must be provided.
#i2c_address: 47
# The i2c address that the chip is using on the i2c bus. The default
# is 47.
#i2c_mcu:
#i2c_bus:
#i2c_software_scl_pin:
#i2c_software_sda_pin:
#i2c_speed:
# See the "common I2C settings" section for a description of the
# above parameters.
wiper:
# The value to statically set the given MCP4018 "wiper" to. This is
# typically set to a number between 0.0 and 1.0 with 1.0 being the
@ -4977,8 +5058,8 @@ detection_length: 7.0
# a state change on the switch_pin
# Default is 7 mm.
extruder:
# The name of the extruder section this sensor is associated with.
# This parameter must be provided.
# The name of the extruder or extruder_stepper section this sensor
# is associated with. This parameter must be provided.
switch_pin:
#pause_on_runout:
#runout_gcode:
@ -5210,7 +5291,7 @@ sensor_type:
# load cell will be igfiltered outnored. This option requires the SciPy
# library. Default: None
#buzz_filter_delay: 2
# The delay, or 'order', of the buzz filter. This controle the number of
# The delay, or 'order', of the buzz filter. This controls the number of
# samples required to make a trigger detection. Can be 1 or 2, the default
# is 2.
#notch_filter_frequencies: 50, 60
@ -5344,7 +5425,7 @@ chip: ADS1115
# 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
# The supply voltage for the device. This allows additional software scaling
# for all values read from the ADC.
i2c_mcu: host
i2c_bus: i2c.1
@ -5363,7 +5444,7 @@ 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
# corresponding lines. For example
# DIFF03 measures the differential between line 0 and 3. Only specific
# combinations for the differentials are allowed.
```

View file

@ -90,7 +90,7 @@ later analyzed. To use this feature, Klipper must be started with the
Data logging is enabled with the `data_logger.py` tool. For example:
```
~/klipper/scripts/motan/data_logger.py /tmp/klippy_uds mylog
~/klipper/scripts/motan/data_logger.py /tmp/klippy_uds mylog -s '*'
```
This command will connect to the Klipper API Server, subscribe to

View file

@ -4,8 +4,10 @@ This document describes how to use an
[eddy current](https://en.wikipedia.org/wiki/Eddy_current) inductive
probe in Klipper.
Currently, an eddy current probe can not be used for Z homing. The
sensor can only be used for Z probing.
Currently, an eddy current probe can not precisely home Z (i.e., `G28 Z`).
The sensor can precisely do Z probing (i.e., `PROBE ...`).
Look at the [homing correction](Eddy_Probe.md#homing-correction-macros)
for further details.
Start by declaring a
[probe_eddy_current config section](Config_Reference.md#probe_eddy_current)
@ -24,18 +26,29 @@ named `[probe_eddy_current my_eddy_probe]` then one would run
complete in a few seconds. After it completes, issue a `SAVE_CONFIG`
command to save the results to the printer.cfg and restart.
Eddy current is used as a proximity/distance sensor (similar to a laser ruler).
The second step in calibration is to correlate the sensor readings to
the corresponding Z heights. Home the printer and navigate the
toolhead so that the nozzle is near the center of the bed. Then run an
toolhead so that the nozzle is near the center of the bed. Then run a
`PROBE_EDDY_CURRENT_CALIBRATE CHIP=my_eddy_probe` command. Once the
tool starts, follow the steps described at
["the paper test"](Bed_Level.md#the-paper-test) to determine the
actual distance between the nozzle and bed at the given location. Once
those steps are complete one can `ACCEPT` the position. The tool will
then move the the toolhead so that the sensor is above the point where
the nozzle used to be and run a series of movements to correlate the
then move the toolhead so that the sensor is above the point where the
nozzle used to be and run a series of movements to correlate the
sensor to Z positions. This will take a couple of minutes. After the
tool completes, issue a `SAVE_CONFIG` command to save the results to
tool completes it will output the sensor performance data:
```
probe_eddy_current: noise 0.000642mm, MAD_Hz=11.314 in 2525 queries
Total frequency range: 45000.012 Hz
z_offset: 0.250 # noise 0.000200mm, MAD_Hz=11.000
z_offset: 0.530 # noise 0.000300mm, MAD_Hz=12.000
z_offset: 1.010 # noise 0.000400mm, MAD_Hz=14.000
z_offset: 2.010 # noise 0.000600mm, MAD_Hz=12.000
z_offset: 3.010 # noise 0.000700mm, MAD_Hz=9.000
```
issue a `SAVE_CONFIG` command to save the results to
the printer.cfg and restart.
After initial calibration it is a good idea to verify that the
@ -55,6 +68,133 @@ 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.
## Homing correction macros
Because of current limitations, homing and probing
are implemented differently for the eddy sensors.
As a result, homing suffers from an offset error,
while probing handles this correctly.
To correct the homing offset.
One can use the suggested macro inside the homing override or
inside the starting G-Code.
[Force move](Config_Reference.md#force_move) section
have to be defined in the config.
```
[gcode_macro _RELOAD_Z_OFFSET_FROM_PROBE]
gcode:
{% set Z = printer.toolhead.position.z %}
SET_KINEMATIC_POSITION Z={Z - printer.probe.last_probe_position.z}
[gcode_macro SET_Z_FROM_PROBE]
gcode:
{% set METHOD = params.METHOD | default("automatic") %}
PROBE METHOD={METHOD}
_RELOAD_Z_OFFSET_FROM_PROBE
G0 Z5
```
## Tap calibration
The Eddy probe measures the resonance frequency of the coil.
By the absolute value of the frequency and the calibration curve from
`PROBE_EDDY_CURRENT_CALIBRATE`, it is therefore possible to detect
where the bed is without physical contact.
By use of the same knowledge, we know that frequency changes with
the distance. It is possible to track that change in real time and
detect the time/position where contact happens - a change of frequency
starts to change in a different way.
For example, stopped to change because of the collision.
Because eddy output is not perfect: there is sensor noise,
mechanical oscillation, thermal expansion and other discrepancies,
it is required to calibrate the stop threshold for your machine.
Practically, it ensures that the Eddy's output data absolute value
change per second (velocity) is high enough - higher than the noise level,
and that upon collision it always decreases by at least this value.
```
[probe_eddy_current my_probe]
# eddy probe configuration...
# Recommended starting values for the tap
#samples: 3
#samples_tolerance: 0.025
#samples_tolerance_retries: 3
```
Before setting it to any other value, it is necessary to install `scipy`:
```bash
~/klippy-env/bin/pip install scipy
```
The suggested calibration routine works as follows:
1. Home Z
2. Place the toolhead at the center of the bed.
3. Move the Z axis far away (30 mm, for example).
4. Run `PROBE METHOD=tap`
5. If it stops before colliding, increase the `tap_threshold`.
Repeat until the nozzle softly touches the bed.
This is easier to do with a clean nozzle and
by visually inspecting the process.
You can streamline the process by placing the toolhead in the center once.
Then, upon config restart, trick the machine into thinking that Z is homed.
```
SET_KINEMATIC_POSITION X=<middle> Y=<middle> Z=0
G0 Z5 # Optional retract
PROBE METHOD=tap
```
Here is an example sequence of threshold values to test:
```
1 -> 5 -> 10 -> 20 -> 40 -> 80 -> 160
160 -> 120 -> 100
```
Your value will normally be between those.
- Too high a value leaves a less safe margin for early collision -
if something is between the nozzle and the bed, or if the nozzle
is too close to the bed before the tap.
- Too low - can make the toolhead stop in mid-air
because of the noise.
You can estimate the initial threshold value by analyzing your own
calibration routine output:
```
probe_eddy_current: noise 0.000642mm, MAD_Hz=11.314
...
z_offset: 1.010 # noise 0.000400mm, MAD_Hz=14.000
```
The estimation will be:
```
MAD_Hz * 2
11.314 * 2 = 22.628
```
To further fine tune threshold, one can use `PROBE_ACCURACY METHOD=tap`.
The range is expected to be about 0.02 mm,
with the default probe speed of 5 mm/s.
Elevated coil temperature may increase noise and may require additional tuning.
You can validate the tap precision by measuring the paper thickness
from the initial calibration guide. It is expected to be ~0.1mm.
Tap precision is limited by the sampling frequency and
the speed of the descent.
If you take 24 photos per second of the moving train, you can only estimate
where the train was between photos.
It is possible to reduce the descending speed. It may require decrease of
absolute `tap_threshold` value.
It is possible to tap over non-conductive surfaces as long as there is metal
behind it within the sensor's sensitivity range.
Max distance can be approximated to be about 1.5x of the coil's narrowest part.
## Thermal Drift Calibration
As with all inductive probes, eddy current probes are subject to
@ -144,3 +284,38 @@ to perform thermal 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.
## Errors description
Possible homing errors and actionables:
- Sensor error
- Check logs for detailed error
- Eddy I2C STATUS/DATA error.
- Check loose wiring.
- Try software I2C/decrease I2C rate
- Invalid read data
- Same as I2C
Possible sensor errors and actionables:
- Frequency over valid hard range
- Check frequency configuration
- Hardware fault
- Frequency over valid soft range
- Check frequency configuration
- Conversion Watchdog timeout
- Hardware fault
Amplitude Low/High warning messages can mean:
- Sensor close to the bed
- Sensor far from the bed
- Higher temperature than was at the current calibration
- Capacitor missing
On some sensors, it is not possible to completely avoid amplitude
warning indicator.
You can try to redo the `LDC_CALIBRATE_DRIVE_CURRENT` calibration at work
temperature or increase `reg_drive_current` by 1-2 from the calibrated value.
Generally, it is like an engine check light. It may indicate an issue.

View file

@ -98,17 +98,16 @@ bootloaders.
## Can I run Klipper on something other than a Raspberry Pi 3?
The recommended hardware is a Raspberry Pi 2, Raspberry Pi 3, or
Raspberry Pi 4.
The recommended hardware is a Raspberry Pi Zero2w, Raspberry Pi 3,
Raspberry Pi 4 or Raspberry Pi 5. Klipper will also run on other SBC
devices as well as x86 hardware, as described below.
Klipper will run on a Raspberry Pi 1 and on the Raspberry Pi Zero, but
these boards don't have enough processing power to run OctoPrint
Klipper will run on a Raspberry Pi 1, 2 and on the Raspberry Pi Zero1,
but these boards don't have enough processing power to run Klipper
well. It is common for print stalls to occur on these slower machines
when printing directly from OctoPrint. (The printer may move faster
than OctoPrint can send movement commands.) If you wish to run on one
one of these slower boards anyway, consider using the "virtual_sdcard"
feature when printing (see
[config reference](Config_Reference.md#virtual_sdcard) for details).
when printing (The printer may move faster than Klipper can send
movement commands.) It is not reccomended to run Klipper on these older
machines.
For running on the Beaglebone, see the
[Beaglebone specific installation instructions](Beaglebone.md).

View file

@ -120,7 +120,7 @@ Klipper supports many standard 3d printer features:
* Support for common temperature sensors (eg, common thermistors,
AD595, AD597, AD849x, PT100, PT1000, MAX6675, MAX31855, MAX31856,
MAX31865, BME280, HTU21D, DS18B20, AHT10, SHT3x, and LM75). Custom
MAX31865, BME280, HTU21D, DS18B20, AHT1X, AHT2X, AHT3X, SHT3x, and LM75). Custom
thermistors and custom analog temperature sensors can also be
configured. One can monitor the internal micro-controller
temperature sensor and the internal temperature sensor of a

View file

@ -350,18 +350,25 @@ The following command is available when the
enabled.
#### SET_DUAL_CARRIAGE
`SET_DUAL_CARRIAGE CARRIAGE=<carriage> [MODE=[PRIMARY|COPY|MIRROR]]`:
`SET_DUAL_CARRIAGE CARRIAGE=<carriage> [MODE=[PRIMARY|COPY|MIRROR|INACTIVE]]`:
This command will change the mode of the specified carriage.
If no `MODE` is provided it defaults to `PRIMARY`. `<carriage>` must
reference a defined primary or dual carriage for `generic_cartesian`
kinematics or be 0 (for primary carriage) or 1 (for dual carriage)
for all other kinematics supporting IDEX. Setting the mode to `PRIMARY`
deactivates the other carriage and makes the specified carriage execute
subsequent G-Code commands as-is. `COPY` and `MIRROR` modes are supported
only for dual carriages. When set to either of these modes, dual carriage
will then track the subsequent moves of its primary carriage and either
copy relative movements of it (in `COPY` mode) or execute them in the
opposite (mirror) direction (in `MIRROR` mode).
deactivates all other carriages on the same axis and makes the specified
carriage execute subsequent G-Code movement commands as-is. Before activating
`COPY` or `MIRROR` mode for a carriage, a different one must be activated as
`PRIMARY` on the same axis. When set to either of these two modes, the carriage
will track the subsequent G-Code moves and either copy relative movements
(in `COPY` mode) or execute them in the opposite (mirror) direction (in
`MIRROR` mode). Setting the mode to `INACTIVE` deactivates the carriage and
makes it ignore further G-Code moves. Note that deactivating the primary
carriage on the axis does not disable other carriages working in `COPY` or
`MIRROR` mode, which can be used to disable printing a failed part by any of
the tools and park that tool to prevent collisions with an unfinished part, see
this [sample configuration](../config/sample-corexyuv.cfg) for macros examples.
#### SAVE_DUAL_CARRIAGE_STATE
`SAVE_DUAL_CARRIAGE_STATE [NAME=<state_name>]`: Save the current positions
@ -372,14 +379,18 @@ to the given string. If NAME is not provided it defaults to "default".
#### RESTORE_DUAL_CARRIAGE_STATE
`RESTORE_DUAL_CARRIAGE_STATE [NAME=<state_name>] [MOVE=[0|1] [MOVE_SPEED=<speed>]]`:
Restore the previously saved positions of the dual carriages and their modes,
unless "MOVE=0" is specified, in which case only the saved modes will be
restored, but not the positions of the carriages. If positions are being
restored and "MOVE_SPEED" is specified, then the toolhead moves will be
performed with the given speed (in mm/s); otherwise the toolhead move will
use the rail homing speed. Note that the carriages restore their positions
only over their own axis, which may be necessary to correctly restore COPY
and MIRROR mode of the dual carraige.
Restore the previously saved states of all dual and their primary carriages.
This command restores the modes of the carriages and moves them to their
previously saved positions, unless "MOVE=0" is specified. If positions are being
restored and "MOVE_SPEED" is specified, then the carriages will move with at
most the provided speed (in mm/s); otherwise the homing speeds of the
corresponding carriages will be used as a reference. Note that the carriages
restore their positions only over their own axes, which may be necessary to
correctly restore COPY and MIRROR mode of the dual carriage. In addition, this
command updates the Klipper toolhead position for each axis that has some dual
carriages: it is set to match the actual position of the activated primary
carriage of an axis or, if an axis does not have a saved primary carriage,
to the axis position when `SAVE_DUAL_CARRIAGE_STATE` command was called.
### [endstop_phase]
@ -500,7 +511,7 @@ 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>
`SET_FAN_SPEED FAN=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
@ -760,11 +771,11 @@ stepper at a time, some sequences of changes can lead to invalid
intermediate kinematic configurations, even if the final configuration
is valid. In such cases a user can pass `DISABLE_CHECKS=1` parameters to
all but the last command to disable intermediate checks. For example,
if `stepper a` and `stepper b` initially have `x-y` and `x+y` carriages
correspondingly, then the following sequence of commands will let a user
effectively swap the carriage controls:
`SET_STEPPER_CARRIAGES STEPPER=a CARRIAGES=x+y DISABLE_CHECKS=1`
and `SET_STEPPER_CARRIAGES STEPPER=b CARRIAGES=x-y`, while
if `stepper a` and `stepper b` initially have `carriage_x-carriage_y` and
`carriage_x+carriage_y` carriages correspondingly, then the following
sequence of commands will let a user effectively swap the carriage controls:
`SET_STEPPER_CARRIAGES STEPPER=a CARRIAGES=carriage_x+carriage_y DISABLE_CHECKS=1`
and `SET_STEPPER_CARRIAGES STEPPER=b CARRIAGES=carriage_x-carriage_y`, while
still validating the final kinematics state.
### [hall_filament_width_sensor]
@ -835,15 +846,17 @@ been enabled (also see the
#### SET_INPUT_SHAPER
`SET_INPUT_SHAPER [SHAPER_FREQ_X=<shaper_freq_x>]
[SHAPER_FREQ_Y=<shaper_freq_y>] [DAMPING_RATIO_X=<damping_ratio_x>]
[DAMPING_RATIO_Y=<damping_ratio_y>] [SHAPER_TYPE=<shaper>]
[SHAPER_TYPE_X=<shaper_type_x>] [SHAPER_TYPE_Y=<shaper_type_y>]`:
[SHAPER_FREQ_Y=<shaper_freq_y>] [SHAPER_FREQ_Y=<shaper_freq_z>]
[DAMPING_RATIO_X=<damping_ratio_x>] [DAMPING_RATIO_Y=<damping_ratio_y>]
[DAMPING_RATIO_Z=<damping_ratio_z>] [SHAPER_TYPE=<shaper>]
[SHAPER_TYPE_X=<shaper_type_x>] [SHAPER_TYPE_Y=<shaper_type_y>]
[SHAPER_TYPE_Z=<shaper_type_z>]`:
Modify input shaper parameters. Note that SHAPER_TYPE parameter resets
input shaper for both X and Y axes even if different shaper types have
input shaper for all axes even if different shaper types have
been configured in [input_shaper] section. SHAPER_TYPE cannot be used
together with either of SHAPER_TYPE_X and SHAPER_TYPE_Y parameters.
See [config reference](Config_Reference.md#input_shaper) for more
details on each of these parameters.
together with any of SHAPER_TYPE_X, SHAPER_TYPE_Y, and SHAPER_TYPE_Z
parameters. See [config reference](Config_Reference.md#input_shaper)
for more details on each of these parameters.
### [led]
@ -924,10 +937,27 @@ is calibrated a force in grams is also reported.
### [load_cell_probe]
The following commands are enabled if a
The commands below are enabled if a
[load_cell config section](Config_Reference.md#load_cell_probe) has been
enabled.
In addition, commands that perform probes, such as [`PROBE`](#probe),
[`PROBE_ACCURACY`](#probe_accuracy),
[`BED_MESH_CALIBRATE`](#bed_mesh_calibrate) etc. will accept
additional parameters if a `[load_cell_probe]` is defined. The
parameters override the corresponding settings from the
[`[load_cell_probe]`](./Config_Reference.md#load_cell_probe)
configuration:
- `FORCE_SAFETY_LIMIT=<grams>`
- `TRIGGER_FORCE=<grams>`
- `DRIFT_FILTER_CUTOFF_FREQUENCY=<frequency_hz>`
- `DRIFT_FILTER_DELAY=<1|2>`
- `BUZZ_FILTER_CUTOFF_FREQUENCY=<frequency_hz>`
- `BUZZ_FILTER_DELAY=<1|2>`
- `NOTCH_FILTER_FREQUENCIES=<list of frequency_hz>`
- `NOTCH_FILTER_QUALITY=<quality>`
- `TARE_TIME=<seconds>`
### LOAD_CELL_TEST_TAP
`LOAD_CELL_TEST_TAP [TAPS=<taps>] [TIMEOUT=<timeout>]`: Run a testing routine
that reports taps on the load cell. The toolhead will not move but the load cell
@ -938,23 +968,6 @@ QUERY_ENDSTOPS and QUERY_PROBE for load cell probes.
- `TIMEOOUT`: the time, in seconds, that the tool waits for each tab before
aborting.
### Load Cell Command Extensions
Commands that perform probes, such as [`PROBE`](#probe),
[`PROBE_ACCURACY`](#probe_accuracy),
[`BED_MESH_CALIBRATE`](#bed_mesh_calibrate) etc. will accept additional
parameters if a `[load_cell_probe]` is defined. The parameters override the
corresponding settings from the
[`[load_cell_probe]`](./Config_Reference.md#load_cell_probe) configuration:
- `FORCE_SAFETY_LIMIT=<grams>`
- `TRIGGER_FORCE=<grams>`
- `DRIFT_FILTER_CUTOFF_FREQUENCY=<frequency_hz>`
- `DRIFT_FILTER_DELAY=<1|2>`
- `BUZZ_FILTER_CUTOFF_FREQUENCY=<frequency_hz>`
- `BUZZ_FILTER_DELAY=<1|2>`
- `NOTCH_FILTER_FREQUENCIES=<list of frequency_hz>`
- `NOTCH_FILTER_QUALITY=<quality>`
- `TARE_TIME=<seconds>`
### [manual_probe]
The manual_probe module is automatically loaded.
@ -994,22 +1007,34 @@ enabled.
#### MANUAL_STEPPER
`MANUAL_STEPPER STEPPER=config_name [ENABLE=[0|1]]
[SET_POSITION=<pos>] [SPEED=<speed>] [ACCEL=<accel>] [MOVE=<pos>
[STOP_ON_ENDSTOP=[1|2|-1|-2]] [SYNC=0]]`: This command will alter the
state of the stepper. Use the ENABLE parameter to enable/disable the
stepper. Use the SET_POSITION parameter to force the stepper to think
it is at the given position. Use the MOVE parameter to request a
movement to the given position. If SPEED and/or ACCEL is specified
then the given values will be used instead of the defaults specified
in the config file. If an ACCEL of zero is specified then no
acceleration will be performed. If STOP_ON_ENDSTOP=1 is specified then
the move will end early should the endstop report as triggered (use
STOP_ON_ENDSTOP=2 to complete the move without error even if the
endstop does not trigger, use -1 or -2 to stop when the endstop
reports not triggered). Normally future G-Code commands will be
scheduled to run after the stepper move completes, however if a manual
stepper move uses SYNC=0 then future G-Code movement commands may run
in parallel with the stepper movement.
[SET_POSITION=<pos>] [SPEED=<speed>] [ACCEL=<accel>] [MOVE=<pos>]
[SYNC=0]]`: This command will alter the state of the stepper. Use the
ENABLE parameter to enable/disable the stepper. Use the SET_POSITION
parameter to force the stepper to think it is at the given
position. Use the MOVE parameter to request a movement to the given
position. If SPEED and/or ACCEL is specified then the given values
will be used instead of the defaults specified in the config file. If
an ACCEL of zero is specified then no acceleration will be
performed. Normally future G-Code commands will be scheduled to run
after the stepper move completes, however if a manual stepper move
uses SYNC=0 then future G-Code movement commands may run in parallel
with the stepper movement.
`MANUAL_STEPPER STEPPER=config_name [SPEED=<speed>] [ACCEL=<accel>]
MOVE=<pos> STOP_ON_ENDSTOP=<check_type>`: If STOP_ON_ENDSTOP is
specified then the move will end early if an endstop event occurs. The
`STOP_ON_ENDSTOP` parameter may be set to one of the following values:
* `probe`: The movement will stop when the endstop reports triggered.
* `home`: The movement will stop when the endstop reports triggered
and the final position of the manual_stepper will be set such that
the trigger position matches the position specified in the `MOVE`
parameter.
* `inverted_probe`, `inverted_home`: As above, however, the movement
will stop when the endstop reports it is in a non-triggered state.
* `try_probe`, `try_inverted_probe`, `try_home`, `try_inverted_home`:
As above, but no error will be reported if the movement fully
completes without an endstop event stopping the move early.
`MANUAL_STEPPER STEPPER=config_name GCODE_AXIS=[A-Z]
[LIMIT_VELOCITY=<velocity>] [LIMIT_ACCEL=<accel>]
@ -1200,10 +1225,39 @@ Requires a `SAVE_CONFIG` to take effect.
### [probe_eddy_current]
The following commands are available when a
The commands below are available when a
[probe_eddy_current config section](Config_Reference.md#probe_eddy_current)
is enabled.
In addition, commands that perform probes, such as [`PROBE`](#probe),
[`PROBE_ACCURACY`](#probe_accuracy),
[`BED_MESH_CALIBRATE`](#bed_mesh_calibrate) etc. will accept
additional parameters if a `[probe_eddy_current]` section is defined:
- `METHOD=<scan|rapid_scan|tap>`: This alters the probing mechanism:
- `METHOD=scan`: The toolhead does not descend. Instead the toolhead
will pause briefly above each target location and return the
measured height at that position.
- `METHOD=rapid_scan`: The toolhead does not descend and does not
pause at each target location. The value returned is the measured
height around the time that the toolhead was near each target
position.
- `METHOD=tap`: The toolhead will descend until the nozzle makes
contact with the bed. This method is only available if
`tap_threshold` is specified in the `[probe_eddy_current]` config
section.
- default: If no `METHOD` parameter is specified then the default
behavior is for the toolhead to descend until the sensor detects
that the distance to the bed is at or below the `z_offset`
parameter specified in the `[probe_eddy_current]` config section.
- `SAMPLE_TIME=<time>`: When using `METHOD=scan` probing, this
specifies the time (in seconds) to pause at each target point. When
using `METHOD=rapid_scan` this specifies the measurement time window
at each target. If not specified, the default is 0.100 (which is
100ms).
- `TAP_THRESHOLD=<value>`: This overrides the `tap_threshold`
specified in the `[probe_eddy_current]` config section when probing
using `METHOD=tap`.
#### PROBE_EDDY_CURRENT_CALIBRATE
`PROBE_EDDY_CURRENT_CALIBRATE CHIP=<config_name>`: This starts a tool
that calibrates the sensor resonance frequencies to corresponding Z
@ -1291,13 +1345,14 @@ all enabled accelerometer chips.
[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
the respective axis. "axis" can either be X, Y or Z, or specify an
arbitrary direction as `AXIS=dx,dy[,dz]`, where dx, dy, dz 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. `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)
`AXIS=1,-1` to define a diagonal direction in XY plane, or `AXIS=0,1,1`
to define a direction in YZ plane). Note that `AXIS=dx,dy` 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

View file

@ -36,7 +36,7 @@ Things you can check with this data:
* 'Unique values' should be a large percentage of the 'Samples
Collected' value. If 'Unique values' is 1 it is very likely a wiring issue.
* Tap or push on the sensor while `LOAD_CELL_DIAGNOSTIC` runs. If
things are working correctly ths should increase the 'Sample range'.
things are working correctly this should increase the 'Sample range'.
## Calibrating a Load Cell
@ -189,7 +189,7 @@ Multiple cycles of this will result in ever-increasing force on the toolhead.
`force_safety_limit` stops this cycle from running out of control.
Another way this run-away can happen is damage to a strain gauge. If the metal
part is permanently bent it wil change the `reference_tare_counts` of the
part is permanently bent it will change the `reference_tare_counts` of the
device. This puts the starting tare value much closer to the limit making it
more likely to be violated. You want to be notified if this is happening
because your hardware has been permanently damaged.
@ -252,12 +252,12 @@ macro. This requires setting up
Here is a simple macro that can accomplish this. Note that the
`_HOME_Z_FROM_LAST_PROBE` macro has to be separate because of the way macros
work. The sub-call is needed so that the `_HOME_Z_FROM_LAST_PROBE` macro can
see the result of the probe in `printer.probe.last_z_result`.
see the result of the probe in `printer.probe.last_probe_position`.
```gcode
[gcode_macro _HOME_Z_FROM_LAST_PROBE]
gcode:
{% set z_probed = printer.probe.last_z_result %}
{% set z_probed = printer.probe.last_probe_position.z %}
{% set z_position = printer.toolhead.position[2] %}
{% set z_actual = z_position - z_probed %}
SET_KINEMATIC_POSITION Z={z_actual}

View file

@ -152,7 +152,7 @@ Recommended connection scheme for I2C on the Raspberry Pi:
| SDA | 03 | GPIO02 (SDA1) |
| SCL | 05 | GPIO03 (SCL1) |
The RPi has buit-in 1.8K pull-ups on both SCL and SDA.
The RPi has built-in 1.8K pull-ups on both SCL and SDA.
![MPU-9250 connected to Pi](img/mpu9250-PI-fritzing.png)
@ -714,6 +714,95 @@ 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.
### Measuring the resonances of Z axis
Measuring the resonances of Z axis is similar in many aspects to measuring
resonances of X and Y axes, with some subtle differences. Similarly to other
axes measurements, you will need to have an accelerometer mounted on the
moving parts of Z axis - either the bed itself (if the bed moves over Z axis),
or the toolhead (if the toolhead/gantry moves over Z). You will need to
add the appropriate chip configuration to `printer.cfg` and also add it to
`[resonance_tester]` section, e.g.
```
[resonance_tester]
accel_chip_z: <accelerometer full name>
```
Also make sure that `probe_points` configured in `[resonance_tester]` allow
sufficient clearance for Z axis movements (20 mm above bed surface should
provide enough clearance with the default test parameters).
The next consideration is that Z axis can typically reach lower maximum
speeds and accelerations that X and Y axes. Default parameters of the test
take that into consideration and are much less agressive, but it may still
be necessary to increase `max_z_accel` and `max_z_velocity`. If you have
them configured in `[printer]` section, make sure to set them to at least
```
[printer]
max_z_velocity: 20
max_z_accel: 1550
```
but only for the duration of the test, afterwards you can revert them back
to their original values if necessary. And if you use custom test parameters
for Z axis, `TEST_RESONANCES` and `SHAPER_CALIBRATE` will provide the minimum
required limits if necessary for your specific case.
After all changes to `printer.cfg` have been made, restart Klipper and run
either
```
TEST_RESONANCES AXIS=Z
```
or
```
SHAPER_CALIBRATE AXIS=Z
```
and proceed from there accordingly how you would for other axes.
For example, after `TEST_RESONANCES` command you can run
`calibrate_shaper.py` script and get shaper recommendations and
the chart of resonance response:
![Resonances](img/calibrate-z.png)
After the calibration, the shaper parameters can be stored in the
`printer.cfg`, e.g. from the example above:
```
[input_shaper]
...
shaper_type_z: mzv
shaper_freq_z: 42.6
```
Also, given the movements of Z axis are slow, you can easily consider
more aggressive input shapers, e.g.
```
[input_shaper]
...
shaper_type_z: 2hump_ei
shaper_freq_z: 63.0
```
If the test produces bogus results, you may try to increase
`accel_per_hz_z` parameter in `[resonance_tester]` from its
default value 15 to a larger value in the range of 20-30, e.g.
```
[resonance_tester]
accel_per_hz_z: 25
```
and repeat the test. Increasing this value will likely require
increasing `max_z_accel` and `max_z_velocity` parameters as well.
You can run `TEST_RESONANCES AXIS=Z` command to get the required
minimum values.
However, if you are unable to measure the resonances of Z axis,
you can consider just using
```
[input_shaper]
...
shaper_type_z: 3hump_ei
shaper_freq_z: 65
```
as an acceptable all-round choice, given that the smoothing of
Z axis movements is not of particular concerns.
### Unreliable measurements of resonance frequencies
Sometimes the resonance measurements can produce bogus results, leading to

View file

@ -438,10 +438,15 @@ gcode:
SET_DUAL_CARRIAGE CARRIAGE=0
SET_INPUT_SHAPER SHAPER_TYPE_X=<primary_carriage_shaper> SHAPER_FREQ_X=<primary_carriage_freq> SHAPER_TYPE_Y=<y_shaper> SHAPER_FREQ_Y=<y_freq>
```
However, users of `generic_cartesian` kinematics should specify carriage names
in `CARRIAGE=` parameters of `SET_DUAL_CARRIAGE` instead of their numbers.
Note that `SHAPER_TYPE_Y` and `SHAPER_FREQ_Y` should be the same in both
commands. It is also possible to put a similar snippet into the start g-code
in the slicer, however then the shaper will not be enabled until any print
is started.
commands. If you need to configure an input shaper for Z axis, include
its parameters in both `SET_INPUT_SHAPER` commands.
Besides `delayed_gcode`, it is also possible to put a similar snippet into
the start g-code in the slicer, however then the shaper will not be enabled
until any print is started.
Note that the input shaper only needs to be configured once. Subsequent changes
of the carriages or their modes via `SET_DUAL_CARRIAGE` command will preserve
@ -453,15 +458,40 @@ No, `input_shaper` feature has pretty much no impact on the print times by
itself. However, the value of `max_accel` certainly does (tuning of this
parameter described in [this section](#selecting-max_accel)).
### Should I enable and tune input shaper for Z axis?
Most of the users are not likely to see improvements in the quality of
the prints directly, much unlike X and Y shapers. However, users of
delta printers, printers with flying gantry, or printers with heavy
moving beds may be able to increase the `max_z_accel` and `max_z_velocity`
kinematics limits and thus get faster Z movements. This can be especially
useful e.g. for toolchangers, but also when Z-hops are enabled in slicer.
And in general, after enabling Z input shaper many users will hear that
Z axis operates more smoothly, which may increase the comfort of printer
operation, and may somewhat extend lifespan of Z axis parts.
## Technical details
### Input shapers
Input shapers used in Klipper are rather standard, and one can find more
in-depth overview in the articles describing the corresponding shapers.
This section contains a brief overview of some technical aspects of the
supported input shapers. The table below shows some (usually approximate)
parameters of each shaper.
supported input shapers. Input shapers used in Klipper are rather standard,
with the exception of MZV, and one can find more in-depth overview in
the articles describing the corresponding shapers.
MZV stands for a Modified-ZV input shaper. The classic definition of ZV shaper
assumes the duration Ts equal to 1/2 of the damped period of oscillations Td and
has two pulses. However, ZV input shaper has a generalized form for an arbitrary
duration in the range (0, Td] with three pulses (Specified-Duration ZV, see also
SNA-ZV), with a negative middle pulse if Ts < Td and a positive one if Ts > Td.
The MZV shaper was designed as an intermediate shaper between ZV and ZVD,
offering better vibrations suppression than ZV when the determined (measured)
shaper parameters deviate from the ones actually required by the printer,
and smaller smoothing than ZVD. Effectively, it is a SD-ZV shaper with the
specific duration Ts = 3/4 Td, exactly between ZV (Ts = 1/2 Td) and
ZVD (Ts = Td), and it happens to work well for many real-life 3D printers.
The table below shows some (usually approximate) parameters of each shaper.
| Input <br> shaper | Shaper <br> duration | Vibration reduction 20x <br> (5% vibration tolerance) | Vibration reduction 10x <br> (10% vibration tolerance) |
|:--:|:--:|:--:|:--:|
@ -469,8 +499,8 @@ parameters of each shaper.
| MZV | 0.75 / shaper_freq | ± 4% shaper_freq | -10%...+15% shaper_freq |
| ZVD | 1 / shaper_freq | ± 15% shaper_freq | ± 22% shaper_freq |
| EI | 1 / shaper_freq | ± 20% shaper_freq | ± 25% shaper_freq |
| 2HUMP_EI | 1.5 / shaper_freq | ± 35% shaper_freq | ± 40 shaper_freq |
| 3HUMP_EI | 2 / shaper_freq | -45...+50% shaper_freq | -50%...+55% shaper_freq |
| 2HUMP_EI | 1.5 / shaper_freq | -40...+45% shaper_freq | -45..+50% shaper_freq |
| 3HUMP_EI | 2 / shaper_freq | -50...+60% shaper_freq | -55%...+65% shaper_freq |
A note on vibration reduction: the values in the table above are approximate.
If the damping ratio of the printer is known for each axis, the shaper can be
@ -502,11 +532,11 @@ so the values for 10% vibration tolerance are provided only for the reference.
resonances at 35 Hz and 60 Hz on the same axis: a) EI shaper needs to have
shaper_freq = 35 / (1 - 0.2) = 43.75 Hz, and it will reduce resonances
until 43.75 * (1 + 0.2) = 52.5 Hz, so it is not sufficient; b) 2HUMP_EI
shaper needs to have shaper_freq = 35 / (1 - 0.35) = 53.85 Hz and will
reduce vibrations until 53.85 * (1 + 0.35) = 72.7 Hz - so this is an
shaper needs to have shaper_freq = 35 / (1 - 0.4) = 58.3 Hz and will
reduce vibrations until 58.3 * (1 + 0.45) = 84.5 Hz - so this is an
acceptable configuration. Always try to use as high shaper_freq as possible
for a given shaper (perhaps with some safety margin, so in this example
shaper_freq ≈ 50-52 Hz would work best), and try to use a shaper with as
shaper_freq ≈ 55 Hz would work best), and try to use a shaper with as
small shaper duration as possible.
* If one needs to reduce vibrations at several very different frequencies
(say, 30 Hz and 100 Hz), they may see that the table above does not provide

View file

@ -31,7 +31,7 @@ AD do not include the flats on the corners that some test objects provide.
## Configure your skew
Make sure `[skew_correction]` is in printer.cfg. You may now use the `SET_SKEW`
gcode to configure skew_correcton. For example, if your measured lengths
gcode to configure skew_correction. For example, if your measured lengths
along XY are as follows:
```

View file

@ -121,5 +121,5 @@ M104 S0
before the macro call. Also note that SuperSlicer has a
"custom gcode only" button option, which achieves the same outcome.
An example of a START_PRINT macro using these paramaters can
An example of a START_PRINT macro using these parameters can
be found in config/sample-macros.cfg

View file

@ -214,17 +214,16 @@ The following information is available in the `gcode_move` object
(this object is always available):
- `gcode_position`: The current position of the toolhead relative to
the current G-Code origin. That is, positions that one might
directly send to a `G1` command. It is possible to access the x, y,
z, and e components of this position (eg, `gcode_position.x`).
directly send to a `G1` command. This value is encoded as a
[coordinate](#accessing-coordinates).
- `position`: The last commanded position of the toolhead using the
coordinate system specified in the config file. It is possible to
access the x, y, z, and e components of this position (eg,
`position.x`).
coordinate system specified in the config file. This value is
encoded as a [coordinate](#accessing-coordinates).
- `homing_origin`: The origin of the gcode coordinate system (relative
to the coordinate system specified in the config file) to use after
a `G28` command. The `SET_GCODE_OFFSET` command can alter this
position. It is possible to access the x, y, and z components of
this position (eg, `homing_origin.x`).
position. This value is encoded as a
[coordinate](#accessing-coordinates).
- `speed`: The last speed set in a `G1` command (in mm/s).
- `speed_factor`: The "speed factor override" as set by an `M220`
command. This is a floating point value such that 1.0 means no
@ -236,6 +235,10 @@ The following information is available in the `gcode_move` object
coordinate mode or False if in `G91` relative mode.
- `absolute_extrude`: This returns True if in `M82` absolute extrude
mode or False if in `M83` relative mode.
- `axis_map`: Provides a mechanism for finding the coordinate
component for a given G-Code id that is used in `G1` commands. See
the [Accessing Coordinates](#accessing-coordinates) section for
details.
## hall_filament_width_sensor
@ -291,6 +294,9 @@ is always available):
- `printing_time`: The amount of time (in seconds) the printer has
been in the "Printing" state (as tracked by the idle_timeout
module).
- `idle_timeout`: The current 'timeout' (in seconds)
to wait for the gcode to be triggered.
(as set by [SET_IDLE_TIMEOUT](G-Codes.md#set_idle_timeout))
## led
@ -300,7 +306,7 @@ The following information is available for each `[led led_name]`,
- `color_data`: A list of color lists containing the RGBW values for a
led in the chain. Each value is represented as a float from 0.0 to
1.0. Each color list contains 4 items (red, green, blue, white) even
if the underyling LED supports fewer color channels. For example,
if the underlying LED supports fewer color channels. For example,
the blue value (3rd item in color list) of the second neopixel in a
chain could be accessed at
`printer["neopixel <config_name>"].color_data[1][2]`.
@ -358,7 +364,8 @@ The following information is available in the `motion_report` object
(this object is automatically available if any stepper config section
is defined):
- `live_position`: The requested toolhead position interpolated to the
current time.
current time. This value is encoded as a
[coordinate](#accessing-coordinates).
- `live_velocity`: The requested toolhead velocity (in mm/s) at the
current time.
- `live_extruder_velocity`: The requested extruder velocity (in mm/s)
@ -412,10 +419,18 @@ is defined):
during the last QUERY_PROBE command. Note, if this is used in a
macro, due to the order of template expansion, the QUERY_PROBE
command must be run prior to the macro containing this reference.
- `last_z_result`: Returns the Z result value of the last PROBE
command. Note, if this is used in a macro, due to the order of
template expansion, the PROBE (or similar) command must be run prior
to the macro containing this reference.
- `last_probe_position`: The results of the last `PROBE` command. This
value is encoded as a [coordinate](#accessing-coordinates). The
probe hardware estimates that if one were to command the toolhead to
XY position `last_probe_position.x`,`last_probe_position.y` and
descend then the tip of the toolhead would first contact the bed at
a Z height of `last_probe_position.z`. These coordinates are
relative to the frame (that is, they use the coordinate system
specified in the config file). Note, if this is used in a macro,
due to the order of template expansion, the `PROBE` command must be
run prior to the macro containing this reference.
- `last_z_result`: This value is deprecated; it will be removed in the
near future.
## pwm_cycle_time
@ -547,9 +562,8 @@ objects (eg, `[tmc2208 stepper_x]`):
The following information is available in the `toolhead` object
(this object is always available):
- `position`: The last commanded position of the toolhead relative to
the coordinate system specified in the config file. It is possible
to access the x, y, z, and e components of this position (eg,
`position.x`).
the coordinate system specified in the config file. This value is
encoded as a [coordinate](#accessing-coordinates).
- `extruder`: The name of the currently active extruder. For example,
in a macro one could use `printer[printer.toolhead.extruder].target`
to get the target temperature of the current extruder.
@ -557,8 +571,8 @@ The following information is available in the `toolhead` object
"homed" state. This is a string containing one or more of "x", "y",
"z".
- `axis_minimum`, `axis_maximum`: The axis travel limits (mm) after
homing. It is possible to access the x, y, z components of this
limit value (eg, `axis_minimum.x`, `axis_maximum.z`).
homing. This value is encoded as a
[coordinate](#accessing-coordinates).
- For Delta printers the `cone_start_z` is the max z height at
maximum radius (`printer.toolhead.cone_start_z`).
- `max_velocity`, `max_accel`, `minimum_cruise_ratio`,
@ -568,6 +582,10 @@ The following information is available in the `toolhead` object
- `stalls`: The total number of times (since the last restart) that
the printer had to be paused because the toolhead moved faster than
moves could be read from the G-Code input.
- `extra_axes`: Provides a mechanism for finding the coordinate
component for extra axes available in standard `G1` type move
commands. See the [Accessing Coordinates](#accessing-coordinates)
section for details.
## dual_carriage
@ -624,3 +642,29 @@ The following information is available in the `z_tilt` object (this
object is available if z_tilt is defined):
- `applied`: True if the z-tilt leveling process has been run and completed
successfully.
## Accessing Coordinates
Some status fields provide a "coordinate". For macro users these
fields may be accessed by component name
(eg,`{printer.toolhead.position.x}`), where the component name may be
"x", "y", or "z".
For developers using the Klipper API Server these fields are
transmitted as a list - for example: `{"toolhead": {"position": [1.0,
2.0, 3.0, 7.3, 19.2]}}` . The first three components of the list
correspond with the x, y, and z axes.
A coordinate will typically have at least 3 components (x, y, and z),
however there may also be additional components. Care should be taken
when accessing any of these additional components as the ordering and
number of components may change at run-time.
One may use `{printer.gcode_move.axis_map}` and/or
`{printer.toolhead.extra_axes}` to determine the number of components
and the ordering of components. For example, to access the "E"
component one could use
`{printer.toolhead.position[printer.gcode_move.axis_map.E]}`. Or, if
one wanted to find the component associated with the "extruder"
object, one could use
`{printer.toolhead.position[printer.toolhead.extra_axes.extruder]}`.

View file

@ -1,7 +1,8 @@
#!/bin/bash
# This script extracts the Klipper translations and builds multiple
# mdocs sites - one for each supported language. See the README file
# for additional details.
# This script creates the main klipper3d.org website hosted on github.
# It extracts the Klipper translations and builds multiple mdocs sites
# - one for each supported language. See the README file for
# additional details.
MKDOCS_DIR="docs/_klipper3d/"
WORK_DIR="work/"
@ -22,6 +23,11 @@ done < <(egrep -v '^ *(#|$)' ${TRANS_FILE})
echo "building site for en"
mkdocs build -f ${MKDOCS_MAIN}
# Cleanup files (mkdocs copies _klipper3d dir and its sitemap doesn't work)
rm -rf ${PWD}/site/_klipper3d/__pycache__
find ${PWD}/site/_klipper3d ! -name '*.css' -type f -delete
rm ${PWD}/site/sitemap.xml ${PWD}/site/sitemap.xml.gz
# Build each additional language website
while IFS="," read dirname langsite langdesc langsearch; do
new_docs_dir="${WORK_DIR}lang/${langsite}/docs/"
@ -81,4 +87,10 @@ while IFS="," read dirname langsite langdesc langsearch; do
mkdir -p "${PWD}/site/${langsite}/"
ln -sf "${PWD}/site/${langsite}/" "${WORK_DIR}lang/${langsite}/site"
mkdocs build -f "${new_mkdocs_file}"
# Cleanup files (mkdocs copies _klipper3d dir and its sitemap doesn't work)
rm -rf "${PWD}/site/${langsite}/_klipper3d/__pycache__"
find "${PWD}/site/${langsite}/_klipper3d" ! -name '*.css' -type f -delete
rm "${PWD}/site/${langsite}/sitemap.xml" "${PWD}/site/${langsite}/sitemap.xml.gz"
done < <(egrep -v '^ *(#|$)' ${TRANS_FILE})

BIN
docs/img/calibrate-z.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View file

@ -17,16 +17,16 @@ COMPILE_ARGS = ("-Wall -g -O2 -shared -fPIC"
" -o %s %s")
SSE_FLAGS = "-mfpmath=sse -msse2"
SOURCE_FILES = [
'pyhelper.c', 'serialqueue.c', 'stepcompress.c', 'itersolve.c', 'trapq.c',
'pollreactor.c', 'msgblock.c', 'trdispatch.c',
'pyhelper.c', 'serialqueue.c', 'stepcompress.c', 'steppersync.c',
'itersolve.c', 'trapq.c', 'pollreactor.c', 'msgblock.c', 'trdispatch.c',
'kin_cartesian.c', 'kin_corexy.c', 'kin_corexz.c', 'kin_delta.c',
'kin_deltesian.c', 'kin_polar.c', 'kin_rotary_delta.c', 'kin_winch.c',
'kin_extruder.c', 'kin_shaper.c', 'kin_idex.c', 'kin_generic.c'
]
DEST_LIB = "c_helper.so"
OTHER_FILES = [
'list.h', 'serialqueue.h', 'stepcompress.h', 'itersolve.h', 'pyhelper.h',
'trapq.h', 'pollreactor.h', 'msgblock.h'
'list.h', 'serialqueue.h', 'stepcompress.h', 'steppersync.h',
'itersolve.h', 'pyhelper.h', 'trapq.h', 'pollreactor.h', 'msgblock.h'
]
defs_stepcompress = """
@ -36,48 +36,57 @@ defs_stepcompress = """
int step_count, interval, add;
};
struct stepcompress *stepcompress_alloc(uint32_t oid);
void stepcompress_fill(struct stepcompress *sc, uint32_t max_error
, int32_t queue_step_msgtag, int32_t set_next_step_dir_msgtag);
void stepcompress_fill(struct stepcompress *sc, uint32_t oid
, uint32_t max_error, int32_t queue_step_msgtag
, int32_t set_next_step_dir_msgtag);
void stepcompress_set_invert_sdir(struct stepcompress *sc
, uint32_t invert_sdir);
void stepcompress_free(struct stepcompress *sc);
int stepcompress_reset(struct stepcompress *sc, uint64_t last_step_clock);
int stepcompress_set_last_position(struct stepcompress *sc
, uint64_t clock, int64_t last_position);
int64_t stepcompress_find_past_position(struct stepcompress *sc
, uint64_t clock);
int stepcompress_queue_msg(struct stepcompress *sc
, uint32_t *data, int len);
int stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock
, uint32_t *data, int len);
int stepcompress_extract_old(struct stepcompress *sc
, struct pull_history_steps *p, int max
, uint64_t start_clock, uint64_t end_clock);
"""
struct steppersync *steppersync_alloc(struct serialqueue *sq
, struct stepcompress **sc_list, int sc_num, int move_num);
void steppersync_free(struct steppersync *ss);
defs_steppersync = """
struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se);
void syncemitter_set_stepper_kinematics(struct syncemitter *se
, struct stepper_kinematics *sk);
struct stepper_kinematics *syncemitter_get_stepper_kinematics(
struct syncemitter *se);
void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock
, uint32_t *data, int len);
struct syncemitter *steppersync_alloc_syncemitter(struct steppersync *ss
, char name[16], int alloc_stepcompress);
void steppersync_setup_movequeue(struct steppersync *ss
, struct serialqueue *sq, int move_num);
void steppersync_set_time(struct steppersync *ss
, double time_offset, double mcu_freq);
int steppersync_flush(struct steppersync *ss, uint64_t move_clock
, uint64_t clear_history_clock);
struct steppersyncmgr *steppersyncmgr_alloc(void);
void steppersyncmgr_free(struct steppersyncmgr *ssm);
struct steppersync *steppersyncmgr_alloc_steppersync(
struct steppersyncmgr *ssm);
int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm
, double flush_time, double gen_steps_time, double clear_history_time);
"""
defs_itersolve = """
int32_t itersolve_generate_steps(struct stepper_kinematics *sk
, double flush_time);
double itersolve_check_active(struct stepper_kinematics *sk
, double flush_time);
int32_t itersolve_is_active_axis(struct stepper_kinematics *sk, char axis);
void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq);
void itersolve_set_stepcompress(struct stepper_kinematics *sk
, struct stepcompress *sc, double step_dist);
void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq
, double step_dist);
struct trapq *itersolve_get_trapq(struct stepper_kinematics *sk);
double itersolve_calc_position_from_coord(struct stepper_kinematics *sk
, double x, double y, double z);
void itersolve_set_position(struct stepper_kinematics *sk
, double x, double y, double z);
double itersolve_get_commanded_pos(struct stepper_kinematics *sk);
double itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk);
double itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk);
"""
defs_trapq = """
@ -154,8 +163,6 @@ defs_kin_extruder = """
"""
defs_kin_shaper = """
double input_shaper_get_step_generation_window(
struct stepper_kinematics *sk);
int input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis
, int n, double a[], double t[]);
int input_shaper_set_sk(struct stepper_kinematics *sk
@ -182,7 +189,7 @@ defs_serialqueue = """
};
struct serialqueue *serialqueue_alloc(int serial_fd, char serial_fd_type
, int client_id);
, int client_id, char name[16]);
void serialqueue_exit(struct serialqueue *sq);
void serialqueue_free(struct serialqueue *sq);
struct command_queue *serialqueue_alloc_commandqueue(void);
@ -219,6 +226,7 @@ defs_trdispatch = """
defs_pyhelper = """
void set_python_logging_callback(void (*func)(const char *));
double get_monotonic(void);
int set_thread_name(char name[16]);
"""
defs_std = """
@ -227,7 +235,7 @@ defs_std = """
defs_all = [
defs_pyhelper, defs_serialqueue, defs_std, defs_stepcompress,
defs_itersolve, defs_trapq, defs_trdispatch,
defs_steppersync, defs_itersolve, defs_trapq, defs_trdispatch,
defs_kin_cartesian, defs_kin_corexy, defs_kin_corexz, defs_kin_delta,
defs_kin_deltesian, defs_kin_polar, defs_kin_rotary_delta, defs_kin_winch,
defs_kin_extruder, defs_kin_shaper, defs_kin_idex,
@ -270,11 +278,33 @@ def do_build_code(cmd):
logging.error(msg)
raise Exception(msg)
# Build the main c_helper.so c code library
def check_build_c_library():
srcdir = os.path.dirname(os.path.realpath(__file__))
srcfiles = get_abs_files(srcdir, SOURCE_FILES)
ofiles = get_abs_files(srcdir, OTHER_FILES)
destlib = get_abs_files(srcdir, [DEST_LIB])[0]
if not check_build_code(srcfiles+ofiles+[__file__], destlib):
# Code already built
return destlib
# Select command line options
if check_gcc_option(SSE_FLAGS):
cmd = "%s %s %s" % (GCC_CMD, SSE_FLAGS, COMPILE_ARGS)
else:
cmd = "%s %s" % (GCC_CMD, COMPILE_ARGS)
# Invoke compiler
logging.info("Building C code module %s", DEST_LIB)
tempdestlib = get_abs_files(srcdir, ["_temp_" + DEST_LIB])[0]
do_build_code(cmd % (tempdestlib, ' '.join(srcfiles)))
# Rename from temporary file to final file name
os.rename(tempdestlib, destlib)
return destlib
FFI_main = None
FFI_lib = None
pyhelper_logging_callback = None
# Hepler invoked from C errorf() code to log errors
# Helper invoked from C errorf() code to log errors
def logging_callback(msg):
logging.error(FFI_main.string(msg))
@ -282,17 +312,9 @@ def logging_callback(msg):
def get_ffi():
global FFI_main, FFI_lib, pyhelper_logging_callback
if FFI_lib is None:
srcdir = os.path.dirname(os.path.realpath(__file__))
srcfiles = get_abs_files(srcdir, SOURCE_FILES)
ofiles = get_abs_files(srcdir, OTHER_FILES)
destlib = get_abs_files(srcdir, [DEST_LIB])[0]
if check_build_code(srcfiles+ofiles+[__file__], destlib):
if check_gcc_option(SSE_FLAGS):
cmd = "%s %s %s" % (GCC_CMD, SSE_FLAGS, COMPILE_ARGS)
else:
cmd = "%s %s" % (GCC_CMD, COMPILE_ARGS)
logging.info("Building C code module %s", DEST_LIB)
do_build_code(cmd % (destlib, ' '.join(srcfiles)))
# Check if library needs to be built, and build if so
destlib = check_build_c_library()
# Open library
FFI_main = cffi.FFI()
for d in defs_all:
FFI_main.cdef(d)

View file

@ -26,8 +26,8 @@ struct timepos {
// Generate step times for a portion of a move
static int32_t
itersolve_gen_steps_range(struct stepper_kinematics *sk, struct move *m
, double abs_start, double abs_end)
itersolve_gen_steps_range(struct stepper_kinematics *sk, struct stepcompress *sc
, struct move *m, double abs_start, double abs_end)
{
sk_calc_callback calc_position_cb = sk->calc_position_cb;
double half_step = .5 * sk->step_dist;
@ -37,7 +37,7 @@ itersolve_gen_steps_range(struct stepper_kinematics *sk, struct move *m
if (end > m->move_t)
end = m->move_t;
struct timepos old_guess = {start, sk->commanded_pos}, guess = old_guess;
int sdir = stepcompress_get_step_dir(sk->sc);
int sdir = stepcompress_get_step_dir(sc);
int is_dir_change = 0, have_bracket = 0, check_oscillate = 0;
double target = sk->commanded_pos + (sdir ? half_step : -half_step);
double last_time=start, low_time=start, high_time=start + SEEK_TIME_RESET;
@ -99,13 +99,13 @@ itersolve_gen_steps_range(struct stepper_kinematics *sk, struct move *m
if (!have_bracket || high_time - low_time > .000000001) {
if (!is_dir_change && rel_dist >= -half_step)
// Avoid rollback if stepper fully reaches step position
stepcompress_commit(sk->sc);
stepcompress_commit(sc);
// Guess is not close enough - guess again with new time
continue;
}
}
// Found next step - submit it
int ret = stepcompress_append(sk->sc, sdir, m->print_time, guess.time);
int ret = stepcompress_append(sc, sdir, m->print_time, guess.time);
if (ret)
return ret;
target = sdir ? target+half_step+half_step : target-half_step-half_step;
@ -143,14 +143,14 @@ check_active(struct stepper_kinematics *sk, struct move *m)
}
// Generate step times for a range of moves on the trapq
int32_t __visible
itersolve_generate_steps(struct stepper_kinematics *sk, double flush_time)
int32_t
itersolve_generate_steps(struct stepper_kinematics *sk, struct stepcompress *sc
, double flush_time)
{
double last_flush_time = sk->last_flush_time;
sk->last_flush_time = flush_time;
if (!sk->tq)
return 0;
trapq_check_sentinels(sk->tq);
struct move *m = list_first_entry(&sk->tq->moves, struct move, node);
while (last_flush_time >= m->print_time + m->move_t)
m = list_next_entry(m, node);
@ -170,15 +170,15 @@ itersolve_generate_steps(struct stepper_kinematics *sk, double flush_time)
while (--skip_count && pm->print_time > abs_start)
pm = list_prev_entry(pm, node);
do {
int32_t ret = itersolve_gen_steps_range(sk, pm, abs_start
, flush_time);
int32_t ret = itersolve_gen_steps_range(
sk, sc, pm, abs_start, flush_time);
if (ret)
return ret;
pm = list_next_entry(pm, node);
} while (pm != m);
}
// Generate steps for this move
int32_t ret = itersolve_gen_steps_range(sk, m, last_flush_time
int32_t ret = itersolve_gen_steps_range(sk, sc, m, last_flush_time
, flush_time);
if (ret)
return ret;
@ -195,8 +195,8 @@ itersolve_generate_steps(struct stepper_kinematics *sk, double flush_time)
double abs_end = force_steps_time;
if (abs_end > flush_time)
abs_end = flush_time;
int32_t ret = itersolve_gen_steps_range(sk, m, last_flush_time
, abs_end);
int32_t ret = itersolve_gen_steps_range(
sk, sc, m, last_flush_time, abs_end);
if (ret)
return ret;
skip_count = 1;
@ -240,17 +240,17 @@ itersolve_is_active_axis(struct stepper_kinematics *sk, char axis)
}
void __visible
itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq)
itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq
, double step_dist)
{
sk->tq = tq;
sk->step_dist = step_dist;
}
void __visible
itersolve_set_stepcompress(struct stepper_kinematics *sk
, struct stepcompress *sc, double step_dist)
struct trapq * __visible
itersolve_get_trapq(struct stepper_kinematics *sk)
{
sk->sc = sc;
sk->step_dist = step_dist;
return sk->tq;
}
double __visible
@ -278,3 +278,15 @@ itersolve_get_commanded_pos(struct stepper_kinematics *sk)
{
return sk->commanded_pos;
}
double __visible
itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk)
{
return sk->gen_steps_pre_active;
}
double __visible
itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk)
{
return sk->gen_steps_post_active;
}

View file

@ -26,16 +26,18 @@ struct stepper_kinematics {
};
int32_t itersolve_generate_steps(struct stepper_kinematics *sk
, double flush_time);
, struct stepcompress *sc, double flush_time);
double itersolve_check_active(struct stepper_kinematics *sk, double flush_time);
int32_t itersolve_is_active_axis(struct stepper_kinematics *sk, char axis);
void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq);
void itersolve_set_stepcompress(struct stepper_kinematics *sk
, struct stepcompress *sc, double step_dist);
void itersolve_set_trapq(struct stepper_kinematics *sk, struct trapq *tq
, double step_dist);
struct trapq *itersolve_get_trapq(struct stepper_kinematics *sk);
double itersolve_calc_position_from_coord(struct stepper_kinematics *sk
, double x, double y, double z);
void itersolve_set_position(struct stepper_kinematics *sk
, double x, double y, double z);
double itersolve_get_commanded_pos(struct stepper_kinematics *sk);
double itersolve_get_gen_steps_pre_active(struct stepper_kinematics *sk);
double itersolve_get_gen_steps_post_active(struct stepper_kinematics *sk);
#endif // itersolve.h

View file

@ -1,7 +1,7 @@
// Kinematic input shapers to minimize motion vibrations in XY plane
//
// Copyright (C) 2019-2020 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
// Copyright (C) 2020-2025 Dmitry Butyugin <dmbutyugin@google.com>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
@ -18,6 +18,8 @@
* Shaper initialization
****************************************************************/
static const int KIN_FLAGS[3] = { AF_X, AF_Y, AF_Z };
struct shaper_pulses {
int num_pulses;
struct {
@ -113,7 +115,7 @@ struct input_shaper {
struct stepper_kinematics sk;
struct stepper_kinematics *orig_sk;
struct move m;
struct shaper_pulses sx, sy;
struct shaper_pulses sp[3];
};
// Optimized calc_position when only x axis is needed
@ -122,9 +124,10 @@ shaper_x_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->sx.num_pulses)
struct shaper_pulses *sx = &is->sp[0];
if (!sx->num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx);
is->m.start_pos.x = calc_position(m, 'x', move_time, sx);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
@ -134,25 +137,41 @@ shaper_y_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->sy.num_pulses)
struct shaper_pulses *sy = &is->sp[1];
if (!sy->num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy);
is->m.start_pos.y = calc_position(m, 'y', move_time, sy);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
// General calc_position for both x and y axes
// Optimized calc_position when only z axis is needed
static double
shaper_xy_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
shaper_z_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->sx.num_pulses && !is->sy.num_pulses)
struct shaper_pulses *sz = &is->sp[2];
if (!sz->num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos.z = calc_position(m, 'z', move_time, sz);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
// General calc_position for all x, y, and z axes
static double
shaper_xyz_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->sp[0].num_pulses && !is->sp[1].num_pulses && !is->sp[2].num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos = move_get_coord(m, move_time);
if (is->sx.num_pulses)
is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx);
if (is->sy.num_pulses)
is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy);
if (is->sp[0].num_pulses)
is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sp[0]);
if (is->sp[1].num_pulses)
is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sp[1]);
if (is->sp[2].num_pulses)
is->m.start_pos.z = calc_position(m, 'z', move_time, &is->sp[2]);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
@ -170,15 +189,24 @@ static void
shaper_note_generation_time(struct input_shaper *is)
{
double pre_active = 0., post_active = 0.;
if ((is->sk.active_flags & AF_X) && is->sx.num_pulses) {
pre_active = is->sx.pulses[is->sx.num_pulses-1].t;
post_active = -is->sx.pulses[0].t;
struct shaper_pulses *sx = &is->sp[0];
if ((is->sk.active_flags & AF_X) && sx->num_pulses) {
pre_active = sx->pulses[sx->num_pulses-1].t;
post_active = -sx->pulses[0].t;
}
if ((is->sk.active_flags & AF_Y) && is->sy.num_pulses) {
pre_active = is->sy.pulses[is->sy.num_pulses-1].t > pre_active
? is->sy.pulses[is->sy.num_pulses-1].t : pre_active;
post_active = -is->sy.pulses[0].t > post_active
? -is->sy.pulses[0].t : post_active;
struct shaper_pulses *sy = &is->sp[1];
if ((is->sk.active_flags & AF_Y) && sy->num_pulses) {
pre_active = sy->pulses[sy->num_pulses-1].t > pre_active
? sy->pulses[sy->num_pulses-1].t : pre_active;
post_active = -sy->pulses[0].t > post_active
? -sy->pulses[0].t : post_active;
}
struct shaper_pulses *sz = &is->sp[2];
if ((is->sk.active_flags & AF_Z) && sz->num_pulses) {
pre_active = sz->pulses[sz->num_pulses-1].t > pre_active
? sz->pulses[sz->num_pulses-1].t : pre_active;
post_active = -sz->pulses[0].t > post_active
? -sz->pulses[0].t : post_active;
}
is->sk.gen_steps_pre_active = pre_active;
is->sk.gen_steps_post_active = post_active;
@ -188,12 +216,15 @@ void __visible
input_shaper_update_sk(struct stepper_kinematics *sk)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if ((is->orig_sk->active_flags & (AF_X | AF_Y)) == (AF_X | AF_Y))
is->sk.calc_position_cb = shaper_xy_calc_position;
else if (is->orig_sk->active_flags & AF_X)
int kin_flags = is->orig_sk->active_flags & (AF_X | AF_Y | AF_Z);
if (kin_flags == AF_X)
is->sk.calc_position_cb = shaper_x_calc_position;
else if (is->orig_sk->active_flags & AF_Y)
else if (kin_flags == AF_Y)
is->sk.calc_position_cb = shaper_y_calc_position;
else if (kin_flags == AF_Z)
is->sk.calc_position_cb = shaper_z_calc_position;
else
is->sk.calc_position_cb = shaper_xyz_calc_position;
is->sk.active_flags = is->orig_sk->active_flags;
shaper_note_generation_time(is);
}
@ -207,8 +238,10 @@ input_shaper_set_sk(struct stepper_kinematics *sk
is->sk.calc_position_cb = shaper_x_calc_position;
else if (orig_sk->active_flags == AF_Y)
is->sk.calc_position_cb = shaper_y_calc_position;
else if (orig_sk->active_flags & (AF_X | AF_Y))
is->sk.calc_position_cb = shaper_xy_calc_position;
else if (orig_sk->active_flags == AF_Z)
is->sk.calc_position_cb = shaper_z_calc_position;
else if (orig_sk->active_flags & (AF_X | AF_Y | AF_Z))
is->sk.calc_position_cb = shaper_xyz_calc_position;
else
return -1;
is->sk.active_flags = orig_sk->active_flags;
@ -226,27 +259,20 @@ int __visible
input_shaper_set_shaper_params(struct stepper_kinematics *sk, char axis
, int n, double a[], double t[])
{
if (axis != 'x' && axis != 'y')
int axis_ind = axis-'x';
if (axis_ind < 0 || axis_ind >= ARRAY_SIZE(KIN_FLAGS))
return -1;
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
struct shaper_pulses *sp = axis == 'x' ? &is->sx : &is->sy;
struct shaper_pulses *sp = &is->sp[axis_ind];
int status = 0;
// Ignore input shaper update if the axis is not active
if (is->orig_sk->active_flags & (axis == 'x' ? AF_X : AF_Y)) {
if (is->orig_sk->active_flags & KIN_FLAGS[axis_ind]) {
status = init_shaper(n, a, t, sp);
shaper_note_generation_time(is);
}
return status;
}
double __visible
input_shaper_get_step_generation_window(struct stepper_kinematics *sk)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
return is->sk.gen_steps_pre_active > is->sk.gen_steps_post_active
? is->sk.gen_steps_pre_active : is->sk.gen_steps_post_active;
}
struct stepper_kinematics * __visible
input_shaper_alloc(void)
{

View file

@ -116,6 +116,11 @@ list_join_tail(struct list_head *add, struct list_head *h)
; &pos->member != &(head)->root \
; pos = list_next_entry(pos, member))
#define list_for_each_entry_reverse(pos, head, member) \
for (pos = list_last_entry((head), typeof(*pos), member) \
; &pos->member != &(head)->root \
; pos = list_prev_entry(pos, member))
#define list_for_each_entry_safe(pos, n, head, member) \
for (pos = list_first_entry((head), typeof(*pos), member) \
, n = list_next_entry(pos, member) \

View file

@ -207,3 +207,14 @@ clock_from_time(struct clock_estimate *ce, double time)
{
return (int64_t)((time - ce->conv_time)*ce->est_freq + .5) + ce->conv_clock;
}
// Fill the fields of a 'struct clock_estimate'
void
clock_fill(struct clock_estimate *ce, double est_freq, double conv_time
, uint64_t conv_clock, uint64_t last_clock)
{
ce->est_freq = est_freq;
ce->conv_time = conv_time;
ce->conv_clock = conv_clock;
ce->last_clock = last_clock;
}

View file

@ -50,5 +50,7 @@ void message_queue_free(struct list_head *root);
uint64_t clock_from_clock32(struct clock_estimate *ce, uint32_t clock32);
double clock_to_time(struct clock_estimate *ce, uint64_t clock);
uint64_t clock_from_time(struct clock_estimate *ce, double time);
void clock_fill(struct clock_estimate *ce, double est_freq, double conv_time
, uint64_t conv_clock, uint64_t last_clock);
#endif // msgblock.h

View file

@ -10,6 +10,7 @@
#include <stdio.h> // fprintf
#include <string.h> // strerror
#include <time.h> // struct timespec
#include <sys/prctl.h> // prctl
#include "compiler.h" // __visible
#include "pyhelper.h" // get_monotonic
@ -92,3 +93,10 @@ dump_string(char *outbuf, int outbuf_size, char *inbuf, int inbuf_size)
*o = '\0';
return outbuf;
}
// Set custom thread names
int __visible
set_thread_name(char name[16])
{
return prctl(PR_SET_NAME, name);
}

View file

@ -7,5 +7,6 @@ void set_python_logging_callback(void (*func)(const char *));
void errorf(const char *fmt, ...) __attribute__ ((format (printf, 1, 2)));
void report_errno(char *where, int rc);
char *dump_string(char *outbuf, int outbuf_size, char *inbuf, int inbuf_size);
int set_thread_name(char name[16]);
#endif // pyhelper.h

View file

@ -1,6 +1,6 @@
// Serial port command queuing
//
// Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2016-2025 Kevin O'Connor <kevin@koconnor.net>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
@ -29,24 +29,45 @@
#include "pyhelper.h" // get_monotonic
#include "serialqueue.h" // struct queue_message
struct command_queue {
struct list_head upcoming_queue, ready_queue;
struct message_sub_queue {
struct list_head msg_queue;
struct list_node node;
};
struct command_queue {
struct message_sub_queue ready, upcoming;
};
struct receiver {
pthread_mutex_t lock;
pthread_cond_t cond;
int waiting;
struct list_head queue;
struct list_head old_receive;
};
struct transmit_requests {
int pipe_fds[2];
pthread_mutex_t lock; // protects variables below
struct list_head upcoming_queues;
int upcoming_bytes;
uint64_t need_kick_clock, min_release_clock;
};
struct serialqueue {
// Input reading
struct pollreactor *pr;
int serial_fd, serial_fd_type, client_id;
int pipe_fds[2];
uint8_t input_buf[4096];
uint8_t need_sync;
int input_pos;
// Multi-threaded support for pushing and pulling messages
struct receiver receiver;
struct transmit_requests transmit_requests;
// Threading
char name[16];
pthread_t tid;
pthread_mutex_t lock; // protects variables below
pthread_cond_t cond;
int receive_waiting;
// Baud / clock tracking
int receive_window;
double bittime_adjust, idle_time;
@ -58,18 +79,15 @@ struct serialqueue {
struct list_head sent_queue;
double srtt, rttvar, rto;
// Pending transmission message queues
struct list_head pending_queues;
int ready_bytes, upcoming_bytes, need_ack_bytes, last_ack_bytes;
uint64_t need_kick_clock;
struct list_head ready_queues;
int ready_bytes, need_ack_bytes, last_ack_bytes;
struct list_head notify_queue;
double last_write_fail_time;
// Received messages
struct list_head receive_queue;
// Fastreader support
pthread_mutex_t fast_reader_dispatch_lock;
struct list_head fast_readers;
// Debugging
struct list_head old_sent, old_receive;
struct list_head old_sent;
// Stats
uint32_t bytes_write, bytes_read, bytes_retransmit, bytes_invalid;
};
@ -89,7 +107,7 @@ struct serialqueue {
#define MIN_RTO 0.025
#define MAX_RTO 5.000
#define MAX_PENDING_BLOCKS 12
#define MIN_REQTIME_DELTA 0.250
#define MIN_REQTIME_DELTA 0.100
#define MIN_BACKGROUND_DELTA 0.005
#define IDLE_QUERY_TIME 1.0
@ -108,31 +126,44 @@ debug_queue_alloc(struct list_head *root, int count)
}
// Copy a message to a debug queue and free old debug messages
static void
debug_queue_add(struct list_head *root, struct queue_message *qm)
static struct queue_message *
_debug_queue_add(struct list_head *root, struct queue_message *qm)
{
list_add_tail(&qm->node, root);
struct queue_message *old = list_first_entry(
root, struct queue_message, node);
list_del(&old->node);
return old;
}
static void
debug_queue_add(struct list_head *root, struct queue_message *qm)
{
struct queue_message *old = _debug_queue_add(root, qm);
message_free(old);
}
// Wake up the receiver thread if it is waiting
// Add messages and wake up the receiver thread if it is waiting
static void
check_wake_receive(struct serialqueue *sq)
receive_append_wake(struct receiver *receiver, struct list_head *msgs)
{
if (sq->receive_waiting) {
sq->receive_waiting = 0;
pthread_cond_signal(&sq->cond);
int dokick = 0;
pthread_mutex_lock(&receiver->lock);
list_join_tail(msgs, &receiver->queue);
if (receiver->waiting) {
receiver->waiting = 0;
dokick = 1;
}
pthread_mutex_unlock(&receiver->lock);
if (dokick)
pthread_cond_signal(&receiver->cond);
}
// Write to the internal pipe to wake the background thread if in poll
static void
kick_bg_thread(struct serialqueue *sq)
{
int ret = write(sq->pipe_fds[1], ".", 1);
int ret = write(sq->transmit_requests.pipe_fds[1], ".", 1);
if (ret < 0)
report_errno("pipe write", ret);
}
@ -238,7 +269,8 @@ handle_message(struct serialqueue *sq, double eventtime, int len)
sq->bytes_read += len;
// Check for pending messages on notify_queue
int must_wake = 0;
struct list_head received;
list_init(&received);
while (!list_empty(&sq->notify_queue)) {
struct queue_message *qm = list_first_entry(
&sq->notify_queue, struct queue_message, node);
@ -250,8 +282,7 @@ handle_message(struct serialqueue *sq, double eventtime, int len)
qm->len = 0;
qm->sent_time = sq->last_receive_sent_time;
qm->receive_time = eventtime;
list_add_tail(&qm->node, &sq->receive_queue);
must_wake = 1;
list_add_tail(&qm->node, &received);
}
// Process message
@ -269,10 +300,12 @@ handle_message(struct serialqueue *sq, double eventtime, int len)
? sq->last_receive_sent_time : 0.);
qm->receive_time = get_monotonic(); // must be time post read()
qm->receive_time -= calculate_bittime(sq, len);
list_add_tail(&qm->node, &sq->receive_queue);
must_wake = 1;
list_add_tail(&qm->node, &received);
}
if (!list_empty(&received))
receive_append_wake(&sq->receiver, &received);
// Check fast readers
struct fastreader *fr;
list_for_each_entry(fr, &sq->fast_readers, node) {
@ -282,16 +315,11 @@ handle_message(struct serialqueue *sq, double eventtime, int len)
continue;
// Release main lock and invoke callback
pthread_mutex_lock(&sq->fast_reader_dispatch_lock);
if (must_wake)
check_wake_receive(sq);
pthread_mutex_unlock(&sq->lock);
fr->func(fr, sq->input_buf, len);
pthread_mutex_unlock(&sq->fast_reader_dispatch_lock);
return;
}
if (must_wake)
check_wake_receive(sq);
pthread_mutex_unlock(&sq->lock);
}
@ -350,7 +378,7 @@ static void
kick_event(struct serialqueue *sq, double eventtime)
{
char dummy[4096];
int ret = read(sq->pipe_fds[0], dummy, sizeof(dummy));
int ret = read(sq->transmit_requests.pipe_fds[0], dummy, sizeof(dummy));
if (ret < 0)
report_errno("pipe read", ret);
pollreactor_update_timer(sq->pr, SQPT_COMMAND, PR_NOW);
@ -451,23 +479,21 @@ build_and_send_command(struct serialqueue *sq, uint8_t *buf, int pending
uint64_t min_clock = MAX_CLOCK;
struct command_queue *q, *cq = NULL;
struct queue_message *qm = NULL;
list_for_each_entry(q, &sq->pending_queues, node) {
if (!list_empty(&q->ready_queue)) {
struct queue_message *m = list_first_entry(
&q->ready_queue, struct queue_message, node);
if (m->req_clock < min_clock) {
min_clock = m->req_clock;
cq = q;
qm = m;
}
list_for_each_entry(q, &sq->ready_queues, ready.node) {
struct queue_message *m = list_first_entry(
&q->ready.msg_queue, struct queue_message, node);
if (m->req_clock < min_clock) {
min_clock = m->req_clock;
cq = q;
qm = m;
}
}
// Append message to outgoing command
if (len + qm->len > MESSAGE_MAX - MESSAGE_TRAILER_SIZE)
break;
list_del(&qm->node);
if (list_empty(&cq->ready_queue) && list_empty(&cq->upcoming_queue))
list_del(&cq->node);
if (list_empty(&cq->ready.msg_queue))
list_del(&cq->ready.node);
memcpy(&buf[len], qm->msg, qm->len);
len += qm->len;
sq->ready_bytes -= qm->len;
@ -507,74 +533,129 @@ build_and_send_command(struct serialqueue *sq, uint8_t *buf, int pending
return len;
}
// Determine the time the next serial data should be sent
static double
check_send_command(struct serialqueue *sq, int pending, double eventtime)
// Move messages from upcoming queues to ready queues
static uint64_t
check_upcoming_queues(struct serialqueue *sq, uint64_t ack_clock)
{
if (sq->send_seq - sq->receive_seq >= MAX_PENDING_BLOCKS
&& sq->receive_seq != (uint64_t)-1)
// Need an ack before more messages can be sent
return PR_NEVER;
if (sq->send_seq > sq->receive_seq && sq->receive_window) {
int need_ack_bytes = sq->need_ack_bytes + MESSAGE_MAX;
if (sq->last_ack_seq < sq->receive_seq)
need_ack_bytes += sq->last_ack_bytes;
if (need_ack_bytes > sq->receive_window)
// Wait for ack from past messages before sending next message
return PR_NEVER;
pthread_mutex_lock(&sq->transmit_requests.lock);
sq->transmit_requests.need_kick_clock = 0;
uint64_t min_release_clock = sq->transmit_requests.min_release_clock;
if (ack_clock < min_release_clock) {
pthread_mutex_unlock(&sq->transmit_requests.lock);
return min_release_clock;
}
// Check for stalled messages now ready
double idletime = eventtime > sq->idle_time ? eventtime : sq->idle_time;
idletime += calculate_bittime(sq, pending + MESSAGE_MIN);
uint64_t ack_clock = clock_from_time(&sq->ce, idletime);
uint64_t min_stalled_clock = MAX_CLOCK, min_ready_clock = MAX_CLOCK;
struct command_queue *cq;
list_for_each_entry(cq, &sq->pending_queues, node) {
// Move messages from the upcoming_queue to the ready_queue
while (!list_empty(&cq->upcoming_queue)) {
struct queue_message *qm = list_first_entry(
&cq->upcoming_queue, struct queue_message, node);
uint64_t min_stalled_clock = MAX_CLOCK;
struct command_queue *cq, *_ncq;
list_for_each_entry_safe(cq, _ncq, &sq->transmit_requests.upcoming_queues,
upcoming.node) {
int not_in_ready_queues = list_empty(&cq->ready.msg_queue);
// Move messages from the upcoming.msg_queue to the ready.msg_queue
struct queue_message *qm, *_nqm;
list_for_each_entry_safe(qm, _nqm, &cq->upcoming.msg_queue, node) {
if (ack_clock < qm->min_clock) {
if (qm->min_clock < min_stalled_clock)
min_stalled_clock = qm->min_clock;
break;
}
list_del(&qm->node);
list_add_tail(&qm->node, &cq->ready_queue);
sq->upcoming_bytes -= qm->len;
list_add_tail(&qm->node, &cq->ready.msg_queue);
sq->transmit_requests.upcoming_bytes -= qm->len;
sq->ready_bytes += qm->len;
}
// Update min_ready_clock
if (!list_empty(&cq->ready_queue)) {
struct queue_message *qm = list_first_entry(
&cq->ready_queue, struct queue_message, node);
uint64_t req_clock = qm->req_clock;
double bgtime = pending ? idletime : sq->idle_time;
double bgoffset = MIN_REQTIME_DELTA + MIN_BACKGROUND_DELTA;
if (req_clock == BACKGROUND_PRIORITY_CLOCK)
req_clock = clock_from_time(&sq->ce, bgtime + bgoffset);
if (req_clock < min_ready_clock)
min_ready_clock = req_clock;
}
// Remove cq from the list if it is now empty
if (list_empty(&cq->upcoming.msg_queue))
list_del(&cq->upcoming.node);
// Add to ready queues
if (not_in_ready_queues && !list_empty(&cq->ready.msg_queue))
list_add_tail(&cq->ready.node, &sq->ready_queues);
}
sq->transmit_requests.min_release_clock = min_stalled_clock;
pthread_mutex_unlock(&sq->transmit_requests.lock);
return min_stalled_clock;
}
// Set the next transmit queue need_kick_clock
static int
update_need_kick_clock(struct serialqueue *sq, uint64_t wantclock)
{
pthread_mutex_lock(&sq->transmit_requests.lock);
if (wantclock > sq->transmit_requests.min_release_clock) {
pthread_mutex_unlock(&sq->transmit_requests.lock);
return -1;
}
sq->transmit_requests.need_kick_clock = wantclock;
pthread_mutex_unlock(&sq->transmit_requests.lock);
return 0;
}
// Determine if ready to send commands (or the amount of time to sleep if not)
static double
check_send_command(struct serialqueue *sq, int pending, double eventtime)
{
// Check for upcoming messages now ready
double idletime = eventtime > sq->idle_time ? eventtime : sq->idle_time;
idletime += calculate_bittime(sq, pending + MESSAGE_MIN);
uint64_t ack_clock = clock_from_time(&sq->ce, idletime);
uint64_t min_stalled_clock = check_upcoming_queues(sq, ack_clock);
// Check if valid to send messages
if (sq->send_seq - sq->receive_seq >= MAX_PENDING_BLOCKS
&& sq->receive_seq != (uint64_t)-1)
// Need an ack before more messages can be sent
return eventtime + 0.250;
if (sq->send_seq > sq->receive_seq && sq->receive_window) {
int need_ack_bytes = sq->need_ack_bytes + MESSAGE_MAX;
if (sq->last_ack_seq < sq->receive_seq)
need_ack_bytes += sq->last_ack_bytes;
if (need_ack_bytes > sq->receive_window)
// Wait for ack from past messages before sending next message
return eventtime + 0.250;
}
// Check for messages to send
// Check if a block is fully ready to send
if (sq->ready_bytes >= MESSAGE_PAYLOAD_MAX)
return PR_NOW;
if (! sq->ce.est_freq) {
// Clock unknown during initial startup - recheck on each add
if (sq->ready_bytes)
return PR_NOW;
sq->need_kick_clock = MAX_CLOCK;
int mustwake = update_need_kick_clock(sq, 1);
if (mustwake)
return eventtime;
return PR_NEVER;
}
// Check if it is still needed to send messages from the ready_queues
uint64_t min_ready_clock = MAX_CLOCK;
struct command_queue *cq;
list_for_each_entry(cq, &sq->ready_queues, ready.node) {
// Update min_ready_clock
struct queue_message *qm = list_first_entry(
&cq->ready.msg_queue, struct queue_message, node);
uint64_t req_clock = qm->req_clock;
double bgtime = pending ? idletime : sq->idle_time;
double bgoffset = MIN_REQTIME_DELTA + MIN_BACKGROUND_DELTA;
if (req_clock == BACKGROUND_PRIORITY_CLOCK)
req_clock = clock_from_time(&sq->ce, bgtime + bgoffset);
if (req_clock < min_ready_clock)
min_ready_clock = req_clock;
}
uint64_t reqclock_delta = MIN_REQTIME_DELTA * sq->ce.est_freq;
if (min_ready_clock <= ack_clock + reqclock_delta)
return PR_NOW;
// Determine next wakeup time
if (pending)
// Caller wont sleep anyway - just return
return eventtime;
uint64_t wantclock = min_ready_clock - reqclock_delta;
if (min_stalled_clock < wantclock)
wantclock = min_stalled_clock;
sq->need_kick_clock = wantclock;
int mustwake = update_need_kick_clock(sq, wantclock);
if (mustwake)
// Raced with add of new command - avoid sleeping
return eventtime;
return idletime + (wantclock - ack_clock) / sq->ce.est_freq;
}
@ -588,20 +669,19 @@ command_event(struct serialqueue *sq, double eventtime)
double waketime;
for (;;) {
waketime = check_send_command(sq, buflen, eventtime);
if (waketime != PR_NOW || buflen + MESSAGE_MAX > sizeof(buf)) {
if (buflen) {
// Write message blocks
do_write(sq, buf, buflen);
sq->bytes_write += buflen;
double idletime = (eventtime > sq->idle_time
? eventtime : sq->idle_time);
sq->idle_time = idletime + calculate_bittime(sq, buflen);
buflen = 0;
}
if (waketime != PR_NOW)
break;
}
if (waketime != PR_NOW)
break;
buflen += build_and_send_command(sq, &buf[buflen], buflen, eventtime);
if (buflen + MESSAGE_MAX > sizeof(buf))
break;
}
if (buflen) {
// Write message blocks
do_write(sq, buf, buflen);
sq->bytes_write += buflen;
double idletime = eventtime > sq->idle_time ? eventtime : sq->idle_time;
sq->idle_time = idletime + calculate_bittime(sq, buflen);
waketime = PR_NOW;
}
pthread_mutex_unlock(&sq->lock);
return waketime;
@ -612,26 +692,31 @@ static void *
background_thread(void *data)
{
struct serialqueue *sq = data;
set_thread_name(sq->name);
pollreactor_run(sq->pr);
pthread_mutex_lock(&sq->lock);
check_wake_receive(sq);
pthread_mutex_unlock(&sq->lock);
// Wake any waiting receivers
struct list_head dummy;
list_init(&dummy);
receive_append_wake(&sq->receiver, &dummy);
return NULL;
}
// Create a new 'struct serialqueue' object
struct serialqueue * __visible
serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id)
serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id
, char name[16])
{
struct serialqueue *sq = malloc(sizeof(*sq));
memset(sq, 0, sizeof(*sq));
sq->serial_fd = serial_fd;
sq->serial_fd_type = serial_fd_type;
sq->client_id = client_id;
strncpy(sq->name, name, sizeof(sq->name));
sq->name[sizeof(sq->name)-1] = '\0';
int ret = pipe(sq->pipe_fds);
int ret = pipe(sq->transmit_requests.pipe_fds);
if (ret)
goto fail;
@ -639,12 +724,13 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id)
sq->pr = pollreactor_alloc(SQPF_NUM, SQPT_NUM, sq);
pollreactor_add_fd(sq->pr, SQPF_SERIAL, serial_fd, input_event
, serial_fd_type==SQT_DEBUGFILE);
pollreactor_add_fd(sq->pr, SQPF_PIPE, sq->pipe_fds[0], kick_event, 0);
pollreactor_add_fd(sq->pr, SQPF_PIPE, sq->transmit_requests.pipe_fds[0]
, kick_event, 0);
pollreactor_add_timer(sq->pr, SQPT_RETRANSMIT, retransmit_event);
pollreactor_add_timer(sq->pr, SQPT_COMMAND, command_event);
fd_set_non_blocking(serial_fd);
fd_set_non_blocking(sq->pipe_fds[0]);
fd_set_non_blocking(sq->pipe_fds[1]);
fd_set_non_blocking(sq->transmit_requests.pipe_fds[0]);
fd_set_non_blocking(sq->transmit_requests.pipe_fds[1]);
// Retransmit setup
sq->send_seq = 1;
@ -658,24 +744,30 @@ serialqueue_alloc(int serial_fd, char serial_fd_type, int client_id)
}
// Queues
sq->need_kick_clock = MAX_CLOCK;
list_init(&sq->pending_queues);
sq->transmit_requests.need_kick_clock = MAX_CLOCK;
sq->transmit_requests.min_release_clock = MAX_CLOCK;
list_init(&sq->transmit_requests.upcoming_queues);
pthread_mutex_init(&sq->transmit_requests.lock, NULL);
list_init(&sq->ready_queues);
list_init(&sq->sent_queue);
list_init(&sq->receive_queue);
list_init(&sq->receiver.queue);
list_init(&sq->notify_queue);
list_init(&sq->fast_readers);
// Debugging
list_init(&sq->old_sent);
list_init(&sq->old_receive);
list_init(&sq->receiver.old_receive);
debug_queue_alloc(&sq->old_sent, DEBUG_QUEUE_SENT);
debug_queue_alloc(&sq->old_receive, DEBUG_QUEUE_RECEIVE);
debug_queue_alloc(&sq->receiver.old_receive, DEBUG_QUEUE_RECEIVE);
// Thread setup
ret = pthread_mutex_init(&sq->lock, NULL);
if (ret)
goto fail;
ret = pthread_cond_init(&sq->cond, NULL);
ret = pthread_mutex_init(&sq->receiver.lock, NULL);
if (ret)
goto fail;
ret = pthread_cond_init(&sq->receiver.cond, NULL);
if (ret)
goto fail;
ret = pthread_mutex_init(&sq->fast_reader_dispatch_lock, NULL);
@ -713,17 +805,27 @@ serialqueue_free(struct serialqueue *sq)
serialqueue_exit(sq);
pthread_mutex_lock(&sq->lock);
message_queue_free(&sq->sent_queue);
message_queue_free(&sq->receive_queue);
pthread_mutex_lock(&sq->receiver.lock);
message_queue_free(&sq->receiver.queue);
message_queue_free(&sq->receiver.old_receive);
pthread_mutex_unlock(&sq->receiver.lock);
message_queue_free(&sq->notify_queue);
message_queue_free(&sq->old_sent);
message_queue_free(&sq->old_receive);
while (!list_empty(&sq->pending_queues)) {
struct command_queue *cq = list_first_entry(
&sq->pending_queues, struct command_queue, node);
list_del(&cq->node);
message_queue_free(&cq->ready_queue);
message_queue_free(&cq->upcoming_queue);
while (!list_empty(&sq->ready_queues)) {
struct command_queue* cq = list_first_entry(
&sq->ready_queues, struct command_queue, ready.node);
list_del(&cq->ready.node);
message_queue_free(&cq->ready.msg_queue);
}
pthread_mutex_lock(&sq->transmit_requests.lock);
while (!list_empty(&sq->transmit_requests.upcoming_queues)) {
struct command_queue *cq = list_first_entry(
&sq->transmit_requests.upcoming_queues,
struct command_queue, upcoming.node);
list_del(&cq->upcoming.node);
message_queue_free(&cq->upcoming.msg_queue);
}
pthread_mutex_unlock(&sq->transmit_requests.lock);
pthread_mutex_unlock(&sq->lock);
pollreactor_free(sq->pr);
free(sq);
@ -735,8 +837,8 @@ serialqueue_alloc_commandqueue(void)
{
struct command_queue *cq = malloc(sizeof(*cq));
memset(cq, 0, sizeof(*cq));
list_init(&cq->ready_queue);
list_init(&cq->upcoming_queue);
list_init(&cq->ready.msg_queue);
list_init(&cq->upcoming.msg_queue);
return cq;
}
@ -746,7 +848,8 @@ serialqueue_free_commandqueue(struct command_queue *cq)
{
if (!cq)
return;
if (!list_empty(&cq->ready_queue) || !list_empty(&cq->upcoming_queue)) {
if (!list_empty(&cq->ready.msg_queue) ||
!list_empty(&cq->upcoming.msg_queue)) {
errorf("Memory leak! Can't free non-empty commandqueue");
return;
}
@ -783,27 +886,33 @@ serialqueue_send_batch(struct serialqueue *sq, struct command_queue *cq
int len = 0;
struct queue_message *qm;
list_for_each_entry(qm, msgs, node) {
if (qm->min_clock + (1LL<<31) < qm->req_clock
if (qm->min_clock + (3LL<<29) < qm->req_clock
&& qm->req_clock != BACKGROUND_PRIORITY_CLOCK)
qm->min_clock = qm->req_clock - (1LL<<31);
// Avoid mcu clock comparison 31-bit overflow issues
qm->min_clock = qm->req_clock - (3LL<<29);
len += qm->len;
}
if (! len)
return;
qm = list_first_entry(msgs, struct queue_message, node);
uint64_t min_clock = qm->min_clock;
// Add list to cq->upcoming_queue
pthread_mutex_lock(&sq->lock);
if (list_empty(&cq->ready_queue) && list_empty(&cq->upcoming_queue))
list_add_tail(&cq->node, &sq->pending_queues);
list_join_tail(msgs, &cq->upcoming_queue);
sq->upcoming_bytes += len;
int mustwake = 0;
if (qm->min_clock < sq->need_kick_clock) {
sq->need_kick_clock = 0;
mustwake = 1;
pthread_mutex_lock(&sq->transmit_requests.lock);
if (list_empty(&cq->upcoming.msg_queue)) {
list_add_tail(&cq->upcoming.node,
&sq->transmit_requests.upcoming_queues);
if (min_clock < sq->transmit_requests.min_release_clock)
sq->transmit_requests.min_release_clock = min_clock;
if (min_clock < sq->transmit_requests.need_kick_clock) {
sq->transmit_requests.need_kick_clock = 0;
mustwake = 1;
}
}
pthread_mutex_unlock(&sq->lock);
list_join_tail(msgs, &cq->upcoming.msg_queue);
sq->transmit_requests.upcoming_bytes += len;
pthread_mutex_unlock(&sq->transmit_requests.lock);
// Wake the background thread if necessary
if (mustwake)
@ -840,20 +949,21 @@ serialqueue_send(struct serialqueue *sq, struct command_queue *cq, uint8_t *msg
void __visible
serialqueue_pull(struct serialqueue *sq, struct pull_queue_message *pqm)
{
pthread_mutex_lock(&sq->lock);
struct receiver *receiver = &sq->receiver;
pthread_mutex_lock(&receiver->lock);
// Wait for message to be available
while (list_empty(&sq->receive_queue)) {
while (list_empty(&receiver->queue)) {
if (pollreactor_is_exit(sq->pr))
goto exit;
sq->receive_waiting = 1;
int ret = pthread_cond_wait(&sq->cond, &sq->lock);
receiver->waiting = 1;
int ret = pthread_cond_wait(&receiver->cond, &receiver->lock);
if (ret)
report_errno("pthread_cond_wait", ret);
}
// Remove message from queue
struct queue_message *qm = list_first_entry(
&sq->receive_queue, struct queue_message, node);
&receiver->queue, struct queue_message, node);
list_del(&qm->node);
// Copy message
@ -863,16 +973,14 @@ serialqueue_pull(struct serialqueue *sq, struct pull_queue_message *pqm)
pqm->receive_time = qm->receive_time;
pqm->notify_id = qm->notify_id;
if (qm->len)
debug_queue_add(&sq->old_receive, qm);
else
message_free(qm);
pthread_mutex_unlock(&sq->lock);
qm = _debug_queue_add(&receiver->old_receive, qm);
pthread_mutex_unlock(&receiver->lock);
message_free(qm);
return;
exit:
pqm->len = -1;
pthread_mutex_unlock(&sq->lock);
pthread_mutex_unlock(&receiver->lock);
}
void __visible
@ -904,10 +1012,7 @@ serialqueue_set_clock_est(struct serialqueue *sq, double est_freq
, uint64_t last_clock)
{
pthread_mutex_lock(&sq->lock);
sq->ce.est_freq = est_freq;
sq->ce.conv_time = conv_time;
sq->ce.conv_clock = conv_clock;
sq->ce.last_clock = last_clock;
clock_fill(&sq->ce, est_freq, conv_time, conv_clock, last_clock);
pthread_mutex_unlock(&sq->lock);
}
@ -926,7 +1031,9 @@ serialqueue_get_stats(struct serialqueue *sq, char *buf, int len)
{
struct serialqueue stats;
pthread_mutex_lock(&sq->lock);
pthread_mutex_lock(&sq->transmit_requests.lock);
memcpy(&stats, sq, sizeof(stats));
pthread_mutex_unlock(&sq->transmit_requests.lock);
pthread_mutex_unlock(&sq->lock);
snprintf(buf, len, "bytes_write=%u bytes_read=%u"
@ -939,7 +1046,7 @@ serialqueue_get_stats(struct serialqueue *sq, char *buf, int len)
, (int)stats.send_seq, (int)stats.receive_seq
, (int)stats.retransmit_seq
, stats.srtt, stats.rttvar, stats.rto
, stats.ready_bytes, stats.upcoming_bytes);
, stats.ready_bytes, stats.transmit_requests.upcoming_bytes);
}
// Extract old messages stored in the debug queues
@ -948,18 +1055,25 @@ serialqueue_extract_old(struct serialqueue *sq, int sentq
, struct pull_queue_message *q, int max)
{
int count = sentq ? DEBUG_QUEUE_SENT : DEBUG_QUEUE_RECEIVE;
struct list_head *rootp = sentq ? &sq->old_sent : &sq->old_receive;
struct list_head replacement, current;
list_init(&replacement);
debug_queue_alloc(&replacement, count);
list_init(&current);
// Atomically replace existing debug list with new zero'd list
pthread_mutex_lock(&sq->lock);
list_join_tail(rootp, &current);
list_init(rootp);
list_join_tail(&replacement, rootp);
pthread_mutex_unlock(&sq->lock);
if (sentq) {
pthread_mutex_lock(&sq->lock);
list_join_tail(&sq->old_sent, &current);
list_init(&sq->old_sent);
list_join_tail(&replacement, &sq->old_sent);
pthread_mutex_unlock(&sq->lock);
} else {
pthread_mutex_lock(&sq->receiver.lock);
list_join_tail(&sq->receiver.old_receive, &current);
list_init(&sq->receiver.old_receive);
list_join_tail(&replacement, &sq->receiver.old_receive);
pthread_mutex_unlock(&sq->receiver.lock);
}
// Walk the debug list
int pos = 0;

View file

@ -27,7 +27,7 @@ struct pull_queue_message {
struct serialqueue;
struct serialqueue *serialqueue_alloc(int serial_fd, char serial_fd_type
, int client_id);
, int client_id, char name[16]);
void serialqueue_exit(struct serialqueue *sq);
void serialqueue_free(struct serialqueue *sq);
struct command_queue *serialqueue_alloc_commandqueue(void);

View file

@ -1,6 +1,6 @@
// Stepper pulse schedule compression
//
// Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2016-2025 Kevin O'Connor <kevin@koconnor.net>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
@ -28,15 +28,22 @@
#define CHECK_LINES 1
#define QUEUE_START_SIZE 1024
// Storage for queuing steps (only lower 32 bits of step clock are stored as
// optimization to reduce memory, improve cache usage, and reduce 64 bit ops)
struct qstep {
uint32_t clock32;
};
// Main stepcompress object storage
struct stepcompress {
// Buffer management
uint32_t *queue, *queue_end, *queue_pos, *queue_next;
struct qstep *queue, *queue_end, *queue_pos, *queue_next;
// Internal tracking
uint32_t max_error;
double mcu_time_offset, mcu_freq, last_step_print_time;
// Message generation
uint64_t last_step_clock;
struct list_head msg_queue;
struct list_head *msg_queue;
uint32_t oid;
int32_t queue_step_msgtag, set_next_step_dir_msgtag;
int sdir, invert_sdir;
@ -48,12 +55,14 @@ struct stepcompress {
struct list_head history_list;
};
// Parameters of a single queue_step command
struct step_move {
uint32_t interval;
uint16_t count;
int16_t add;
};
// Storage for internal history of recently sent queue_step commands
struct history_steps {
struct list_node node;
uint64_t first_clock, last_clock;
@ -85,10 +94,10 @@ struct points {
// Given a requested step time, return the minimum and maximum
// acceptable times
static inline struct points
minmax_point(struct stepcompress *sc, uint32_t *pos)
minmax_point(struct stepcompress *sc, struct qstep *pos)
{
uint32_t lsc = sc->last_step_clock, point = *pos - lsc;
uint32_t prevpoint = pos > sc->queue_pos ? *(pos-1) - lsc : 0;
uint32_t lsc = sc->last_step_clock, point = pos->clock32 - lsc;
uint32_t prevpoint = pos > sc->queue_pos ? (pos-1)->clock32 - lsc : 0;
uint32_t max_error = (point - prevpoint) / 2;
if (max_error > sc->max_error)
max_error = sc->max_error;
@ -105,7 +114,7 @@ minmax_point(struct stepcompress *sc, uint32_t *pos)
static struct step_move
compress_bisect_add(struct stepcompress *sc)
{
uint32_t *qlast = sc->queue_next;
struct qstep *qlast = sc->queue_next;
if (qlast > sc->queue_pos + 65535)
qlast = sc->queue_pos + 65535;
struct points point = minmax_point(sc, sc->queue_pos);
@ -242,23 +251,23 @@ check_line(struct stepcompress *sc, struct step_move move)
****************************************************************/
// Allocate a new 'stepcompress' object
struct stepcompress * __visible
stepcompress_alloc(uint32_t oid)
struct stepcompress *
stepcompress_alloc(struct list_head *msg_queue)
{
struct stepcompress *sc = malloc(sizeof(*sc));
memset(sc, 0, sizeof(*sc));
list_init(&sc->msg_queue);
list_init(&sc->history_list);
sc->oid = oid;
sc->sdir = -1;
sc->msg_queue = msg_queue;
return sc;
}
// Fill message id information
void __visible
stepcompress_fill(struct stepcompress *sc, uint32_t max_error
stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error
, int32_t queue_step_msgtag, int32_t set_next_step_dir_msgtag)
{
sc->oid = oid;
sc->max_error = max_error;
sc->queue_step_msgtag = queue_step_msgtag;
sc->set_next_step_dir_msgtag = set_next_step_dir_msgtag;
@ -276,9 +285,9 @@ stepcompress_set_invert_sdir(struct stepcompress *sc, uint32_t invert_sdir)
}
}
// Helper to free items from the history_list
static void
free_history(struct stepcompress *sc, uint64_t end_clock)
// Expire the stepcompress history older than the given clock
void
stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock)
{
while (!list_empty(&sc->history_list)) {
struct history_steps *hs = list_last_entry(
@ -290,22 +299,14 @@ free_history(struct stepcompress *sc, uint64_t end_clock)
}
}
// Expire the stepcompress history older than the given clock
static void
stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock)
{
free_history(sc, end_clock);
}
// Free memory associated with a 'stepcompress' object
void __visible
void
stepcompress_free(struct stepcompress *sc)
{
if (!sc)
return;
free(sc->queue);
message_queue_free(&sc->msg_queue);
free_history(sc, UINT64_MAX);
stepcompress_history_expire(sc, UINT64_MAX);
free(sc);
}
@ -330,7 +331,7 @@ calc_last_step_print_time(struct stepcompress *sc)
}
// Set the conversion rate of 'print_time' to mcu clock
static void
void
stepcompress_set_time(struct stepcompress *sc
, double time_offset, double mcu_freq)
{
@ -358,7 +359,7 @@ add_move(struct stepcompress *sc, uint64_t first_clock, struct step_move *move)
qm->min_clock = qm->req_clock = sc->last_step_clock;
if (move->count == 1 && first_clock >= sc->last_step_clock + CLOCK_DIFF_MAX)
qm->req_clock = first_clock;
list_add_tail(&qm->node, &sc->msg_queue);
list_add_tail(&qm->node, sc->msg_queue);
sc->last_step_clock = last_clock;
// Create and store move in history tracking
@ -422,7 +423,7 @@ set_next_step_dir(struct stepcompress *sc, int sdir)
};
struct queue_message *qm = message_alloc_and_encode(msg, 3);
qm->req_clock = sc->last_step_clock;
list_add_tail(&qm->node, &sc->msg_queue);
list_add_tail(&qm->node, sc->msg_queue);
return 0;
}
@ -437,7 +438,8 @@ queue_append_far(struct stepcompress *sc)
return ret;
if (step_clock >= sc->last_step_clock + CLOCK_DIFF_MAX)
return stepcompress_flush_far(sc, step_clock);
*sc->queue_next++ = step_clock;
sc->queue_next->clock32 = step_clock;
sc->queue_next++;
return 0;
}
@ -447,7 +449,7 @@ queue_append_extend(struct stepcompress *sc)
{
if (sc->queue_next - sc->queue_pos > 65535 + 2000) {
// No point in keeping more than 64K steps in memory
uint32_t flush = (*(sc->queue_next-65535)
uint32_t flush = ((sc->queue_next-65535)->clock32
- (uint32_t)sc->last_step_clock);
int ret = queue_flush(sc, sc->last_step_clock + flush);
if (ret)
@ -474,7 +476,8 @@ queue_append_extend(struct stepcompress *sc)
sc->queue_next = sc->queue + in_use;
}
*sc->queue_next++ = sc->next_step_clock;
sc->queue_next->clock32 = sc->next_step_clock;
sc->queue_next++;
sc->next_step_clock = 0;
return 0;
}
@ -492,7 +495,8 @@ queue_append(struct stepcompress *sc)
return queue_append_far(sc);
if (unlikely(sc->queue_next >= sc->queue_end))
return queue_append_extend(sc);
*sc->queue_next++ = sc->next_step_clock;
sc->queue_next->clock32 = sc->next_step_clock;
sc->queue_next++;
sc->next_step_clock = 0;
return 0;
}
@ -539,7 +543,7 @@ stepcompress_commit(struct stepcompress *sc)
}
// Flush pending steps
static int
int
stepcompress_flush(struct stepcompress *sc, uint64_t move_clock)
{
if (sc->next_step_clock && move_clock >= sc->next_step_clock) {
@ -611,35 +615,6 @@ stepcompress_find_past_position(struct stepcompress *sc, uint64_t clock)
return last_position;
}
// Queue an mcu command to go out in order with stepper commands
int __visible
stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len)
{
int ret = stepcompress_flush(sc, UINT64_MAX);
if (ret)
return ret;
struct queue_message *qm = message_alloc_and_encode(data, len);
qm->req_clock = sc->last_step_clock;
list_add_tail(&qm->node, &sc->msg_queue);
return 0;
}
// Queue an mcu command that will consume space in the mcu move queue
int __visible
stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock
, uint32_t *data, int len)
{
int ret = stepcompress_flush(sc, UINT64_MAX);
if (ret)
return ret;
struct queue_message *qm = message_alloc_and_encode(data, len);
qm->min_clock = qm->req_clock = req_clock;
list_add_tail(&qm->node, &sc->msg_queue);
return 0;
}
// Return history of queue_step commands
int __visible
stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p
@ -663,165 +638,3 @@ stepcompress_extract_old(struct stepcompress *sc, struct pull_history_steps *p
}
return res;
}
/****************************************************************
* Step compress synchronization
****************************************************************/
// The steppersync object is used to synchronize the output of mcu
// step commands. The mcu can only queue a limited number of step
// commands - this code tracks when items on the mcu step queue become
// free so that new commands can be transmitted. It also ensures the
// mcu step queue is ordered between steppers so that no stepper
// starves the other steppers of space in the mcu step queue.
struct steppersync {
// Serial port
struct serialqueue *sq;
struct command_queue *cq;
// Storage for associated stepcompress objects
struct stepcompress **sc_list;
int sc_num;
// Storage for list of pending move clocks
uint64_t *move_clocks;
int num_move_clocks;
};
// Allocate a new 'steppersync' object
struct steppersync * __visible
steppersync_alloc(struct serialqueue *sq, struct stepcompress **sc_list
, int sc_num, int move_num)
{
struct steppersync *ss = malloc(sizeof(*ss));
memset(ss, 0, sizeof(*ss));
ss->sq = sq;
ss->cq = serialqueue_alloc_commandqueue();
ss->sc_list = malloc(sizeof(*sc_list)*sc_num);
memcpy(ss->sc_list, sc_list, sizeof(*sc_list)*sc_num);
ss->sc_num = sc_num;
ss->move_clocks = malloc(sizeof(*ss->move_clocks)*move_num);
memset(ss->move_clocks, 0, sizeof(*ss->move_clocks)*move_num);
ss->num_move_clocks = move_num;
return ss;
}
// Free memory associated with a 'steppersync' object
void __visible
steppersync_free(struct steppersync *ss)
{
if (!ss)
return;
free(ss->sc_list);
free(ss->move_clocks);
serialqueue_free_commandqueue(ss->cq);
free(ss);
}
// Set the conversion rate of 'print_time' to mcu clock
void __visible
steppersync_set_time(struct steppersync *ss, double time_offset
, double mcu_freq)
{
int i;
for (i=0; i<ss->sc_num; i++) {
struct stepcompress *sc = ss->sc_list[i];
stepcompress_set_time(sc, time_offset, mcu_freq);
}
}
// Expire the stepcompress history before the given clock time
static void
steppersync_history_expire(struct steppersync *ss, uint64_t end_clock)
{
int i;
for (i = 0; i < ss->sc_num; i++)
{
struct stepcompress *sc = ss->sc_list[i];
stepcompress_history_expire(sc, end_clock);
}
}
// Implement a binary heap algorithm to track when the next available
// 'struct move' in the mcu will be available
static void
heap_replace(struct steppersync *ss, uint64_t req_clock)
{
uint64_t *mc = ss->move_clocks;
int nmc = ss->num_move_clocks, pos = 0;
for (;;) {
int child1_pos = 2*pos+1, child2_pos = 2*pos+2;
uint64_t child2_clock = child2_pos < nmc ? mc[child2_pos] : UINT64_MAX;
uint64_t child1_clock = child1_pos < nmc ? mc[child1_pos] : UINT64_MAX;
if (req_clock <= child1_clock && req_clock <= child2_clock) {
mc[pos] = req_clock;
break;
}
if (child1_clock < child2_clock) {
mc[pos] = child1_clock;
pos = child1_pos;
} else {
mc[pos] = child2_clock;
pos = child2_pos;
}
}
}
// Find and transmit any scheduled steps prior to the given 'move_clock'
int __visible
steppersync_flush(struct steppersync *ss, uint64_t move_clock
, uint64_t clear_history_clock)
{
// Flush each stepcompress to the specified move_clock
int i;
for (i=0; i<ss->sc_num; i++) {
int ret = stepcompress_flush(ss->sc_list[i], move_clock);
if (ret)
return ret;
}
// Order commands by the reqclock of each pending command
struct list_head msgs;
list_init(&msgs);
for (;;) {
// Find message with lowest reqclock
uint64_t req_clock = MAX_CLOCK;
struct queue_message *qm = NULL;
for (i=0; i<ss->sc_num; i++) {
struct stepcompress *sc = ss->sc_list[i];
if (!list_empty(&sc->msg_queue)) {
struct queue_message *m = list_first_entry(
&sc->msg_queue, struct queue_message, node);
if (m->req_clock < req_clock) {
qm = m;
req_clock = m->req_clock;
}
}
}
if (!qm || (qm->min_clock && req_clock > move_clock))
break;
uint64_t next_avail = ss->move_clocks[0];
if (qm->min_clock)
// The qm->min_clock field is overloaded to indicate that
// the command uses the 'move queue' and to store the time
// that move queue item becomes available.
heap_replace(ss, qm->min_clock);
// Reset the min_clock to its normal meaning (minimum transmit time)
qm->min_clock = next_avail;
// Batch this command
list_del(&qm->node);
list_add_tail(&qm->node, &msgs);
}
// Transmit commands
if (!list_empty(&msgs))
serialqueue_send_batch(ss->sq, ss->cq, &msgs);
steppersync_history_expire(ss, clear_history_clock);
return 0;
}

View file

@ -11,38 +11,30 @@ struct pull_history_steps {
int step_count, interval, add;
};
struct stepcompress *stepcompress_alloc(uint32_t oid);
void stepcompress_fill(struct stepcompress *sc, uint32_t max_error
struct list_head;
struct stepcompress *stepcompress_alloc(struct list_head *msg_queue);
void stepcompress_fill(struct stepcompress *sc, uint32_t oid, uint32_t max_error
, int32_t queue_step_msgtag
, int32_t set_next_step_dir_msgtag);
void stepcompress_set_invert_sdir(struct stepcompress *sc
, uint32_t invert_sdir);
void stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock);
void stepcompress_free(struct stepcompress *sc);
uint32_t stepcompress_get_oid(struct stepcompress *sc);
int stepcompress_get_step_dir(struct stepcompress *sc);
void stepcompress_set_time(struct stepcompress *sc
, double time_offset, double mcu_freq);
int stepcompress_append(struct stepcompress *sc, int sdir
, double print_time, double step_time);
int stepcompress_commit(struct stepcompress *sc);
int stepcompress_flush(struct stepcompress *sc, uint64_t move_clock);
int stepcompress_reset(struct stepcompress *sc, uint64_t last_step_clock);
int stepcompress_set_last_position(struct stepcompress *sc, uint64_t clock
, int64_t last_position);
int64_t stepcompress_find_past_position(struct stepcompress *sc
, uint64_t clock);
int stepcompress_queue_msg(struct stepcompress *sc, uint32_t *data, int len);
int stepcompress_queue_mq_msg(struct stepcompress *sc, uint64_t req_clock
, uint32_t *data, int len);
int stepcompress_extract_old(struct stepcompress *sc
, struct pull_history_steps *p, int max
, uint64_t start_clock, uint64_t end_clock);
struct serialqueue;
struct steppersync *steppersync_alloc(
struct serialqueue *sq, struct stepcompress **sc_list, int sc_num
, int move_num);
void steppersync_free(struct steppersync *ss);
void steppersync_set_time(struct steppersync *ss, double time_offset
, double mcu_freq);
int steppersync_flush(struct steppersync *ss, uint64_t move_clock
, uint64_t clear_history_clock);
#endif // stepcompress.h

View file

@ -0,0 +1,436 @@
// Stepper step transmit synchronization
//
// Copyright (C) 2016-2025 Kevin O'Connor <kevin@koconnor.net>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
// The steppersync object is used to synchronize the output of mcu
// step commands. The mcu can only queue a limited number of step
// commands - this code tracks when items on the mcu step queue become
// free so that new commands can be transmitted. It also ensures the
// mcu step queue is ordered between steppers so that no stepper
// starves the other steppers of space in the mcu step queue.
#include <pthread.h> // pthread_mutex_lock
#include <stddef.h> // offsetof
#include <stdlib.h> // malloc
#include <string.h> // memset
#include "compiler.h" // __visible
#include "pyhelper.h" // set_thread_name
#include "itersolve.h" // itersolve_generate_steps
#include "serialqueue.h" // struct queue_message
#include "stepcompress.h" // stepcompress_flush
#include "steppersync.h" // steppersync_alloc
#include "trapq.h" // trapq_check_sentinels
/****************************************************************
* SyncEmitter - message generation for each stepper
****************************************************************/
struct syncemitter {
// List node for storage in steppersync list
struct list_node ss_node;
// Transmit message queue
struct list_head msg_queue;
// Thread for step generation
struct stepcompress *sc;
struct stepper_kinematics *sk;
char name[16];
pthread_t tid;
pthread_mutex_t lock; // protects variables below
pthread_cond_t cond;
int have_work;
double bg_gen_steps_time;
uint64_t bg_flush_clock, bg_clear_history_clock;
int32_t bg_result;
};
// Return this emitters 'struct stepcompress' (or NULL if not allocated)
struct stepcompress * __visible
syncemitter_get_stepcompress(struct syncemitter *se)
{
return se->sc;
}
// Store a reference to stepper_kinematics
void __visible
syncemitter_set_stepper_kinematics(struct syncemitter *se
, struct stepper_kinematics *sk)
{
se->sk = sk;
}
// Report current stepper_kinematics
struct stepper_kinematics * __visible
syncemitter_get_stepper_kinematics(struct syncemitter *se)
{
return se->sk;
}
// Queue an mcu command that will consume space in the mcu move queue
void __visible
syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock
, uint32_t *data, int len)
{
struct queue_message *qm = message_alloc_and_encode(data, len);
qm->min_clock = qm->req_clock = req_clock;
list_add_tail(&qm->node, &se->msg_queue);
}
// Generate steps (via itersolve) and flush
static int32_t
se_generate_steps(struct syncemitter *se)
{
if (!se->sc || !se->sk)
return 0;
double gen_steps_time = se->bg_gen_steps_time;
uint64_t flush_clock = se->bg_flush_clock;
uint64_t clear_history_clock = se->bg_clear_history_clock;
// Generate steps
int32_t ret = itersolve_generate_steps(se->sk, se->sc, gen_steps_time);
if (ret)
return ret;
// Flush steps
ret = stepcompress_flush(se->sc, flush_clock);
if (ret)
return ret;
// Clear history
stepcompress_history_expire(se->sc, clear_history_clock);
return 0;
}
// Main background thread for generating steps
static void *
se_background_thread(void *data)
{
struct syncemitter *se = data;
set_thread_name(se->name);
pthread_mutex_lock(&se->lock);
for (;;) {
if (!se->have_work) {
pthread_cond_wait(&se->cond, &se->lock);
continue;
}
if (se->have_work < 0)
// Exit request
break;
// Request to generate steps
se->bg_result = se_generate_steps(se);
if (se->bg_result)
errorf("Error in syncemitter '%s' step generation", se->name);
se->have_work = 0;
pthread_cond_signal(&se->cond);
}
pthread_mutex_unlock(&se->lock);
return NULL;
}
// Signal background thread to start step generation
static void
se_start_gen_steps(struct syncemitter *se, double gen_steps_time
, uint64_t flush_clock, uint64_t clear_history_clock)
{
if (!se->sc || !se->sk)
return;
pthread_mutex_lock(&se->lock);
while (se->have_work)
pthread_cond_wait(&se->cond, &se->lock);
se->bg_gen_steps_time = gen_steps_time;
se->bg_flush_clock = flush_clock;
se->bg_clear_history_clock = clear_history_clock;
se->have_work = 1;
pthread_mutex_unlock(&se->lock);
pthread_cond_signal(&se->cond);
}
// Wait for background thread to complete last step generation request
static int32_t
se_finalize_gen_steps(struct syncemitter *se)
{
if (!se->sc || !se->sk)
return 0;
pthread_mutex_lock(&se->lock);
while (se->have_work)
pthread_cond_wait(&se->cond, &se->lock);
int32_t res = se->bg_result;
pthread_mutex_unlock(&se->lock);
return res;
}
// Allocate syncemitter and start thread
static struct syncemitter *
syncemitter_alloc(char name[16], int alloc_stepcompress)
{
struct syncemitter *se = malloc(sizeof(*se));
memset(se, 0, sizeof(*se));
list_init(&se->msg_queue);
strncpy(se->name, name, sizeof(se->name));
se->name[sizeof(se->name)-1] = '\0';
if (!alloc_stepcompress)
return se;
se->sc = stepcompress_alloc(&se->msg_queue);
int ret = pthread_mutex_init(&se->lock, NULL);
if (ret)
goto fail;
ret = pthread_cond_init(&se->cond, NULL);
if (ret)
goto fail;
ret = pthread_create(&se->tid, NULL, se_background_thread, se);
if (ret)
goto fail;
return se;
fail:
report_errno("se alloc", ret);
return NULL;
}
// Free syncemitter and exit background thread
static void
syncemitter_free(struct syncemitter *se)
{
if (!se)
return;
if (se->sc) {
pthread_mutex_lock(&se->lock);
while (se->have_work)
pthread_cond_wait(&se->cond, &se->lock);
se->have_work = -1;
pthread_cond_signal(&se->cond);
pthread_mutex_unlock(&se->lock);
int ret = pthread_join(se->tid, NULL);
if (ret)
report_errno("se pthread_join", ret);
stepcompress_free(se->sc);
}
message_queue_free(&se->msg_queue);
free(se);
}
/****************************************************************
* StepperSync - sort move queue for a micro-controller
****************************************************************/
struct steppersync {
// List node for storage in steppersyncmgr list
struct list_node ssm_node;
// Serial port
struct serialqueue *sq;
struct command_queue *cq;
// The syncemitters that generate messages on this mcu
struct list_head se_list;
// Convert from time to clock
struct clock_estimate ce;
// Storage for list of pending move clocks
uint64_t *move_clocks;
int num_move_clocks;
};
// Allocate a new syncemitter instance
struct syncemitter * __visible
steppersync_alloc_syncemitter(struct steppersync *ss, char name[16]
, int alloc_stepcompress)
{
struct syncemitter *se = syncemitter_alloc(name, alloc_stepcompress);
if (se)
list_add_tail(&se->ss_node, &ss->se_list);
return se;
}
// Fill information on mcu move queue
void __visible
steppersync_setup_movequeue(struct steppersync *ss, struct serialqueue *sq
, int move_num)
{
serialqueue_free_commandqueue(ss->cq);
free(ss->move_clocks);
ss->sq = sq;
ss->cq = serialqueue_alloc_commandqueue();
ss->move_clocks = malloc(sizeof(*ss->move_clocks)*move_num);
memset(ss->move_clocks, 0, sizeof(*ss->move_clocks)*move_num);
ss->num_move_clocks = move_num;
}
// Set the conversion rate of 'print_time' to mcu clock
void __visible
steppersync_set_time(struct steppersync *ss, double time_offset
, double mcu_freq)
{
clock_fill(&ss->ce, mcu_freq, time_offset, 0, 0);
struct syncemitter *se;
list_for_each_entry(se, &ss->se_list, ss_node) {
if (se->sc)
stepcompress_set_time(se->sc, time_offset, mcu_freq);
}
}
// Implement a binary heap algorithm to track when the next available
// 'struct move' in the mcu will be available
static void
heap_replace(struct steppersync *ss, uint64_t req_clock)
{
uint64_t *mc = ss->move_clocks;
int nmc = ss->num_move_clocks, pos = 0;
for (;;) {
int child1_pos = 2*pos+1, child2_pos = 2*pos+2;
uint64_t child2_clock = child2_pos < nmc ? mc[child2_pos] : UINT64_MAX;
uint64_t child1_clock = child1_pos < nmc ? mc[child1_pos] : UINT64_MAX;
if (req_clock <= child1_clock && req_clock <= child2_clock) {
mc[pos] = req_clock;
break;
}
if (child1_clock < child2_clock) {
mc[pos] = child1_clock;
pos = child1_pos;
} else {
mc[pos] = child2_clock;
pos = child2_pos;
}
}
}
// Find and transmit any scheduled steps prior to the given 'move_clock'
static void
steppersync_flush(struct steppersync *ss, uint64_t move_clock)
{
// Order commands by the reqclock of each pending command
struct list_head msgs;
list_init(&msgs);
for (;;) {
// Find message with lowest reqclock
uint64_t req_clock = MAX_CLOCK;
struct queue_message *qm = NULL;
struct syncemitter *se;
list_for_each_entry(se, &ss->se_list, ss_node) {
if (!list_empty(&se->msg_queue)) {
struct queue_message *m = list_first_entry(
&se->msg_queue, struct queue_message, node);
if (m->req_clock < req_clock) {
qm = m;
req_clock = m->req_clock;
}
}
}
if (!qm || (qm->min_clock && req_clock > move_clock))
break;
uint64_t next_avail = ss->move_clocks[0];
if (qm->min_clock)
// The qm->min_clock field is overloaded to indicate that
// the command uses the 'move queue' and to store the time
// that move queue item becomes available.
heap_replace(ss, qm->min_clock);
// Reset the min_clock to its normal meaning (minimum transmit time)
qm->min_clock = next_avail;
// Batch this command
list_del(&qm->node);
list_add_tail(&qm->node, &msgs);
}
// Transmit commands
if (!list_empty(&msgs))
serialqueue_send_batch(ss->sq, ss->cq, &msgs);
}
/****************************************************************
* StepperSyncMgr - manage a list of steppersync
****************************************************************/
struct steppersyncmgr {
struct list_head ss_list;
};
// Allocate a new 'steppersyncmgr' object
struct steppersyncmgr * __visible
steppersyncmgr_alloc(void)
{
struct steppersyncmgr *ssm = malloc(sizeof(*ssm));
memset(ssm, 0, sizeof(*ssm));
list_init(&ssm->ss_list);
return ssm;
}
// Free memory associated with a 'steppersync' object
void __visible
steppersyncmgr_free(struct steppersyncmgr *ssm)
{
if (!ssm)
return;
while (!list_empty(&ssm->ss_list)) {
struct steppersync *ss = list_first_entry(
&ssm->ss_list, struct steppersync, ssm_node);
list_del(&ss->ssm_node);
free(ss->move_clocks);
serialqueue_free_commandqueue(ss->cq);
while (!list_empty(&ss->se_list)) {
struct syncemitter *se = list_first_entry(
&ss->se_list, struct syncemitter, ss_node);
list_del(&se->ss_node);
syncemitter_free(se);
}
free(ss);
}
free(ssm);
}
// Allocate a new 'steppersync' object
struct steppersync * __visible
steppersyncmgr_alloc_steppersync(struct steppersyncmgr *ssm)
{
struct steppersync *ss = malloc(sizeof(*ss));
memset(ss, 0, sizeof(*ss));
list_init(&ss->se_list);
list_add_tail(&ss->ssm_node, &ssm->ss_list);
return ss;
}
// Generate and flush steps
int32_t __visible
steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time
, double gen_steps_time, double clear_history_time)
{
struct steppersync *ss;
// Prepare trapqs for step generation
list_for_each_entry(ss, &ssm->ss_list, ssm_node) {
struct syncemitter *se;
list_for_each_entry(se, &ss->se_list, ss_node) {
if (!se->sc || !se->sk)
continue;
struct trapq *tq = itersolve_get_trapq(se->sk);
if (tq)
trapq_check_sentinels(tq);
}
}
// Start step generation threads
list_for_each_entry(ss, &ssm->ss_list, ssm_node) {
uint64_t flush_clock = clock_from_time(&ss->ce, flush_time);
uint64_t clear_clock = clock_from_time(&ss->ce, clear_history_time);
struct syncemitter *se;
list_for_each_entry(se, &ss->se_list, ss_node) {
se_start_gen_steps(se, gen_steps_time, flush_clock, clear_clock);
}
}
// Wait for step generation threads to complete
int32_t res = 0;
list_for_each_entry(ss, &ssm->ss_list, ssm_node) {
struct syncemitter *se;
list_for_each_entry(se, &ss->se_list, ss_node) {
int32_t ret = se_finalize_gen_steps(se);
if (ret)
res = ret;
}
if (res)
continue;
uint64_t flush_clock = clock_from_time(&ss->ce, flush_time);
steppersync_flush(ss, flush_clock);
}
return res;
}

View file

@ -0,0 +1,32 @@
#ifndef STEPPERSYNC_H
#define STEPPERSYNC_H
#include <stdint.h> // uint64_t
struct syncemitter;
struct stepcompress *syncemitter_get_stepcompress(struct syncemitter *se);
void syncemitter_set_stepper_kinematics(struct syncemitter *se
, struct stepper_kinematics *sk);
struct stepper_kinematics *syncemitter_get_stepper_kinematics(
struct syncemitter *se);
void syncemitter_queue_msg(struct syncemitter *se, uint64_t req_clock
, uint32_t *data, int len);
struct steppersync;
struct syncemitter *steppersync_alloc_syncemitter(
struct steppersync *ss, char name[16], int alloc_stepcompress);
void steppersync_setup_movequeue(struct steppersync *ss, struct serialqueue *sq
, int move_num);
void steppersync_set_time(struct steppersync *ss, double time_offset
, double mcu_freq);
struct steppersyncmgr *steppersyncmgr_alloc(void);
void steppersyncmgr_free(struct steppersyncmgr *ssm);
struct serialqueue;
struct steppersync *steppersyncmgr_alloc_steppersync(
struct steppersyncmgr *ssm);
int32_t steppersyncmgr_gen_steps(struct steppersyncmgr *ssm, double flush_time
, double gen_steps_time
, double clear_history_time);
#endif // steppersync.h

View file

@ -49,6 +49,7 @@ trapq_alloc(void)
list_init(&tq->moves);
list_init(&tq->history);
struct move *head_sentinel = move_alloc(), *tail_sentinel = move_alloc();
head_sentinel->print_time = -1.0;
tail_sentinel->print_time = tail_sentinel->move_t = NEVER_TIME;
list_add_head(&head_sentinel->node, &tq->moves);
list_add_tail(&tail_sentinel->node, &tq->moves);
@ -103,7 +104,7 @@ trapq_add_move(struct trapq *tq, struct move *m)
// Add a null move to fill time gap
struct move *null_move = move_alloc();
null_move->start_pos = m->start_pos;
if (!prev->print_time && m->print_time > MAX_NULL_MOVE)
if (prev->print_time <= 0. && m->print_time > MAX_NULL_MOVE)
// Limit the first null move to improve numerical stability
null_move->print_time = m->print_time - MAX_NULL_MOVE;
else
@ -227,6 +228,22 @@ trapq_set_position(struct trapq *tq, double print_time
list_add_head(&m->node, &tq->history);
}
// Copy the info in a 'struct move' to a 'struct pull_move'
static void
copy_pull_move(struct pull_move *p, struct move *m)
{
p->print_time = m->print_time;
p->move_t = m->move_t;
p->start_v = m->start_v;
p->accel = 2. * m->half_accel;
p->start_x = m->start_pos.x;
p->start_y = m->start_pos.y;
p->start_z = m->start_pos.z;
p->x_r = m->axes_r.x;
p->y_r = m->axes_r.y;
p->z_r = m->axes_r.z;
}
// Return history of movement queue
int __visible
trapq_extract_old(struct trapq *tq, struct pull_move *p, int max
@ -234,21 +251,21 @@ trapq_extract_old(struct trapq *tq, struct pull_move *p, int max
{
int res = 0;
struct move *m;
list_for_each_entry_reverse(m, &tq->moves, node) {
if (start_time >= m->print_time + m->move_t || res >= max)
break;
if (end_time <= m->print_time || (!m->start_v && !m->half_accel))
continue;
copy_pull_move(p, m);
p++;
res++;
}
list_for_each_entry(m, &tq->history, node) {
if (start_time >= m->print_time + m->move_t || res >= max)
break;
if (end_time <= m->print_time)
continue;
p->print_time = m->print_time;
p->move_t = m->move_t;
p->start_v = m->start_v;
p->accel = 2. * m->half_accel;
p->start_x = m->start_pos.x;
p->start_y = m->start_pos.y;
p->start_z = m->start_pos.z;
p->x_r = m->axes_r.x;
p->y_r = m->axes_r.y;
p->z_r = m->axes_r.z;
copy_pull_move(p, m);
p++;
res++;
}

View file

@ -130,14 +130,8 @@ class ConfigWrapper:
def deprecate(self, option, value=None):
if not self.fileconfig.has_option(self.section, option):
return
if value is None:
msg = ("Option '%s' in section '%s' is deprecated."
% (option, self.section))
else:
msg = ("Value '%s' in option '%s' in section '%s' is deprecated."
% (value, option, self.section))
pconfig = self.printer.lookup_object("configfile")
pconfig.deprecate(self.section, option, value, msg)
pconfig.deprecate(self.section, option, value)
######################################################################
@ -468,8 +462,6 @@ class PrinterConfig:
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):
@ -496,27 +488,58 @@ class PrinterConfig:
def check_unused_options(self, config):
self.validate.check_unused(config.fileconfig)
# Deprecation warnings
def _add_deprecated(self, data):
key = tuple(list(data.items()))
if key in self.deprecated:
return False
self.deprecated[key] = True
self.status_warnings = self.status_warnings + [data]
return True
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
did_add = self._add_deprecated(res)
if did_add:
logging.warning(msg)
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
if value is None:
res = {'type': 'deprecated_option'}
defmsg = ("Option '%s' in section '%s' is deprecated."
% (option, self.section))
else:
res = {'type': 'deprecated_value', 'value': value}
defmsg = ("Value '%s' in option '%s' in section '%s' is deprecated."
% (value, option, self.section))
if msg is None:
msg = defmsg
res['message'] = msg
res['section'] = section
res['option'] = option
self._add_deprecated(res)
def deprecate_gcode(self, cmd, param=None, value=None, msg=None):
if param is None:
defmsg = "Command '%s' is deprecated." % (cmd,)
elif value is None:
defmsg = ("Parameter '%s' in command '%s' is deprecated."
% (param, cmd))
else:
defmsg = ("Value '%s=%s' in command '%s' is deprecated."
% (param, value, cmd))
if msg is None:
msg = defmsg
res = {'type': 'deprecated_gcode', 'message': msg,
'command': cmd, 'parameter': param, 'value': str(value)}
self._add_deprecated(res)
def deprecate_mcu_code(self, mcu, feature, msg=None):
mcu_name = mcu.get_name()
if msg is None:
vhost = self.printer.start_args['software_version']
vmcu = mcu.get_status()['mcu_version']
msg = ("MCU '%s' has deprecated code (it is missing feature '%s')."
" Recompiling and flashing is recommended (MCU version '%s',"
" host version '%s')." % (mcu_name, feature, vmcu, vhost))
res = {'type': 'deprecated_mcu_code', 'message': msg,
'mcu': mcu_name, 'feature': feature}
self._add_deprecated(res)
# Status reporting
def _build_status_config(self, config):
self.status_raw_config = {}

View file

@ -19,15 +19,16 @@ class MCU_scaled_adc:
self._callback = None
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):
def _handle_callback(self, samples):
max_adc = self._main.last_vref[1]
min_adc = self._main.last_vssa[1]
scaled_val = (read_value - min_adc) / (max_adc - min_adc)
self._last_state = (scaled_val, read_time)
self._callback(read_time, scaled_val)
def setup_adc_callback(self, report_time, callback):
adjsamples = [(t, (read_value - min_adc) / (max_adc - min_adc))
for t, read_value in samples]
self._last_state = adjsamples[-1]
self._callback(adjsamples)
def setup_adc_callback(self, callback):
self._callback = callback
self._mcu_adc.setup_adc_callback(report_time, self._handle_callback)
self._mcu_adc.setup_adc_callback(self._handle_callback)
def get_last_value(self):
return self._last_state
@ -52,8 +53,8 @@ class PrinterADCScaled:
pin_name = config.get(name + '_pin')
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_adc_sample(SAMPLE_TIME, SAMPLE_COUNT)
mcu_adc.setup_adc_callback(callback)
mcu_adc.setup_adc_sample(REPORT_TIME, 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
@ -68,9 +69,11 @@ class PrinterADCScaled:
adj_time = min(time_diff * self.inv_smooth_time, 1.)
smoothed_value = last_value + value_diff * adj_time
return (read_time, smoothed_value)
def vref_callback(self, read_time, read_value):
def vref_callback(self, samples):
read_time, read_value = samples[-1]
self.last_vref = self.calc_smooth(read_time, read_value, self.last_vref)
def vssa_callback(self, read_time, read_value):
def vssa_callback(self, samples):
read_time, read_value = samples[-1]
self.last_vssa = self.calc_smooth(read_time, read_value, self.last_vssa)
def load_config_prefix(config):

View file

@ -21,20 +21,21 @@ class PrinterADCtoTemperature:
self.adc_convert = adc_convert
ppins = config.get_printer().lookup_object('pins')
self.mcu_adc = ppins.setup_pin('adc', config.get('sensor_pin'))
self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback)
self.mcu_adc.setup_adc_callback(self.adc_callback)
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):
return REPORT_TIME
def adc_callback(self, read_time, read_value):
def adc_callback(self, samples):
read_time, read_value = samples[-1]
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):
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,
self.mcu_adc.setup_adc_sample(REPORT_TIME, 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)
@ -57,7 +58,7 @@ class HelperTemperatureDiagnostics:
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()
last_read_time, last_value = 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:

View file

@ -96,7 +96,6 @@ class ADS1220:
self.printer, self._process_batch, self._start_measurements,
self._finish_measurements, UPDATE_INTERVAL)
# Command Configuration
self.attach_probe_cmd = None
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))
@ -105,12 +104,15 @@ class ADS1220:
mcu.register_config_callback(self._build_config)
self.query_ads1220_cmd = None
def setup_trigger_analog(self, trigger_analog_oid):
self.mcu.add_config_cmd(
"ads1220_attach_trigger_analog oid=%d trigger_analog_oid=%d"
% (self.oid, trigger_analog_oid), is_init=True)
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.attach_probe_cmd = self.mcu.lookup_command(
"ads1220_attach_load_cell_probe oid=%c load_cell_probe_oid=%c")
self.ffreader.setup_query_command("query_ads1220_status oid=%c",
oid=self.oid, cq=cmdqueue)
@ -120,6 +122,9 @@ class ADS1220:
def get_samples_per_second(self):
return self.sps
def lookup_sensor_error(self, error_code):
return "Unknown ads1220 error" % (error_code,)
# 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):
@ -129,9 +134,6 @@ class ADS1220:
def add_client(self, callback):
self.batch_bulk.add_client(callback)
def attach_load_cell_probe(self, load_cell_probe_oid):
self.attach_probe_cmd.send([self.oid, load_cell_probe_oid])
# Measurement decoding
def _convert_samples(self, samples):
adc_factor = 1. / (1 << 23)
@ -175,6 +177,8 @@ class ADS1220:
# read startup register state and validate
val = self.read_reg(0x0, 4)
if val != RESET_STATE:
if self.mcu.is_fileoutput():
return
raise self.printer.command_error(
"Invalid ads1220 reset state (got %s vs %s).\n"
"This is generally indicative of connection problems\n"
@ -209,6 +213,8 @@ class ADS1220:
self.spi.spi_send(write_command)
stored_val = self.read_reg(reg, len(register_bytes))
if bytearray(register_bytes) != stored_val:
if self.mcu.is_fileoutput():
return
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)" % (

View file

@ -210,28 +210,28 @@ class ADS1X1X_chip:
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'])
pcfg = 0
pcfg |= (ADS1X1X_OS['OS_SINGLE'] & \
ADS1X1X_REG_CONFIG['OS_MASK'])
pcfg |= (ADS1X1X_MUX[pin_params['pin']] & \
ADS1X1X_REG_CONFIG['MULTIPLEXER_MASK'])
pcfg |= (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'] & \
pcfg |= (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'])
pcfg |= (self.comp_mode \
& ADS1X1X_REG_CONFIG['COMPARATOR_MODE_MASK'])
pcfg |= (self.comp_polarity \
& ADS1X1X_REG_CONFIG['COMPARATOR_POLARITY_MASK'])
pcfg |= (self.comp_latching \
& ADS1X1X_REG_CONFIG['COMPARATOR_LATCHING_MASK'])
pcfg |= (self.comp_queue \
& ADS1X1X_REG_CONFIG['COMPARATOR_QUEUE_MASK'])
pin_obj = ADS1X1X_pin(self, config)
pin_obj = ADS1X1X_pin(self, pcfg)
if pin in self._pins:
raise pins.error(
'pin %s for chip %s is used multiple times' \
@ -250,8 +250,8 @@ class ADS1X1X_chip:
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']) == \
cfg = self._read_register(ADS1X1X_REG_POINTER['CONFIG'])
return bool((cfg & ADS1X1X_REG_CONFIG['OS_MASK']) == \
ADS1X1X_OS['OS_IDLE'])
def calculate_sample_rate(self):
@ -281,7 +281,7 @@ class ADS1X1X_chip:
(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']) \
pin.pcfg = (pin.pcfg & ~ADS1X1X_REG_CONFIG['DATA_RATE_MASK']) \
| (sample_rate_bits & ADS1X1X_REG_CONFIG['DATA_RATE_MASK'])
self.delay = 1 / float(sample_rate)
@ -289,7 +289,7 @@ class ADS1X1X_chip:
def sample(self, pin):
with self._mutex:
try:
self._write_register(ADS1X1X_REG_POINTER['CONFIG'], pin.config)
self._write_register(ADS1X1X_REG_POINTER['CONFIG'], pin.pcfg)
self._reactor.pause(self._reactor.monotonic() + self.delay)
start_time = self._reactor.monotonic()
while not self.is_ready():
@ -318,10 +318,11 @@ class ADS1X1X_chip:
self._i2c.i2c_write(data)
class ADS1X1X_pin:
def __init__(self, chip, config):
def __init__(self, chip, pcfg):
self.mcu = chip.mcu
self.chip = chip
self.pcfg = pcfg
self._last_state = (0., 0.)
self.invalid_count = 0
self.chip._printer.register_event_handler("klippy:connect", \
@ -360,9 +361,10 @@ class ADS1X1X_pin:
self.invalid_count = 0
# Publish result
measured_time = self._reactor.monotonic()
self.callback(self.chip.mcu.estimated_print_time(measured_time),
target_value)
systime = self._reactor.monotonic()
measured_time = self.chip.mcu.estimated_print_time(systime)
self._last_state = (measured_time, target_value)
self.callback([(measured_time, target_value)])
else:
self.invalid_count = self.invalid_count + 1
self.check_invalid()
@ -377,16 +379,20 @@ class ADS1X1X_pin:
def get_mcu(self):
return self.mcu
def setup_adc_callback(self, report_time, callback):
self.report_time = report_time
def setup_adc_callback(self, callback):
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):
def setup_adc_sample(self, report_time, sample_time=0., sample_count=1,
batch_num=1, minval=0., maxval=1.,
range_check_count=0):
self.report_time = report_time
self.minval = minval
self.maxval = maxval
self.range_check_count = range_check_count
self.chip.handle_report_time_update()
def get_last_value(self):
return self._last_state
def load_config_prefix(config):
return ADS1X1X_chip(config)

View file

@ -1,6 +1,7 @@
# AHT10/AHT20/AHT21 I2c-based humiditure sensor support
# Support for AHTxx family I2C temperature and humidity sensors
#
# Copyright (C) 2023 Scott Mudge <mail@scottmudge.com>
# Copyright (C) 2025 Lev Voronov <minicx@disroot.org>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
@ -9,49 +10,67 @@ from . import bus
######################################################################
# Compatible Sensors:
# AHT10 - Tested w/ BTT GTR 1.0 MCU on i2c3
# AHT20 - Untested but should work
# AHT20 - Tested w/ N32G455 on i2c2
# AHT21 - Tested w/ BTT GTR 1.0 MCU on i2c3
# AHT30 - Untested, but should work
######################################################################
AHT10_I2C_ADDR= 0x38
I2C_ADDR = 0x38
AHT10_COMMANDS = {
'INIT' :[0xE1, 0x08, 0x00],
'MEASURE' :[0xAC, 0x33, 0x00],
'RESET' :[0xBA, 0x08, 0x00]
}
CMD_MEASURE = [0xAC, 0x33, 0x00]
CMD_RESET = [0xBA]
CMD_INIT_AHT1X = [0xE1, 0x08, 0x00]
CMD_INIT_AHT2X = [0xBE, 0x08, 0x00]
AHT10_MAX_BUSY_CYCLES= 5
# Status bits
STATUS_BUSY = 0x80
STATUS_CALIBRATED = 0x08
MAX_BUSY_CYCLES = 5
class AHTBase:
model = None
class AHT10:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[-1]
self.reactor = self.printer.get_reactor()
self.i2c = bus.MCU_I2C_from_config(
config, default_addr=AHT10_I2C_ADDR, default_speed=100000)
self.report_time = config.getint('aht10_report_time',30,minval=5)
config, default_addr=I2C_ADDR, default_speed=100000)
self.report_time = config.getint('aht10_report_time', 30, minval=5)
self.temp = self.min_temp = self.max_temp = self.humidity = 0.
self.sample_timer = self.reactor.register_timer(self._sample_aht10)
self.sample_timer = self.reactor.register_timer(self._sample_aht)
self.printer.add_object("aht10 " + self.name, self)
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
self.is_calibrated = False
self.handle_connect)
self.is_calibrated = False
self.init_sent = False
self._callback = None
def handle_connect(self):
self._init_aht10()
self._init_sensor()
self.reactor.update_timer(self.sample_timer, self.reactor.NOW)
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
def _send_init(self):
raise NotImplementedError("Subclass must implement _send_init")
def setup_callback(self, cb):
self._callback = cb
def _init_sensor(self):
self._send_init()
def get_report_time_delta(self):
return self.report_time
self.init_sent = True
if self._make_measurement():
if not self.is_calibrated:
logging.warning("%s %s: not calibrated, possible OTP fault"
% (self.model, self.name))
logging.info("%s %s: successfully initialized, "
"initial temp: %.3f, humidity: %.3f"
% (self.model, self.name, self.temp, self.humidity))
def _soft_reset(self):
logging.info("%s %s: performing soft reset" % (self.model, self.name))
self.i2c.i2c_write(CMD_RESET)
self.reactor.pause(self.reactor.monotonic() + 0.020)
def _make_measurement(self):
if not self.init_sent:
@ -66,45 +85,51 @@ class AHT10:
while is_busy:
# Check if we're constantly busy. If so, send soft-reset
# and issue warning.
if is_busy and cycles > AHT10_MAX_BUSY_CYCLES:
logging.warning("aht10: device reported busy after " +
"%d cycles, resetting device"% AHT10_MAX_BUSY_CYCLES)
self._reset_device()
if is_busy and cycles > MAX_BUSY_CYCLES:
logging.warning("%s %s: device reported busy after "
"%d cycles, resetting device"
% (self.model, self.name, MAX_BUSY_CYCLES))
self._soft_reset()
data = None
break
cycles += 1
# Write command for updating temperature+status bit
self.i2c.i2c_write(AHT10_COMMANDS['MEASURE'])
self.i2c.i2c_write(CMD_MEASURE)
# Wait 110ms after first read, 75ms minimum
self.reactor.pause(self.reactor.monotonic() + .110)
# Read data
# Read 6 bytes of data
read = self.i2c.i2c_read([], 6)
if read is None:
logging.warning("aht10: received data from" +
" i2c_read is None")
continue
data = bytearray(read['response'])
if len(data) < 6:
logging.warning("aht10: received bytes less than" +
" expected 6 [%d]"%len(data))
logging.warning("%s %s: received data from i2c_read is None"
% (self.model, self.name))
continue
self.is_calibrated = True if (data[0] & 0b00000100) else False
is_busy = True if (data[0] & 0b01000000) else False
data = bytearray(read['response'])
if len(data) < 6:
logging.warning("%s %s: received bytes less than expected:"
" got %d, need 6"
% (self.model, self.name, len(data)))
continue
self.is_calibrated = bool(data[0] & STATUS_CALIBRATED)
is_busy = bool(data[0] & STATUS_BUSY)
if is_busy:
return False
except Exception as e:
logging.exception("aht10: exception encountered" +
" reading data: %s"%str(e))
logging.exception("%s %s: exception encountered reading data: %s"
% (self.model, self.name, str(e)))
return False
temp = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]
self.temp = ((temp*200) / 1048576) - 50
hum = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4
self.humidity = int(hum * 100 / 1048576)
# Parse temperature: 20 bits starting at data[3] (low nibble)
temp_raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]
self.temp = ((temp_raw * 200.0) / 1048576.0) - 50.0
# Parse humidity: 20 bits starting at data[1]
hum_raw = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4
self.humidity = int(hum_raw * 100 / 1048576)
# Clamp humidity
if (self.humidity > 100):
@ -114,49 +139,65 @@ class AHT10:
return True
def _reset_device(self):
if not self.init_sent:
return
# Reset device
self.i2c.i2c_write(AHT10_COMMANDS['RESET'])
# Wait 100ms after reset
self.reactor.pause(self.reactor.monotonic() + .10)
def _init_aht10(self):
# Init device
self.i2c.i2c_write(AHT10_COMMANDS['INIT'])
# Wait 100ms after init
self.reactor.pause(self.reactor.monotonic() + .10)
self.init_sent = True
if self._make_measurement():
logging.info("aht10: successfully initialized, initial temp: " +
"%.3f, humidity: %.3f"%(self.temp, self.humidity))
def _sample_aht10(self, eventtime):
def _sample_aht(self, eventtime):
if not self._make_measurement():
self.temp = self.humidity = .0
return self.reactor.NEVER
if self.temp < self.min_temp or self.temp > self.max_temp:
self.printer.invoke_shutdown(
"AHT10 temperature %0.1f outside range of %0.1f:%.01f"
% (self.temp, self.min_temp, self.max_temp))
"%s temperature %.1f outside range of %.1f:%.1f" %
(self.model.upper(), self.temp, self.min_temp, self.max_temp))
measured_time = self.reactor.monotonic()
print_time = self.i2c.get_mcu().estimated_print_time(measured_time)
self._callback(print_time, self.temp)
return measured_time + self.report_time
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
def setup_callback(self, cb):
self._callback = cb
def get_report_time_delta(self):
return self.report_time
def get_status(self, eventtime):
return {
'temperature': round(self.temp, 2),
'humidity': self.humidity,
}
class AHT1x(AHTBase):
model = "aht1x"
def _send_init(self):
self.i2c.i2c_write(CMD_INIT_AHT1X)
self.reactor.pause(self.reactor.monotonic() + 0.040)
class AHT2x(AHTBase):
model = "aht2x"
def _send_init(self):
self.i2c.i2c_write(CMD_INIT_AHT2X)
self.reactor.pause(self.reactor.monotonic() + 0.100)
class AHT3x(AHTBase):
model = "aht3x"
def _send_init(self):
# Wait for auto-calibration at power-on
self.reactor.pause(self.reactor.monotonic() + 0.100)
def load_config(config):
# Register sensor
pheater = config.get_printer().lookup_object("heaters")
pheater.add_sensor_factory("AHT10", AHT10)
# Backwards compatibility alias
pheater.add_sensor_factory("AHT10", AHT1x)
pheater.add_sensor_factory("AHT1X", AHT1x)
pheater.add_sensor_factory("AHT2X", AHT2x)
pheater.add_sensor_factory("AHT3X", AHT3x)

View file

@ -97,7 +97,7 @@ class AngleCalibration:
return None
return self.mcu_stepper.mcu_to_commanded_position(self.mcu_pos_offset)
def load_calibration(self, angles):
# Calculate linear intepolation calibration buckets by solving
# Calculate linear interpolation calibration buckets by solving
# linear equations
angle_max = 1 << ANGLE_BITS
calibration_count = 1 << CALIBRATION_BITS

View file

@ -51,20 +51,27 @@ class AxisTwistCompensation:
self.printer.register_event_handler("probe:update_results",
self._update_z_compensation_value)
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
)
def _update_z_compensation_value(self, poslist):
for i in range(len(poslist)):
pos = poslist[i]
zo = 0.
if self.z_compensations:
zo += self._get_interpolated_z_compensation(
pos.test_x, 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
)
if self.zy_compensations:
zo += self._get_interpolated_z_compensation(
pos.test_y, self.zy_compensations,
self.compensation_start_y,
self.compensation_end_y
)
pos = manual_probe.ProbeResult(pos.bed_x, pos.bed_y, pos.bed_z + zo,
pos.test_x, pos.test_y, pos.test_z)
poslist[i] = pos
def _get_interpolated_z_compensation(
self, coord, z_compensations,
@ -101,8 +108,7 @@ class Calibrater:
self.gcode = self.printer.lookup_object('gcode')
self.probe = None
# probe settings are set to none, until they are available
self.lift_speed, self.probe_x_offset, self.probe_y_offset, _ = \
None, None, None, None
self.lift_speed = None
self.printer.register_event_handler("klippy:connect",
self._handle_connect)
self.speed = compensation.speed
@ -129,8 +135,6 @@ class Calibrater:
raise self.printer.config_error(
"AXIS_TWIST_COMPENSATION requires [probe] to be defined")
self.lift_speed = self.probe.get_probe_params()['lift_speed']
self.probe_x_offset, self.probe_y_offset, _ = \
self.probe.get_offsets()
def _register_gcode_handlers(self):
# register gcode handlers
@ -148,6 +152,7 @@ class Calibrater:
def cmd_AXIS_TWIST_COMPENSATION_CALIBRATE(self, gcmd):
self.gcmd = gcmd
probe_x_offset, probe_y_offset, _ = self.probe.get_offsets(gcmd)
sample_count = gcmd.get_int('SAMPLE_COUNT', DEFAULT_SAMPLE_COUNT)
axis = gcmd.get('AXIS', 'X')
@ -219,7 +224,7 @@ class Calibrater:
"Invalid axis.")
probe_points = self._calculate_probe_points(
nozzle_points, self.probe_x_offset, self.probe_y_offset)
nozzle_points, probe_x_offset, probe_y_offset)
# verify no other manual probe is in progress
manual_probe.verify_no_manual_probe(self.printer)
@ -231,7 +236,7 @@ class Calibrater:
self._calibration(probe_points, nozzle_points, interval_dist)
def _calculate_probe_points(self, nozzle_points,
probe_x_offset, probe_y_offset):
probe_x_offset, probe_y_offset):
# calculate the points to put the nozzle at
# returned as a list of tuples
probe_points = []
@ -267,7 +272,7 @@ class Calibrater:
# probe the point
pos = probe.run_single_probe(self.probe, self.gcmd)
self.current_measured_z = pos[2]
self.current_measured_z = pos.bed_z
# horizontal_move_z (to prevent probe trigger or hitting bed)
self._move_helper((None, None, self.horizontal_move_z))
@ -286,14 +291,14 @@ class Calibrater:
# returns a callback function for the manual probe
is_end = self.current_point_index == len(probe_points) - 1
def callback(kin_pos):
if kin_pos is None:
def callback(mpresult):
if mpresult is None:
# probe was cancelled
self.gcmd.respond_info(
"AXIS_TWIST_COMPENSATION_CALIBRATE: Probe cancelled, "
"calibration aborted")
return
z_offset = self.current_measured_z - kin_pos[2]
z_offset = self.current_measured_z - mpresult.bed_z
self.results.append(z_offset)
if is_end:
# end of calibration

View file

@ -34,7 +34,7 @@ def constrain(val, min_val, max_val):
def lerp(t, v0, v1):
return (1. - t) * v0 + t * v1
# retreive commma separated pair from config
# retrieve comma separated pair from config
def parse_config_pair(config, option, default, minval=None, maxval=None):
pair = config.getintlist(option, (default, default))
if len(pair) != 2:
@ -54,7 +54,7 @@ def parse_config_pair(config, option, default, minval=None, maxval=None):
% (option, str(maxval)))
return pair
# retreive commma separated pair from a g-code command
# retrieve comma separated pair from a g-code command
def parse_gcmd_pair(gcmd, name, minval=None, maxval=None):
try:
pair = [int(v.strip()) for v in gcmd.get(name).split(',')]
@ -74,7 +74,7 @@ def parse_gcmd_pair(gcmd, name, minval=None, maxval=None):
% (name, maxval))
return pair
# retreive commma separated coordinate from a g-code command
# retrieve comma separated coordinate from a g-code command
def parse_gcmd_coord(gcmd, name):
try:
v1, v2 = [float(v.strip()) for v in gcmd.get(name).split(',')]
@ -308,7 +308,7 @@ class BedMesh:
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()
offsets = [0, 0, 0] if prb is None else prb.get_offsets(gcmd)
result["probe_offsets"] = offsets
result["axis_minimum"] = th_sts["axis_minimum"]
result["axis_maximum"] = th_sts["axis_maximum"]
@ -651,9 +651,9 @@ class BedMeshCalibrate:
except BedMeshError as e:
raise gcmd.error(str(e))
self.probe_mgr.start_probe(gcmd)
def probe_finalize(self, offsets, positions):
z_offset = offsets[2]
positions = [[round(p[0], 2), round(p[1], 2), p[2]]
def probe_finalize(self, positions):
z_offset = 0.
positions = [[round(p.bed_x, 2), round(p.bed_y, 2), p.bed_z]
for p in positions]
if self.probe_mgr.get_zero_ref_mode() == ZrefMode.PROBE:
ref_pos = positions.pop()
@ -682,7 +682,7 @@ class BedMeshCalibrate:
idx_offset = 0
start_idx = 0
for i, pts in substitutes.items():
fpt = [p - o for p, o in zip(base_points[i], offsets[:2])]
fpt = list(base_points[i][:2])
# offset the index to account for additional samples
idx = i + idx_offset
# Add "normal" points
@ -702,7 +702,7 @@ class BedMeshCalibrate:
# validate length of result
if len(base_points) != len(positions):
self._dump_points(probed_pts, positions, offsets)
self._dump_points(probed_pts, positions)
raise self.gcode.error(
"bed_mesh: invalid position list size, "
"generated count: %d, probed count: %d"
@ -713,7 +713,7 @@ class BedMeshCalibrate:
row = []
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])]
offset_pos = pos[:2]
if (
not isclose(offset_pos[0], result[0], abs_tol=.5) or
not isclose(offset_pos[1], result[1], abs_tol=.5)
@ -786,7 +786,7 @@ class BedMeshCalibrate:
self.gcode.respond_info("Mesh Bed Leveling Complete")
if self._profile_name is not None:
self.bedmesh.save_profile(self._profile_name)
def _dump_points(self, probed_pts, corrected_pts, offsets):
def _dump_points(self, probed_pts, corrected_pts):
# logs generated points with offset applied, points received
# from the finalize callback, and the list of corrected points
points = self.probe_mgr.get_base_points()
@ -797,7 +797,7 @@ class BedMeshCalibrate:
for i in list(range(max_len)):
gen_pt = probed_pt = corr_pt = ""
if i < len(points):
off_pt = [p - o for p, o in zip(points[i], offsets[:2])]
off_pt = points[i][:2]
gen_pt = "(%.2f, %.2f)" % tuple(off_pt)
if i < len(probed_pts):
probed_pt = "(%.2f, %.2f, %.4f)" % tuple(probed_pts[i])
@ -914,7 +914,7 @@ class ProbeManager:
for i in range(y_cnt):
for j in range(x_cnt):
if not i % 2:
# move in positive directon
# move in positive direction
pos_x = min_x + j * x_dist
else:
# move in negative direction
@ -1164,7 +1164,7 @@ class ProbeManager:
def _gen_arc(self, origin, radius, start, step, count):
end = start + step * count
# create a segent for every 3 degress of travel
# create a segent for every 3 degrees of travel
for angle in range(start, end, step):
rad = math.radians(angle % 360)
opp = math.sin(rad) * radius
@ -1209,7 +1209,7 @@ class RapidScanHelper:
gcmd_params["SAMPLE_TIME"] = half_window * 2
self._raise_tool(gcmd, scan_height)
probe_session = pprobe.start_probe_session(gcmd)
offsets = pprobe.get_offsets()
offsets = pprobe.get_offsets(gcmd)
initial_move = True
for pos, is_probe_pt in self.probe_manager.iter_rapid_path():
pos = self._apply_offsets(pos[:2], offsets)
@ -1221,7 +1221,7 @@ class RapidScanHelper:
probe_session.run_probe(gcmd)
results = probe_session.pull_probed_results()
toolhead.get_last_move_time()
self.finalize_callback(offsets, results)
self.finalize_callback(results)
probe_session.end_probe_session()
def _raise_tool(self, gcmd, scan_height):

View file

@ -58,19 +58,17 @@ class BedTiltCalibrate:
cmd_BED_TILT_CALIBRATE_help = "Bed tilt calibration script"
def cmd_BED_TILT_CALIBRATE(self, gcmd):
self.probe_helper.start_probe(gcmd)
def probe_finalize(self, offsets, positions):
def probe_finalize(self, positions):
# Setup for coordinate descent analysis
z_offset = offsets[2]
logging.info("Calculating bed_tilt with: %s", positions)
params = { 'x_adjust': self.bedtilt.x_adjust,
'y_adjust': self.bedtilt.y_adjust,
'z_adjust': z_offset }
'z_adjust': 0. }
logging.info("Initial bed_tilt parameters: %s", params)
# Perform coordinate descent
def adjusted_height(pos, params):
x, y, z = pos
return (z - x*params['x_adjust'] - y*params['y_adjust']
- params['z_adjust'])
return (pos.bed_z - pos.bed_x*params['x_adjust']
- pos.bed_y*params['y_adjust'] - params['z_adjust'])
def errorfunc(params):
total_error = 0.
for pos in positions:
@ -81,8 +79,7 @@ class BedTiltCalibrate:
# Update current bed_tilt calculations
x_adjust = new_params['x_adjust']
y_adjust = new_params['y_adjust']
z_adjust = (new_params['z_adjust'] - z_offset
- x_adjust * offsets[0] - y_adjust * offsets[1])
z_adjust = new_params['z_adjust']
self.bedtilt.update_adjust(x_adjust, y_adjust, z_adjust)
# Log and report results
logging.info("Calculated bed_tilt parameters: %s", new_params)

View file

@ -65,8 +65,8 @@ class BLTouchProbe:
config, self, self.mcu_endstop.query_endstop)
self.probe_offsets = probe.ProbeOffsetsHelper(config)
self.param_helper = probe.ProbeParameterHelper(config)
self.homing_helper = probe.HomingViaProbeHelper(config, self,
self.param_helper)
self.homing_helper = probe.HomingViaProbeHelper(
config, self, self.probe_offsets, self.param_helper)
self.probe_session = probe.ProbeSessionHelper(
config, self.param_helper, self.homing_helper.start_probe_session)
# Register BLTOUCH_DEBUG command
@ -80,8 +80,8 @@ class BLTouchProbe:
self.handle_connect)
def get_probe_params(self, gcmd=None):
return self.param_helper.get_probe_params(gcmd)
def get_offsets(self):
return self.probe_offsets.get_offsets()
def get_offsets(self, gcmd=None):
return self.probe_offsets.get_offsets(gcmd)
def get_status(self, eventtime):
return self.cmd_helper.get_status(eventtime)
def start_probe_session(self, gcmd):

View file

@ -87,8 +87,8 @@ MODE_PERIODIC = 3
RUN_GAS = 1 << 4
NB_CONV_0 = 0
EAS_NEW_DATA = 1 << 7
GAS_DONE = 1 << 6
MEASURE_DONE = 1 << 5
GAS_IN_PROGRESS = 1 << 6
MEASURE_IN_PROGRESS = 1 << 5
RESET_CHIP_VALUE = 0xB6
BME_CHIPS = {
@ -284,7 +284,7 @@ class BME280:
self.chip_type, self.i2c.i2c_address))
# Reset chip
self.write_register('RESET', [RESET_CHIP_VALUE], wait=True)
self.write_register('RESET', [RESET_CHIP_VALUE])
self.reactor.pause(self.reactor.monotonic() + .5)
# Make sure non-volatile memory has been copied to registers
@ -394,7 +394,7 @@ class 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)
self.write_register('CTRL_MEAS', meas)
if self.chip_type == 'BME680':
self.write_register('CONFIG', self.iir_filter << 2)
@ -511,14 +511,6 @@ class BME280:
return comp_press
def _sample_bme680(self, eventtime):
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:
@ -528,7 +520,7 @@ class BME280:
# Enter forced mode
meas = self.os_temp << 5 | self.os_pres << 2 | MODE
self.write_register('CTRL_MEAS', meas, wait=True)
self.write_register('CTRL_MEAS', meas)
max_sample_time = self.max_sample_time
if run_gas:
max_sample_time += self.gas_heat_duration / 1000
@ -536,11 +528,14 @@ class BME280:
try:
# wait until results are ready
status = self.read_register('EAS_STATUS_0', 1)[0]
while not data_ready(status, run_gas):
while status & MEASURE_IN_PROGRESS:
self.reactor.pause(
self.reactor.monotonic() + self.max_sample_time)
status = self.read_register('EAS_STATUS_0', 1)[0]
# Nothing in progress and no new data
if not status & EAS_NEW_DATA:
return self.reactor.monotonic() + REPORT_TIME
data = self.read_register('PRESSURE_MSB', 8)
gas_data = [0, 0]
if run_gas:
@ -776,15 +771,12 @@ class BME280:
params = self.i2c.i2c_read(regs, read_len)
return bytearray(params['response'])
def write_register(self, reg_name, data, wait = False):
def write_register(self, reg_name, data):
if type(data) is not list:
data = [data]
reg = self.chip_registers[reg_name]
data.insert(0, reg)
if not wait:
self.i2c.i2c_write(data)
else:
self.i2c.i2c_write_wait_ack(data)
self.i2c.i2c_write(data)
def get_status(self, eventtime):
data = {

188
klippy/extras/bmi160.py Normal file
View file

@ -0,0 +1,188 @@
# Support for reading acceleration data from a BMI160 chip
#
# Copyright (C) 2025 Francisco Stephens <francisco.stephens.g@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import bus, adxl345, bulk_sensor
# BMI160 registers
REG_CHIPID = 0x00
REG_ACC_DATA_START = 0x12
REG_ACC_CONF = 0x40
REG_ACC_RANGE = 0x41
REG_FIFO_DOWNS = 0x45
REG_FIFO_CONFIG_0 = 0x46
REG_FIFO_CONFIG_1 = 0x47
REG_FIFO_DATA = 0x24
REG_FIFO_LENGTH_0 = 0x22
REG_CMD = 0x7E
REG_MOD_READ = 0x80
# BMI160 commands for CMD register
CMD_ACC_PM_SUSPEND = 0x10
CMD_ACC_PM_NORMAL = 0x11
CMD_FIFO_FLUSH = 0xB0
# BMI160 constants
BMI160_DEV_ID = 0xD1
# Target 1600Hz ODR, normal bandwidth, no undersampling
SET_ACC_CONF_1600HZ = 0x2C
# Set accelerometer range to +/-16g
SET_ACC_RANGE_16G = 0x0C
# Enable accelerometer FIFO, headerless mode
SET_FIFO_CONFIG_1 = 0x40
# No FIFO downsampling
SET_FIFO_DOWNS = 0x00
# Scale factor for +/-16g range (Datasheet: 2048 LSB/g)
FREEFALL_ACCEL = 9.80665 * 1000.
SCALE = FREEFALL_ACCEL / 2048.
BATCH_UPDATES = 0.100
BMI_I2C_ADDR = 0x69
# Printer class that controls BMI160 chip
class BMI160:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
adxl345.AccelCommandHelper(config, self)
self.axes_map = adxl345.read_axes_map(config, SCALE, SCALE, SCALE)
self.data_rate = 1600
# Setup mcu sensor_bmi160 bulk query code
# Check for SPI or I2C
if config.get('cs_pin', None) is not None:
# Using 1MHz to match working Arduino test
self.bus = bus.MCU_SPI_from_config(config, 0, default_speed=1000000)
self.bus_type = 'spi'
else:
self.bus = bus.MCU_I2C_from_config(config,
default_addr=BMI_I2C_ADDR, default_speed=400000)
self.bus_type = 'i2c'
self.mcu = mcu = self.bus.get_mcu()
self.oid = oid = mcu.create_oid()
self.query_bmi160_cmd = None
mcu.add_config_cmd("config_bmi160 oid=%d bus_oid=%d bus_oid_type=%s"
% (oid, self.bus.get_oid(), self.bus_type))
mcu.add_config_cmd("query_bmi160 oid=%d rest_ticks=0"
% (oid,), on_restart=True)
mcu.register_config_callback(self._build_config)
# Bulk sample message reading
chip_smooth = self.data_rate * BATCH_UPDATES * 2
self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, "<hhh")
self.last_error_count = 0
# Process messages in batches
self.batch_bulk = bulk_sensor.BatchBulkHelper(
self.printer, self._process_batch,
self._start_measurements, self._finish_measurements, BATCH_UPDATES)
self.name = config.get_name().split()[-1]
hdr = ('time', 'x_acceleration', 'y_acceleration', 'z_acceleration')
self.batch_bulk.add_mux_endpoint("bmi160/dump_bmi160", "sensor",
self.name, {'header': hdr})
def _build_config(self):
cmdqueue = self.bus.get_command_queue()
self.query_bmi160_cmd = self.mcu.lookup_command(
"query_bmi160 oid=%c rest_ticks=%u", cq=cmdqueue)
self.ffreader.setup_query_command("query_bmi160_status oid=%c",
oid=self.oid, cq=cmdqueue)
def read_reg(self, reg):
if self.bus_type == 'spi':
params = self.bus.spi_transfer([reg | REG_MOD_READ, 0x00])
response = bytearray(params['response'])
return response[1]
else:
params = self.bus.i2c_read([reg], 1)
return bytearray(params['response'])[0]
def set_reg(self, reg, val, minclock=0):
if self.bus_type == 'spi':
# spi_transfer to ensure command completes on MCU before continuing
self.bus.spi_transfer([reg, val & 0xFF], minclock=minclock)
else:
# I2C already waits for completion by default
self.bus.i2c_write([reg, val & 0xFF], minclock=minclock)
# Small delay between register writes for stability
self.reactor.pause(0.002)
# Don't verify CMD register (0x7E) or registers below 0x40
if reg >= 0x40 and reg != REG_CMD:
stored_val = self.read_reg(reg)
if stored_val != val:
raise self.printer.command_error(
"Failed to set BMI160 register [0x%x] to 0x%x: "
"got 0x%x. This is generally indicative of connection "
"problems (e.g. faulty wiring) or a faulty bmi160 "
"chip." % (reg, val, stored_val))
def start_internal_client(self):
aqh = adxl345.AccelQueryHelper(self.printer)
self.batch_bulk.add_client(aqh.handle_batch)
return aqh
def _convert_samples(self, samples):
(x_pos, x_scale), (y_pos, y_scale), (z_pos, z_scale) = self.axes_map
count = 0
for ptime, rx, ry, rz in samples:
raw_xyz = (rx, ry, rz)
x = round(raw_xyz[x_pos] * x_scale, 6)
y = round(raw_xyz[y_pos] * y_scale, 6)
z = round(raw_xyz[z_pos] * z_scale, 6)
samples[count] = (round(ptime, 6), x, y, z)
count += 1
del samples[count:]
def _start_measurements(self):
# 1. Force SPI Mode (Dummy Read)
if self.bus_type == 'spi':
self.read_reg(0x7F)
self.reactor.pause(0.010) # 10ms for mode switch
# 2. Verify ID
dev_id = self.read_reg(REG_CHIPID)
if dev_id != BMI160_DEV_ID:
raise self.printer.command_error(
"Invalid bmi160 id (got %x vs %x).\n"
"This is generally indicative of connection problems\n"
"(e.g. faulty wiring) or a faulty bmi160 chip."
% (dev_id, BMI160_DEV_ID))
# 3. Wake Up FIRST
# Send Normal Mode command
self.set_reg(REG_CMD, CMD_ACC_PM_NORMAL)
# CRITICAL: Wait 50ms for startup/PLL locking
self.reactor.pause(0.050)
# 4. Configure Registers (While Awake)
self.set_reg(REG_ACC_CONF, SET_ACC_CONF_1600HZ)
self.set_reg(REG_ACC_RANGE, SET_ACC_RANGE_16G)
self.set_reg(REG_FIFO_DOWNS, SET_FIFO_DOWNS)
self.set_reg(REG_FIFO_CONFIG_1, SET_FIFO_CONFIG_1)
# 5. Flush FIFO
self.set_reg(REG_CMD, CMD_FIFO_FLUSH)
self.reactor.pause(0.010)
# 6. Start Bulk Reading
# Start timer roughly immediately
rest_ticks = self.mcu.seconds_to_clock(4. / self.data_rate)
self.query_bmi160_cmd.send([self.oid, rest_ticks])
logging.info("BMI160 starting '%s' measurements", self.name)
self.ffreader.note_start()
self.last_error_count = 0
def _finish_measurements(self):
self.set_reg(REG_CMD, CMD_ACC_PM_SUSPEND)
self.query_bmi160_cmd.send_wait_ack([self.oid, 0])
self.ffreader.note_end()
logging.info("BMI160 finished '%s' measurements", self.name)
def _process_batch(self, eventtime):
samples = self.ffreader.pull_samples()
self._convert_samples(samples)
if not samples:
return {}
return {'data': samples, 'errors': self.last_error_count,
'overflows': self.ffreader.get_last_overflows()}
def load_config(config):
return BMI160(config)
def load_config_prefix(config):
return BMI160(config)

View file

@ -4,6 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import mcu
import logging
def resolve_bus_name(mcu, param, bus):
# Find enumerations for the given bus
@ -91,6 +92,9 @@ class MCU_SPI:
"mode=%u pulse_ticks=%u"):
pulse_ticks = self.mcu.seconds_to_clock(1./self.speed)
self.config_fmt = self.config_fmt_ticks % (pulse_ticks,)
else:
configfile = self.mcu.get_printer().lookup_object('configfile')
configfile.deprecate_mcu_code(self.mcu, 'spi_set_sw_bus')
self.mcu.add_config_cmd(self.config_fmt)
self.spi_send_cmd = self.mcu.lookup_command(
"spi_send oid=%c data=%*s", cq=self.cmd_queue)
@ -155,12 +159,14 @@ def MCU_SPI_from_config(config, mode, pin_option="cs_pin",
# Helper code for working with devices connected to an MCU via an I2C bus
class MCU_I2C:
def __init__(self, mcu, bus, addr, speed, sw_pins=None):
def __init__(self, mcu, bus, addr, speed, sw_pins=None,
async_write_only=False):
self.mcu = mcu
self.bus = bus
self.i2c_address = addr
self.oid = self.mcu.create_oid()
self.speed = speed
self.async_write_only = async_write_only
self.config_fmt_ticks = None
mcu.add_config_cmd("config_i2c oid=%d" % (self.oid,))
# Generate I2C bus config message
@ -180,6 +186,16 @@ class MCU_I2C:
self.cmd_queue = self.mcu.alloc_command_queue()
self.mcu.register_config_callback(self.build_config)
self.i2c_write_cmd = self.i2c_read_cmd = None
self.i2c_transfer_cmd = self.i2c_write_only_cmd = None
printer = self.mcu.get_printer()
printer.register_event_handler("klippy:connect", self._handle_connect)
# backward support i2c_write inside the init section
self._to_write = []
# Host side I2C error handling
self._configured = False
def _handle_connect(self):
for data in self._to_write:
self.i2c_write(data)
def get_oid(self):
return self.oid
def get_mcu(self):
@ -188,6 +204,13 @@ class MCU_I2C:
return self.i2c_address
def get_command_queue(self):
return self.cmd_queue
def _async_write_status(self, params):
status = params["i2c_bus_status"]
if status == "SUCCESS":
return
err_msg = "MCU '%s' I2C request to addr %i reports error %s" % (
self.mcu.get_name(), self.i2c_address, status)
logging.error(err_msg)
def build_config(self):
if '%' in self.config_fmt:
bus = resolve_bus_name(self.mcu, "i2c_bus", self.bus)
@ -198,29 +221,84 @@ class MCU_I2C:
" pulse_ticks=%u address=%u"):
pulse_ticks = self.mcu.seconds_to_clock(1./self.speed/2)
self.config_fmt = self.config_fmt_ticks % (pulse_ticks,)
else:
configfile = self.mcu.get_printer().lookup_object('configfile')
configfile.deprecate_mcu_code(self.mcu, 'i2c_set_sw_bus')
self.mcu.add_config_cmd(self.config_fmt)
self.i2c_write_cmd = self.mcu.lookup_command(
"i2c_write oid=%c data=%*s", cq=self.cmd_queue)
self.i2c_read_cmd = self.mcu.lookup_query_command(
"i2c_read oid=%c reg=%*s read_len=%u",
"i2c_read_response oid=%c response=%*s", oid=self.oid,
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
data_msg = "".join(["%02x" % (x,) for x in data])
self.mcu.add_config_cmd("i2c_write oid=%d data=%s" % (
self.oid, data_msg), is_init=True)
if self.mcu.try_lookup_command("i2c_read oid=%c reg=%*s read_len=%u"):
self.i2c_write_cmd = self.mcu.lookup_command(
"i2c_write oid=%c data=%*s", cq=self.cmd_queue)
self.i2c_write_only_cmd = self.i2c_write_cmd
self.i2c_read_cmd = self.mcu.lookup_query_command(
"i2c_read oid=%c reg=%*s read_len=%u",
"i2c_read_response oid=%c response=%*s", oid=self.oid,
cq=self.cmd_queue)
configfile = self.mcu.get_printer().lookup_object('configfile')
configfile.deprecate_mcu_code(self.mcu, 'i2c_transfer')
else:
self.i2c_transfer_cmd = self.mcu.lookup_query_command(
"i2c_transfer oid=%c write=%*s read_len=%u",
"i2c_response oid=%c i2c_bus_status=%c response=%*s",
oid=self.oid, cq=self.cmd_queue)
self.i2c_write_only_cmd = self.mcu.lookup_command(
"i2c_transfer oid=%c write=%*s read_len=%u",
cq=self.cmd_queue)
if self.mcu.is_fileoutput():
self.i2c_transfer_cmd = self.mcu.lookup_command(
"i2c_transfer oid=%c write=%*s read_len=%u",
cq=self.cmd_queue)
if self.async_write_only:
self.mcu.register_response(self._async_write_status,
"i2c_response", self.oid)
self._configured = True
def i2c_write_noack(self, data, minclock=0, reqclock=0):
if self.async_write_only:
self.i2c_write_only_cmd.send([self.oid, data, 0],
minclock=minclock, reqclock=reqclock)
return
self.i2c_write_cmd.send([self.oid, data],
minclock=minclock, reqclock=reqclock)
def i2c_write_wait_ack(self, data, minclock=0, reqclock=0):
def i2c_write(self, data, minclock=0, reqclock=0, retry=True):
if not self._configured:
self._to_write.append(data)
return
if self.async_write_only:
self.i2c_write_only_cmd.send([self.oid, data, 0],
minclock=minclock, reqclock=reqclock)
return
if self.i2c_transfer_cmd is not None:
self.i2c_transfer(data, minclock=minclock, reqclock=reqclock,
retry=retry)
return
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])
minclock=minclock, reqclock=reqclock)
def i2c_read(self, write, read_len, retry=True):
if self.async_write_only:
raise mcu.error("I2C dev is write only")
if self.i2c_transfer_cmd is not None:
return self.i2c_transfer(write, read_len, retry=retry)
return self.i2c_read_cmd.send([self.oid, write, read_len],
retry=retry)
def i2c_transfer(self, write, read_len=0, minclock=0, reqclock=0,
retry=True):
if self.async_write_only:
raise mcu.error("I2C dev is write only")
cmd = self.i2c_transfer_cmd
if self.mcu.is_fileoutput():
cmd.send([self.oid, write, read_len],
minclock=minclock, reqclock=reqclock)
return
param = cmd.send([self.oid, write, read_len],
minclock=minclock, reqclock=reqclock, retry=retry)
status = param["i2c_bus_status"]
if status == "SUCCESS":
return param
err_msg = "MCU '%s' I2C request to addr %i reports error %s" % (
self.mcu.get_name(), self.i2c_address, status)
self.mcu.get_printer().invoke_shutdown(err_msg)
def MCU_I2C_from_config(config, default_addr=None, default_speed=100000):
def MCU_I2C_from_config(config, default_addr=None, default_speed=100000,
async_write_only=False):
# Load bus parameters
printer = config.get_printer()
i2c_mcu = mcu.get_printer_mcu(printer, config.get('i2c_mcu', 'mcu'))
@ -236,13 +314,17 @@ def MCU_I2C_from_config(config, default_addr=None, default_speed=100000):
for name in ['scl', 'sda']]
sw_pin_params = [ppins.lookup_pin(config.get(name), share_type=name)
for name in sw_pin_names]
for pin_params in sw_pin_params:
if pin_params['chip'] != i2c_mcu:
raise ppins.error("%s: i2c pins must be on same mcu" % (
config.get_name(),))
sw_pins = tuple([pin_params['pin'] for pin_params in sw_pin_params])
bus = None
else:
bus = config.get('i2c_bus', None)
sw_pins = None
# Create MCU_I2C object
return MCU_I2C(i2c_mcu, bus, addr, speed, sw_pins)
return MCU_I2C(i2c_mcu, bus, addr, speed, sw_pins, async_write_only)
######################################################################

View file

@ -86,10 +86,11 @@ class MCU_buttons:
# ADC button tracking
######################################################################
ADC_REPORT_TIME = 0.015
ADC_REPORT_TIME = 0.010
ADC_DEBOUNCE_TIME = 0.025
ADC_SAMPLE_TIME = 0.001
ADC_SAMPLE_COUNT = 6
ADC_BATCH_COUNT = 5
class MCU_ADC_buttons:
def __init__(self, printer, pin, pullup):
@ -104,8 +105,10 @@ 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_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback)
self.mcu_adc.setup_adc_sample(ADC_REPORT_TIME,
ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT,
batch_num=ADC_BATCH_COUNT)
self.mcu_adc.setup_adc_callback(self.adc_callback)
query_adc = printer.lookup_object('query_adc')
query_adc.register_adc('adc_button:' + pin.strip(), self.mcu_adc)
@ -114,35 +117,36 @@ class MCU_ADC_buttons:
self.max_value = max(self.max_value, max_value)
self.buttons.append((min_value, max_value, callback))
def adc_callback(self, read_time, read_value):
adc = max(.00001, min(.99999, read_value))
value = self.pullup * adc / (1.0 - adc)
def adc_callback(self, samples):
for read_time, read_value in samples:
adc = max(.00001, min(.99999, read_value))
value = self.pullup * adc / (1.0 - adc)
# Determine button pressed
btn = None
if self.min_value <= value <= self.max_value:
for i, (min_value, max_value, cb) in enumerate(self.buttons):
if min_value < value < max_value:
btn = i
break
# Determine button pressed
btn = None
if self.min_value <= value <= self.max_value:
for i, (min_value, max_value, cb) in enumerate(self.buttons):
if min_value < value < max_value:
btn = i
break
# If the button changed, due to noise or pressing:
if btn != self.last_button:
# reset the debouncing timer
self.last_debouncetime = read_time
# If the button changed, due to noise or pressing:
if btn != self.last_button:
# reset the debouncing timer
self.last_debouncetime = read_time
# button debounce check & new button pressed
if ((read_time - self.last_debouncetime) >= ADC_DEBOUNCE_TIME
and self.last_button == btn and self.last_pressed != btn):
# release last_pressed
if self.last_pressed is not None:
self.call_button(self.last_pressed, False)
self.last_pressed = None
if btn is not None:
self.call_button(btn, True)
self.last_pressed = btn
# button debounce check & new button pressed
if ((read_time - self.last_debouncetime) >= ADC_DEBOUNCE_TIME
and self.last_button == btn and self.last_pressed != btn):
# release last_pressed
if self.last_pressed is not None:
self.call_button(self.last_pressed, False)
self.last_pressed = None
if btn is not None:
self.call_button(btn, True)
self.last_pressed = btn
self.last_button = btn
self.last_button = btn
def call_button(self, button, state):
minval, maxval, callback = self.buttons[button]

View file

@ -32,6 +32,8 @@ class PrinterCANBusStats:
self.mcu = self.printer.lookup_object(mcu_name)
# Lookup status query command
if self.mcu.try_lookup_command("get_canbus_status") is None:
configfile = self.printer.lookup_object('configfile')
configfile.deprecate_mcu_code(self.mcu, 'get_canbus_status')
return
self.get_canbus_status_cmd = self.mcu.lookup_query_command(
"get_canbus_status",

View file

@ -152,12 +152,12 @@ class DeltaCalibrate:
"%.3f,%.3f,%.3f" % tuple(spos1))
configfile.set(section, "distance%d_pos2" % (i,),
"%.3f,%.3f,%.3f" % tuple(spos2))
def probe_finalize(self, offsets, positions):
def probe_finalize(self, positions):
# Convert positions into (z_offset, stable_position) pairs
z_offset = offsets[2]
kin = self.printer.lookup_object('toolhead').get_kinematics()
delta_params = kin.get_calibration()
probe_positions = [(z_offset, delta_params.calc_stable_position(p))
csp = kin.get_calibration().calc_stable_position
probe_positions = [(p.test_z - p.bed_z,
csp([p.test_x, p.test_y, p.test_z]))
for p in positions]
# Perform analysis
self.calculate_params(probe_positions, self.last_distances)

View file

@ -12,7 +12,7 @@ def load_config_prefix(config):
if not config.has_section('display'):
raise config.error(
"A primary [display] section must be defined in printer.cfg "
"to use auxilary displays")
"to use auxiliary displays")
name = config.get_name().split()[-1]
if name == "display":
raise config.error(

View file

@ -236,6 +236,8 @@ class PrinterLCD:
except:
logging.exception("Error during display screen update")
self.lcd_chip.flush()
if self.redraw_request_pending:
return self.redraw_time
return eventtime + REDRAW_TIME
def request_redraw(self):
if self.redraw_request_pending:

View file

@ -13,7 +13,7 @@
# ftp://ftp.simtel.net/pub/simtelnet/msdos/screen/fntcol16.zip
# (c) Joseph Gil
#
# Indivdual fonts are public domain
# Individual fonts are public domain
######################################################################
VGA_FONT = [

View file

@ -130,7 +130,8 @@ class SPI4wire:
class I2C:
def __init__(self, config, default_addr):
self.i2c = bus.MCU_I2C_from_config(config, default_addr=default_addr,
default_speed=400000)
default_speed=400000,
async_write_only=True)
def send(self, cmds, is_data=False):
if is_data:
hdr = 0x40
@ -138,7 +139,7 @@ class I2C:
hdr = 0x00
cmds = bytearray(cmds)
cmds.insert(0, hdr)
self.i2c.i2c_write(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK)
self.i2c.i2c_write_noack(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK)
# Helper code for toggling a reset pin on startup
class ResetHelper:

View file

@ -62,8 +62,8 @@ 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:analyze_shutdown",
self._handle_analyze_shutdown)
self.printer.register_event_handler("klippy:notify_mcu_error",
self._handle_notify_mcu_error)
def add_clarify(self, msg, callback):
@ -88,11 +88,14 @@ class PrinterMCUError:
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):
def _handle_analyze_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))
# Report reactor info (no good place to do this, so done here)
logging.info("Reactor garbage collection: %s",
self.printer.get_reactor().get_gc_stats())
def _check_protocol_error(self, msg, details):
host_version = self.printer.start_args['software_version']
msg_update = []

View file

@ -89,7 +89,7 @@ class ExcludeObject:
offset = [0.] * num_coord
self.extrusion_offsets[ename] = offset
if len(offset) < num_coord:
offset.extend([0.] * (len(num_coord) - len(offset)))
offset.extend([0.] * (num_coord - len(offset)))
return offset
def get_position(self):

View file

@ -15,6 +15,8 @@ class PrinterExtruderStepper:
self.handle_connect)
def handle_connect(self):
self.extruder_stepper.sync_to_extruder(self.extruder_name)
def find_past_position(self, print_time):
return self.extruder_stepper.find_past_position(print_time)
def get_status(self, eventtime):
return self.extruder_stepper.get_status(eventtime)

View file

@ -63,7 +63,7 @@ class Fan:
self.last_req_value = value
self.last_fan_value = self.max_power
self.mcu_fan.set_pwm(print_time, self.max_power)
return "delay", self.kick_start_time
return "repeat", print_time + self.kick_start_time
self.last_fan_value = self.last_req_value = value
self.mcu_fan.set_pwm(print_time, value)
def set_speed(self, value, print_time=None):

View file

@ -43,7 +43,7 @@ class FirmwareRetraction:
self.unretract_length = (self.retract_length
+ self.unretract_extra_length)
self.is_retracted = False
cmd_GET_RETRACTION_help = ("Report firmware retraction paramters")
cmd_GET_RETRACTION_help = ("Report firmware retraction parameters")
def cmd_GET_RETRACTION(self, gcmd):
gcmd.respond_info("RETRACT_LENGTH=%.5f RETRACT_SPEED=%.5f"
" UNRETRACT_EXTRA_LENGTH=%.5f UNRETRACT_SPEED=%.5f"

View file

@ -1,6 +1,6 @@
# Utility for manually moving a stepper for diagnostic purposes
#
# Copyright (C) 2018-2019 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018-2025 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging
@ -10,7 +10,6 @@ BUZZ_DISTANCE = 1.
BUZZ_VELOCITY = BUZZ_DISTANCE / .250
BUZZ_RADIANS_DISTANCE = math.radians(1.)
BUZZ_RADIANS_VELOCITY = BUZZ_RADIANS_DISTANCE / .250
STALL_TIME = 0.100
# Calculate a move's accel_t, cruise_t, and cruise_v
def calc_move_time(dist, speed, accel):
@ -33,47 +32,46 @@ class ForceMove:
self.printer = config.get_printer()
self.steppers = {}
# Setup iterative solver
self.motion_queuing = self.printer.load_object(config, 'motion_queuing')
self.trapq = self.motion_queuing.allocate_trapq()
self.trapq_append = self.motion_queuing.lookup_trapq_append()
ffi_main, ffi_lib = chelper.get_ffi()
self.trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free)
self.trapq_append = ffi_lib.trapq_append
self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves
self.stepper_kinematics = ffi_main.gc(
ffi_lib.cartesian_stepper_alloc(b'x'), ffi_lib.free)
# Register commands
gcode = self.printer.lookup_object('gcode')
gcode.register_command('STEPPER_BUZZ', self.cmd_STEPPER_BUZZ,
desc=self.cmd_STEPPER_BUZZ_help)
if config.getboolean("enable_force_move", False):
gcode.register_command('FORCE_MOVE', self.cmd_FORCE_MOVE,
desc=self.cmd_FORCE_MOVE_help)
self._enable_force_move = config.getboolean("enable_force_move", False)
if self._enable_force_move:
gcode = self.printer.lookup_object('gcode')
gcode.register_command('SET_KINEMATIC_POSITION',
self.cmd_SET_KINEMATIC_POSITION,
desc=self.cmd_SET_KINEMATIC_POSITION_help)
def register_stepper(self, config, mcu_stepper):
self.steppers[mcu_stepper.get_name()] = mcu_stepper
name = mcu_stepper.get_name()
self.steppers[name] = mcu_stepper
# Reuse mux helper args checks
gcode = self.printer.lookup_object('gcode')
gcode.register_mux_command('STEPPER_BUZZ', "STEPPER", name,
self.cmd_STEPPER_BUZZ,
desc=self.cmd_STEPPER_BUZZ_help)
if self._enable_force_move:
gcode.register_mux_command('FORCE_MOVE', "STEPPER", name,
self.cmd_FORCE_MOVE,
desc=self.cmd_FORCE_MOVE_help)
def lookup_stepper(self, name):
if name not in self.steppers:
raise self.printer.config_error("Unknown stepper %s" % (name,))
return self.steppers[name]
def _force_enable(self, stepper):
toolhead = self.printer.lookup_object('toolhead')
print_time = toolhead.get_last_move_time()
stepper_name = stepper.get_name()
stepper_enable = self.printer.lookup_object('stepper_enable')
enable = stepper_enable.lookup_enable(stepper.get_name())
was_enable = enable.is_motor_enabled()
if not was_enable:
enable.motor_enable(print_time)
toolhead.dwell(STALL_TIME)
return was_enable
def _restore_enable(self, stepper, was_enable):
if not was_enable:
toolhead = self.printer.lookup_object('toolhead')
toolhead.dwell(STALL_TIME)
print_time = toolhead.get_last_move_time()
stepper_enable = self.printer.lookup_object('stepper_enable')
enable = stepper_enable.lookup_enable(stepper.get_name())
enable.motor_disable(print_time)
toolhead.dwell(STALL_TIME)
did_enable = stepper_enable.set_motors_enable([stepper_name], True)
return did_enable
def _restore_enable(self, stepper, did_enable):
if not did_enable:
return
stepper_name = stepper.get_name()
stepper_enable = self.printer.lookup_object('stepper_enable')
stepper_enable.set_motors_enable([stepper_name], False)
def manual_move(self, stepper, dist, speed, accel=0.):
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
@ -85,24 +83,17 @@ class ForceMove:
self.trapq_append(self.trapq, print_time, accel_t, cruise_t, accel_t,
0., 0., 0., axis_r, 0., 0., 0., cruise_v, accel)
print_time = print_time + accel_t + cruise_t + accel_t
stepper.generate_steps(print_time)
self.trapq_finalize_moves(self.trapq, print_time + 99999.9,
print_time + 99999.9)
stepper.set_trapq(prev_trapq)
stepper.set_stepper_kinematics(prev_sk)
toolhead.note_mcu_movequeue_activity(print_time)
self.motion_queuing.note_mcu_movequeue_activity(print_time)
toolhead.dwell(accel_t + cruise_t + accel_t)
toolhead.flush_step_generation()
def _lookup_stepper(self, gcmd):
name = gcmd.get('STEPPER')
if name not in self.steppers:
raise gcmd.error("Unknown stepper %s" % (name,))
return self.steppers[name]
stepper.set_trapq(prev_trapq)
stepper.set_stepper_kinematics(prev_sk)
self.motion_queuing.wipe_trapq(self.trapq)
cmd_STEPPER_BUZZ_help = "Oscillate a given stepper to help id it"
def cmd_STEPPER_BUZZ(self, gcmd):
stepper = self._lookup_stepper(gcmd)
stepper = self.lookup_stepper(gcmd.get('STEPPER'))
logging.info("Stepper buzz %s", stepper.get_name())
was_enable = self._force_enable(stepper)
did_enable = self._force_enable(stepper)
toolhead = self.printer.lookup_object('toolhead')
dist, speed = BUZZ_DISTANCE, BUZZ_VELOCITY
if stepper.units_in_radians():
@ -112,10 +103,10 @@ class ForceMove:
toolhead.dwell(.050)
self.manual_move(stepper, -dist, speed)
toolhead.dwell(.450)
self._restore_enable(stepper, was_enable)
self._restore_enable(stepper, did_enable)
cmd_FORCE_MOVE_help = "Manually move a stepper; invalidates kinematics"
def cmd_FORCE_MOVE(self, gcmd):
stepper = self._lookup_stepper(gcmd)
stepper = self.lookup_stepper(gcmd.get('STEPPER'))
distance = gcmd.get_float('DISTANCE')
speed = gcmd.get_float('VELOCITY', above=0.)
accel = gcmd.get_float('ACCEL', 0., minval=0.)

View file

@ -24,9 +24,12 @@ class GetStatusWrapper:
po = self.printer.lookup_object(sval, None)
if po is None or not hasattr(po, 'get_status'):
raise KeyError(val)
reactor = self.printer.get_reactor()
if self.eventtime is None:
self.eventtime = self.printer.get_reactor().monotonic()
self.cache[sval] = res = copy.deepcopy(po.get_status(self.eventtime))
self.eventtime = reactor.monotonic()
with reactor.assert_no_pause():
sts = po.get_status(self.eventtime)
self.cache[sval] = res = copy.deepcopy(sts)
return res
def __contains__(self, val):
try:

View file

@ -8,20 +8,6 @@ import logging
class GCodeMove:
def __init__(self, config):
self.printer = printer = config.get_printer()
printer.register_event_handler("klippy:ready", self._handle_ready)
printer.register_event_handler("klippy:shutdown", self._handle_shutdown)
printer.register_event_handler("toolhead:set_position",
self.reset_last_position)
printer.register_event_handler("toolhead:manual_move",
self.reset_last_position)
printer.register_event_handler("toolhead:update_extra_axes",
self._update_extra_axes)
printer.register_event_handler("gcode:command_error",
self.reset_last_position)
printer.register_event_handler("extruder:activate_extruder",
self._handle_activate_extruder)
printer.register_event_handler("homing:home_rails_end",
self._handle_home_rails_end)
self.is_printer_ready = False
# Register g-code commands
gcode = printer.lookup_object('gcode')
@ -52,6 +38,23 @@ class GCodeMove:
self.saved_states = {}
self.move_transform = self.move_with_transform = None
self.position_with_transform = (lambda: [0., 0., 0., 0.])
# Register callbacks
printer.register_event_handler("klippy:ready", self._handle_ready)
printer.register_event_handler("klippy:shutdown", self._handle_shutdown)
printer.register_event_handler("klippy:analyze_shutdown",
self._handle_analyze_shutdown)
printer.register_event_handler("toolhead:set_position",
self.reset_last_position)
printer.register_event_handler("toolhead:manual_move",
self.reset_last_position)
printer.register_event_handler("toolhead:update_extra_axes",
self._update_extra_axes)
printer.register_event_handler("gcode:command_error",
self.reset_last_position)
printer.register_event_handler("extruder:activate_extruder",
self._handle_activate_extruder)
printer.register_event_handler("homing:home_rails_end",
self._handle_home_rails_end)
def _handle_ready(self):
self.is_printer_ready = True
if self.move_transform is None:
@ -60,9 +63,8 @@ class GCodeMove:
self.position_with_transform = toolhead.get_position
self.reset_last_position()
def _handle_shutdown(self):
if not self.is_printer_ready:
return
self.is_printer_ready = False
def _handle_analyze_shutdown(self, msg, details):
logging.info("gcode state: absolute_coord=%s absolute_extrude=%s"
" base_position=%s last_position=%s homing_position=%s"
" speed_factor=%s extrude_factor=%s speed=%s",
@ -105,9 +107,10 @@ class GCodeMove:
'extrude_factor': self.extrude_factor,
'absolute_coordinates': self.absolute_coord,
'absolute_extrude': self.absolute_extrude,
'homing_origin': self.Coord(*self.homing_position[:4]),
'position': self.Coord(*self.last_position[:4]),
'gcode_position': self.Coord(*move_position[:4]),
'homing_origin': self.Coord(self.homing_position),
'position': self.Coord(self.last_position),
'gcode_position': self.Coord(move_position),
'axis_map': self.axis_map,
}
def reset_last_position(self):
if self.is_printer_ready:
@ -120,7 +123,8 @@ class GCodeMove:
if ea is None:
continue
gcode_id = ea.get_axis_gcode_id()
if gcode_id is None or gcode_id in axis_map or gcode_id in "FN":
if (gcode_id is None or len(gcode_id) != 1 or not gcode_id.isupper()
or gcode_id in axis_map or gcode_id in "FN"):
continue
axis_map[gcode_id] = index
self.axis_map = axis_map
@ -187,7 +191,7 @@ class GCodeMove:
def cmd_M114(self, gcmd):
# Get Current Position
p = self._get_gcode_position()
gcmd.respond_raw("X:%.3f Y:%.3f Z:%.3f E:%.3f" % tuple(p))
gcmd.respond_raw("X:%.3f Y:%.3f Z:%.3f E:%.3f" % tuple(p[:4]))
def cmd_M220(self, gcmd):
# Set speed factor override percentage
value = gcmd.get_float('S', 100., above=0.) / (60. * 100.)

View file

@ -49,11 +49,13 @@ 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_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback)
self.mcu_adc.setup_adc_sample(ADC_REPORT_TIME,
ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_callback(self.adc_callback)
self.mcu_adc2 = self.ppins.setup_pin('adc', self.pin2)
self.mcu_adc2.setup_adc_sample(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc2.setup_adc_callback(ADC_REPORT_TIME, self.adc2_callback)
self.mcu_adc2.setup_adc_sample(ADC_REPORT_TIME,
ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc2.setup_adc_callback(self.adc2_callback)
# extrude factor updating
self.extrude_factor_update_timer = self.reactor.register_timer(
self.extrude_factor_update_event)
@ -83,12 +85,14 @@ class HallFilamentWidthSensor:
self.reactor.update_timer(self.extrude_factor_update_timer,
self.reactor.NOW)
def adc_callback(self, read_time, read_value):
def adc_callback(self, samples):
# read sensor value
read_time, read_value = samples[-1]
self.lastFilamentWidthReading = round(read_value * 10000)
def adc2_callback(self, read_time, read_value):
def adc2_callback(self, samples):
# read sensor value
read_time, read_value = samples[-1]
self.lastFilamentWidthReading2 = round(read_value * 10000)
# calculate diameter
diameter_new = round((self.dia2 - self.dia1)/

View file

@ -15,6 +15,7 @@ MAX_HEAT_TIME = 3.0
AMBIENT_TEMP = 25.
PID_PARAM_BASE = 255.
MAX_MAINTHREAD_TIME = 5.0
QUELL_STALE_TIME = 7.0
class Heater:
def __init__(self, config, sensor):
@ -74,7 +75,8 @@ class Heater:
# No significant change in value - can suppress update
return
pwm_time = read_time + self.pwm_delay
self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME
self.next_pwm_time = (pwm_time + MAX_HEAT_TIME
- (3. * self.pwm_delay + 0.001))
self.last_pwm_value = value
self.mcu_pwm.set_pwm(pwm_time, value)
#logging.debug("%s: pwm=%.3f@%.3f (from %.3f@%.3f [%.3f])",
@ -110,9 +112,10 @@ class Heater:
with self.lock:
self.target_temp = degrees
def get_temp(self, eventtime):
print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) - 5.
est_print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime)
quell_time = est_print_time - QUELL_STALE_TIME
with self.lock:
if self.last_temp_time < print_time:
if self.last_temp_time < quell_time:
return 0., self.target_temp
return self.smoothed_temp, self.target_temp
def check_busy(self, eventtime):
@ -252,8 +255,6 @@ class PrinterHeaters:
gcode.register_command("TURN_OFF_HEATERS", self.cmd_TURN_OFF_HEATERS,
desc=self.cmd_TURN_OFF_HEATERS_help)
gcode.register_command("M105", self.cmd_M105, when_not_ready=True)
gcode.register_command("TEMPERATURE_WAIT", self.cmd_TEMPERATURE_WAIT,
desc=self.cmd_TEMPERATURE_WAIT_help)
def load_config(self, config):
self.have_load_sensors = True
# Load default temperature sensors
@ -296,7 +297,12 @@ class PrinterHeaters:
"Unknown temperature sensor '%s'" % (sensor_type,))
return self.sensor_factories[sensor_type](config)
def register_sensor(self, config, psensor, gcode_id=None):
self.available_sensors.append(config.get_name())
sensor_name = config.get_name()
self.available_sensors.append(sensor_name)
gcode = self.printer.lookup_object('gcode')
gcode.register_mux_command('TEMPERATURE_WAIT', "SENSOR", sensor_name,
self.cmd_TEMPERATURE_WAIT,
desc=self.cmd_TEMPERATURE_WAIT_help)
if gcode_id is None:
gcode_id = config.get('gcode_id', None)
if gcode_id is None:
@ -358,8 +364,6 @@ class PrinterHeaters:
cmd_TEMPERATURE_WAIT_help = "Wait for a temperature on a sensor"
def cmd_TEMPERATURE_WAIT(self, gcmd):
sensor_name = gcmd.get('SENSOR')
if sensor_name not in self.available_sensors:
raise gcmd.error("Unknown sensor '%s'" % (sensor_name,))
min_temp = gcmd.get_float('MINIMUM', float('-inf'))
max_temp = gcmd.get_float('MAXIMUM', float('inf'), above=min_temp)
if min_temp == float('-inf') and max_temp == float('inf'):

View file

@ -249,16 +249,18 @@ class PrinterHoming:
gcode = self.printer.lookup_object('gcode')
gcode.register_command('G28', self.cmd_G28)
def manual_home(self, toolhead, endstops, pos, speed,
triggered, check_triggered):
probe_pos, triggered, check_triggered):
hmove = HomingMove(self.printer, endstops, toolhead)
try:
hmove.homing_move(pos, speed, triggered=triggered,
check_triggered=check_triggered)
epos = hmove.homing_move(pos, speed, probe_pos=probe_pos,
triggered=triggered,
check_triggered=check_triggered)
except self.printer.command_error:
if self.printer.is_shutdown():
raise self.printer.command_error(
"Homing failed due to printer shutdown")
raise
return epos
def probing_move(self, mcu_probe, pos, speed):
endstops = [(mcu_probe, "probe")]
hmove = HomingMove(self.printer, endstops)

View file

@ -158,7 +158,7 @@ class HTU21D:
def _sample_htu21d(self, eventtime):
try:
# Read Temeprature
# Read Temperature
if self.hold_master_mode:
params = self.i2c.i2c_write([HTU21D_COMMANDS['HTU21D_TEMP']])
else:

View file

@ -53,7 +53,6 @@ class HX71xBase:
self._finish_measurements, UPDATE_INTERVAL)
# Command Configuration
self.query_hx71x_cmd = None
self.attach_probe_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))
@ -62,14 +61,17 @@ class HX71xBase:
mcu.register_config_callback(self._build_config)
def setup_trigger_analog(self, trigger_analog_oid):
self.mcu.add_config_cmd(
"hx71x_attach_trigger_analog oid=%d trigger_analog_oid=%d"
% (self.oid, trigger_analog_oid), is_init=True)
def _build_config(self):
cmd_queue = self.mcu.alloc_command_queue()
self.query_hx71x_cmd = self.mcu.lookup_command(
"query_hx71x oid=%c rest_ticks=%u")
self.attach_probe_cmd = self.mcu.lookup_command(
"hx71x_attach_load_cell_probe oid=%c load_cell_probe_oid=%c")
"query_hx71x oid=%c rest_ticks=%u", cq=cmd_queue)
self.ffreader.setup_query_command("query_hx71x_status oid=%c",
oid=self.oid,
cq=self.mcu.alloc_command_queue())
oid=self.oid, cq=cmd_queue)
def get_mcu(self):
@ -78,6 +80,9 @@ class HX71xBase:
def get_samples_per_second(self):
return self.sps
def lookup_sensor_error(self, error_code):
return "Unknown hx71x error %d" % (error_code,)
# 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):
@ -87,9 +92,6 @@ class HX71xBase:
def add_client(self, callback):
self.batch_bulk.add_client(callback)
def attach_load_cell_probe(self, load_cell_probe_oid):
self.attach_probe_cmd.send([self.oid, load_cell_probe_oid])
# Measurement decoding
def _convert_samples(self, samples):
adc_factor = 1. / (1 << 23)

View file

@ -35,7 +35,9 @@ class IdleTimeout:
printing_time = 0.
if self.state == "Printing":
printing_time = eventtime - self.last_print_start_systime
return { "state": self.state, "printing_time": printing_time }
return {"state": self.state,
"printing_time": printing_time,
"idle_timeout": self.idle_timeout}
def handle_ready(self):
self.toolhead = self.printer.lookup_object('toolhead')
self.timeout_timer = self.reactor.register_timer(self.timeout_handler)

View file

@ -1,7 +1,7 @@
# Kinematic input shaper to minimize motion vibrations in XY plane
#
# Copyright (C) 2019-2020 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
# Copyright (C) 2020-2025 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import collections
@ -11,34 +11,39 @@ from . import shaper_defs
class InputShaperParams:
def __init__(self, axis, config):
self.axis = axis
self.shapers = {s.name : s.init_func for s in shaper_defs.INPUT_SHAPERS}
self.shapers = {s.name : s for s in shaper_defs.INPUT_SHAPERS}
shaper_type = config.get('shaper_type', 'mzv')
self.shaper_type = config.get('shaper_type_' + axis, shaper_type)
if self.shaper_type not in self.shapers:
raise config.error(
'Unsupported shaper type: %s' % (self.shaper_type,))
self.damping_ratio = config.getfloat('damping_ratio_' + axis,
shaper_defs.DEFAULT_DAMPING_RATIO,
minval=0., maxval=1.)
self.damping_ratio = config.getfloat(
'damping_ratio_' + axis,
shaper_defs.DEFAULT_DAMPING_RATIO, minval=0.,
maxval=self.shapers[self.shaper_type].max_damping_ratio)
self.shaper_freq = config.getfloat('shaper_freq_' + axis, 0., minval=0.)
def update(self, gcmd):
axis = self.axis.upper()
self.damping_ratio = gcmd.get_float('DAMPING_RATIO_' + axis,
self.damping_ratio,
minval=0., maxval=1.)
self.shaper_freq = gcmd.get_float('SHAPER_FREQ_' + axis,
self.shaper_freq, minval=0.)
shaper_type = gcmd.get('SHAPER_TYPE', None)
if shaper_type is None:
shaper_type = gcmd.get('SHAPER_TYPE_' + axis, self.shaper_type)
if shaper_type.lower() not in self.shapers:
raise gcmd.error('Unsupported shaper type: %s' % (shaper_type,))
damping_ratio = gcmd.get_float('DAMPING_RATIO_' + axis,
self.damping_ratio, minval=0.)
if damping_ratio > self.shapers[shaper_type.lower()].max_damping_ratio:
raise gcmd.error(
'Too high value of damping_ratio=%.3f for shaper %s'
' on axis %c' % (damping_ratio, shaper_type, axis))
self.shaper_freq = gcmd.get_float('SHAPER_FREQ_' + axis,
self.shaper_freq, minval=0.)
self.damping_ratio = damping_ratio
self.shaper_type = shaper_type.lower()
def get_shaper(self):
if not self.shaper_freq:
A, T = shaper_defs.get_none_shaper()
else:
A, T = self.shapers[self.shaper_type](
A, T = self.shapers[self.shaper_type].init_func(
self.shaper_freq, self.damping_ratio)
return len(A), A, T
def get_status(self):
@ -95,7 +100,8 @@ class InputShaper:
self._update_kinematics)
self.toolhead = None
self.shapers = [AxisInputShaper('x', config),
AxisInputShaper('y', config)]
AxisInputShaper('y', config),
AxisInputShaper('z', config)]
self.input_shaper_stepper_kinematics = []
self.orig_stepper_kinematics = []
# Register gcode commands
@ -138,6 +144,7 @@ class InputShaper:
if self.toolhead is None:
# Klipper initialization is not yet completed
return
self.toolhead.flush_step_generation()
ffi_main, ffi_lib = chelper.get_ffi()
kin = self.toolhead.get_kinematics()
for s in kin.get_steppers():
@ -146,12 +153,9 @@ class InputShaper:
is_sk = self._get_input_shaper_stepper_kinematics(s)
if is_sk is None:
continue
old_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk)
ffi_lib.input_shaper_update_sk(is_sk)
new_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk)
if old_delay != new_delay:
self.toolhead.note_step_generation_scan_time(new_delay,
old_delay)
motion_queuing = self.printer.lookup_object("motion_queuing")
motion_queuing.check_step_generation_scan_windows()
def _update_input_shaping(self, error=None):
self.toolhead.flush_step_generation()
ffi_main, ffi_lib = chelper.get_ffi()
@ -163,16 +167,13 @@ class InputShaper:
is_sk = self._get_input_shaper_stepper_kinematics(s)
if is_sk is None:
continue
old_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk)
for shaper in self.shapers:
if shaper in failed_shapers:
continue
if not shaper.set_shaper_kinematics(is_sk):
failed_shapers.append(shaper)
new_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk)
if old_delay != new_delay:
self.toolhead.note_step_generation_scan_time(new_delay,
old_delay)
motion_queuing = self.printer.lookup_object("motion_queuing")
motion_queuing.check_step_generation_scan_windows()
if failed_shapers:
error = error or self.printer.command_error
raise error("Failed to configure shaper(s) %s with given parameters"
@ -191,8 +192,9 @@ class InputShaper:
for shaper in self.shapers:
shaper.update(gcmd)
self._update_input_shaping()
for shaper in self.shapers:
shaper.report(gcmd)
for ind, shaper in enumerate(self.shapers):
if ind < 2 or shaper.is_enabled():
shaper.report(gcmd)
def load_config(config):
return InputShaper(config)

View file

@ -84,11 +84,15 @@ class LDC1612:
default_addr=LDC1612_ADDR,
default_speed=400000)
self.mcu = mcu = self.i2c.get_mcu()
self._sensor_errors = {}
self.oid = oid = mcu.create_oid()
self.query_ldc1612_cmd = None
self.ldc1612_setup_home_cmd = self.query_ldc1612_home_state_cmd = None
self.frequency = config.getint("frequency", DEFAULT_LDC1612_FREQ,
2000000, 40000000)
self.clock_freq = config.getint("frequency", DEFAULT_LDC1612_FREQ,
2000000, 40000000)
# Coil frequency divider, assume 12MHz is BTT Eddy
# BTT Eddy's coil frequency is > 1/4 of reference clock
self.sensor_div = 1 if self.clock_freq != DEFAULT_LDC1612_FREQ else 2
self.freq_conv = float(self.clock_freq * self.sensor_div) / (1<<28)
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'))
@ -115,22 +119,25 @@ class LDC1612:
hdr = ('time', 'frequency', 'z')
self.batch_bulk.add_mux_endpoint("ldc1612/dump_ldc1612", "sensor",
self.name, {'header': hdr})
def setup_trigger_analog(self, trigger_analog_oid):
self.mcu.add_config_cmd(
"ldc1612_attach_trigger_analog oid=%d trigger_analog_oid=%d"
% (self.oid, trigger_analog_oid), is_init=True)
def _build_config(self):
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_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 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",
oid=self.oid, cq=cmdqueue)
errors = self.mcu.get_enumerations().get("ldc1612_error:", {})
self._sensor_errors = {v: k for k, v in errors.items()}
def get_mcu(self):
return self.i2c.get_mcu()
def get_samples_per_second(self):
return self.data_rate
def read_reg(self, reg):
if self.mcu.is_fileoutput():
return 0
params = self.i2c.i2c_read([reg], 2)
response = bytearray(params['response'])
return (response[0] << 8) | response[1]
@ -139,48 +146,61 @@ class LDC1612:
minclock=minclock)
def add_client(self, cb):
self.batch_bulk.add_client(cb)
# Homing
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(self.frequency) + 0.5)
self.ldc1612_setup_home_cmd.send(
[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, 0])
if self.mcu.is_fileoutput():
return 0.
params = self.query_ldc1612_home_state_cmd.send([self.oid])
tclock = self.mcu.clock32_to_clock64(params['trigger_clock'])
return self.mcu.clock_to_print_time(tclock)
def lookup_sensor_error(self, error):
return self._sensor_errors.get(error, "Unknown ldc1612 error")
def convert_frequency(self, freq):
return int(freq / self.freq_conv + 0.5)
# Measurement decoding
def _convert_samples(self, samples):
freq_conv = float(self.frequency) / (1<<28)
freq_conv = self.freq_conv
count = 0
errors = {}
def log_once(msg):
if not errors.get(msg, 0):
errors[msg] = 0
errors[msg] += 1
for ptime, val in samples:
mv = val & 0x0fffffff
if mv != val:
if val > 0x03ffffff or val == 0x0:
self.last_error_count += 1
if (val >> 16 & 0xffff) == 0xffff:
# Encoded error from sensor_ldc1612.c
log_once(self.lookup_sensor_error(val & 0xffff))
continue
error_bits = (val >> 28) & 0x0f
if error_bits & 0x8 or mv == 0x0000000:
log_once("Frequency under valid range")
if error_bits & 0x4 or mv > 0x3ffffff:
type = "hard" if error_bits & 0x4 else "soft"
log_once("Frequency over valid %s range" % (type))
if error_bits & 0x2:
log_once("Conversion Watchdog timeout")
if error_bits & 0x1:
log_once("Amplitude Low/High warning")
samples[count] = (round(ptime, 6), round(freq_conv * mv, 3), 999.9)
count += 1
del samples[count:]
for msg in errors:
logging.error("%s: %s (%d)" % (self.name, msg, errors[msg]))
# Start, stop, and process message batches
def _start_measurements(self):
# In case of miswiring, testing LDC1612 device ID prevents treating
# noise or wrong signal as a correctly initialized device
manuf_id = self.read_reg(REG_MANUFACTURER_ID)
dev_id = self.read_reg(REG_DEVICE_ID)
if manuf_id != LDC1612_MANUF_ID or dev_id != LDC1612_DEV_ID:
if ((manuf_id != LDC1612_MANUF_ID or dev_id != LDC1612_DEV_ID)
and not self.mcu.is_fileoutput()):
raise self.printer.command_error(
"Invalid ldc1612 id (got %x,%x vs %x,%x).\n"
"This is generally indicative of connection problems\n"
"(e.g. faulty wiring) or a faulty ldc1612 chip."
% (manuf_id, dev_id, LDC1612_MANUF_ID, LDC1612_DEV_ID))
# Setup chip in requested query rate
rcount0 = self.frequency / (16. * (self.data_rate - 4))
rcount0 = self.clock_freq / (16. * self.data_rate)
self.set_reg(REG_RCOUNT0, int(rcount0 + 0.5))
self.set_reg(REG_OFFSET0, 0)
self.set_reg(REG_SETTLECOUNT0, int(SETTLETIME*self.frequency/16. + .5))
self.set_reg(REG_CLOCK_DIVIDERS0, (1 << 12) | 1)
self.set_reg(REG_SETTLECOUNT0, int(SETTLETIME*self.clock_freq/16. + .5))
self.set_reg(REG_CLOCK_DIVIDERS0, (self.sensor_div << 12) | 1)
self.set_reg(REG_ERROR_CONFIG, (0x1f << 11) | 1)
self.set_reg(REG_MUX_CONFIG, 0x0208 | DEGLITCH)
self.set_reg(REG_CONFIG, 0x001 | (1<<12) | (1<<10) | (1<<9))

View file

@ -10,6 +10,7 @@ from . import output_pin
class LEDHelper:
def __init__(self, config, update_func, led_count=1):
self.printer = config.get_printer()
self.mutex = self.printer.get_reactor().mutex()
self.update_func = update_func
self.led_count = led_count
self.need_transmit = False
@ -59,11 +60,16 @@ class LEDHelper:
def _check_transmit(self, print_time=None):
if not self.need_transmit:
return
# Just avoid any race conditions
led_state = self.led_state
self.need_transmit = False
try:
self.update_func(self.led_state, print_time)
except self.printer.command_error as e:
logging.exception("led update transmit error")
def reactor_cb(eventtime):
try:
with self.mutex:
self.update_func(led_state, print_time)
except self.printer.command_error as e:
logging.exception("led update transmit error")
self.printer.get_reactor().register_callback(reactor_cb)
cmd_SET_LED_help = "Set the color of an LED"
def cmd_SET_LED(self, gcmd):
# Parse parameters

View file

@ -53,7 +53,7 @@ class ApiClientHelper(object):
wh = self.printer.lookup_object('webhooks')
wh.register_mux_endpoint(path, key, value, self._add_webhooks_client)
# Class for handling commands related ot load cells
# Class for handling commands related to load cells
class LoadCellCommandHelper:
def __init__(self, config, load_cell):
self.printer = config.get_printer()
@ -313,6 +313,8 @@ class LoadCellSampleCollector:
self._errors = 0
overflows = self._overflows
self._overflows = 0
if self._mcu.is_fileoutput():
samples = [(0., 0., 0.)]
return samples, (errors, overflows) if errors or overflows else 0
def _collect_until(self, timeout):
@ -324,6 +326,8 @@ class LoadCellSampleCollector:
raise self._printer.command_error(
"LoadCellSampleCollector timed out! Errors: %i,"
" Overflows: %i" % (self._errors, self._overflows))
if self._mcu.is_fileoutput():
break
self._reactor.pause(now + RETRY_DELAY)
return self._finish_collecting()
@ -383,7 +387,7 @@ class LoadCell:
# startup, when klippy is ready, start capturing data
printer.register_event_handler("klippy:ready", self._handle_ready)
def _handle_ready(self):
def _handle_do_ready(self, eventtime):
self.sensor.add_client(self._sensor_data_event)
self.add_client(self._track_force)
# announce calibration status on ready
@ -391,6 +395,8 @@ class LoadCell:
self.printer.send_event("load_cell:calibrate", self)
if self.is_tared():
self.printer.send_event("load_cell:tare", self)
def _handle_ready(self):
self.printer.get_reactor().register_callback(self._handle_do_ready)
# convert raw counts to grams and broadcast to clients
def _sensor_data_event(self, msg):

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