- /s/<id> shows a spool: rest in Spoolman and CC2 Control, where it is, last booking, history, links to Spoolman and the tag generator. - Setting "Adresse der Bridge"; the tag generator can put <address>/s/<id> on the tag, so an iPhone held to the roll opens that page. - New spool: vendor and material chosen from Spoolman, the vendor's filaments offered, values taken from similar filaments, duplicates pointed out and never created twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
255 lines
17 KiB
Markdown
255 lines
17 KiB
Markdown
# cc2-spoolman-bridge
|
||
|
||
Bridge zwischen der Canvas eines oder mehrerer **Elegoo Centauri Carbon 2** (Community-Firmware, CC2 Control) und
|
||
**Spoolman**, mit Weboberfläche und **NFC-Tag-Generator**.
|
||
|
||
- Liegt eine Spule mit eigenem Tag in einer Canvas, erkennt die Bridge die Spoolman-Spule am Tag, legt sie in der
|
||
CC2 Control dieses Druckers an (bzw. findet sie wieder) und weist sie dem Slot zu.
|
||
- In Spoolman steht dann, wo die Rolle liegt: Ort „<Drucker> Slot N“ (z. B. „Centauri Carbon 2 Links Slot 3“) und
|
||
die Zusatzfelder **Drucker** und **Slot**. Entnommene Rollen kommen ins „Lager“, die Felder werden geleert.
|
||
- Den von CC2 Control gemessenen Verbrauch bucht sie nach jedem Druck auf genau diese Spoolman-Spule.
|
||
- Der Tag-Generator gibt für jede Spoolman-Spule die vollständige Befehlskette zum Beschreiben eines leeren NTAG213 aus.
|
||
|
||
## Was die Bridge darf und was nicht
|
||
|
||
| System | Zugriff |
|
||
|---|---|
|
||
| Drucker | **nur lesen**: `cat /opt/usr/canvas_record_info.json` per SSH. Kein MQTT, keine Druckerbefehle, keine Registrierung als MQTT-Client. |
|
||
| CC2 Control | Spulenverwaltung: `GET /api/spools`, `POST /api/spools/save`, `/assign`, `/adjust` (Rest aus Spoolman), `/delete` (nur Kopien archivierter/gelöschter Spoolman-Spulen), `/enable` (nur nach Bestätigung); während eines Drucks zusätzlich `GET /api/printer` (nur für Fortschritt und Restzeit in der Oberfläche) |
|
||
| Spoolman | `GET /spool`, `/vendor`, `/filament`; `PATCH /spool/{id}` (Ort, Zusatzfelder, Rest nach Nachwiegen/Leerlaufen, Archiv), `PUT /spool/{id}/use`; `POST /vendor`, `/filament`, `/spool` nur über „Neue Spule“ im Tag-Generator |
|
||
|
||
**Dry-Run ist Standard.** Solange er aktiv ist, werden alle Schreibaktionen nur als „würde …“ protokolliert.
|
||
Ausschalten unter *Einstellungen* (mit Rückfrage).
|
||
|
||
## Einrichtung
|
||
|
||
### 1. SSH-Schlüssel auf dem Drucker einschränken
|
||
|
||
Eigenen Schlüssel erzeugen (ohne Passphrase):
|
||
|
||
```bash
|
||
ssh-keygen -t ed25519 -N "" -C cc2-spoolman-bridge -f cc2_bridge_key
|
||
```
|
||
|
||
Auf dem Drucker in `/root/.ssh/authorized_keys` (bzw. dem Pfad, den dein SSH-Server nutzt) eintragen. Jede
|
||
Ausführung liefert dann nur noch den Canvas-Datensatz, egal welcher Befehl gesendet wird, und nur von `10.10.6.10`:
|
||
|
||
```
|
||
command="cat /opt/usr/canvas_record_info.json",from="10.10.6.10",no-pty,no-port-forwarding,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAA… cc2-spoolman-bridge
|
||
```
|
||
|
||
Die Oberfläche zeigt diese Zeile mit dem passenden öffentlichen Schlüssel nach dem Hochladen an. Container auf
|
||
`10.10.6.10` verbinden sich über Docker-NAT mit der Host-IP, `from="10.10.6.10"` passt also.
|
||
|
||
### 2. Container starten (Docker-Host 10.10.6.10)
|
||
|
||
```bash
|
||
mkdir -p /opt/cc2-spoolman-bridge && cd /opt/cc2-spoolman-bridge
|
||
# docker-compose.yml und .env.example aus dem Repo hierher kopieren
|
||
cp .env.example .env
|
||
mkdir -p data && sudo chown 1000:1000 data
|
||
# privates Image: einmalig mit einem Gitea-Token (package: Lesen) anmelden
|
||
docker login git.menzel.center -u menzeljonas
|
||
docker compose pull && docker compose up -d
|
||
```
|
||
|
||
Oberfläche: `http://10.10.6.10:5066`
|
||
|
||
### 3. In der Oberfläche
|
||
|
||
1. *Einstellungen* → **Drucker**: Name, IP, SSH-Port/-Benutzer und CC2-Control-URL je Drucker; „Drucker
|
||
hinzufügen“ für weitere. Speichern. Der Name erscheint so in Spoolman (Ort und Feld „Drucker“).
|
||
2. Privaten Schlüssel einfügen oder hochladen und speichern. Er gilt für alle Drucker (dieselbe eingeschränkte
|
||
`authorized_keys`-Zeile auf jedem Drucker) und liegt mit Rechten 600 in `/data/ssh/id_key`; die Oberfläche zeigt
|
||
nur Fingerprint und öffentlichen Schlüssel.
|
||
3. Je Drucker **Verbindung testen**: zeigt die 4 Slots und den Host-Key-Fingerprint. Vergleichen und bestätigen.
|
||
Danach wird er bei jeder Verbindung geprüft; ein geänderter Host-Key wird abgelehnt.
|
||
4. *Status* im Dry-Run beobachten. Wenn die „würde …“-Einträge stimmen, Dry-Run ausschalten.
|
||
|
||
Ein Update von v1 übernimmt Einstellungen, Zuordnungen, Buchungsstände und den Host-Key automatisch; der bisherige
|
||
Drucker heißt danach „CC2“ (Ort bleibt „CC2 Slot N“) und kann in den Einstellungen umbenannt werden – Ort und
|
||
Felder in Spoolman ziehen dann mit.
|
||
|
||
### Neue Spule anlegen
|
||
|
||
Im *Tag-Generator* unter **Neue Spule**: Hersteller aus Spoolman wählen (oder einen neuen), dann ein vorhandenes
|
||
Filament dieses Herstellers (weitere Rolle davon) oder ein neues – Material aus den in Spoolman verwendeten (oder den
|
||
übrigen Elegoo-Typen), Name, Farbe, Dichte, Durchmesser, Düsen-/Betttemperatur, Netto- und Leergewicht, Preis,
|
||
Anzahl. Dichte und Temperaturen kommen von einem vorhandenen Filament mit demselben Material (bevorzugt desselben
|
||
Herstellers), Gewichte von den übrigen Filamenten des Herstellers. Gibt es das Filament schon (gleicher Name oder
|
||
gleiches Material und gleiche Farbe), weist das Formular darauf hin; ein gleichnamiges wird nie doppelt angelegt. Die Bridge legt Hersteller (falls neu), Filament und
|
||
Spule(n) in Spoolman an, Ort „Lager“, Label wie gewohnt Hersteller + ID dreistellig (`SUNLU027`), und öffnet
|
||
gleich den Tag dazu. Die ID vergibt Spoolman (fortlaufend). Im Dry-Run wird nichts angelegt.
|
||
|
||
### Spule mit dem iPhone auslesen
|
||
|
||
Safari kann NFC nicht selbst lesen, das iPhone liest Tags aber im Hintergrund: Steht auf dem Tag ein Link, genügt es,
|
||
das entsperrte iPhone an die Rolle zu halten und die Mitteilung anzutippen. Dafür unter *Einstellungen* → *Adresse
|
||
der Bridge* die Adresse eintragen, unter der das iPhone die Bridge erreicht (z. B. `https://cc2.menzel.center`), und
|
||
im Tag-Generator als Link **Spule in der Bridge** wählen (die Wahl wird gemerkt). Der Tag enthält dann
|
||
`<Adresse>/s/<ID>`; die Seite zeigt Rest (Spoolman und CC2 Control), wo die Spule steckt, die letzte Buchung und den
|
||
Verlauf, mit Links zu Spoolman und zum Tag-Generator. Spoolman-ID und Druckerdaten auf dem Tag bleiben gleich;
|
||
ob der Drucker Tags mit einem längeren Link genauso liest, ist noch an einem Test-Tag zu prüfen.
|
||
|
||
### Am iPhone als Web-App
|
||
|
||
`http://10.10.6.10:5066` in Safari öffnen → *Teilen* → **Zum Home-Bildschirm**. Die Bridge startet dann ohne
|
||
Safari-Leiste mit eigenem Icon. Unten liegen die Tabs Status, Protokoll, Tags und Einstellungen; auf *Status*
|
||
wechselt man oben zwischen den Druckern, ein Tipp auf einen Slot zeigt Details und die Aktionen. Hell/Dunkel folgt
|
||
der Systemeinstellung. Offline-Modus und Push-Mitteilungen gibt es nicht (dafür bräuchte es HTTPS).
|
||
|
||
### Umgebungsvariablen
|
||
|
||
Siehe [`.env.example`](.env.example). `PRINTER_HOST`, `SSH_PORT`, `SSH_USER`, `CC2_URL`, `SPOOLMAN_URL`,
|
||
`ID_OFFSET`, `POLL_INTERVAL`, `DRY_RUN` und `PRINTER_NAME` (erster Drucker) sind nur Startwerte, bis die Einstellungen einmal in der Oberfläche
|
||
gespeichert wurden (`/data/settings.json`). `BRIDGE_USER` und `BRIDGE_PASSWORD` schalten Basic-Auth ein
|
||
(`/healthz` bleibt offen). `PORT` (Standard 5066), `LOG_LEVEL`.
|
||
|
||
## Funktionsweise
|
||
|
||
**Tag erkennen.** Die Bridge liest alle ~5 s über eine stehende SSH-Verbindung den Canvas-Datensatz.
|
||
`channels0`–`channels3` sind Slot 1–4. Ist `weight` ≥ `ID_OFFSET`, ist `weight − ID_OFFSET` die Spoolman-Spulen-ID;
|
||
kleinere Werte sind echte Gewichte von Original- oder Fremdtags und werden ignoriert. Neu ist ein Tag, wenn sich die ID
|
||
im Slot gegenüber dem letzten Datensatz ändert.
|
||
|
||
**Zuordnung.** Die Bridge speichert Drucker/Slot ↔ Spule selbst (`/data/state.json`). Eine Spule liegt höchstens
|
||
in einem Slot eines Druckers. Fällt `weight` nach einer Bearbeitung am Display auf 1000 zurück, bleibt eine
|
||
**gescannte** Zuordnung bestehen; fragt CC2 Control deshalb nach der Spule, weist die Bridge sie erneut zu (höchstens
|
||
einmal pro Minute). Aufgehoben wird sie, wenn
|
||
- der Slot länger als 20 s leer ist (Tray-Status 0),
|
||
- CC2 Control die Spule herausnimmt („Take out“) oder ins Lager legt (Slot ohne Spule und ohne offene Rückfrage),
|
||
- im Slot ein Tag mit einer anderen ID gescannt wird,
|
||
- die Spule in einem anderen Slot oder im anderen Drucker gescannt wird (Umzug) oder
|
||
- nach dem Einlegen einer Rolle die Tag-Daten des Slots nicht mehr ihre ID tragen (siehe unten).
|
||
|
||
Weist du eine gespiegelte Spule in CC2 Control von Hand einem belegten Slot zu, übernimmt die Bridge das.
|
||
|
||
**Rolle wieder einlegen.** Der Scanner sitzt an der Seite, die Rollen werden zum Scannen abgenommen. Der Drucker
|
||
behält die Tag-Daten eines Slots auch, wenn die Rolle draußen ist. Kommt eine Rolle in den Slot zurück (CC2 Control
|
||
fragt „eingelegt“) und stehen dort noch unverändert die Daten der Rolle, die diesen Slot zuletzt verlassen hat, gilt
|
||
sie als dieselbe Rolle: Die Bridge ordnet sie wieder zu und beantwortet die Rückfrage, mit oder ohne erneuten Scan.
|
||
Wird danach am Display ein Filament gewählt (die ID verschwindet), war es eine andere Rolle ohne Tag: Die Zuordnung
|
||
wird aufgehoben, die Rolle kommt ins Lager. Das gilt bis zum ersten Druck mit der Rolle; danach ist eine
|
||
Display-Bearbeitung wieder eine Bearbeitung derselben Rolle.
|
||
|
||
**„Take out“ in CC2 Control** zählt als Entnahme: Ort „Lager“, Felder leer. Bleibt die Rolle physisch im Tray,
|
||
rechnet CC2 Control ihren Verbrauch keiner Spule mehr zu, protokolliert ihn aber für den Slot. Diesen Verbrauch
|
||
(Millimeter aus dem CC2-Protokoll, mit Durchmesser und Dichte der Spoolman-Spule in Gramm umgerechnet) bucht die
|
||
Bridge trotzdem auf die Rolle, bis sie den Slot verlässt oder CC2 Control nach einer Spule fragt. Zum erneuten
|
||
Zuordnen die Rolle herausnehmen und wieder einlegen.
|
||
|
||
**Spoolman synchron halten.** Nach jeder Änderung schreibt die Bridge Ort und Zusatzfelder (die Felder legt sie beim
|
||
ersten Mal selbst an). Zusätzlich vergleicht sie jede Minute alle Spoolman-Spulen: Geladene Rollen bekommen ihren
|
||
Ort/ihre Felder zurück, falls sie dort von Hand geändert wurden (der Drucker gewinnt). Rollen mit einem Druckerort
|
||
oder gesetztem Feld „Drucker“, die in keinem Slot liegen, kommen ins Lager. Eigene Orte wie „Regal 2“ bei nicht
|
||
geladenen Rollen bleiben unangetastet. Wird ein Drucker umbenannt oder entfernt, ziehen Ort und Felder mit bzw. seine
|
||
Rollen kommen ins Lager.
|
||
|
||
**CC2 Control.** Gespiegelte Spulen erkennt die Bridge an `note` = `spoolman:<id>`. Fehlt die Spule, legt sie sie
|
||
mit `name` = Label (`lot_nr`, sonst Hersteller + ID dreistellig), `brand`, `material`, `color`, `diameter`,
|
||
`density`, `net`, `remaining`, `tare` und `price` aus Spoolman an und weist sie dem Slot zu. Damit ist auch die
|
||
Rückfrage beantwortet. Passt das Spoolman-Material nicht zum Tag (CC2-Regel: gleich Typ/Name oder Produktlinie wie
|
||
`PLA+` zu `PLA`), nimmt sie den Untertyp vom Tag, damit CC2 Control nicht erneut fragt.
|
||
|
||
**Verbrauch.** CC2 Control zieht den gemessenen Verbrauch vom `remaining` der zugewiesenen Spule ab. (`used` ist in
|
||
CC2 Control der Zeitstempel der letzten Nutzung, keine Menge.) Die Bridge bucht für jede gespiegelte Spule die
|
||
Abnahme von `remaining` seit der letzten Buchung per `PUT /spool/{id}/use` nach Spoolman. Das geschieht erst, wenn kein
|
||
Druck läuft (`job.active`) und `remaining` 60 s unverändert ist. Der gebuchte Stand wird persistent gespeichert.
|
||
Vor jeder Buchung schreibt die Bridge die Absicht in den Zustand. Bricht eine Buchung ab, prüft sie nach dem
|
||
Neustart an `used_weight` in Spoolman, ob sie schon angekommen ist, statt doppelt zu buchen.
|
||
|
||
CC2 Control misst den echten Extruderweg (`print_stats.filament_used`, mit Spülen und Farbwechseln, mit Flow-Faktor)
|
||
und rechnet ihn dem Slot zu, der gerade fördert. Ein abgebrochener oder fehlgeschlagener Druck zählt also mit dem,
|
||
was er wirklich verbraucht hat; bei Mehrfarbdrucken bekommt jede Rolle ihren Anteil. Die Bridge nimmt Dateiname und
|
||
Ergebnis aus dem CC2-Protokoll in die Buchung auf, z. B. „2.98 g von SUNLU001 … gebucht – benchy.gcode
|
||
(abgebrochen)“.
|
||
|
||
**Rolle leer.** Läuft eine Rolle im Druck leer, setzt CC2 Control sie auf 0 g; die Bridge setzt den Rest in Spoolman
|
||
ebenfalls fest auf 0 g („Rolle leergelaufen“), statt eine Differenz zu buchen – so stimmt Spoolman auch dann, wenn
|
||
CC2 Controls Zählung vorher daneben lag. Danach archiviert sie die Spule in Spoolman (abschaltbar unter
|
||
*Einstellungen*), und ihre Kopie verschwindet aus CC2 Control. Druckt der Drucker mit einer anderen Rolle weiter,
|
||
zählt alles ab dem Wechsel für deren Slot. Hat CC2 Control fälschlich „leergelaufen“ erkannt (Filament gerissen),
|
||
die Spule in Spoolman zurückholen und nachwiegen. Die ID einer archivierten Spule wird nicht wieder vergeben; ihren
|
||
Tag kann man für eine neue Rolle einfach neu beschreiben. Ein Scan des alten Tags ordnet nichts zu.
|
||
|
||
**Nachwiegen.** Wird eine Spule in CC2 Control nachgewogen (Protokolleintrag „correction“), setzt die Bridge den
|
||
Rest in Spoolman auf denselben Wert (`used_weight` = Startgewicht − Rest), statt die Differenz als Verbrauch zu
|
||
buchen. Steigt `remaining` ohne Nachwiegen, wird nichts gebucht, nur protokolliert, und der neue Stand gilt als
|
||
Ausgangspunkt.
|
||
|
||
**Abgleich Spoolman → CC2 Control.** Spoolman ist die Quelle für die Stammdaten: Ändern sich dort Name, Hersteller,
|
||
Material, Farbe, Durchmesser, Dichte, Netto-/Leergewicht oder Preis, zieht die Bridge die Kopie in CC2 Control
|
||
nach (höchstens einmal pro Minute, nie während eines Drucks). Für eingelegte Spulen gilt das auch für den Rest:
|
||
Weicht er mehr als 1 g von Spoolman ab und ist aller Verbrauch gebucht, setzt die Bridge ihn per Nachwiegen
|
||
(`/api/spools/adjust`) auf den Spoolman-Wert. Das betrifft vor allem eine Rolle, die im anderen Drucker gedruckt
|
||
hat und zurückkommt (CC2 Control hätte sonst ihren alten Stand), und ein Nachwiegen in Spoolman. Kopien von
|
||
Spulen, die in Spoolman archiviert oder gelöscht sind und in keinem Slot stecken, löscht die Bridge aus CC2
|
||
Control (dort passen höchstens 128 Spulen).
|
||
|
||
**Rolle ohne Tag.** Druckt ein Slot, für den CC2 Control noch fragt, welche Spule es ist, hält CC2 Control die
|
||
Millimeter fest. Die Bridge zeigt das am Slot an und schreibt nach dem Druck einen Hinweis ins Protokoll. Wird die
|
||
Rolle danach getaggt und gescannt, rechnet CC2 Control die Millimeter dieser Spule an und die Bridge bucht sie.
|
||
|
||
**Grenzen.** Slots ohne Spoolman-Tag und mit „keine Spule“ beantwortete Rückfragen werden nicht gebucht. Bei einem
|
||
Stromausfall des Druckers gehen bis zu 2 min Verbrauch verloren (CC2 Control speichert beim Drucken höchstens alle
|
||
2 min). Bekommt CC2 Control eine eigene Spoolman-Anbindung, darf nur eins von beidem buchen.
|
||
|
||
**Spulenverwaltung aus.** Ist sie in CC2 Control ausgeschaltet, zählt CC2 Control keinen Verbrauch. Die Oberfläche
|
||
zeigt das deutlich an und bietet einen Button zum Einschalten (mit Rückfrage, im Dry-Run nur protokolliert).
|
||
|
||
## Tag-Format (Canvas, NTAG213/215/216)
|
||
|
||
NFC Tools → *Other* → *Advanced NFC commands*, Befehle `A2:PP:XXXXXXXX` (PP = Seite hex), kommagetrennt.
|
||
Seiten 0–3 werden nie geschrieben. **Nie „Lock tag“ oder „Set password“ verwenden.**
|
||
|
||
| Seite | Inhalt |
|
||
|---|---|
|
||
| 04–09 | NDEF-URL (`https://www.elegoo.com`; der Drucker ignoriert sie) |
|
||
| 10 | `36` + Herstellercode `EEEEEE` |
|
||
| 11 | `EE` + Filament-Code (2 Byte) + `00` |
|
||
| 12 | Material (4 Byte) |
|
||
| 13 | Untertyp (2 Byte) + `0000`; danach wählt der Elegoo Slicer das Profil |
|
||
| 14 | Farbe RRGGBB + Finish-Byte (`FF`) |
|
||
| 15 | Düse min / max (je 2 Byte, °C) |
|
||
| 16 | `00000000` |
|
||
| 17 | Durchmesser in 1/100 mm + **Gewicht = Spulen-ID + 10000** |
|
||
| 18 | `0036C800` |
|
||
|
||
Die Farbe wird auf die nächste Farbe der Display-Palette vorgeschlagen (Abstand in CIELAB; gesättigtes Blau wird auf
|
||
`2850DF` abgebildet). Material und Untertyp werden aus dem Spoolman-Material vorgeschlagen; alles ist änderbar.
|
||
Optional kann statt der Elegoo-URL ein Link auf die Spule in Spoolman auf den Tag (noch nicht am Drucker getestet).
|
||
|
||
## Entwicklung
|
||
|
||
```bash
|
||
docker build --target test . # Tests (pytest) im Container
|
||
docker build -t cc2-spoolman-bridge:dev . # Image
|
||
python tools/mock_env.py --host mock --printers 2 # Mock: 2 Drucker (SSH :2222/2223, CC2 :8081/8082), Spoolman
|
||
```
|
||
|
||
Einen Test-Container der Bridge gegen den Mock nur in einem Netz ohne Außenverbindung starten
|
||
(`docker network create --internal …`) und mit `SPOOLMAN_URL=http://mock:7912`. Sonst spricht er bis zum Speichern
|
||
der Einstellungen mit der voreingestellten echten Spoolman-Instanz.
|
||
|
||
Die Tests decken ab: Tag-Kodierung gegen die verifizierte Vorlage, ID-Dekodierung, Palette und Materialvorschlag,
|
||
Zuordnung bei verschwindender ID, Verbrauchs-Delta und Idempotenz (auch nach Abbruch), Dry-Run, mehrere Drucker
|
||
(Umzug, Ausfall eines Druckers, Entfernen, Umbenennen), Spoolman-Ort und Zusatzfelder inkl. Korrektur von Hand-
|
||
Änderungen, Migration von v1, Request-Format für
|
||
CC2 Control (Host-Header, kein Origin, `X-CC2-Request`, `key=value`-Body, `enable` als nacktes `on`/`off`) und SSH
|
||
mit Host-Key-Prüfung gegen einen lokalen SSH-Server.
|
||
|
||
CI (Gitea Actions, `.gitea/workflows/ci.yml`): pytest → Build `linux/amd64` → auf `main` Push nach
|
||
`git.menzel.center/menzeljonas/cc2-spoolman-bridge` mit den Tags `latest` und `sha-<commit>`. Für den Push braucht
|
||
das Repo ggf. ein Actions-Secret `REGISTRY_TOKEN` (Token mit *package*: Lesen und Schreiben).
|
||
|
||
## Quellen und Lizenzen
|
||
|
||
- Material- und Untertyp-Codes (`app/materials.py`, Lizenztext in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)) aus
|
||
[cjtallman/canvas-rfid-tool](https://github.com/cjtallman/canvas-rfid-tool) (`index.html`), **MIT License**,
|
||
Copyright (c) cjtallman. Dort wiederum nach
|
||
[DnG-Crafts/ELG-RFID](https://github.com/DnG-Crafts/ELG-RFID) und
|
||
[Savion/elegoo-rfid-editor](https://github.com/Savion/elegoo-rfid-editor).
|
||
- Spool-API und HTTP-Guard von CC2 Control:
|
||
[Neme77/centauri-carbon-2-community](https://github.com/Neme77/centauri-carbon-2-community)
|
||
(`cc2-control/docs/API.md`, Abschnitt „Spool tracking“, und `src/spools.h`, Branch `develop`).
|