Ausführliche Fassung. Für den Schnelleinstieg reicht die README.
- 1. Wie Klipper baut — und warum das bei mehreren Boards stört
- 2. Einrichtung
- 3. Boards anlegen
- 4. Der Update-Lauf
- 5. Mehrere baugleiche Boards
- 6. Versionsstand im Blick behalten
- 7. Moonraker update_manager
- 8. Troubleshooting
Klippers Makefile beginnt so:
OUT=out/
export KCONFIG_CONFIG := $(CURDIR)/.configBeide Zuweisungen lassen sich auf der Kommandozeile überschreiben — in Make gewinnt ein Kommandozeilen-Argument gegen eine Zuweisung im Makefile. Also:
cd ~/klipper
make menuconfig KCONFIG_CONFIG=~/klipper_boards/octopus.config \
OUT=~/klipper_builds/octopus/
make KCONFIG_CONFIG=~/klipper_boards/octopus.config \
OUT=~/klipper_builds/octopus/ -j4Mehr steckt im Kern nicht dahinter. Das Script nimmt dir nur das Tippen ab und kümmert sich um die Details, über die man sonst stolpert.
OUTmuss auf/enden. Make hängt den Dateinamen direkt an, ausOUT=out-octopuswird sonstout-octopusklipper.elf.
Ein zweiter wichtiger Baustein ist olddefconfig:
make olddefconfig KCONFIG_CONFIG=~/klipper_boards/octopus.configDas übernimmt neue Kconfig-Optionen aus einem Klipper-Update mit ihren Standardwerten in deine gespeicherte Config — ohne interaktive Rückfrage. Genau das macht den unbeaufsichtigten Update-Lauf möglich.
Weil git pull sonst irgendwann meckert und weil make distclean alles
mitnimmt. Configs und Builds außerhalb des Repos bleiben davon unberührt.
Nach der Umstellung darf die alte Datei weg:
cd ~/klipper && make distclean # entfernt .config und out/git clone https://github.com/<dein-user>/klipper-multiboard.git ~/klipper-multiboard
chmod +x ~/klipper-multiboard/klipper-build.shEs ist sinnvoll, die Configs dort abzulegen, wo dein restliches Drucker-Setup liegt — dann werden sie von deinem bestehenden Backup miterfasst:
mkdir -p ~/printer_data/klipper_boards
echo 'export CONFIG_DIR=~/printer_data/klipper_boards' >> ~/.bashrcAlternativ pro Aufruf: CONFIG_DIR=~/printer_data/klipper_boards ./klipper-build.sh …
Zuerst schauen, was überhaupt am Host hängt:
./klipper-build.sh usb==> Angeschlossene USB-MCUs (/dev/serial/by-id):
/dev/serial/by-id/usb-Klipper_stm32f446xx_45002C001050535556323420-if00
/dev/serial/by-id/usb-Klipper_rp2040_323232323619413F-if00
/dev/serial/by-id/usb-Klipper_rp2040_3232323236598300-if00
Dann pro Board-Typ eine Config anlegen:
./klipper-build.sh new octopusDas öffnet menuconfig. Nach dem Speichern liegt die Auswahl in
$CONFIG_DIR/octopus.config und wird nie wieder überschrieben.
Zuletzt die Geräte eintragen — eine Zeile pro physischem Board, Kommentare mit
# sind erlaubt:
cat > ~/printer_data/klipper_boards/octopus.devices <<'EOF'
/dev/serial/by-id/usb-Klipper_stm32f446xx_45002C001050535556323420-if00
EOFVorlagen liegen unter examples/.
Immer
by-id, nie/dev/ttyACM0. Die Nummerierung derttyACM-Geräte hängt an der Einschaltreihenfolge. Derby-id-Pfad enthält die Chip-ID und ist dauerhaft stabil — dieselbe ID, die auch in deinerprinter.cfgsteht.
cd ~/klipper && git pull
cd ~/klipper-multiboard
./klipper-build.sh flash allWas dabei pro Board passiert:
olddefconfig— neue Kconfig-Optionen übernehmen- Build nach
$BUILD_DIR/<board>/ - Klipper-Dienst anhalten (sonst hält
klippydie seriellen Ports offen) - Für jedes Gerät: Bootloader anfordern, warten, flashen
- Klipper-Dienst starten
Der Dienst wird über ein trap auch dann wieder gestartet, wenn ein Board
scheitert oder du mit Strg-C abbrichst. Fehlgeschlagene Boards brechen den Lauf
nicht ab, sondern werden am Ende gesammelt gemeldet und setzen den Exit-Code.
Vorher trocken üben:
DRY_RUN=1 ./klipper-build.sh flash allZeigt jeden Schritt inklusive der konkreten flashtool.py-Aufrufe, fasst aber
kein Board an.
Ein „Board" ist in diesem Script ein Board-Typ, kein physisches Gerät.
Sechs identische Toolhead-Boards brauchen genau eine .config und genau eine
Firmware — unterschieden werden sie erst beim Flashen und in der printer.cfg.
nhk36.config <- einmal
nhk36.devices <- sechs Zeilen
Das funktioniert, weil die USB-Seriennummer bei RP2040 und STM32 aus der
eindeutigen Chip-ID stammt. Jedes Board hat damit dauerhaft seinen eigenen
by-id-Pfad, obwohl alle dieselbe Firmware tragen.
Das Script nutzt genau diese Chip-ID, um beim Bootloader-Wechsel das richtige Board wiederzufinden:
/dev/serial/by-id/usb-Klipper_rp2040_3232323236598300-if00
^^^^^^^^^^^^^^^^ Chip-ID
/dev/serial/by-id/usb-katapult_rp2040_3232323236598300-if00
Ohne diesen Abgleich würde bei sechs gleichen Boards schlicht das erstbeste Katapult-Gerät erwischt.
./klipper-build.sh statusKlipper-Repo (Host): v0.13.0-743-gac2a7f8b
BOARD / CHIP-ID VERSION STAND
nhk36 v0.13.0-743-gac2a7f8b (gebaut)
323232323619413F v0.13.0-743-gac2a7f8b 2026-08-24 09:15
3232323236598300 v0.13.0-743-gac2a7f8b 2026-08-24 09:16
octopus v0.13.0-743-gac2a7f8b (gebaut)
45002C001050535556323420 v0.13.0-700-gaaaaaaa 2026-07-02 21:40
Nicht von diesem Script verwaltete USB-MCUs:
usb-Klipper_stm32f042x6_1A2B3C4D5E6F-if00
Die Version kommt aus klipper.dict des jeweiligen Builds — genau der String,
den der MCU später auch meldet. Nach jedem erfolgreichen Flash wird sie pro
Chip-ID in $BUILD_DIR/<board>/.flashed festgehalten.
Der Block „nicht verwaltet" ist bewusst dabei. Dort tauchen MCUs auf, die eigene Firmware haben — ein Cartographer oder Beacon etwa — und die nicht über dieses Script laufen sollen. Sichtbar, aber außen vor.
Weniger schlimm, als oft behauptet. Klippers Versionsvergleich sitzt in
klippy/extras/error_mcu.py und läuft erst, nachdem ein Protokollfehler
aufgetreten ist — er erklärt den Fehler, er löst ihn nicht aus. Eine reine
Versionsdifferenz ist unkritisch, solange sich das Message-Protokoll nicht
geändert hat. Deshalb laufen Boards mit eigener Firmware problemlos mit.
Wenn ein Update aber das Protokoll anfasst, fällt jedes zurückgebliebene Board
aus — und dann willst du wissen, welches. Dafür ist status da.
Damit Updates des Scripts über Mainsail/Fluidd kommen, in moonraker.conf:
[update_manager klipper-multiboard]
type: git_repo
channel: dev
path: ~/klipper-multiboard
origin: https://github.com/<dein-user>/klipper-multiboard.git
primary_branch: main
managed_services: klipper
is_system_service: FalseDanach Moonraker neu starten:
sudo systemctl restart moonrakerchannel: dev ist hier richtig, weil das Repo keine getaggten Releases
verwendet. Wer mit Tags arbeitet, kann auf channel: stable wechseln.
Das ist der Fehler, der zu diesem Script geführt hat. Typischer Log:
Entering bootloader on /dev/serial/by-id/usb-Klipper_stm32f446xx_...-if00
Device reconnect on /sys/devices/.../1-6.2/1-6.2:1.0
sudo dfu-util -p 1-6.2 -R -a 0 -s 0x8008000:leave -D .../klipper.bin
dfu-util: No DFU capable USB device available
Failed to flash: Error running dfu-util
Beim zweiten Anlauf klappt es dann oft. Was tatsächlich passiert, in Klippers
scripts/flash_usb.py:
- Der 1200-Baud-Reset geht raus, das Board springt in den Bootloader.
wait_path()wartet 100 ms und prüft, ob der sysfs-Pfad existiert. Das ist aber noch der Pfad des alten Klipper-Geräts. Hat sich das Board noch nicht abgemeldet, ist er noch da — undwait_path()meldet fälschlich „Device reconnect".detect_canboot()liestidVendor/idProductaus genau diesem noch alten sysfs-Eintrag und liest1d50:614e(Klipper) statt1d50:6177(Katapult). Ergebnis: „kein Katapult".- Fallback auf
dfu-util— das findet natürlich kein DFU-Gerät, weil das Board in Katapult sitzt und nicht in DFU.
Die Fehlermeldung zeigt also auf DFU, obwohl es eine Fehlerkennung ist. Das Zeitfenster von 100 ms reicht bei direkt angestecktem Board meist; über einen USB-Hub oder an einem gut belegten Host kippt es.
Was dieses Script dagegen tut: es umgeht flash_usb.py beim Flashen. Es
sucht das Katapult-Gerät über die Chip-ID, fordert bei Bedarf den Bootloader
per flashtool.py -r an, pollt aktiv bis zu BOOTLOADER_WAIT Sekunden und
flasht dann direkt mit flashtool.py. Kein 100-ms-Fenster, kein DFU-Umweg.
Von Hand geht dasselbe so:
python3 ~/klipper/lib/katapult/flashtool.py \
-d /dev/serial/by-id/usb-katapult_stm32f446xx_<CHIPID>-if00 \
-f ~/klipper_builds/octopus/klipper.binKein Drama. Prüfen mit:
lsusb | grep -Ei "1d50|0483"
ls /dev/serial/by-id/1d50:6177 heißt Katapult, 0483:df11 heißt STM32-ROM-DFU. In beiden Fällen
erkennt das Script beim nächsten flash-Lauf, dass das Board schon im
Bootloader steht, und bedient es direkt.
klippy hält die seriellen Ports offen. Das Script stoppt den Dienst selbst;
läuft bei dir ein anderer Dienstname, setze KLIPPER_SERVICE:
KLIPPER_SERVICE=klipper-drucker2 ./klipper-build.sh flash allFLASH_METHOD=make ./klipper-build.sh flash allDann läuft alles über make flash, mit Warteschleife und zweitem Anlauf über
den Bootloader als Absicherung. Umgekehrt erzwingt FLASH_METHOD=katapult den
direkten Weg ohne Fallback.
OUT ohne Schrägstrich am Ende. Make hängt den Dateinamen direkt an.
Katapult springt nach dem Schreiben in die Anwendung; die Neu-Enumeration
dauert einen Moment. Das Script wartet zwischen zwei Boards drei Sekunden. Bei
langsamen Hubs hilft eine höhere BOOTLOADER_WAIT:
BOOTLOADER_WAIT=40 ./klipper-build.sh flash allBleibt es dabei, ist meist die USB-Verkabelung dran — Hub-Kette, Kabellänge, Stromversorgung. Ein Board testweise direkt an den Host stecken.