Files
appRobotHoming/doc/homingAPI.md
2026-06-17 22:57:52 +02:00

15 KiB
Raw Blame History

Homing Offline-API

Beschreibung eines geplanten HTTP-Endpunkts, der die vollständige Homing-Pipeline (Schritte 15) ohne Live-Kameras betreibt. Gedacht für die Simulations-Pipeline (appRobotRendering), die synthetische oder aufgezeichnete Bilder liefert und die aktuelle Pose-Erkennung von appRobotHoming nutzen möchte — ohne den Umweg über echte Kameras und den WebCam-Service. Stand: 2026-06-16, noch nicht implementiert.


Motivation

Die Live-Homing-Pipeline (POST /api/homing/run) setzt voraus, dass WEBCAM_URL auf einen laufenden WebCam-Service zeigt, der Bilder auf Abruf liefert. Das ist für zwei Szenarien unpraktisch:

  1. Simulations-ValidierungappRobotRendering rendert synthetische Bilder zu bekannten Gelenkwinkeln und will prüfen, wie gut die aktuelle Pose-Erkennung die Winkel zurückrechnet. Die Pipeline in appRobotRendering liegt lokal und braucht keine Live-Kamera.

  2. Offline-Replay — früher aufgenommene Bildsätze sollen mit dem aktuellen Stand der Algorithmen neu ausgewertet werden (z. B. nach Verbesserungen an 4b_revolute_angle.py oder 5_pose_estimation.py).

In beiden Fällen sind Bilder und Kalibrierungsdaten bereits vorhanden. Die API soll diese entgegennehmen, die Pipeline identisch zum Live-Modus durchlaufen und die Ergebnisdateien zurückgeben.


Abgrenzung zum Live-Modus

Aspekt Live (/api/homing/run) Offline (diese API)
Bilder WebCam-Service liefert auf Abruf Caller liefert im Request
NPZ Server sucht neueste Session in data/calibration/ Caller liefert im Request
robot.json Server nutzt ROBOT_JSON-Env Caller liefert im Request
Pipeline (1→2→3b→4b→5) identisch identisch
Antwort SSE-Stream während Lauf Synchrones JSON mit allen Ergebnisdateien
Zweck Produktions-Homing Simulation / Replay

Die Pipeline-Skripte (1_detect_aruco_observations.py bis 5_pose_estimation.py) werden unverändert aufgerufen — die Offline-API ist nur eine andere Eingangsschicht. Neue Algorithmen erscheinen automatisch in beiden Pfaden.


API-Beschreibung

POST /api/homing/run-offline

Content-Type: multipart/form-data

Felder

Feld Typ Pflicht Beschreibung
images Datei(en), image/jpeg ja Ein oder mehrere JPEG-Bilder. Dateiname muss {cameraId}.jpg sein (z. B. cam0.jpg, cam1.jpg).
calibrations Datei(en), application/octet-stream ja Je eine .npz-Datei pro Kamera. Dateiname muss {cameraId}_calibration.npz sein (z. B. cam0_calibration.npz).
robot Datei, application/json ja robot.json für diesen Lauf. Wird nur für diesen Aufruf verwendet, nicht dauerhaft gespeichert.
refSet Text nein Optionaler Referenz-Set-Name für Script 2 (--refSet), z. B. A0.

Das Pairing {cameraId}.jpg{cameraId}_calibration.npz erfolgt rein über den Dateinamen-Prefix vor dem ersten _ bzw. vor .jpg. Kamera-IDs ohne passende NPZ werden wie im Live-Modus übersprungen (Log-Eintrag, kein Fehler).

Antwort (Erfolg): 200 OK

{
  "ok": true,
  "runDir": "20260616_183042",
  "state": {
    "x": 312.4,
    "y": 44.8,
    "z": -12.1,
    "a": 7.3,
    "b": 0.0,
    "c": null,
    "e": null
  },
  "files": {
    "aruco_marker_poses.json":    { /* Inhalt */ },
    "robot_state.json":           { /* Inhalt */ },
    "state_Arm1.json":            { /* Inhalt */ },
    "state_Ellbow.json":          { /* Inhalt */ },
    "state_Arm2.json":            { /* Inhalt */ },
    "state_Hand.json":            { /* Inhalt */ },
    "cam0_aruco_detection.json":  { /* Inhalt */ },
    "cam0_camera_pose.json":      { /* Inhalt */ },
    "cam1_aruco_detection.json":  { /* Inhalt */ },
    "cam1_camera_pose.json":      { /* Inhalt */ }
  },
  "log": [
    "▶ Homing-Run: 20260616_183042",
    "▶ cam0: 14 Marker erkannt",
    "…"
  ]
}

state entspricht dem accumulated_state aus der 4b-Kette (wenn erfolgreich) oder den movements-Werten aus robot_state.json (wenn 4b abbricht und 5_pose_estimation einspringt). null-Werte bedeuten: Gelenk nicht beobachtbar (wie im Live-Modus).

files enthält alle JSON-Ausgabedateien des Laufs als geparste Objekte — kein Base64, da es sich ausschliesslich um JSON handelt.

Antwort (Fehler)

Code Bedeutung
400 Pflichtfelder fehlen, Dateinamen-Convention verletzt, robot.json ungültig
422 Pipeline abgebrochen: Script 3b konnte aruco_marker_poses.json nicht erzeugen (zu wenige Kameras o. ä.)
500 Unerwarteter Server-Fehler (Python nicht gefunden, Datei-I/O-Fehler, …)

Fehlerresponse immer { "error": "…", "log": ["…"] }.


Datenfluss (detailliert)

multipart/form-data
  images:        cam0.jpg, cam1.jpg
  calibrations:  cam0_calibration.npz, cam1_calibration.npz
  robot:         robot_xxx.json
        │
        ▼
Server: Temp-Verzeichnis anlegen  data/homing-offline/{timestamp}/
  → Bilder speichern:        cam0.jpg, cam1.jpg
  → NPZ speichern:           cam0_calibration.npz, cam1_calibration.npz
  → robot.json speichern:    robot_run.json  (nur für diesen Lauf)
        │
        ▼  für jede Kamera:
  1_detect_aruco_observations.py
       -i cam0.jpg  -npz cam0_calibration.npz  -robot robot_run.json
       -cameraId cam0  -outDir {runDir}
  → cam0_aruco_detection.json

  2_estimate_camera_from_observations.py
       -i cam0_aruco_detection.json  -robot robot_run.json
       -outDir {runDir}  [--refSet A0]
  → cam0_camera_pose.json
        │
        ▼  nach allen Kameras:
  3b_corner_marker_poses.py
       --evalDir {runDir}  --robot robot_run.json
  → aruco_marker_poses.json
        │
        ▼  X-Position schätzen (homingXEstimate)
  x_mm ← estimateXFromMarkers(aruco_marker_poses.json, robot_run.json)
        │
        ▼  4b-Kette sequenziell Arm1→Ellbow→Arm2→Hand:
  4b_revolute_angle.py
       --robot robot_run.json  --aruco aruco_marker_poses.json
       --link Arm1  --x-mm {x_mm}  --output state_Arm1.json
  4b_revolute_angle.py  --link Ellbow  --from-state state_Arm1.json  …
  …
  → state_Arm1.json, state_Ellbow.json, state_Arm2.json, state_Hand.json
        │
        ▼  (falls 4b-Kette vollständig)
  accumulated_state  → state in der Antwort
        │
        ▼  (Verfeinerung / Fallback bei abgebrochener 4b-Kette)
  5_pose_estimation.py
       aruco_marker_poses.json  -robot robot_run.json
       --from-state state_Hand.json  -out robot_state.json
  → robot_state.json
        │
        ▼
  Antwort: { ok, state, files: { alle JSON-Dateien }, log }

Der Ablauf ist identisch zu runHoming() in homingOrchestrator.js, mit dem einzigen Unterschied, dass runBoardPipeline() die Bilder nicht vom WebCam-Service holt, sondern aus dem vorab befüllten {runDir}.


Umsetzungsplan

Schritt 1 — runBoardPipelineOffline(runDir, send, opts) (Kern)

Was: Neue Funktion in server/server.js oder server/homingOrchestrator.js, analog zu runBoardPipeline(). Unterschied: statt WEBCAM_URL und findLatestNpzForCamera() leitet sie die bereits-im-Verzeichnis-liegenden Dateien direkt weiter.

Konkret: Der einzige geänderte Teil in runBoardPipeline() ist die Kamera-Schleife — statt Snapshot + NPZ-Suche wird einfach geprüft, ob {camId}.jpg und {camId}_calibration.npz im runDir existieren. Danach rufts Script 1 und 2 identisch auf.

Risiko: keines — die Script-Aufrufe selbst bleiben unverändert. Nur der Pfad zur NPZ ändert sich von data/calibration/{session}/ zu {runDir}/.

Testbar: Einzel-Test mit einer Kamera, geprüft, ob cam0_aruco_detection.json korrekt erzeugt wird.


Schritt 2 — Multipart-Upload-Handling

Was: Parsing von multipart/form-data für den neuen Endpoint. Express verarbeitet application/json und urlencoded nativ, aber nicht multipart. Benötigt entweder:

  • Option Amulter (npm-Package), etabliert, 0 Boilerplate
  • Option B — manuell mit dem Node busboy-Parser (bereits in Node 18+, kein Extra-Package)

Empfehlung: multer — es ist bereits multer in vielen Express-Projekten Standard, klar dokumentiert, und diskStorage legt Dateien direkt in {runDir} ab. Vermeidet Buffer-Accumulation für grosse NPZ-Dateien.

Risiko: Dateinamen-Sanitising — Multer übergibt den Originaldateinamen. Vor dem Speichern: path.basename() und nur Zeichen [a-zA-Z0-9_.-] zulassen, sonst 400.

Testbar: curl-Upload-Test mit zwei kleinen Dummy-Dateien, geprüft, ob sie im richtigen Verzeichnis landen.


Schritt 3 — runHomingOffline() Orchestrator

Was: Analoge Funktion zu runHoming() in homingOrchestrator.js, jedoch:

  • kein SSE-Stream, sondern Log-Akkumulation in ein Array
  • runBoardPipelineOffline() statt runBoardPipeline()
  • robotJsonPath zeigt auf die hochgeladene, temporäre robot_run.json
  • Rückgabewert: { state, files, log } statt SSE-Events

Die 4b-Kette und der 5_pose-Aufruf bleiben unverändert — gleiche Args, gleiche Exit-Code-Logik, gleiche accumulated_state-Extraktion.

Risiko: 5_pose_estimation.py braucht scipy. In der lokalen Entwicklungsumgebung muss scipy im Python-Umfeld vorhanden sein (in docker-compose.yaml ist es bereits eingetragen, lokal muss pip install scipy geprüft werden).


Schritt 4 — Endpoint POST /api/homing/run-offline

Was: Express-Route in server.js:

  1. Multer-Middleware: Dateien in Temp-Verzeichnis
  2. Validierung: mindestens 1 Kamera mit passendem .jpg + .npz-Pair; robot-Feld vorhanden
  3. runHomingOffline() aufrufen
  4. Alle JSON-Dateien aus {runDir} einlesen und in files-Objekt verpacken
  5. Temp-Verzeichnis aufräumen (oder unter data/homing-offline/ für Replay behalten?)

Aufräumen vs. Behalten: Empfehlung — Verzeichnis behalten, wie bei Live-Homing-Runs. So ist Replay/Debug möglich. Ein periodischer Aufräum-Cron ist eine separate Aufgabe.

Risiko: Timeouts — bei vielen Kameras oder langsamer Maschine kann die Pipeline 2060 Sekunden dauern. Express hat kein Default-Timeout, aber ein vorgeschalteter Reverse-Proxy (nginx, Caddy) schon. In der Doku festhalten: Client soll Timeout ≥ 120 s setzen. Alternativ: SSE-Variante (gleiche Daten, aber inkrementell gestreamt).


Schritt 5 — Aufräumen temporärer robot.json

Was: Die hochgeladene robot.json wird nur für diesen Lauf im {runDir} als robot_run.json gespeichert. Sie liegt nicht an der Stelle von ROBOT_JSON und wird daher nie vom Live-Modus versehentlich gelesen. Kein Konflikt.

Risiko: keines — rein additive Datei in einem neuen Verzeichnis.


Testplan

Test Was wird geprüft Werkzeug
Smoke-Test Upload Endpoint antwortet 200, runDir im Response vorhanden curl mit zwei Dummy-JPEGs + NPZs + robot.json
Kamera-Pairing .jpg ohne passende NPZ wird übersprungen (kein 500) curl mit fehlendem NPZ
Dateinamen-Sanitising ../../../evil.npz → 400 curl mit bösem Dateinamen
Simulations-Roundtrip appRobotRendering rendert 10 Posen mit bekannten GT-Winkeln, API gibt state zurück, Abweichung < Toleranz automatisiert aus appRobotRendering-Pipeline
Identität Live vs. Offline Denselben Bildsatz einmal per Live-Run und einmal per Offline-API auswerten → state-Differenz ≈ 0 manuell mit aufgezeichnetem Homing-Run
Fallback-Pfad Script 3b schlägt fehl (< 2 Kameras) → 422 mit log curl mit nur einem Bild
Timeout-Robustheit 4b bricht ab → 5_pose_estimation greift ein, 200 wird trotzdem zurückgegeben simulierter Abbruch durch fehlende Marker

Bekannte Risiken und Probleme

Dateinamen-Convention ist implizit

Die Kamera-ID wird aus dem Dateinamen abgeleitet — kein explizites Metadaten-Feld. Das funktioniert solange die Namenskonvention ({cameraId}.jpg / {cameraId}_calibration.npz) eingehalten wird. Abweichungen (z. B. frame_cam0_001.jpg) führen zu einem ungematchten Bild, das still übersprungen wird — kein Fehler, aber unerwartetes Verhalten.

Mitigation: Explizite Validierung und Fehlermeldung wenn kein Match gefunden wird. Alternativ: Metadaten-Feld cameraMappings: {"cam0": {"image": "frame_cam0_001.jpg", "npz": "cam0_calibration.npz"}}. Für den Simulations-Use-Case ist die einfache Konvention ausreichend.

robot.json-Versionskonflikt

Simulation und Live-Homing teilen sich robot.json konzeptionell, aber die Datei entwickelt sich weiter (Kalibrierung, neue Marker, geänderte Positionen). Eine veraltete robot.json aus appRobotRendering kann zu systematisch falschen Posen führen, die schwer von Algorithmus-Fehlern zu unterscheiden sind.

Mitigation: Im Simulations-Roundtrip-Test die robot.json-Version (z. B. Timestamp im Dateinamen) protokollieren und mit dem API-Response abgleichen.

Gleichzeitige Anfragen

Mehrere Offline-Runs gleichzeitig schreiben in separate {timestamp}-Verzeichnisse — kein Konflikt. Aber: Python-Subprozesse multiplizieren sich. Bei parallelen Requests aus der Simulations-Pipeline könnte die CPU-Last (scipy + ArUco) den Server blockieren.

Mitigation (optional): Request-Queue mit max. 12 parallelen Runs. Für den Simulations-Use-Case wird sequenzieller Aufruf empfohlen.

scipy-Abhängigkeit auf Deployment-Maschine

5_pose_estimation.py braucht scipy. In docker-compose.yaml ist es vorhanden. Lokal (Entwicklung ohne Docker) muss pip install scipy sichergestellt sein, sonst schlägt Schritt 5 stumm fehl (Exit 1, aber Schritt 4 hat bereits ein Ergebnis).

Festplattenverbrauch

Jeder Offline-Run erzeugt ein Verzeichnis mit JPEGs + NPZs + ca. 10 JSON-Dateien. Bei intensiver Simulations-Nutzung (100 Posen/Tag) kann das summieren.

Mitigation: TTL-basiertes Aufräumen als gelegentliche Wartungsaufgabe (kein Teil dieser Implementierung).


Offene Entscheidungen

  • multer hinzufügen (npm install multer) oder busboy nutzen?
  • Offline-Runs in data/homing-offline/ oder data/homing/ speichern? (data/homing/ würde sie im boardViewer sichtbar machen, was praktisch sein kann)
  • Timeout-Handling: synchrone Antwort mit Timeout-Header-Hinweis oder SSE-Stream wie Live-Modus (Client liest bis done-Event)?
  • Soll 5_pose_estimation.py immer laufen (auch nach vollständiger 4b-Kette), oder nur als Fallback wie im Live-Modus?

Abhängigkeiten und Voraussetzungen

Vor der Implementierung müssen vorhanden sein:

  • multer installiert (oder busboy-basierte Alternative entschieden)
  • scipy im Python-Environment verfügbar (lokal + Docker)
  • scripts/5_pose_estimation.py + scripts/robot_fk.py im Repo (beide vorhanden, )
  • homingOrchestrator.js für Orientierung (vorhanden, )
  • Test-Bilder + Test-NPZs für automatisierten Smoke-Test (aus test/homing/ oder test/y-axis-finder-examples/; die NPZs müssen dazu noch als Testfixtures bereitgestellt werden)

Verweise

  • Homing.md — Gesamtüberblick Homing-Ablauf
  • Homing_0_Camera.md — Schritte 13b (Board-Pipeline)
  • Homing_1_StepByStep.md — 4b-Kette (Gelenkwinkel-Schätzung)
  • Homing_5_Pose.md — 5_pose_estimation.py (Bundle-Adjustment)
  • server/server.js — bestehende Endpoints (/api/homing/run, runBoardPipeline())
  • server/homingOrchestrator.jsrunHoming(), Vorlage für runHomingOffline()