boardViewer

This commit is contained in:
chk
2026-06-19 06:43:06 +02:00
parent d36ef6189d
commit aa78116837
8 changed files with 593 additions and 73 deletions

View File

@@ -1,14 +1,25 @@
# robot.json Zugriff via appRobotDriver
> Stand: 2026-06-15
> Beschreibt die geplante Umstellung: robot.json kommt vom appRobotDriver, nicht
> mehr aus einer lokalen Datei.
> **Status: umgesetzt** (2026-06-17) — `server/robotConfig.js` ist aktiv.
> Dieses Dokument beschreibt Entwurf und Implementierung. Der Implementierungsplan
> (Schritte 13) ist vollständig abgearbeitet.
---
## Ist-Zustand
## Verhalten je Env-Variable
`appRobotHoming` liest und schreibt die Roboter-Konfiguration direkt aus einer
| Variable | nicht gesetzt | gesetzt |
|----------|--------------|---------|
| `ROBOT_URL` | Kein Driver-Kontakt; alle Lese-/Schreibvorgänge direkt auf die lokale Datei | `fetchRobot()` liest von `GET {ROBOT_URL}/api/robot/config`; `pushRobot()` schreibt nach `POST {ROBOT_URL}/api/robot/config` |
| `ROBOT_JSON` | Standardpfad `scripts/robot_1781069752019.json` | Angegebener Pfad wird als lokale Cache-Datei verwendet |
Beide Variablen nicht gesetzt = **reiner Lokal-Modus**, identisch zum Verhalten vor dem Umbau.
---
## Ehemaliger Ist-Zustand (vor 2026-06-17)
`appRobotHoming` las und schrieb die Roboter-Konfiguration direkt aus einer
lokalen Datei:
```
@@ -16,8 +27,8 @@ ROBOT_JSON = process.env.ROBOT_JSON
|| 'scripts/robot_1781069752019.json'
```
Die Python-Skripte erhalten den Dateipfad als CLI-Argument (`-robot`, `--robot`).
Alle Kalibrierungs-Endpoints schreiben ebenfalls in diese Datei.
Die Python-Skripte erhielten den Dateipfad als CLI-Argument (`-robot`, `--robot`).
Alle Kalibrierungs-Endpoints schrieben ebenfalls in diese Datei.
**Problem:** Der appRobotDriver besitzt die maßgebliche Konfiguration — nicht das
Homing-System. Nach einem Neustart könnten Konfiguration und Driver auseinanderlaufen.

View File

@@ -1,10 +1,12 @@
# 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**.
> **Status: implementiert** (2026-06-18).
> `POST /api/homing/run-offline` ist aktiv in `server/server.js`.
> Abhängigkeit: `multer` (npm-Package, bereits installiert).
>
> 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.
---
@@ -35,7 +37,7 @@ Ergebnisdateien zurückgeben.
|---|---|---|
| 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 |
| `robot.json` | Server nutzt `robotConfig.js` (Driver oder lokale Cache-Datei) | 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 |
@@ -119,6 +121,110 @@ Fehlerresponse immer `{ "error": "…", "log": ["…"] }`.
---
## Aufruf-Beispiele
### curl
```bash
curl -X POST https://thinkcentre.local:2093/api/homing/run-offline \
-F "images=@cam0.jpg" \
-F "images=@cam1.jpg" \
-F "images=@cam2.jpg" \
-F "calibrations=@cam0_calibration.npz" \
-F "calibrations=@cam1_calibration.npz" \
-F "calibrations=@cam2_calibration.npz" \
-F "robot=@robot_1781069752019.json;type=application/json" \
-F "refSet=A0"
```
Ohne `refSet` (alle Board-Marker als Referenz):
```bash
curl -X POST https://thinkcentre.local:2093/api/homing/run-offline \
-F "images=@cam0.jpg" -F "images=@cam1.jpg" -F "images=@cam2.jpg" \
-F "calibrations=@cam0_calibration.npz" -F "calibrations=@cam1_calibration.npz" \
-F "calibrations=@cam2_calibration.npz" \
-F "robot=@robot_1781069752019.json;type=application/json"
```
### JavaScript / fetch (Browser oder Node)
```javascript
const formData = new FormData();
// Bilder Dateiname MUSS {cameraId}.jpg sein
formData.append('images', cam0Blob, 'cam0.jpg');
formData.append('images', cam1Blob, 'cam1.jpg');
formData.append('images', cam2Blob, 'cam2.jpg');
// Kalibrierungen Dateiname MUSS {cameraId}_calibration.npz sein
formData.append('calibrations', cam0NpzBlob, 'cam0_calibration.npz');
formData.append('calibrations', cam1NpzBlob, 'cam1_calibration.npz');
formData.append('calibrations', cam2NpzBlob, 'cam2_calibration.npz');
// robot.json
formData.append('robot', new Blob([JSON.stringify(robotJson)], { type: 'application/json' }), 'robot.json');
// optional
formData.append('refSet', 'A0');
const res = await fetch('/api/homing/run-offline', { method: 'POST', body: formData });
const data = await res.json();
// data.state → { x, y, z, a, b, c, e }
// data.files → { "aruco_marker_poses.json": {...}, "state_Arm1.json": {...}, … }
// data.log → ["▶ Kameras: cam0, cam1, cam2", …]
// data.runDir → "20260618_143022"
```
### Python (requests)
```python
import requests
url = 'https://thinkcentre.local:2093/api/homing/run-offline'
files = [
('images', ('cam0.jpg', open('cam0.jpg', 'rb'), 'image/jpeg')),
('images', ('cam1.jpg', open('cam1.jpg', 'rb'), 'image/jpeg')),
('images', ('cam2.jpg', open('cam2.jpg', 'rb'), 'image/jpeg')),
('calibrations', ('cam0_calibration.npz', open('cam0_calibration.npz', 'rb'), 'application/octet-stream')),
('calibrations', ('cam1_calibration.npz', open('cam1_calibration.npz', 'rb'), 'application/octet-stream')),
('calibrations', ('cam2_calibration.npz', open('cam2_calibration.npz', 'rb'), 'application/octet-stream')),
('robot', ('robot.json', open('robot.json', 'rb'), 'application/json')),
]
data = {'refSet': 'A0'} # optional
resp = requests.post(url, files=files, data=data, verify=False, timeout=120)
result = resp.json()
print(result['state']) # {'x': 312.4, 'y': 44.8, 'z': -12.1, ...}
```
### Wichtige Regeln für Dateinamen
| Feld | Pflichtformat | Beispiel |
|------|--------------|---------|
| `images` | `{cameraId}.jpg` | `cam0.jpg`, `cam1.jpg` |
| `calibrations` | `{cameraId}_calibration.npz` | `cam0_calibration.npz` |
| `robot` | beliebig (wird intern als `robot_run.json` gespeichert) | `robot.json` |
Das Pairing Bild ↔ NPZ erfolgt rein über den `cameraId`-Prefix.
Eine Kamera ohne passende NPZ wird übersprungen (kein Fehler, aber Log-Eintrag).
### HTTP-Statuscodes
| Code | Bedeutung |
|------|-----------|
| `200` | Erfolg: `{ ok: true, runDir, state, files, log }` |
| `400` | Pflichtfelder fehlen oder `robot.json` ist kein valides JSON |
| `422` | Pipeline abgebrochen: `aruco_marker_poses.json` nicht erzeugt (< 2 Kamera-Posen) |
| `500` | Homing fehlgeschlagen (4b-Kette + Fallback gescheitert) |
### Timeout-Hinweis
Die Pipeline kann 2060 s dauern. Client-Timeout auf **≥ 120 s** setzen
(curl: `--max-time 120`, requests: `timeout=120`, nginx/Caddy: `proxy_read_timeout 120s`).
---
## Datenfluss (detailliert)
```
@@ -179,7 +285,7 @@ holt, sondern aus dem vorab befüllten `{runDir}`.
---
## Umsetzungsplan
## Umsetzungsplan (abgeschlossen 2026-06-18)
### Schritt 1 — `runBoardPipelineOffline(runDir, send, opts)` (Kern)
@@ -260,7 +366,7 @@ 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`
`robot_run.json` gespeichert. Sie liegt **nicht** an der Stelle von `robotCachePath` (aus `robotConfig.js`)
und wird daher nie vom Live-Modus versehentlich gelesen. Kein Konflikt.
**Risiko:** keines — rein additive Datei in einem neuen Verzeichnis.
@@ -329,15 +435,12 @@ dieser Implementierung).
---
## Offene Entscheidungen
## Offene Entscheidungen (getroffen)
- [ ] `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?
- [x] `multer` (v2.2.0) — installiert, `diskStorage` schreibt direkt in `{runDir}`
- [x] Offline-Runs in `data/homing-offline/` (eigenes Verzeichnis, nicht im boardViewer)
- [x] Synchrone Antwort — Pipeline läuft durch, dann JSON; Client-Timeout ≥ 120 s empfohlen
- [x] `5_pose_estimation.py` nur als Fallback (identisch zum Live-Modus)
---