Merge branch 'Klipper3d:master' into feature/vsdlist_sorters

This commit is contained in:
Dragos Galalae 2025-02-15 00:46:40 +02:00 committed by GitHub
commit 8a953675e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 1405 additions and 153 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,6 +8,10 @@ All dates in this document are approximate.
## Changes
20250131: Option `VARIABLE=<name>` in `SAVE_VARIABLE` requires lowercase
value. For example, `extruder` instead of mixedcase `Extruder` or
uppercase `EXTRUDER`. Using any uppercase letter will raise an error.
20241203: The resonance test has been changed to include slow sweeping
moves. This change requires that testing point(s) have some clearance
in X/Y plane (+/- 30 mm from the test point should suffice when using

View file

@ -3846,6 +3846,7 @@ run_current:
#driver_SEIMIN: 0
#driver_SFILT: 0
#driver_SG4_ANGLE_OFFSET: 1
#driver_SLOPE_CONTROL: 0
# Set the given register during the configuration of the TMC2240
# chip. This may be used to set custom motor parameters. The
# defaults for each parameter are next to the parameter name in the
@ -4950,6 +4951,50 @@ vssa_pin:
# noise. The default is 2 seconds.
```
### [ads1x1x]
ADS1013, ADS1014, ADS1015, ADS1113, ADS1114 and ADS1115 are I2C based Analog to
Digital Converters that can be used for temperature sensors. They provide 4
analog input pins either as single line or as differential input.
Note: Use caution if using this sensor to control heaters. The heater min_temp
and max_temp are only verified in the host and only if the host is running and
operating normally. (ADC inputs directly connected to the micro-controller
verify min_temp and max_temp within the micro-controller and do not require a
working connection to the host.)
```
[ads1x1x my_ads1x1x]
chip: ADS1115
#pga: 4.096V
# Default value is 4.096V. The maximum voltage range used for the input. This
# scales all values read from the ADC. Options are: 6.144V, 4.096V, 2.048V,
# 1.024V, 0.512V, 0.256V
#adc_voltage: 3.3
# The suppy voltage for the device. This allows additional software scaling
# for all values read from the ADC.
i2c_mcu: host
i2c_bus: i2c.1
#address_pin: GND
# Default value is GND. There can be up to four addressed devices depending
# upon wiring of the device. Check the datasheet for details. The i2c_address
# can be specified directly instead of using the address_pin.
```
The chip provides pins that can be used on other sensors.
```
sensor_type: ...
# Can be any thermistor or adc_temperature.
sensor_pin: my_ads1x1x:AIN0
# A combination of the name of the ads1x1x chip and the pin. Possible
# pin values are AIN0, AIN1, AIN2 and AIN3 for single ended lines and
# DIFF01, DIFF03, DIFF13 and DIFF23 for differential between their
# correspoding lines. For example
# DIFF03 measures the differential between line 0 and 3. Only specific
# combinations for the differentials are allowed.
```
### [replicape]
Replicape support - see the [beaglebone guide](Beaglebone.md) and the

View file

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

View file

@ -585,15 +585,18 @@ state; issue a G28 afterwards to reset the kinematics. This command is
intended for low-level diagnostics and debugging.
#### SET_KINEMATIC_POSITION
`SET_KINEMATIC_POSITION [X=<value>] [Y=<value>] [Z=<value>]`: Force
the low-level kinematic code to believe the toolhead is at the given
cartesian position. This is a diagnostic and debugging command; use
SET_GCODE_OFFSET and/or G92 for regular axis transformations. If an
axis is not specified then it will default to the position that the
head was last commanded to. Setting an incorrect or invalid position
may lead to internal software errors. This command may invalidate
future boundary checks; issue a G28 afterwards to reset the
kinematics.
`SET_KINEMATIC_POSITION [X=<value>] [Y=<value>] [Z=<value>]
[CLEAR=<[X][Y][Z]>]`: Force the low-level kinematic code to believe the
toolhead is at the given cartesian position. This is a diagnostic and
debugging command; use SET_GCODE_OFFSET and/or G92 for regular axis
transformations. If an axis is not specified then it will default to the
position that the head was last commanded to. Setting an incorrect or
invalid position may lead to internal software errors. Use the CLEAR
parameter to forget the homing state for the given axes. Note that CLEAR
will not override the previous functionality; if an axis is not specified
to CLEAR it will have its kinematic position set as per above. This
command may invalidate future boundary checks; issue a G28 afterwards to
reset the kinematics.
### [gcode]
@ -1198,8 +1201,9 @@ has been enabled.
#### SAVE_VARIABLE
`SAVE_VARIABLE VARIABLE=<name> VALUE=<value>`: Saves the variable to
disk so that it can be used across restarts. All stored variables are
loaded into the `printer.save_variables.variables` dict at startup and
disk so that it can be used across restarts. The VARIABLE must be lowercase.
All stored variables are loaded into the
`printer.save_variables.variables` dict at startup and
can be used in gcode macros. The provided VALUE is parsed as a Python
literal.

View file

@ -61,7 +61,7 @@ their own Klipper-centric images.
The two main Moonraker based front ends are [Fluidd](https://docs.fluidd.xyz/)
and [Mainsail](https://docs.mainsail.xyz/), the latter of which has a premade install
image ["MainsailOS"](http://docs.mainsailOS.xyz), this has the option for Raspberry Pi
image ["MainsailOS"](https://docs-os.mainsail.xyz/), this has the option for Raspberry Pi
and some OrangePi varianta.
Fluidd can be installed via KIAUH(Klipper Install And Update Helper), which

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -408,6 +408,8 @@ class TMC2240:
set_config_field(config, "tpowerdown", 10)
# SG4_THRS
set_config_field(config, "sg4_angle_offset", 1)
# DRV_CONF
set_config_field(config, "slope_control", 0)
def load_config_prefix(config):
return TMC2240(config)

View file

@ -40,8 +40,6 @@ class CartKinematics:
for s in self.get_steppers():
s.set_trapq(toolhead.get_trapq())
toolhead.register_step_generator(s.generate_steps)
self.printer.register_event_handler("stepper_enable:motor_off",
self._motor_off)
# Setup boundary checks
max_velocity, max_accel = toolhead.get_max_velocity()
self.max_z_velocity = config.getfloat('max_z_velocity', max_velocity,
@ -67,15 +65,17 @@ class CartKinematics:
def set_position(self, newpos, homing_axes):
for i, rail in enumerate(self.rails):
rail.set_position(newpos)
for axis in homing_axes:
for axis_name in homing_axes:
axis = "xyz".index(axis_name)
if self.dc_module and axis == self.dc_module.axis:
rail = self.dc_module.get_primary_rail().get_rail()
else:
rail = self.rails[axis]
self.limits[axis] = rail.get_range()
def note_z_not_homed(self):
# Helper for Safe Z Home
self.limits[2] = (1.0, -1.0)
def clear_homing_state(self, clear_axes):
for axis, axis_name in enumerate("xyz"):
if axis_name in clear_axes:
self.limits[axis] = (1.0, -1.0)
def home_axis(self, homing_state, axis, rail):
# Determine movement
position_min, position_max = rail.get_range()
@ -96,8 +96,6 @@ class CartKinematics:
self.dc_module.home(homing_state)
else:
self.home_axis(homing_state, axis, self.rails[axis])
def _motor_off(self, print_time):
self.limits = [(1.0, -1.0)] * 3
def _check_endstops(self, move):
end_pos = move.end_pos
for i in (0, 1, 2):

View file

@ -21,8 +21,6 @@ class CoreXYKinematics:
for s in self.get_steppers():
s.set_trapq(toolhead.get_trapq())
toolhead.register_step_generator(s.generate_steps)
config.get_printer().register_event_handler("stepper_enable:motor_off",
self._motor_off)
# Setup boundary checks
max_velocity, max_accel = toolhead.get_max_velocity()
self.max_z_velocity = config.getfloat(
@ -41,11 +39,12 @@ class CoreXYKinematics:
def set_position(self, newpos, homing_axes):
for i, rail in enumerate(self.rails):
rail.set_position(newpos)
if i in homing_axes:
if "xyz"[i] in homing_axes:
self.limits[i] = rail.get_range()
def note_z_not_homed(self):
# Helper for Safe Z Home
self.limits[2] = (1.0, -1.0)
def clear_homing_state(self, clear_axes):
for axis, axis_name in enumerate("xyz"):
if axis_name in clear_axes:
self.limits[axis] = (1.0, -1.0)
def home(self, homing_state):
# Each axis is homed independently and in order
for axis in homing_state.get_axes():
@ -62,8 +61,6 @@ class CoreXYKinematics:
forcepos[axis] += 1.5 * (position_max - hi.position_endstop)
# Perform homing
homing_state.home_rails([rail], forcepos, homepos)
def _motor_off(self, print_time):
self.limits = [(1.0, -1.0)] * 3
def _check_endstops(self, move):
end_pos = move.end_pos
for i in (0, 1, 2):

View file

@ -21,8 +21,6 @@ class CoreXZKinematics:
for s in self.get_steppers():
s.set_trapq(toolhead.get_trapq())
toolhead.register_step_generator(s.generate_steps)
config.get_printer().register_event_handler("stepper_enable:motor_off",
self._motor_off)
# Setup boundary checks
max_velocity, max_accel = toolhead.get_max_velocity()
self.max_z_velocity = config.getfloat(
@ -41,11 +39,12 @@ class CoreXZKinematics:
def set_position(self, newpos, homing_axes):
for i, rail in enumerate(self.rails):
rail.set_position(newpos)
if i in homing_axes:
if "xyz"[i] in homing_axes:
self.limits[i] = rail.get_range()
def note_z_not_homed(self):
# Helper for Safe Z Home
self.limits[2] = (1.0, -1.0)
def clear_homing_state(self, clear_axes):
for axis, axis_name in enumerate("xyz"):
if axis_name in clear_axes:
self.limits[axis] = (1.0, -1.0)
def home(self, homing_state):
# Each axis is homed independently and in order
for axis in homing_state.get_axes():
@ -62,8 +61,6 @@ class CoreXZKinematics:
forcepos[axis] += 1.5 * (position_max - hi.position_endstop)
# Perform homing
homing_state.home_rails([rail], forcepos, homepos)
def _motor_off(self, print_time):
self.limits = [(1.0, -1.0)] * 3
def _check_endstops(self, move):
end_pos = move.end_pos
for i in (0, 1, 2):

View file

@ -23,8 +23,6 @@ class DeltaKinematics:
stepper_configs[2], need_position_minmax = False,
default_position_endstop=a_endstop)
self.rails = [rail_a, rail_b, rail_c]
config.get_printer().register_event_handler("stepper_enable:motor_off",
self._motor_off)
# Setup max velocity
self.max_velocity, self.max_accel = toolhead.get_max_velocity()
self.max_z_velocity = config.getfloat(
@ -90,7 +88,7 @@ class DeltaKinematics:
math.sqrt(self.very_slow_xy2)))
self.axes_min = toolhead.Coord(-max_xy, -max_xy, self.min_z, 0.)
self.axes_max = toolhead.Coord(max_xy, max_xy, self.max_z, 0.)
self.set_position([0., 0., 0.], ())
self.set_position([0., 0., 0.], "")
def get_steppers(self):
return [s for rail in self.rails for s in rail.get_steppers()]
def _actuator_to_cartesian(self, spos):
@ -103,17 +101,19 @@ class DeltaKinematics:
for rail in self.rails:
rail.set_position(newpos)
self.limit_xy2 = -1.
if tuple(homing_axes) == (0, 1, 2):
if homing_axes == "xyz":
self.need_home = False
def clear_homing_state(self, clear_axes):
# Clearing homing state for each axis individually is not implemented
if clear_axes:
self.limit_xy2 = -1
self.need_home = True
def home(self, homing_state):
# All axes are homed simultaneously
homing_state.set_axes([0, 1, 2])
forcepos = list(self.home_position)
forcepos[2] = -1.5 * math.sqrt(max(self.arm2)-self.max_xy2)
homing_state.home_rails(self.rails, forcepos, self.home_position)
def _motor_off(self, print_time):
self.limit_xy2 = -1.
self.need_home = True
def check_move(self, move):
end_pos = move.end_pos
end_xy2 = end_pos[0]**2 + end_pos[1]**2

View file

@ -41,8 +41,6 @@ class DeltesianKinematics:
for s in self.get_steppers():
s.set_trapq(toolhead.get_trapq())
toolhead.register_step_generator(s.generate_steps)
config.get_printer().register_event_handler(
"stepper_enable:motor_off", self._motor_off)
self.limits = [(1.0, -1.0)] * 3
# X axis limits
min_angle = config.getfloat('min_angle', MIN_ANGLE,
@ -89,7 +87,7 @@ class DeltesianKinematics:
self.axes_min = toolhead.Coord(*[l[0] for l in self.limits], e=0.)
self.axes_max = toolhead.Coord(*[l[1] for l in self.limits], e=0.)
self.homed_axis = [False] * 3
self.set_position([0., 0., 0.], ())
self.set_position([0., 0., 0.], "")
def get_steppers(self):
return [s for rail in self.rails for s in rail.get_steppers()]
def _actuator_to_cartesian(self, sp):
@ -115,8 +113,13 @@ class DeltesianKinematics:
def set_position(self, newpos, homing_axes):
for rail in self.rails:
rail.set_position(newpos)
for n in homing_axes:
self.homed_axis[n] = True
for axis_name in homing_axes:
axis = "xyz".index(axis_name)
self.homed_axis[axis] = True
def clear_homing_state(self, clear_axes):
for axis, axis_name in enumerate("xyz"):
if axis_name in clear_axes:
self.homed_axis[axis] = False
def home(self, homing_state):
homing_axes = homing_state.get_axes()
home_xz = 0 in homing_axes or 2 in homing_axes
@ -142,8 +145,6 @@ class DeltesianKinematics:
else:
forcepos[1] += 1.5 * (position_max - hi.position_endstop)
homing_state.home_rails([self.rails[2]], forcepos, homepos)
def _motor_off(self, print_time):
self.homed_axis = [False] * 3
def check_move(self, move):
limits = list(map(list, self.limits))
spos, epos = move.start_pos, move.end_pos

View file

@ -42,8 +42,6 @@ class HybridCoreXYKinematics:
for s in self.get_steppers():
s.set_trapq(toolhead.get_trapq())
toolhead.register_step_generator(s.generate_steps)
self.printer.register_event_handler("stepper_enable:motor_off",
self._motor_off)
# Setup boundary checks
max_velocity, max_accel = toolhead.get_max_velocity()
self.max_z_velocity = config.getfloat(
@ -69,15 +67,17 @@ class HybridCoreXYKinematics:
def set_position(self, newpos, homing_axes):
for i, rail in enumerate(self.rails):
rail.set_position(newpos)
for axis in homing_axes:
for axis_name in homing_axes:
axis = "xyz".index(axis_name)
if self.dc_module and axis == self.dc_module.axis:
rail = self.dc_module.get_primary_rail().get_rail()
else:
rail = self.rails[axis]
self.limits[axis] = rail.get_range()
def note_z_not_homed(self):
# Helper for Safe Z Home
self.limits[2] = (1.0, -1.0)
def clear_homing_state(self, clear_axes):
for axis, axis_name in enumerate("xyz"):
if axis_name in clear_axes:
self.limits[axis] = (1.0, -1.0)
def home_axis(self, homing_state, axis, rail):
position_min, position_max = rail.get_range()
hi = rail.get_homing_info()
@ -96,8 +96,6 @@ class HybridCoreXYKinematics:
self.dc_module.home(homing_state)
else:
self.home_axis(homing_state, axis, self.rails[axis])
def _motor_off(self, print_time):
self.limits = [(1.0, -1.0)] * 3
def _check_endstops(self, move):
end_pos = move.end_pos
for i in (0, 1, 2):

View file

@ -42,8 +42,6 @@ class HybridCoreXZKinematics:
for s in self.get_steppers():
s.set_trapq(toolhead.get_trapq())
toolhead.register_step_generator(s.generate_steps)
self.printer.register_event_handler("stepper_enable:motor_off",
self._motor_off)
# Setup boundary checks
max_velocity, max_accel = toolhead.get_max_velocity()
self.max_z_velocity = config.getfloat(
@ -69,15 +67,17 @@ class HybridCoreXZKinematics:
def set_position(self, newpos, homing_axes):
for i, rail in enumerate(self.rails):
rail.set_position(newpos)
for axis in homing_axes:
for axis_name in homing_axes:
axis = "xyz".index(axis_name)
if self.dc_module and axis == self.dc_module.axis:
rail = self.dc_module.get_primary_rail().get_rail()
else:
rail = self.rails[axis]
self.limits[axis] = rail.get_range()
def note_z_not_homed(self):
# Helper for Safe Z Home
self.limits[2] = (1.0, -1.0)
def clear_homing_state(self, clear_axes):
for axis, axis_name in enumerate("xyz"):
if axis_name in clear_axes:
self.limits[axis] = (1.0, -1.0)
def home_axis(self, homing_state, axis, rail):
position_min, position_max = rail.get_range()
hi = rail.get_homing_info()
@ -96,8 +96,6 @@ class HybridCoreXZKinematics:
self.dc_module.home(homing_state)
else:
self.home_axis(homing_state, axis, self.rails[axis])
def _motor_off(self, print_time):
self.limits = [(1.0, -1.0)] * 3
def _check_endstops(self, move):
end_pos = move.end_pos
for i in (0, 1, 2):

View file

@ -13,6 +13,8 @@ class NoneKinematics:
return [0, 0, 0]
def set_position(self, newpos, homing_axes):
pass
def clear_homing_state(self, clear_axes):
pass
def home(self, homing_state):
pass
def check_move(self, move):

View file

@ -22,8 +22,6 @@ class PolarKinematics:
for s in self.get_steppers():
s.set_trapq(toolhead.get_trapq())
toolhead.register_step_generator(s.generate_steps)
config.get_printer().register_event_handler("stepper_enable:motor_off",
self._motor_off)
# Setup boundary checks
max_velocity, max_accel = toolhead.get_max_velocity()
self.max_z_velocity = config.getfloat(
@ -47,13 +45,16 @@ class PolarKinematics:
def set_position(self, newpos, homing_axes):
for s in self.steppers:
s.set_position(newpos)
if 2 in homing_axes:
if "z" in homing_axes:
self.limit_z = self.rails[1].get_range()
if 0 in homing_axes and 1 in homing_axes:
if "x" in homing_axes and "y" in homing_axes:
self.limit_xy2 = self.rails[0].get_range()[1]**2
def note_z_not_homed(self):
# Helper for Safe Z Home
self.limit_z = (1.0, -1.0)
def clear_homing_state(self, clear_axes):
if "x" in clear_axes or "y" in clear_axes:
# X and Y cannot be cleared separately
self.limit_xy2 = -1.
if "z" in clear_axes:
self.limit_z = (1.0, -1.0)
def _home_axis(self, homing_state, axis, rail):
# Determine movement
position_min, position_max = rail.get_range()
@ -85,9 +86,6 @@ class PolarKinematics:
self._home_axis(homing_state, 0, self.rails[0])
if home_z:
self._home_axis(homing_state, 2, self.rails[1])
def _motor_off(self, print_time):
self.limit_z = (1.0, -1.0)
self.limit_xy2 = -1.
def check_move(self, move):
end_pos = move.end_pos
xy2 = end_pos[0]**2 + end_pos[1]**2

View file

@ -21,8 +21,6 @@ class RotaryDeltaKinematics:
stepper_configs[2], need_position_minmax=False,
default_position_endstop=a_endstop, units_in_radians=True)
self.rails = [rail_a, rail_b, rail_c]
config.get_printer().register_event_handler("stepper_enable:motor_off",
self._motor_off)
# Read config
max_velocity, max_accel = toolhead.get_max_velocity()
self.max_z_velocity = config.getfloat('max_z_velocity', max_velocity,
@ -76,7 +74,7 @@ class RotaryDeltaKinematics:
max_xy = math.sqrt(self.max_xy2)
self.axes_min = toolhead.Coord(-max_xy, -max_xy, self.min_z, 0.)
self.axes_max = toolhead.Coord(max_xy, max_xy, self.max_z, 0.)
self.set_position([0., 0., 0.], ())
self.set_position([0., 0., 0.], "")
def get_steppers(self):
return [s for rail in self.rails for s in rail.get_steppers()]
def calc_position(self, stepper_positions):
@ -86,8 +84,13 @@ class RotaryDeltaKinematics:
for rail in self.rails:
rail.set_position(newpos)
self.limit_xy2 = -1.
if tuple(homing_axes) == (0, 1, 2):
if homing_axes == "xyz":
self.need_home = False
def clear_homing_state(self, clear_axes):
# Clearing homing state for each axis individually is not implemented
if clear_axes:
self.limit_xy2 = -1
self.need_home = True
def home(self, homing_state):
# All axes are homed simultaneously
homing_state.set_axes([0, 1, 2])
@ -96,9 +99,6 @@ class RotaryDeltaKinematics:
#forcepos[2] = self.calibration.actuator_to_cartesian(min_angles)[2]
forcepos[2] = -1.
homing_state.home_rails(self.rails, forcepos, self.home_position)
def _motor_off(self, print_time):
self.limit_xy2 = -1.
self.need_home = True
def check_move(self, move):
end_pos = move.end_pos
end_xy2 = end_pos[0]**2 + end_pos[1]**2

View file

@ -26,7 +26,7 @@ class WinchKinematics:
acoords = list(zip(*self.anchors))
self.axes_min = toolhead.Coord(*[min(a) for a in acoords], e=0.)
self.axes_max = toolhead.Coord(*[max(a) for a in acoords], e=0.)
self.set_position([0., 0., 0.], ())
self.set_position([0., 0., 0.], "")
def get_steppers(self):
return list(self.steppers)
def calc_position(self, stepper_positions):
@ -36,6 +36,9 @@ class WinchKinematics:
def set_position(self, newpos, homing_axes):
for s in self.steppers:
s.set_position(newpos)
def clear_homing_state(self, clear_axes):
# XXX - homing not implemented
pass
def home(self, homing_state):
# XXX - homing not implemented
homing_state.set_axes([0, 1, 2])

View file

@ -565,6 +565,7 @@ class MCU:
self._canbus_iface = config.get('canbus_interface', 'can0')
cbid = self._printer.load_object(config, 'canbus_ids')
cbid.add_uuid(config, canbus_uuid, self._canbus_iface)
self._printer.load_object(config, 'canbus_stats %s' % (self._name,))
else:
self._serialport = config.get('serial')
if not (self._serialport.startswith("/dev/rpmsg_")

View file

@ -403,7 +403,7 @@ class PrinterRail:
changed_invert = pin_params['invert'] != endstop['invert']
changed_pullup = pin_params['pullup'] != endstop['pullup']
if changed_invert or changed_pullup:
raise error("Pinter rail %s shared endstop pin %s "
raise error("Printer rail %s shared endstop pin %s "
"must specify the same pullup/invert settings" % (
self.get_name(), pin_name))
mcu_endstop.add_stepper(stepper)

View file

@ -290,7 +290,7 @@ class ToolHead:
self._handle_shutdown)
# Load some default modules
modules = ["gcode_move", "homing", "idle_timeout", "statistics",
"manual_probe", "tuning_tower"]
"manual_probe", "tuning_tower", "garbage_collection"]
for module_name in modules:
self.printer.load_object(config, module_name)
# Print time and flush tracking
@ -457,7 +457,7 @@ class ToolHead:
# Movement commands
def get_position(self):
return list(self.commanded_pos)
def set_position(self, newpos, homing_axes=()):
def set_position(self, newpos, homing_axes=""):
self.flush_step_generation()
ffi_main, ffi_lib = chelper.get_ffi()
ffi_lib.trapq_set_position(self.trapq, self.print_time,

View file

@ -174,7 +174,7 @@ used to upload firmware to devices flashed with the CanBoot bootloader.
The can2040 directory contains code from:
https://github.com/KevinOConnor/can2040
commit 13321ce2bc046e059a47def70f977a579a984462.
version v1.7.0 (90515f53ce89442f1bcc3033aae222e9eb77818c).
The Huada HC32F460 directory contains code from:
https://www.hdsc.com.cn/Category83-1490

View file

@ -1,6 +1,6 @@
// Software CANbus implementation for rp2040
//
// Copyright (C) 2022,2023 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2022-2024 Kevin O'Connor <kevin@koconnor.net>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
@ -20,6 +20,13 @@
* rp2040 and low-level helper functions
****************************************************************/
// Determine if the target is an rp2350
#ifdef PICO_RP2350
#define IS_RP2350 1
#else
#define IS_RP2350 0
#endif
// Helper compiler definitions
#define barrier() __asm__ __volatile__("": : :"memory")
#define likely(x) __builtin_expect(!!(x), 1)
@ -123,19 +130,29 @@ static const uint16_t can2040_program_instructions[] = {
#define SI_RX_DATA PIO_IRQ0_INTE_SM1_RXNEMPTY_BITS
#define SI_TXPENDING PIO_IRQ0_INTE_SM1_BITS // Misc bit manually forced
// Return the gpio bank offset (on rp2350 chips)
static uint32_t
pio_gpiobase(struct can2040 *cd)
{
if (!IS_RP2350)
return 0;
return (cd->gpio_rx > 31 || cd->gpio_tx > 31) ? 16 : 0;
}
// Setup PIO "sync" state machine (state machine 0)
static void
pio_sync_setup(struct can2040 *cd)
{
pio_hw_t *pio_hw = cd->pio_hw;
pio_sm_hw_t *sm = &pio_hw->sm[0];
uint32_t gpio_rx = (cd->gpio_rx - pio_gpiobase(cd)) & 0x1f;
sm->execctrl = (
cd->gpio_rx << PIO_SM0_EXECCTRL_JMP_PIN_LSB
gpio_rx << PIO_SM0_EXECCTRL_JMP_PIN_LSB
| (can2040_offset_sync_end - 1) << PIO_SM0_EXECCTRL_WRAP_TOP_LSB
| can2040_offset_sync_signal_start << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB);
sm->pinctrl = (
1 << PIO_SM0_PINCTRL_SET_COUNT_LSB
| cd->gpio_rx << PIO_SM0_PINCTRL_SET_BASE_LSB);
| gpio_rx << PIO_SM0_PINCTRL_SET_BASE_LSB);
sm->instr = 0xe080; // set pindirs, 0
sm->pinctrl = 0;
pio_hw->txf[0] = 9 + 6 * PIO_CLOCK_PER_BIT / 2;
@ -149,10 +166,11 @@ pio_rx_setup(struct can2040 *cd)
{
pio_hw_t *pio_hw = cd->pio_hw;
pio_sm_hw_t *sm = &pio_hw->sm[1];
uint32_t gpio_rx = (cd->gpio_rx - pio_gpiobase(cd)) & 0x1f;
sm->execctrl = (
(can2040_offset_shared_rx_end - 1) << PIO_SM0_EXECCTRL_WRAP_TOP_LSB
| can2040_offset_shared_rx_read << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB);
sm->pinctrl = cd->gpio_rx << PIO_SM0_PINCTRL_IN_BASE_LSB;
sm->pinctrl = gpio_rx << PIO_SM0_PINCTRL_IN_BASE_LSB;
sm->shiftctrl = 0; // flush fifo on a restart
sm->shiftctrl = (PIO_SM0_SHIFTCTRL_FJOIN_RX_BITS
| PIO_RX_WAKE_BITS << PIO_SM0_SHIFTCTRL_PUSH_THRESH_LSB
@ -169,7 +187,8 @@ pio_match_setup(struct can2040 *cd)
sm->execctrl = (
(can2040_offset_match_end - 1) << PIO_SM0_EXECCTRL_WRAP_TOP_LSB
| can2040_offset_shared_rx_read << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB);
sm->pinctrl = cd->gpio_rx << PIO_SM0_PINCTRL_IN_BASE_LSB;
uint32_t gpio_rx = (cd->gpio_rx - pio_gpiobase(cd)) & 0x1f;
sm->pinctrl = gpio_rx << PIO_SM0_PINCTRL_IN_BASE_LSB;
sm->shiftctrl = 0;
sm->instr = 0xe040; // set y, 0
sm->instr = 0xa0e2; // mov osr, y
@ -183,16 +202,18 @@ pio_tx_setup(struct can2040 *cd)
{
pio_hw_t *pio_hw = cd->pio_hw;
pio_sm_hw_t *sm = &pio_hw->sm[3];
uint32_t gpio_rx = (cd->gpio_rx - pio_gpiobase(cd)) & 0x1f;
uint32_t gpio_tx = (cd->gpio_tx - pio_gpiobase(cd)) & 0x1f;
sm->execctrl = (
cd->gpio_rx << PIO_SM0_EXECCTRL_JMP_PIN_LSB
gpio_rx << PIO_SM0_EXECCTRL_JMP_PIN_LSB
| can2040_offset_tx_conflict << PIO_SM0_EXECCTRL_WRAP_TOP_LSB
| can2040_offset_tx_conflict << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB);
sm->shiftctrl = (PIO_SM0_SHIFTCTRL_FJOIN_TX_BITS
| PIO_SM0_SHIFTCTRL_AUTOPULL_BITS);
sm->pinctrl = (1 << PIO_SM0_PINCTRL_SET_COUNT_LSB
| 1 << PIO_SM0_PINCTRL_OUT_COUNT_LSB
| cd->gpio_tx << PIO_SM0_PINCTRL_SET_BASE_LSB
| cd->gpio_tx << PIO_SM0_PINCTRL_OUT_BASE_LSB);
| gpio_tx << PIO_SM0_PINCTRL_SET_BASE_LSB
| gpio_tx << PIO_SM0_PINCTRL_OUT_BASE_LSB);
sm->instr = 0xe001; // set pins, 1
sm->instr = 0xe081; // set pindirs, 1
}
@ -382,6 +403,10 @@ pio_setup(struct can2040 *cd, uint32_t sys_clock, uint32_t bitrate)
{
// Configure pio0 clock
uint32_t rb = cd->pio_num ? RESETS_RESET_PIO1_BITS : RESETS_RESET_PIO0_BITS;
#if IS_RP2350
if (cd->pio_num == 2)
rb = RESETS_RESET_PIO2_BITS;
#endif
rp2040_clear_reset(rb);
// Setup and sync pio state machine clocks
@ -391,11 +416,16 @@ pio_setup(struct can2040 *cd, uint32_t sys_clock, uint32_t bitrate)
for (i=0; i<4; i++)
pio_hw->sm[i].clkdiv = div << PIO_SM0_CLKDIV_FRAC_LSB;
// Configure gpiobase (on rp2350)
#if IS_RP2350
pio_hw->gpiobase = pio_gpiobase(cd);
#endif
// Configure state machines
pio_sm_setup(cd);
// Map Rx/Tx gpios
uint32_t pio_func = cd->pio_num ? 7 : 6;
uint32_t pio_func = 6 + cd->pio_num;
rp2040_gpio_peripheral(cd->gpio_rx, pio_func, 1);
rp2040_gpio_peripheral(cd->gpio_tx, pio_func, 0);
}
@ -495,7 +525,7 @@ unstuf_restore_state(struct can2040_bitunstuffer *bu, uint32_t data)
// Pull bits from unstuffer (as specified in unstuf_set_count() )
static int
unstuf_pull_bits(struct can2040_bitunstuffer *bu)
unstuf_pull_bits_rp2040(struct can2040_bitunstuffer *bu)
{
uint32_t sb = bu->stuffed_bits, edges = sb ^ (sb >> 1);
uint32_t e2 = edges | (edges >> 1), e4 = e2 | (e2 >> 2), rm_bits = ~e4;
@ -539,6 +569,49 @@ unstuf_pull_bits(struct can2040_bitunstuffer *bu)
}
}
// Pull bits from unstuffer (optimized for rp2350)
static int
unstuf_pull_bits(struct can2040_bitunstuffer *bu)
{
if (!IS_RP2350)
return unstuf_pull_bits_rp2040(bu);
uint32_t sb = bu->stuffed_bits, edges = sb ^ (sb >> 1);
uint32_t e2 = edges | (edges >> 1), e4 = e2 | (e2 >> 2), rm_bits = ~e4;
uint32_t cs = bu->count_stuff, cu = bu->count_unstuff;
for (;;) {
if (!cs)
// Need more data
return 1;
uint32_t try_cnt = cs > cu ? cu : cs;
uint32_t try_mask = ((1 << try_cnt) - 1) << (cs + 1 - try_cnt);
uint32_t rm_masked_bits = rm_bits & try_mask;
if (likely(!rm_masked_bits)) {
// No stuff bits in try_cnt bits - copy into unstuffed_bits
bu->count_unstuff = cu = cu - try_cnt;
bu->count_stuff = cs = cs - try_cnt;
bu->unstuffed_bits |= ((sb >> cs) & ((1 << try_cnt) - 1)) << cu;
if (! cu)
// Extracted desired bits
return 0;
// Need more data
return 1;
}
// Copy any leading bits prior to stuff bit (may be zero)
uint32_t copy_cnt = cs - (31 - __builtin_clz(rm_masked_bits));
cs -= copy_cnt;
bu->count_unstuff = cu = cu - copy_cnt;
bu->unstuffed_bits |= ((sb >> cs) & ((1 << copy_cnt) - 1)) << cu;
// High bit is now a stuff bit - remove it
bu->count_stuff = cs = cs - 1;
if (unlikely(rm_bits & (1 << cs))) {
// Six consecutive bits - a bitstuff error
if (sb & (1 << cs))
return -1;
return -2;
}
}
}
// Return most recent raw (still stuffed) bits
static uint32_t
unstuf_get_raw(struct can2040_bitunstuffer *bu)
@ -553,7 +626,7 @@ unstuf_get_raw(struct can2040_bitunstuffer *bu)
// Stuff 'num_bits' bits in '*pb' - upper bits must already be stuffed
static uint32_t
bitstuff(uint32_t *pb, uint32_t num_bits)
bitstuff_rp2040(uint32_t *pb, uint32_t num_bits)
{
uint32_t b = *pb, count = num_bits;
for (;;) {
@ -590,6 +663,34 @@ done:
return count;
}
// Stuff 'num_bits' bits in '*pb' (optimized for rp2350)
static uint32_t
bitstuff(uint32_t *pb, uint32_t num_bits)
{
if (!IS_RP2350)
return bitstuff_rp2040(pb, num_bits);
uint32_t b = *pb, count = num_bits;
for (;;) {
uint32_t edges = b ^ (b >> 1);
uint32_t e2 = edges | (edges >> 1), e4 = e2 | (e2 >> 2), add_bits = ~e4;
uint32_t mask = (1 << num_bits) - 1, add_masked_bits = add_bits & mask;
if (!add_masked_bits)
// No more stuff bits needed
break;
// Insert a stuff bit
uint32_t stuff_pos = 1 + 31 - __builtin_clz(add_masked_bits);
uint32_t low_mask = (1 << stuff_pos) - 1, low = b & low_mask;
uint32_t high = (b & ~(low_mask >> 1)) << 1;
b = high ^ low ^ (1 << (stuff_pos - 1));
count += 1;
if (stuff_pos <= 4)
break;
num_bits = stuff_pos - 4;
}
*pb = b;
return count;
}
// State storage for building bit stuffed transmit messages
struct bitstuffer_s {
uint32_t prev_stuffed, bitpos, *buf;
@ -1326,6 +1427,12 @@ can2040_setup(struct can2040 *cd, uint32_t pio_num)
memset(cd, 0, sizeof(*cd));
cd->pio_num = !!pio_num;
cd->pio_hw = cd->pio_num ? pio1_hw : pio0_hw;
#if IS_RP2350
if (pio_num == 2) {
cd->pio_num = pio_num;
cd->pio_hw = pio2_hw;
}
#endif
}
// API function to configure callback

View file

@ -9,3 +9,4 @@ greenlet==3.0.3 ; python_version >= '3.12'
Jinja2==2.11.3
python-can==3.3.4
markupsafe==1.1.1
setuptools==75.6.0 ; python_version >= '3.12' # Needed by python-can

View file

@ -1,11 +1,12 @@
// CANbus support on atsame70 chips
//
// Copyright (C) 2021-2022 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2021-2025 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2019 Eug Krashtan <eug.krashtan@gmail.com>
// Copyright (C) 2020 Pontus Borg <glpontus@gmail.com>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
#include "board/irq.h" // irq_save
#include "command.h" // DECL_CONSTANT_STR
#include "generic/armcm_boot.h" // armcm_enable_irq
#include "generic/canbus.h" // canbus_notify_tx
@ -147,6 +148,38 @@ canhw_set_filter(uint32_t id)
CANx->MCAN_CCCR &= ~MCAN_CCCR_INIT;
}
static struct {
uint32_t rx_error, tx_error;
} CAN_Errors;
// Report interface status
void
canhw_get_status(struct canbus_status *status)
{
irqstatus_t flag = irq_save();
uint32_t psr = CANx->MCAN_PSR, lec = psr & MCAN_PSR_LEC_Msk;
if (lec && lec != 7) {
// Reading PSR clears it - so update state here
if (lec >= 3 && lec <= 5)
CAN_Errors.tx_error += 1;
else
CAN_Errors.rx_error += 1;
}
uint32_t rx_error = CAN_Errors.rx_error, tx_error = CAN_Errors.tx_error;
irq_restore(flag);
status->rx_error = rx_error;
status->tx_error = tx_error;
if (psr & MCAN_PSR_BO)
status->bus_state = CANBUS_STATE_OFF;
else if (psr & MCAN_PSR_EP)
status->bus_state = CANBUS_STATE_PASSIVE;
else if (psr & MCAN_PSR_EW)
status->bus_state = CANBUS_STATE_WARN;
else
status->bus_state = 0;
}
// This function handles CAN global interrupts
void
CAN_IRQHandler(void)
@ -183,6 +216,18 @@ CAN_IRQHandler(void)
CANx->MCAN_IR = FDCAN_IE_TC;
canbus_notify_tx();
}
if (ir & (MCAN_IR_PED | MCAN_IR_PEA)) {
// Bus error
uint32_t psr = CANx->MCAN_PSR;
CANx->MCAN_IR = MCAN_IR_PED | MCAN_IR_PEA;
uint32_t lec = psr & MCAN_PSR_LEC_Msk;
if (lec && lec != 7) {
if (lec >= 3 && lec <= 5)
CAN_Errors.tx_error += 1;
else
CAN_Errors.rx_error += 1;
}
}
}
static inline const uint32_t
@ -302,6 +347,6 @@ can_init(void)
/*##-3- Configure Interrupts #################################*/
armcm_enable_irq(CAN_IRQHandler, CANx_IRQn, 1);
CANx->MCAN_ILE = MCAN_ILE_EINT0;
CANx->MCAN_IE = MCAN_IE_RF0NE | FDCAN_IE_TC;
CANx->MCAN_IE = MCAN_IE_RF0NE | FDCAN_IE_TC | MCAN_IE_PEDE | MCAN_IE_PEAE;
}
DECL_INIT(can_init);

View file

@ -1,11 +1,12 @@
// CANbus support on atsame51 chips
//
// Copyright (C) 2021-2022 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2021-2025 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2019 Eug Krashtan <eug.krashtan@gmail.com>
// Copyright (C) 2020 Pontus Borg <glpontus@gmail.com>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
#include "board/irq.h" // irq_save
#include "command.h" // DECL_CONSTANT_STR
#include "generic/armcm_boot.h" // armcm_enable_irq
#include "generic/canbus.h" // canbus_notify_tx
@ -163,6 +164,38 @@ canhw_set_filter(uint32_t id)
CANx->CCCR.reg &= ~CAN_CCCR_INIT;
}
static struct {
uint32_t rx_error, tx_error;
} CAN_Errors;
// Report interface status
void
canhw_get_status(struct canbus_status *status)
{
irqstatus_t flag = irq_save();
uint32_t psr = CANx->PSR.reg, lec = psr & CAN_PSR_LEC_Msk;
if (lec && lec != 7) {
// Reading PSR clears it - so update state here
if (lec >= 3 && lec <= 5)
CAN_Errors.tx_error += 1;
else
CAN_Errors.rx_error += 1;
}
uint32_t rx_error = CAN_Errors.rx_error, tx_error = CAN_Errors.tx_error;
irq_restore(flag);
status->rx_error = rx_error;
status->tx_error = tx_error;
if (psr & CAN_PSR_BO)
status->bus_state = CANBUS_STATE_OFF;
else if (psr & CAN_PSR_EP)
status->bus_state = CANBUS_STATE_PASSIVE;
else if (psr & CAN_PSR_EW)
status->bus_state = CANBUS_STATE_WARN;
else
status->bus_state = 0;
}
// This function handles CAN global interrupts
void
CAN_IRQHandler(void)
@ -199,6 +232,18 @@ CAN_IRQHandler(void)
CANx->IR.reg = FDCAN_IE_TC;
canbus_notify_tx();
}
if (ir & (CAN_IR_PED | CAN_IR_PEA)) {
// Bus error
uint32_t psr = CANx->PSR.reg;
CANx->IR.reg = CAN_IR_PED | CAN_IR_PEA;
uint32_t lec = psr & CAN_PSR_LEC_Msk;
if (lec && lec != 7) {
if (lec >= 3 && lec <= 5)
CAN_Errors.tx_error += 1;
else
CAN_Errors.rx_error += 1;
}
}
}
static inline const uint32_t
@ -309,6 +354,6 @@ can_init(void)
/*##-3- Configure Interrupts #################################*/
armcm_enable_irq(CAN_IRQHandler, CANx_IRQn, 1);
CANx->ILE.reg = CAN_ILE_EINT0;
CANx->IE.reg = CAN_IE_RF0NE | FDCAN_IE_TC;
CANx->IE.reg = CAN_IE_RF0NE | FDCAN_IE_TC | CAN_IE_PEDE | CAN_IE_PEAE;
}
DECL_INIT(can_init);

View file

@ -17,9 +17,20 @@ struct canbus_msg {
#define CANMSG_DATA_LEN(msg) ((msg)->dlc > 8 ? 8 : (msg)->dlc)
struct canbus_status {
uint32_t rx_error, tx_error, tx_retries;
uint32_t bus_state;
};
enum {
CANBUS_STATE_ACTIVE, CANBUS_STATE_WARN, CANBUS_STATE_PASSIVE,
CANBUS_STATE_OFF,
};
// callbacks provided by board specific code
int canhw_send(struct canbus_msg *msg);
void canhw_set_filter(uint32_t id);
void canhw_get_status(struct canbus_status *status);
// canbus.c
int canbus_send(struct canbus_msg *msg);

View file

@ -2,7 +2,7 @@
//
// Copyright (C) 2019 Eug Krashtan <eug.krashtan@gmail.com>
// Copyright (C) 2020 Pontus Borg <glpontus@gmail.com>
// Copyright (C) 2021 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2021-2025 Kevin O'Connor <kevin@koconnor.net>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
@ -318,6 +318,25 @@ DECL_TASK(canserial_rx_task);
* Setup and shutdown
****************************************************************/
DECL_ENUMERATION("canbus_bus_state", "active", CANBUS_STATE_ACTIVE);
DECL_ENUMERATION("canbus_bus_state", "warn", CANBUS_STATE_WARN);
DECL_ENUMERATION("canbus_bus_state", "passive", CANBUS_STATE_PASSIVE);
DECL_ENUMERATION("canbus_bus_state", "off", CANBUS_STATE_OFF);
void
command_get_canbus_status(uint32_t *args)
{
struct canbus_status status;
memset(&status, 0, sizeof(status));
canhw_get_status(&status);
sendf("canbus_status rx_error=%u tx_error=%u tx_retries=%u"
" canbus_bus_state=%u"
, status.rx_error, status.tx_error, status.tx_retries
, status.bus_state);
}
DECL_COMMAND_FLAGS(command_get_canbus_status, HF_IN_SHUTDOWN
, "get_canbus_status");
void
command_get_canbus_id(uint32_t *args)
{

View file

@ -1,6 +1,6 @@
// Support for Linux "gs_usb" CANbus adapter emulation
//
// Copyright (C) 2018-2022 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.
@ -108,6 +108,10 @@ static struct usbcan_data {
uint8_t notify_local, usb_send_busy;
uint32_t assigned_id;
// State tracking for messages to be sent from host to canbus
uint32_t bus_send_discard_time;
uint8_t bus_send_state;
// Canbus data from host
uint8_t host_status;
uint32_t host_pull_pos, host_push_pos;
@ -118,6 +122,10 @@ static struct usbcan_data {
struct canbus_msg canhw_queue[32];
} UsbCan;
enum {
BSS_READY = 0, BSS_BLOCKING, BSS_DISCARDING
};
enum {
HS_TX_ECHO = 1,
HS_TX_HW = 2,
@ -205,10 +213,56 @@ fill_usb_host_queue(void)
}
}
// Report bus stall state
static void
note_discard_state(uint32_t discard)
{
sendf("usb_canbus_state discard=%u", discard);
}
// Check if canbus queue has gotten stuck
static int
check_need_discard(void)
{
if (UsbCan.bus_send_state != BSS_BLOCKING)
return 0;
return timer_is_before(UsbCan.bus_send_discard_time, timer_read_time());
}
// Attempt to send a message on the canbus
static int
try_canmsg_send(struct canbus_msg *msg)
{
int ret = canhw_send(msg);
if (ret >= 0) {
// Success
if (UsbCan.bus_send_state == BSS_DISCARDING)
note_discard_state(0);
UsbCan.bus_send_state = BSS_READY;
return ret;
}
// Unable to send message
if (check_need_discard()) {
// The canbus is stalled - start discarding messages
note_discard_state(1);
UsbCan.bus_send_state = BSS_DISCARDING;
}
if (UsbCan.bus_send_state == BSS_DISCARDING)
// Queue is stalled - just discard the message
return 0;
if (UsbCan.bus_send_state == BSS_READY) {
// Just starting to block - setup stall detection after 50ms
UsbCan.bus_send_state = BSS_BLOCKING;
UsbCan.bus_send_discard_time = timer_read_time() + timer_from_us(50000);
}
return ret;
}
void
usbcan_task(void)
{
if (!sched_check_wake(&UsbCan.wake))
if (!sched_check_wake(&UsbCan.wake) && !check_need_discard())
return;
// Send any pending hw frames to host
@ -235,7 +289,7 @@ usbcan_task(void)
UsbCan.host_status = host_status = host_status & ~HS_TX_LOCAL;
}
if (host_status & HS_TX_HW) {
int ret = canhw_send(&msg);
int ret = try_canmsg_send(&msg);
if (ret < 0)
break;
UsbCan.host_status = host_status = host_status & ~HS_TX_HW;

View file

@ -1,6 +1,6 @@
// Serial over CAN emulation for rp2040 using can2040 software canbus
//
// Copyright (C) 2022 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2022-2025 Kevin O'Connor <kevin@koconnor.net>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
@ -41,6 +41,23 @@ canhw_set_filter(uint32_t id)
// Filter not implemented (and not necessary)
}
static uint32_t last_tx_retries;
// Report interface status
void
canhw_get_status(struct canbus_status *status)
{
struct can2040_stats stats;
can2040_get_statistics(&cbus, &stats);
uint32_t tx_extra = stats.tx_attempt - stats.tx_total;
if (last_tx_retries != tx_extra)
last_tx_retries = tx_extra - 1;
status->rx_error = stats.parse_error;
status->tx_retries = last_tx_retries;
status->bus_state = CANBUS_STATE_ACTIVE;
}
// can2040 callback function - handle rx and tx notifications
static void
can2040_cb(struct can2040 *cd, uint32_t notify, struct can2040_msg *msg)

View file

@ -2,7 +2,7 @@
//
// Copyright (C) 2019 Eug Krashtan <eug.krashtan@gmail.com>
// Copyright (C) 2020 Pontus Borg <glpontus@gmail.com>
// Copyright (C) 2021 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2021-2025 Kevin O'Connor <kevin@koconnor.net>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
@ -168,6 +168,31 @@ canhw_set_filter(uint32_t id)
fcan->FMR = fmr & ~CAN_FMR_FINIT;
}
static struct {
uint32_t rx_error, tx_error;
} CAN_Errors;
// Report interface status
void
canhw_get_status(struct canbus_status *status)
{
irqstatus_t flag = irq_save();
uint32_t esr = SOC_CAN->ESR;
uint32_t rx_error = CAN_Errors.rx_error, tx_error = CAN_Errors.tx_error;
irq_restore(flag);
status->rx_error = rx_error;
status->tx_error = tx_error;
if (esr & CAN_ESR_BOFF)
status->bus_state = CANBUS_STATE_OFF;
else if (esr & CAN_ESR_EPVF)
status->bus_state = CANBUS_STATE_PASSIVE;
else if (esr & CAN_ESR_EWGF)
status->bus_state = CANBUS_STATE_WARN;
else
status->bus_state = 0;
}
// This function handles CAN global interrupts
void
CAN_IRQHandler(void)
@ -190,6 +215,8 @@ CAN_IRQHandler(void)
// Process packet
canbus_process_data(&msg);
}
// Check for transmit ready
uint32_t ier = SOC_CAN->IER;
if (ier & CAN_IER_TMEIE
&& SOC_CAN->TSR & (CAN_TSR_RQCP0|CAN_TSR_RQCP1|CAN_TSR_RQCP2)) {
@ -197,6 +224,21 @@ CAN_IRQHandler(void)
SOC_CAN->IER = ier & ~CAN_IER_TMEIE;
canbus_notify_tx();
}
// Check for error irq
uint32_t msr = SOC_CAN->MSR;
if (msr & CAN_MSR_ERRI) {
uint32_t esr = SOC_CAN->ESR;
uint32_t lec = (esr & CAN_ESR_LEC_Msk) >> CAN_ESR_LEC_Pos;
if (lec && lec != 7) {
SOC_CAN->ESR = 7 << CAN_ESR_LEC_Pos;
if (lec >= 3 && lec <= 5)
CAN_Errors.tx_error += 1;
else
CAN_Errors.rx_error += 1;
}
SOC_CAN->MSR = CAN_MSR_ERRI;
}
}
static inline const uint32_t
@ -289,6 +331,8 @@ can_init(void)
armcm_enable_irq(CAN_IRQHandler, CAN_RX1_IRQn, 0);
if (CAN_RX0_IRQn != CAN_TX_IRQn)
armcm_enable_irq(CAN_IRQHandler, CAN_TX_IRQn, 0);
SOC_CAN->IER = CAN_IER_FMPIE0;
if (CAN_RX0_IRQn != CAN_SCE_IRQn)
armcm_enable_irq(CAN_IRQHandler, CAN_SCE_IRQn, 0);
SOC_CAN->IER = CAN_IER_FMPIE0 | CAN_IER_ERRIE;
}
DECL_INIT(can_init);

View file

@ -1,11 +1,12 @@
// FDCAN support on stm32 chips
//
// Copyright (C) 2021-2022 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2021-2025 Kevin O'Connor <kevin@koconnor.net>
// Copyright (C) 2019 Eug Krashtan <eug.krashtan@gmail.com>
// Copyright (C) 2020 Pontus Borg <glpontus@gmail.com>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
#include "board/irq.h" // irq_save
#include "command.h" // DECL_CONSTANT_STR
#include "generic/armcm_boot.h" // armcm_enable_irq
#include "generic/canbus.h" // canbus_notify_tx
@ -184,6 +185,38 @@ canhw_set_filter(uint32_t id)
SOC_CAN->CCCR &= ~FDCAN_CCCR_INIT;
}
static struct {
uint32_t rx_error, tx_error;
} CAN_Errors;
// Report interface status
void
canhw_get_status(struct canbus_status *status)
{
irqstatus_t flag = irq_save();
uint32_t psr = SOC_CAN->PSR, lec = psr & FDCAN_PSR_LEC_Msk;
if (lec && lec != 7) {
// Reading PSR clears it - so update state here
if (lec >= 3 && lec <= 5)
CAN_Errors.tx_error += 1;
else
CAN_Errors.rx_error += 1;
}
uint32_t rx_error = CAN_Errors.rx_error, tx_error = CAN_Errors.tx_error;
irq_restore(flag);
status->rx_error = rx_error;
status->tx_error = tx_error;
if (psr & FDCAN_PSR_BO)
status->bus_state = CANBUS_STATE_OFF;
else if (psr & FDCAN_PSR_EP)
status->bus_state = CANBUS_STATE_PASSIVE;
else if (psr & FDCAN_PSR_EW)
status->bus_state = CANBUS_STATE_WARN;
else
status->bus_state = 0;
}
// This function handles CAN global interrupts
void
CAN_IRQHandler(void)
@ -220,6 +253,18 @@ CAN_IRQHandler(void)
SOC_CAN->IR = FDCAN_IE_TC;
canbus_notify_tx();
}
if (ir & (FDCAN_IR_PED | FDCAN_IR_PEA)) {
// Bus error
uint32_t psr = SOC_CAN->PSR;
SOC_CAN->IR = FDCAN_IR_PED | FDCAN_IR_PEA;
uint32_t lec = psr & FDCAN_PSR_LEC_Msk;
if (lec && lec != 7) {
if (lec >= 3 && lec <= 5)
CAN_Errors.tx_error += 1;
else
CAN_Errors.rx_error += 1;
}
}
}
static inline const uint32_t
@ -320,6 +365,6 @@ can_init(void)
/*##-3- Configure Interrupts #################################*/
armcm_enable_irq(CAN_IRQHandler, CAN_IT0_IRQn, 1);
SOC_CAN->ILE = FDCAN_ILE_EINT0;
SOC_CAN->IE = FDCAN_IE_RF0NE | FDCAN_IE_TC;
SOC_CAN->IE = FDCAN_IE_RF0NE | FDCAN_IE_TC | FDCAN_IE_PEDE | FDCAN_IE_PEAE;
}
DECL_INIT(can_init);

View file

@ -38,7 +38,13 @@ void gpio_adc_cancel_sample(struct gpio_adc g);
struct spi_config {
void *spi;
uint32_t spi_cr1;
union {
uint32_t spi_cr1;
struct {
uint8_t div;
uint8_t mode;
};
};
};
struct spi_config spi_setup(uint32_t bus, uint8_t mode, uint32_t rate);
void spi_prepare(struct spi_config config);

View file

@ -100,19 +100,27 @@ spi_setup(uint32_t bus, uint8_t mode, uint32_t rate)
while ((pclk >> (div + 1)) > rate && div < 7)
div++;
uint32_t cr1 = SPI_CR1_SPE;
spi->CFG1 |= (div << SPI_CFG1_MBR_Pos) | (7 << SPI_CFG1_DSIZE_Pos);
CLEAR_BIT(spi->CFG1, SPI_CFG1_CRCSIZE);
spi->CFG2 |= ((mode << SPI_CFG2_CPHA_Pos) | SPI_CFG2_MASTER | SPI_CFG2_SSM
| SPI_CFG2_AFCNTR | SPI_CFG2_SSOE);
spi->CR1 |= SPI_CR1_SSI;
return (struct spi_config){ .spi = spi, .spi_cr1 = cr1 };
return (struct spi_config){ .spi = spi, .div = div, .mode = mode };
}
void
spi_prepare(struct spi_config config)
{
uint32_t div = config.div;
uint32_t mode = config.mode;
SPI_TypeDef *spi = config.spi;
// Reload frequency
spi->CFG1 = (spi->CFG1 & ~SPI_CFG1_MBR_Msk);
spi->CFG1 |= (div << SPI_CFG1_MBR_Pos);
// Reload mode
spi->CFG2 = (spi->CFG2 & ~SPI_CFG2_CPHA_Msk);
spi->CFG2 |= (mode << SPI_CFG2_CPHA_Pos);
}
void

View file

@ -40,6 +40,7 @@ CONFIG ../../config/printer-creality-cr20-pro-2019.cfg
CONFIG ../../config/printer-creality-ender5plus-2019.cfg
CONFIG ../../config/printer-eryone-thinker-series-v2-2020.cfg
CONFIG ../../config/printer-flashforge-creator-pro-2018.cfg
CONFIG ../../config/printer-geeetech-A10T-A20T-2021.cfg
CONFIG ../../config/printer-hiprecy-leo-2019.cfg
CONFIG ../../config/printer-longer-lk4-pro-2019.cfg
CONFIG ../../config/printer-lulzbot-mini1-2016.cfg