- Rust 100%
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| config.example.json | ||
| README.md | ||
printer-watch
Kleiner Wächter, der Kamerabilder von 3D-Druckern an einen Bildklassifikator gibt und meldet, wenn ein Fehldruck wahrscheinlich ist. Gedacht für Drucker, die kein Obico-Sentinel können: das Programm holt die Bilder selbst aus Home Assistant und schickt am Ende nur einen Webhook.
Warum
Ein Fehldruck kostet Filament, Strom und Druckzeit — und man merkt es oft erst
Stunden später. Obico/Spaghetti-Detection läuft normalerweise nur dort, wo
OctoPrint oder Moonraker die Bilder liefert. Dieses Programm schließt die
Lücke: es fragt Home Assistant nach dem Druckstatus, holt das aktuelle
Kamerabild und lässt es von einem obico-ml-Dienst bewerten. Erst wenn
mehrere Frames in Folge über der Schwelle liegen, geht ein Alarm raus —
ein einzelner schiefer Frame soll nicht die Nacht ruinieren.
Das Programm pausiert nicht selbst. Es schickt nur den Webhook; was daraus wird (Pause, Benachrichtigung, Lampe), entscheidet eine Automation in Home Assistant.
Ablauf
Pro Drucker läuft ein eigener Thread. Pro Runde (poll_secs):
-
GET {ha_url}/api/states/{state_entity}→stateals String. Fehler oder fehlendes Feld ergeben einen leeren State. -
Aktiv? Nein, wenn der State leer ist oder (case-insensitive) in
inactive_valuessteht. Alles andere —printing,preheating,homing, unbekannte Werte — gilt als laufender Druck. Eine Positivliste wäre bei Herstellern, die ihre Statusstrings ständig ändern, zu löchrig. -
Nicht aktiv: Streak zurücksetzen, Score
0.0an HA pushen, schlafen. -
Aktiv und Warmup noch nicht abgelaufen: nur schlafen. Der Warmup gilt für Drücke, die während der Laufzeit starten; läuft beim Start schon einer (Container-Neustart mitten im Druck), wird sofort bewertet.
-
GET {ha_url}/api/camera_proxy/{camera}→ JPEG-Bytes, die im Speicher unter dem Druckernamen liegen bleiben (maximal der letzte Frame). -
Der ML-Dienst wird gefragt:
GET {ml_url}/p/?img=<urlencoded self_url/frame/<name>>. Wichtig: obico-ml holt sich das Bild selbst überself_url. Deshalb muss dieser Dienst aus Sicht des ML-Containers erreichbar sein (im gleichen Docker-Netz z.B.http://watcher:8000) — nicht aus Sicht deines Browsers. -
Aus der Antwort (
detections) wird die höchste Konfidenz genommen. Fehler, Timeout, kaputtes JSON: Score0.0undml_ok = false. -
Der Score geht als Sensor nach HA:
sensor.<name>_sentinel_score, State = Score × 100 mit einer Dezimalen, Attribute u.a.unit_of_measurement: %,threshold,printer. -
Streak: Score
< threshold→ 0; sonst +1. BeiconfirmTreffern in Folge wird ausgelöst und der Zähler auf 0 gesetzt (nicht bei jedem Frame feuern). -
Auslösen nur, wenn der Cooldown (
cooldown_secsseit dem letzten Alarm) abgelaufen ist →POST {ha_url}/api/webhook/{webhook_id}mit:{ "printer": "cc1", "title": "⚠️ Fehldruck erkannt – Centauri Carbon", "message": "3 Frames in Folge über 40 % (Score 87 %).", "score": 0.87, "event": "detection", "pause_entity": "button.…", "camera": "camera.…", "state_entity": "sensor.…" }
Alle HTTP-Aufrufe haben ein globales Timeout von 15 s, damit ein toter Host nicht den Thread blockiert.
HTTP-Endpunkte
Der Dienst bringt einen minimalen HTTP-Server mit (Standard 0.0.0.0:8000).
Er existiert, damit obico-ml die Frames abholen kann.
| Pfad | Antwort |
|---|---|
/healthz |
200, text/plain, Body ok |
/status |
200, application/json, Abbild Druckername → [score, ml_ok, frames_ok, alter] |
/diag |
200, text/plain, die letzten ≤ 25 Meldungen, zeilenweise |
/frame/<name> |
200, image/jpeg, der zuletzt geholte Frame; unbekannt/leer → 404 |
| alles andere | 404 |
Zu /status: frames_ok ist derzeit immer 1 und alter immer 0 — die
Felder sind im Tupel vorhanden, werden aber noch nicht fortgeschrieben. Der
Eintrag erscheint erst, nachdem ein Drucker einmal bewertet wurde.
Der Server hat keine Authentifizierung und kein TLS. Nur im internen Netz betreiben.
Konfiguration
Pfad: erstes Kommandozeilen-Argument, sonst /config/config.json.
Vorlage: config.example.json kopieren und anpassen.
cp config.example.json config.json
config.json, config.test.json und *.local.json sind per .gitignore
ausgeschlossen — dort stehen echte LAN-Adressen und ggf. Tokens. Nicht
einchecken.
Umgebungsvariablen
Variablen schlagen die Werte aus der Datei.
| Variable | Wirkung | Default |
|---|---|---|
HA_URL |
Home-Assistant-Basis-URL | "" (aus Datei) |
HA_TOKEN |
Bearer-Token für HA; besser hier als in der Datei | "" |
ML_URL |
Basis-URL des obico-ml-Dienstes | http://obico-ml:3333 |
SELF_URL |
wie obico-ml diesen Dienst erreicht | http://watcher:8000 |
LISTEN |
Bind-Adresse des HTTP-Servers | 0.0.0.0:8000 |
LISTEN wird ausschließlich aus der Umgebung gelesen; ein listen-Feld in
config.json wird ignoriert. Der Default 0.0.0.0:8000 ist für den Betrieb im
Container gedacht.
Felder
| Feld | Bedeutung | Default |
|---|---|---|
ha_url |
Home-Assistant-Basis-URL | "" |
ha_token |
Bearer-Token (oder HA_TOKEN) |
"" |
ml_url |
obico-ml, Endpunkt /p/?img=… |
http://obico-ml:3333 |
self_url |
eigene URL, wie obico-ml sie sieht | http://watcher:8000 |
listen |
wird ignoriert, siehe LISTEN |
— |
threshold |
Score, ab dem ein Frame zählt (0…1) | 0.4 |
confirm |
so viele Treffer in Folge lösen aus | 3 |
poll_secs |
Pause zwischen den Runden | 10 |
warmup_secs |
Schonzeit nach Druckstart | 300 |
cooldown_secs |
Mindestabstand zwischen zwei Alarmen | 900 |
webhook_id |
ID des HA-Webhooks | printer-sentinel |
inactive_values |
States, die „kein Druck" bedeuten | siehe unten |
printers[] |
Liste der Drucker (Pflicht) | — |
Default für inactive_values: idle, standby, unavailable, unknown,
offline, complete, completed, finished, stopped, stop, paused,
error.
Pro Drucker: name (Pflicht, landet in Entitätsnamen und URLs), camera
(Pflicht), state_entity (Pflicht), title (Anzeigename, optional),
pause_entity (optional, wird nur in den Webhook durchgereicht).
Fehlende Datei, kaputtes JSON, fehlendes printers[] oder ein Drucker ohne
name/camera/state_entity: das Programm bricht beim Start mit einer
Klartext-Meldung ab (config file, config json, printers[], camera, …).
Lieber laut sterben als stumm nichts tun.
Bauen und starten
cargo build --release
./target/release/printer-watch-rs /pfad/zu/config.json
Im Container: config.json nach /config/config.json mounten (dort sucht das
Programm ohne Argument), HA_TOKEN als Umgebungsvariable setzen, Port 8000 im
gemeinsamen Docker-Netz erreichbar machen. Ein Dockerfile liegt nicht bei.
Tests
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
Alles offline, ohne Netzzugriffe. Abgedeckt sind:
load_config: vollständige Datei, Defaults für fehlende Felder, Env schlägt Datei,LISTENignoriert die Datei, Panics bei fehlender/kaputter Config.- Aktiv-Erkennung: case-insensitive Inaktiv-Liste, leerer State, unbekannte States gelten als Druck.
urlencode: Leerzeichen, Umlaute (UTF-8, zwei Bytes), Sonderzeichen, Unreserved-Zeichen, leerer String.next_streak: Schwelle exakt getroffen,confirm == 0, Streak überconfirmhinaus, Abbruch durch einen Frame unter der Schwelle.- HTTP-Server: echte Anfragen über einen Socket auf
127.0.0.1:0gegen die Handler-Logik —/healthz→ 200ok,/status→ 200 JSON, unbekannter Pfad und unbekannter Frame → 404.
Die Tests, die load_config prüfen, laufen serialisiert (gemeinsamer Mutex),
weil sie Prozess-Umgebungsvariablen setzen.
Was das Tool nicht kann
Ehrlich, damit niemand mehr erwartet als drin ist:
- Keine eigene Bildanalyse. Der Score ist die höchste Konfidenz, die obico-ml für einen Frame liefert. Was das Modell erkennt (und wie gut), ist komplett Sache des ML-Dienstes. Dieses Programm zählt und meldet nur.
- Keine Spaghetti-Erkennung im engeren Sinn. Kein Vergleich mit dem Soll-Druck, keine Layer- oder G-Code-Prüfung, keine Erkennung von Verzug/Detachment/Binderfehlern — nur das, was das Modell im Bild sieht.
- Ein Frame pro Intervall, kein Video. Kurze Ereignisse zwischen zwei Polls können komplett übersehen werden.
- ML-Ausfall sieht aus wie „alles gut". Bei Fehler wird Score
0.0gepusht, damit die Streak nicht stehen bleibt. Sichtbar wird das nur in/diagund anml_ok = falsein/status. Es gibt keinen eigenen Alarm dafür. - Pausiert nicht selbst. Ohne passende HA-Automation passiert auf den Webhook hin nichts.
- Kein Zustand über einen Neustart. Streak, Cooldown und Warmup-Zeitpunkt leben nur im Speicher; nach einem Neustart beginnt alles von vorn.
- Nur der letzte Frame liegt vor.
/frame/<name>liefert genau ein Bild, keine Historie. - Keine Retries, kein Backoff. Ein einzelner fehlgeschlagener Request kostet eine Runde, nicht mehr.
- Keine Authentifizierung am eigenen HTTP-Server, kein TLS, ein Request pro Verbindung, 2048-Byte-Puffer für die Anfrage. Fürs interne Netz gebaut, nicht fürs Internet.