A Klipper firmware extra for an automated multi-filament loader built around the BTT MMB CAN V2.0 board.
Supports up to 32 filament paths with a single shared drive motor, a carriage-style selector motor, a latching servo to engage the drive gear, and one fixed optical encoder per path. Optional per-path entry, toolhead, and extruder entry sensors enable fully automated loading, accurate Bowden tube length detection, and real-time slip monitoring.
Installing for the first time? Read docs/INSTALL.md. It is written for someone who has not seen this project before: what it is, what it changes on your printer, what the setup menu asks, and how to undo it.
wget -O - https://raw.githubusercontent.com/Cstm3DBldr/autoloader/main/install.sh | bashAfter the command finishes, add this line to printer.cfg:
[include autoloader/autoloader.cfg]
This single include pulls in the rest (pin_aliases, hardware, parameters, macros) in the correct order.
Then save and restart Klipper.
cd ~/autoloader
./install.sh --uninstall
rm -rf ~/autoloader| Component | Hardware | Notes |
|---|---|---|
| MCU | BTT MMB CAN V2.0 | STM32G0B1, CAN bus |
| Drive motor | M3 — TMC5160 | Single gear, shared across all paths |
| Selector motor | M1 — TMC5160 | Moves carriage to active path, endstop homing |
| Engage servo | BTT servo header | Latching servo, PWM cut after move |
| Path encoders | 6x fixed optical | Never move; one per path |
| Entry sensors | 6x switch sensors | At roll end of each path |
| Toolhead sensors | 6x EBB36 PB8 | Past extruder gears, entering hotend (final load confirmation) |
| Extruder sensors | 6x EBB36 PB5 | At toolhead entry, before extruder gears (Bowden calibration endpoint) |
klipper/extras/
autoloader.py Main controller, config parsing, GCode registration
sa_motion.py Motion primitives (servo, selector, drive, idle timeouts)
sa_sequences.py Load and unload sequences
sa_calibration.py Calibration routines (drive, encoder, selector, bowden)
sa_encoder.py Encoder driver — pulse counting via Klipper buttons module
autoloader/
autoloader.cfg Aggregator (printer.cfg pulls only this file)
pin_aliases.cfg Physical pins → aliases (one [board_pins] per MCU)
hardware.cfg Hardware sections only (MCU, drivers, steppers, servo, encoders, sensors)
parameters.cfg The [autoloader] section — all user-tunable values
macros.cfg Thin gcode wrappers around the Python backend
moonraker/
sa_moonraker.py Moonraker component — REST endpoints + status broadcast
web/
mainsail/AutoloaderPanel.vue Mainsail UI panel
fluidd/AutoloaderPanel.vue Fluidd UI panel
KlipperScreen/
panels/sa_*.py Touchscreen panels (sa_main, sa_load_unload, sa_macros, etc.)
sa_filament_db.py Filament profile DB loader (shared with Moonraker)
sa_klipperscreen.conf Menu registration
filaments/brands/ Per-brand filament profile .cfg files (color/material/temps)
Each of the 6 paths has a state that persists across moves:
| State | Meaning |
|---|---|
unknown |
Not confirmed — state after boot or explicit reset |
empty |
No filament in path |
partial |
Filament in Bowden tube but not loaded to nozzle — includes filament parked at the entry sensor |
loaded |
Filament loaded all the way to nozzle tip |
The [autoloader] state monitor polls every entry sensor once per second
and owns these transitions:
- Filament reaches the entry sensor → the path is promoted to
partialimmediately and holds there, and an auto-park is queued. Only a load promotes further, toloaded. - Entry sensor reads clear on a
loaded/partialpath forrunout_timeoutseconds (default 10) → the path drops toemptyand its stored filament profile is wiped, so the next load cannot inherit the old brand, colour and temperatures. - A profile is selected on an
emptypath and nothing is loaded withinmaterial_select_timeoutseconds (default 60) → the profile is wiped. A path whose entry sensor sees filament keeps its profile until the filament is removed or the profile is changed by hand.
A path with a load, unload or park in flight is skipped entirely: its
sensor goes clear and back mid-sequence, and a load can run longer than
material_select_timeout.
Inserting filament queues SA_PARK for that path. Parks run one at a
time, oldest first, and a request that arrives while another is running
is held and run next rather than dropped. Each is re-checked against the
sensor immediately before it runs, and skipped during a real print.
This is driven by the state monitor, not by the entry sensors'
insert_gcode — see the note in autoloader/hardware.cfg for why that
hook silently loses insertions. Trigger latency is up to 1 second.
Homes the selector carriage to the endstop using a double-touch sequence (fast approach, back-off, slow re-approach). Always run this before any path selection after power-on or if the carriage position is uncertain.
SA_HOME
Disengages the servo and moves the selector carriage to path N (neutral, no filament gripped).
SA_SELECT TOOL=2
Engages the drive servo — grips filament in the currently selected path. Run SA_SELECT TOOL=N first.
SA_ENGAGE
Disengages the drive servo — path returns to neutral.
SA_DISENGAGE
Full automated load sequence for path N:
- Check entry sensor — abort if no filament.
- Select path, engage drive gear.
- Feed until encoder confirms grip.
- Feed through Bowden tube until extruder sensor triggers (or bowden_length reached).
- Monitor encoder slip throughout.
- Release drive gear.
- Heat extruder to
load_temperature. - Extrude
nozzle_distancemm to push filament to nozzle tip. - Purge
purge_lengthmm.
SA_LOAD TOOL=0
Full automated unload sequence for path N:
- Retract
nozzle_distance + purge_lengthmm via extruder motor. - Select path, engage drive gear.
- Drive in reverse until entry sensor clears.
- Disengage drive gear.
SA_UNLOAD TOOL=3
Prints a full status table showing all paths — entry/toolhead/extruder sensor states, path state, encoder distance, and mm_per_pulse.
SA_STATUS
Snapshot of encoder distances and sensor states. Optional RESET=1 zeros counters before reading.
SA_ENCODER_QUERY
SA_ENCODER_QUERY TOOL=2
SA_ENCODER_QUERY RESET=1
SA_ENCODER_QUERY TOOL=2 RESET=1
Live encoder delta stream. Prints one line per interval showing movement on each encoder since the previous tick. Useful for confirming which encoder responds to which path.
- Paths with any movement are marked with
*. - Press
CTRL+C(SSH) orESTOPto abort early.
SA_ENCODER_WATCH
SA_ENCODER_WATCH TOOL=2
SA_ENCODER_WATCH DURATION=60 INTERVAL=1.0
Oscillates the drive motor back and forth to confirm it moves. Safe to run without filament loaded. The motor is disabled after the test.
SA_BUZZ_DRIVE
SA_BUZZ_DRIVE DISTANCE=10 SPEED=20 REPS=5
Oscillates the selector motor back and forth. Run after SA_HOME so position is known.
SA_BUZZ_SELECTOR
SA_BUZZ_SELECTOR DISTANCE=20 SPEED=100
Manually override the path state. Valid states: unknown, empty, partial, loaded.
SA_SET_STATE TOOL=0 STATE=empty
SA_SET_STATE TOOL=2 STATE=loaded
Calibration uses a non-blocking phase state machine. Each calibration command does its automated work, prints the result, then pauses and tells you exactly what to send next. You respond with SA_RESPOND VALUE=<answer> and the next phase runs immediately as a normal GCode command — no polling, no timeouts.
- Send
SA_RESPOND VALUE=abortat any prompt to cancel the calibration. - A Klipper restart (including from
SAVE_CONFIG) always clears any in-progress calibration state.
Advances the active calibration to its next phase.
SA_RESPOND VALUE=yes
SA_RESPOND VALUE=no
SA_RESPOND VALUE=103.5
SA_RESPOND VALUE=abort
Calibrates rotation_distance for the drive motor. One motor drives all paths, so this only needs to be done once.
What it does: Commands 100mm of filament movement, asks you to measure the actual distance, then calculates the correct rotation_distance. Repeats up to 3 times if error is greater than 1mm.
Requirements: Filament loaded past the drive gear on at least one path. A ruler or calipers.
Process:
- Run
SA_CALIBRATE_DRIVE. - When prompted, enter the path number that has filament loaded.
- Mark the filament at the encoder exit (piece of tape works well).
- Send
SA_RESPOND VALUE=yeswhen ready. - The routine commands 100mm.
- Measure from your mark to the new filament position.
- Send
SA_RESPOND VALUE=<measured mm>(e.g.,SA_RESPOND VALUE=103.5). - Repeat up to 3 passes if needed.
- Send
SA_RESPOND VALUE=yesto save and restart, ornoto queue for later.
Result: Updates rotation_distance in [manual_stepper sa_drive] via SAVE_CONFIG.
Calibrates mm_per_pulse for one path encoder using 5 x 400mm feed/retract cycles.
Requirements: Filament loaded past drive gear and through encoder for path N. Enough free filament for ~2000mm total travel.
Process:
- Run
SA_CALIBRATE_ENCODER TOOL=N. - Position filament flush with encoder exit as a zero reference point.
- Send
SA_RESPOND VALUE=yes. - The routine runs 5 feed + 5 retract cycles automatically.
- When prompted, measure the final filament exit distance (should be ~200mm from start).
- Send
SA_RESPOND VALUE=<measurement>orSA_RESPOND VALUE=okif correct. - Confirm save and restart.
Result: Updates mm_per_pulse in [sa_encoder N] via SAVE_CONFIG.
Fully automated one-time calibration. The routine homes the carriage, sweeps to the far mechanical stop using TMC5160 stallguard, then homes back to measure the total rail length precisely. Path positions are calculated automatically from the measured travel — no manual measurement needed.
Requirements: SA_HOME must work correctly first. No filament loaded. [gcode_button selector_stall] must be present in hardware.cfg (pin ^!autoloader:SA_SELECTOR_DIAG).
Process:
- Run
SA_CALIBRATE_SELECTOR. - Send
SA_RESPOND VALUE=yeswhen prompted to confirm ready. - The carriage homes, then sweeps outward. The TMC5160
stop_enablebit latches the stall signal so it is reliably detected after the move. If no stall is detected the carriage briefly contacts the hard stop at reduced current (0.4 A), which is harmless. - The routine homes back to the physical endstop and measures total travel from the MCU step delta.
- Calculated positions are displayed. Send
SA_RESPOND VALUE=yesto accept ornoto cancel. - Send
SA_RESPOND VALUE=yesto save and restart, ornoto queue for later.
Tuning if the carriage stalls mid-travel (before reaching the far end):
- Raise
selector_stall_threshold(less sensitive stallguard) - Raise
selector_stall_current(more traversal torque)
Result: Updates selector_position_0 through selector_position_N in [autoloader] via SAVE_CONFIG.
Manual alternative: Jog the selector manually with MANUAL_STEPPER STEPPER=sa_selector ENABLE=1 MOVE=<mm> SPEED=30, use SA_ENCODER_WATCH to watch while manually checking carriage alignment, and update selector_position_N values in hardware.cfg by hand.
Calibrates the exact Bowden tube length for one path by detecting when filament reaches the extruder gears. Requires extruder_sensor_N configured.
Requirements: extruder_sensor_N wired and configured. Filament loaded to entry sensor on path N.
Process:
- Run
SA_CALIBRATE_BOWDEN TOOL=N. - Enter the estimated tube length when prompted (over-estimate is safer).
- The routine runs 3 trials: fast approach to 90% of estimate, then slow inch-forward until the extruder sensor triggers.
- Results are averaged.
- Confirm save and restart.
Result: Updates bowden_length_N in [autoloader] via SAVE_CONFIG. The load sequence uses this value as the target for the Bowden feed phase.
Recommended order for a new installation:
- Flash and connect the BTT MMB CAN V2.0 board.
- Update
canbus_uuidinhardware.cfg. - Test motors:
SA_BUZZ_DRIVEthenSA_BUZZ_SELECTOR— confirm both move. - Test servo:
SA_ENGAGEthenSA_DISENGAGE— confirm servo moves. - Home selector:
SA_HOME— confirm endstop triggers and carriage returns. - Calibrate selector:
SA_CALIBRATE_SELECTOR— auto-calculates path positions. - Load filament on path 0 past the drive gear.
- Calibrate drive motor:
SA_CALIBRATE_DRIVE— setsrotation_distance. - Calibrate encoders:
SA_CALIBRATE_ENCODER TOOL=Nfor each path. - Calibrate Bowden lengths:
SA_CALIBRATE_BOWDEN TOOL=Nfor each path (requires extruder sensors). - Test full load:
SA_LOAD TOOL=0— verify complete sequence.
The autoloader state is accessible in macros as printer['autoloader']:
{% set sa = printer['autoloader'] %}
{% if sa.filament_loaded[0] %}
{ action_respond_info("Path 0 is loaded") }
{% endif %}Available keys:
| Key | Type | Description |
|---|---|---|
num_paths |
int | Number of configured paths |
current_path |
int | Active path index, -1 if unhomed |
servo_engaged |
bool | True if drive servo is engaged |
path_states |
list[str] | State string per path |
encoder_dist |
list[float] | Current encoder distance per path (mm) |
entry_filament |
list[bool] | Filament at roll end per path |
toolhead_filament |
list[bool] | Filament at toolhead per path |
extruder_filament |
list[bool] | Filament at extruder gears per path |
filament_loaded |
list[bool] | True if path state is loaded |
selector_position |
float | Current path position in mm, -1 if unhomed |
All parameters are set in the [autoloader] section of hardware.cfg.
| Key | Default | Description |
|---|---|---|
drive_stepper |
required | Full name, e.g. manual_stepper sa_drive |
selector_stepper |
required | Full name, e.g. manual_stepper sa_selector |
servo |
required | Full name, e.g. servo sa_engage |
| Key | Default | Description |
|---|---|---|
servo_engaged_angle |
30 |
Degrees — drive gear grips filament |
servo_disengaged_angle |
160 |
Degrees — drive gear releases |
| Key | Default | Description |
|---|---|---|
encoder_N |
sa_encoder N |
Encoder section name |
entry_sensor_N |
none | Entry sensor section name (optional) |
toolhead_sensor_N |
none | Toolhead sensor section name (optional) |
extruder_sensor_N |
none | Extruder sensor section name (optional, required for bowden cal) |
selector_position_N |
N * 21.0 |
Selector position in mm from home |
extruder_N |
extruder (0) / extruderN |
Extruder name for heating |
bowden_length_N |
800.0 |
Bowden tube length mm (calibrate with SA_CALIBRATE_BOWDEN) |
| Key | Default | Description |
|---|---|---|
num_paths |
6 |
Number of paths (1-32) |
tube_length |
800 |
Legacy Bowden length (mm), used if bowden_length_N not set |
nozzle_distance |
50 |
Distance from extruder gears to nozzle tip (mm) |
purge_length |
30 |
Purge extrusion after nozzle is reached (mm) |
engage_max_distance |
60 |
Max drive travel before expecting encoder motion (mm) |
load_temperature |
200 |
Minimum hotend temp before extruder moves (C) |
feed_speed |
50 |
Drive motor speed (mm/s) |
selector_speed |
200 |
Selector motor speed (mm/s) |
feed_step_size |
10 |
Drive step per loop iteration (mm) |
slip_tolerance |
15 |
Encoder vs stepper slip warning threshold (%) |
sensor_polling_delay |
0.2 |
Seconds between sensor checks in feed loops |
servo_move_delay |
0.3 |
Seconds to wait after servo command |
stepper_timeout |
120 |
Seconds before idle steppers auto-disable |
runout_timeout |
10 |
Seconds the entry sensor must read clear before a loaded path is declared empty and its filament profile wiped |
material_select_timeout |
60 |
Seconds a filament profile may sit on an empty path before it is wiped (0 disables) |
selector_max_travel |
200.0 |
Max mm for selector far-end detection / cal sweep |
selector_homing_speed |
50.0 |
Homing approach speed (mm/s) |
selector_homing_backoff |
5.0 |
Back-off distance before slow re-approach (mm) |
selector_stall_current |
0.4 |
Motor current (A) during SA_CALIBRATE_SELECTOR sweep — lower than run_current |
selector_stall_threshold |
3 |
TMC5160 SGT value for stallguard sensitivity (raise to reduce false triggers) |
selector_stall_speed |
50.0 |
Sweep speed (mm/s) during SA_CALIBRATE_SELECTOR |
| Key | Default | Description |
|---|---|---|
cooling_pad_enabled |
True |
Call PARK_ON_COOLING_PAD after load/unload |
clean_nozzle_enabled |
True |
Call SA_CLEAN_NOZZLE before parking — disable if you have no brush |
The autoloader calls two macros that you define in your printer config. Both must exist when the corresponding config options are enabled.
Moves the toolhead to your cooling pad or parking position. Do not change Z — the autoloader holds Z at load_park_z throughout the load/unload sequence.
[gcode_macro PARK_ON_COOLING_PAD]
gcode:
G90
G0 X0 Y300 F5000
G0 Y350 F5000
G0 X30 F5000Wipes the nozzle on your brush or cleaning pad. Called after the heater is commanded off but while the nozzle is still hot. Do not change Z — keep all moves at the current height so the autoloader can maintain load_park_z.
A starter template is included in autoloader/macros.cfg. Customize the X/Y coordinates for your brush position:
[gcode_macro SA_CLEAN_NOZZLE]
description: Wipe nozzle on brush pad. Called by autoloader after load/unload.
gcode:
G90
G0 X80 Y350 F5000 ; approach brush
G0 X40 F5000 ; wipe pass 1
G0 X70 F5000
G0 X40 F5000 ; wipe pass 2
G0 X70 F5000
G0 X30 Y300 F5000 ; clear brush areaSet clean_nozzle_enabled: False in hardware.cfg to skip nozzle wiping entirely.
Pull-up + invert — sensors read HIGH (active) when empty on this hardware. If your sensors read the opposite, remove the ! from switch_pin.
One per BTT EBB36 toolhead board. Pull-up only (not inverted). Detects filament past the extruder gears, entering the hotend. Used as final load confirmation.
One per BTT EBB36 toolhead board. Detects filament arriving at the extruder gears (toolhead entry, before the gears). Enables sensor-based load targeting (more reliable than a fixed tube length). Required for SA_CALIBRATE_BOWDEN.
All steppers are automatically disabled after stepper_timeout seconds (default 120s) following the last move. This prevents heat build-up and motor noise. The timeout is reset on every motion command and cancelled immediately when a new move starts.
To disable auto-timeout behavior, set stepper_timeout: 0 in hardware.cfg. Note: this is a per-extra timeout independent of Klipper's main [idle_timeout].
scripts/backup_printer_config.sh snapshots the printer's live configuration
onto a dedicated printer-backup/known-good-<date> branch, so there is a
known-good reference to diff against or restore from after a change goes
wrong. Run it from the repo on your PC, not on the printer:
bash "/c/Users/Mike/Claude Project Files/Autoloader/scripts/backup_printer_config.sh"
Add a label to distinguish two snapshots taken the same day:
bash "/c/Users/Mike/Claude Project Files/Autoloader/scripts/backup_printer_config.sh" before-respooler
It captures two sets of files, for two different reasons:
- Files the repo already tracks and deploys (
autoloader/*.cfg, KlipperScreen panels,sa_klipperscreen.conf) land in their normal repo paths, sogit diff main <branch>shows exactly what drifted on the printer — tuned values, Mainsail config-editor edits, and whateverSAVE_CONFIGwrote. In practice the drift isautoloader/variables.cfg, which holds the calibration results. - The rest of
~/printer_data/config—printer.cfg,macros.cfg,homing.cfg,moonraker.conf,Toolchanger/— is not tracked anywhere else and exists only on the SD card. It lands underprinter_snapshot/, along with aMANIFEST.mdrecording the deployed commit and the Klipper, Moonraker, KlipperScreen and Mainsail versions.
Gcode files, logs and the Moonraker database are skipped. The script refuses to run with uncommitted work or an unreachable printer, and returns you to your original branch when it finishes.
Restoring is a normal checkout:
git checkout printer-backup/known-good-2026-08-30 -- autoloader/parameters.cfg
git checkout printer-backup/known-good-2026-08-30 -- printer_snapshot/
The branch is an archive and is never merged.
- Check filament is actually past the drive gear engagement point.
- Run
SA_ENGAGEmanually then push filament by hand while watchingSA_ENCODER_WATCH TOOL=N. - Verify
mm_per_pulseis non-zero in[sa_encoder N]. - Run
SA_CALIBRATE_ENCODER TOOL=Nto re-calibrate.
- Confirm endstop wiring:
QUERY_ENDSTOPSshould showsa_selector:openwhen clear andsa_selector:TRIGGEREDwhen pressed. - Check
endstop_pinin[manual_stepper sa_selector]. - Reduce
selector_homing_speedif the carriage overshoots.
- Check
bowden_length_Nmatches your actual tube length. - If extruder sensor is wired, run
SA_CALIBRATE_BOWDEN TOOL=N. - Watch
SA_ENCODER_WATCHduring a manual load to see where motion stops.
- Increase
engage_max_distanceif warnings appear early in the load. - Check that the servo engaged angle actually grips the filament (
SA_ENGAGEthen try to pull filament by hand). - Run
SA_CALIBRATE_DRIVEto verifyrotation_distanceis accurate. - Reduce
feed_speedfor the initial Bowden feed.
- The calibration routine waits 300 seconds (5 minutes) by default for
SA_RESPOND. - If you miss the window, re-run the calibration command.
- Calibration can always be aborted mid-sequence with
SA_RESPOND VALUE=abort.
Expected when the entry sensor is inactive. park_filament() checks the entry
sensor first because parking is drive-gear work — with no filament at the
entry there is nothing to grip. Feed filament in and it will park normally.
Raised when the extruder or toolhead sensor reads present while the entry sensor does not. Usually a cut or snapped length left behind after a manual unload. Clear it from the toolhead before loading that path — this guard is protecting you from feeding new filament into an occupied tube.
Path state describes the autoloader's path, and is inferred from the entry
sensor only — _initialize_states_from_sensors() and the runout monitor both
ignore the extruder and toolhead sensors. An orphaned length sitting in a
toolhead is therefore invisible to path state, which is correct: the
autoloader path really is empty. Use SA_SET_STATE TOOL=N STATE=loaded only
if the state is genuinely wrong.
do_load() and do_unload() check _is_homed() and home the printer
themselves before any toolhead motion, so an unhomed machine is not a blocker
for either. park_filament() never moves the toolhead at all — it is selector
and drive work only.
Point-in-time captures of what works and what doesn't live in docs/:
| File | Notes |
|---|---|
docs/SYSTEMS_TEST_2026-08-30.md |
Baseline after the LED work. Nothing broken — two reported failures were the entry guards correctly refusing paths whose filament had been cut out. Encoders and selector calibrated, 5 of 6 bowden lengths set. |
GPL-3.0-or-later. See LICENSE.
Copyright (C) 2026 Cstm3DBldr
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
GPL-3.0 rather than something permissive because everything this plugs into is already copyleft, and a Klipper extra runs inside Klipper's own process:
| Project | Licence |
|---|---|
| Klipper | GPL-3.0 |
| Moonraker | GPL-3.0 |
| KlipperScreen | AGPL-3.0 |
| Mainsail | GPL-3.0 |
The KlipperScreen panels are the strictest case — AGPL-3.0 — so anything derived from them carries that obligation regardless of what this file says.
Last updated: 2026-08-30 — added the printer backup script and the first systems-test log.