- One worker per printer (name, IP, CC2 Control URL, own pinned host key); a spool sits in at most one slot of one printer, moves between printers - Spoolman: location "<printer> Slot N" plus extra fields "drucker"/"slot" (created by the bridge), cleared and "Lager" on removal - Full comparison every minute: the printer wins over manual edits of loaded spools; stale printer locations go to storage; own locations untouched - Automatic migration of v1 settings, state and host key - Status hint when a tag is still in a tray but unmapped (e.g. after "Take out" in CC2 Control) - SSH test: connection drops during exec give a clean error, not a 500 - Mock environment with several printers; tests for all of the above Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
177 lines
11 KiB
Markdown
177 lines
11 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`, `/enable` (nur nach Bestätigung) |
|
||
| Spoolman | `GET /spool`, `PATCH /spool/{id}` (`location`), `PUT /spool/{id}/use` |
|
||
|
||
**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.
|
||
|
||
### 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 die
|
||
Zuordnung. Aufgehoben wird sie nur, 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 oder
|
||
- die Spule in einem anderen Slot oder im anderen Drucker gescannt wird (Umzug).
|
||
|
||
Weist du eine gespiegelte Spule in CC2 Control von Hand einem belegten Slot zu, übernimmt die Bridge das.
|
||
|
||
**„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; der Status zeigt dann „Tag steckt noch, ist aber keinem
|
||
Slot zugeordnet“. Zum erneuten Zuordnen die Rolle neu einlegen und den Scan bestätigen.
|
||
|
||
**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. Steigt `remaining`
|
||
(z. B. durch Nachwiegen in CC2 Control), wird nichts gebucht, nur protokolliert, und der neue Stand gilt als
|
||
Ausgangspunkt.
|
||
|
||
**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
|
||
```
|
||
|
||
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`).
|