Files
cc2-spoolman-bridge/README.md
T
Jonas MenzelandClaude Opus 5.5 bf32667f77
CI / test (push) Successful in 1m4s
CI / image (push) Successful in 1m1s
CC2 ↔ Spoolman bridge with NFC tag generator
- Reads /opt/usr/canvas_record_info.json over SSH (pinned host key,
  restricted key), decodes Spoolman spool ids from the tag weight
- Mirrors spools into CC2 Control's spool library and assigns slots,
  keeps its own slot mapping, sets Spoolman locations
- Books the drop of CC2 Control's measured remaining weight to Spoolman
  after each print, idempotent across restarts and interrupted bookings
- Dry-run by default; web UI for settings, SSH key, status and log
- Tag generator for NFC Tools command chains (NTAG213/215/216)
- Tests, mock environment, Dockerfile, Gitea Actions CI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
2026-10-25 03:01:17 +01:00

156 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# cc2-spoolman-bridge
Bridge zwischen der Canvas des **Elegoo Centauri Carbon 2** (Community-Firmware, CC2 Control) und **Spoolman**,
mit Weboberfläche und **NFC-Tag-Generator**.
- Liegt eine Spule mit eigenem Tag in der Canvas, erkennt die Bridge die Spoolman-Spule am Tag, legt sie in
CC2 Control an (bzw. findet sie wieder), weist sie dem Slot zu und setzt in Spoolman den Ort („CC2 Slot 1–4“,
beim Entnehmen „Lager“).
- 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* prüfen (Drucker-IP, CC2-Control- und Spoolman-URL, ID-Offset 10000).
2. Privaten Schlüssel einfügen oder hochladen und speichern. Er liegt dann mit Rechten 600 in
`/data/ssh/id_key`; die Oberfläche zeigt nur Fingerprint und öffentlichen Schlüssel.
3. **Verbindung testen**: zeigt die 4 Slots und den Host-Key-Fingerprint des Druckers. 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.
### Umgebungsvariablen
Siehe [`.env.example`](.env.example). `PRINTER_HOST`, `SSH_PORT`, `SSH_USER`, `CC2_URL`, `SPOOLMAN_URL`,
`ID_OFFSET`, `POLL_INTERVAL` und `DRY_RUN` 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 Slot ↔ Spule selbst (`/data/state.json`). 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 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 gescannt wird (Umzug).
Weist du eine gespiegelte Spule in CC2 Control von Hand einem belegten Slot zu, übernimmt die Bridge das.
**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-header mock:8081 # Mock für Drucker (SSH :2222), CC2 Control, 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, 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`).