spin Marker Callibration

This commit is contained in:
chk
2026-06-15 09:23:21 +02:00
parent 375ee4cf69
commit 15d4175fd1
18 changed files with 1191 additions and 239 deletions

View File

@@ -1,6 +1,6 @@
# Homing appRobotHoming
> Stand: 2026-06-14
> Stand: 2026-06-15
> Homing läuft bei **jedem Einschalten** — schnell, vollautomatisch, ohne mechanische Endschalter.
---
@@ -44,7 +44,7 @@ Homing setzt eine abgeschlossene Kalibrierung voraus:
| Board-Marker-Positionen | ✅ |
| X-Achsen-Richtung | ✅ |
| Arm1 Joint-Origin Y/Z | ✅ Button vorhanden und ausführbar |
| Arm-Marker in robot.json | 🔶 Nutzer trägt ein (`links.Arm1/Ellbow/Arm2/Hand.markers`) |
| Arm-Marker in robot.json | 🔶 Position manuell eintragen; Spin-Verifikation via Kalibrierung → Tab „Marker" (→ `doc/Kalibrierung_Marker.md`) |
---
@@ -101,7 +101,7 @@ X-Slider-Position über `--x-mm`.
| State senden | `POST /api/homing/send-state` | Weiterleitung an `ROBOT_URL/api/state` |
| Run-Daten | `GET /api/homing/run-data?run=ts` | Debug-Bilder (base64) + finalState |
| Frontend | `public/index.html` + `public/client.js` | Homing-Buttons, Fortschrittsbalken, Tree View; schreibt Teil-Pose als `G92`-GCode ins Eingabefeld |
| Board-Viewer (Homing) | `public/boardViewer.html?mode=homing` | Skelett + Arm-Marker per FK (Three.js), gemessene Marker als Kugeln + Fehlerlinien; progressiver Update je erkanntem Gelenk |
| Board-Viewer (Homing) | `public/boardViewer.html?mode=homing` | Skelett + Arm-Marker per FK (Three.js): Marker-Quadrat spin-korrekt rotiert + Orientierungszeiger zu Ecke 0 (Modell-Seite); gemessene Marker als Kugeln + Fehlerlinien; progressiver Update je erkanntem Gelenk |
**Lauf-Verzeichnisse:** `data/homing/{timestamp}/`

View File

@@ -1,6 +1,6 @@
# Kalibrierung appRobotHoming
> Stand: 2026-06-14
> Stand: 2026-06-15
> Einmaliger Vorgang — nur nach mechanischen Änderungen wiederholen.
> Jede Stufe baut auf der vorherigen auf.
@@ -9,7 +9,7 @@
## Übersicht
```
[1] Camera NPZ → [2] Board → [3] X-Achse → [4] Arm1 Y-Achse
[1] Camera NPZ → [2] Board → [3] X-Achse → [4] Arm1 Y-Achse → [5] Arm-Marker
```
| Schritt | UI-Tab | Ergebnis | Status |
@@ -18,7 +18,7 @@
| [2] Board | Board | `links.Board.markers[].position` in `robot.json` | ✅ |
| [3] X-Achse | Robot X Axis | Alle Marker-Positionen in `robot.json` rotiert | ✅ |
| [4] Arm1 Y-Achse | Arm1 Y | `links.Arm1.jointToParent.origin[1,2]` in `robot.json` | ✅ |
| Arm-Marker eintragen | — | `links.Arm1/Ellbow/Arm2/Hand.markers` | 🔶 Nutzer |
| [5] Arm-Marker | Marker | `links.*.markers[].{position,spin}` visuell prüfen und korrigieren | ✅ |
---
@@ -283,6 +283,33 @@ Die Implementierung in `public/yAxisCompute.js` setzt **Verfahren B** um:
---
## [5] Arm-Marker — Spin-Verifikation und Korrektur
**Ziel:** Sicherstellen, dass die in `robot.json` eingetragenen Arm-Marker
(Position, Normale, Spin) mit dem realen Aufkleber übereinstimmen — insbesondere
die `spin`-Orientierung, die der Homing-Viewer korrekt darstellen muss.
**Ablauf:**
1. Arm-Marker physisch aufkleben und Position/Normale grob in `robot.json` eintragen
2. Homing-Run starten (Tab „Homing" oder `homing.html`)
3. Kalibrierung → Tab **„Marker"** öffnen:
- Tabelle zeigt alle Arm-Marker mit aktuellem `spin`-Wert
- Viewer zeigt 3D-Skeleton mit spin-orientiertem Marker-Quadrat und Orientierungszeiger (→ Ecke 0)
4. Visuelle Prüfung: zeigt die Viewer-Grafik dieselbe Orientierung wie der echte Aufkleber?
5. Falls nicht: Link und Marker-ID wählen → **90° / +90° / 180°** klicken → Viewer lädt neu
6. Wiederholen bis Modell und Realität übereinstimmen
**Aktionen im Marker-Tab:**
- **Link-Dropdown + ID-Dropdown**: Marker auswählen
- **Spin-Buttons** (90°, +90°, 180°): Spin in `robot.json` korrigieren und Viewer neu laden
- **Viewer** (boardViewer.html?mode=homing): zeigt Skeleton mit Marker-Quadraten und Orientierungszeigern
**Speichert:** `links.{Link}.markers[].spin` in `robot.json` (normalisiert auf [0, 360))
**Details und Roadmap:** [`doc/Kalibrierung_Marker.md`](Kalibrierung_Marker.md)
---
## Dateistruktur
```

341
doc/Kalibrierung_Marker.md Normal file
View File

@@ -0,0 +1,341 @@
# Kalibrierung Arm-Marker
> Stand: 2026-06-15
> Ergänzung zu `doc/Kalibrierung.md` → Schritt „Arm-Marker eintragen und verifizieren"
> Dient als Programmier-Roadmap für den UI-Tab „Marker" und die Verifikations-Pipeline.
---
## Was ist ein Arm-Marker?
Ein ArUco-Aufkleber, der auf einem beweglichen Roboter-Glied (Arm1, Ellbow, Arm2, …)
befestigt ist. Arm-Marker unterscheiden sich von Board-Markern:
| | Board-Marker | Arm-Marker |
|--|--|--|
| Position | fest, kalibriert | bewegt sich mit dem Gelenk |
| Zweck | Referenz für Kamera-Pose | Gelenk-Winkel-Schätzung (Homing) |
| Position in robot.json | `links.Board.markers` | `links.{Link}.markers` |
| Koordinatensystem | Welt (Board) | lokal im Link-Frame |
---
## Marker-Daten in robot.json
```jsonc
{
"links": {
"Arm1": {
"markers": [
{
"id": 198, // ArUco-ID (eindeutig)
"name": "aruco_198", // optional, lesbar
"position": [0, -160, 35], // Mittelpunkt im lokalen Link-Frame [mm]
"normal": [0, 0, 1], // Normale der Marker-Fläche (Link-Frame)
"size": 25, // Kantenlänge mm
"spin": 0 // Drehung der Marker-Grafik um die Normale [°]
}
]
}
}
}
```
### Felder
| Feld | Typ | Bedeutung |
|------|-----|-----------|
| `id` | int | ArUco-ID — muss mit gedrucktem Marker übereinstimmen |
| `position` | `[x,y,z]` mm | Mittelpunkt im **lokalen Link-Frame** (nicht Welt) |
| `normal` | `[nx,ny,nz]` | Flächennormale des Markers im Link-Frame; `[0,0,1]` = Marker schaut in +Z |
| `size` | mm | Kantenlänge des ArUco-Quadrats |
| `spin` | ° | Drehung der Aufkleber-Grafik um die Normale — 0 / 90 / 180 / 270 |
### Spin-Semantik
Ein ArUco-Aufkleber kann in 4 Lagen aufgeklebt werden. Physisch ist das egal — der
Detektor findet die ID unabhängig von der Orientierung, und auch die gemessene
Mittelpunkt-Position ist spin-unabhängig.
`spin` beschreibt nur die visuelle Darstellung im 3D-Viewer, damit die angezeigte
Marker-Grafik mit dem echten Aufkleber übereinstimmt. Das hilft beim visuellen
Abgleich: wenn die Grafik im Viewer verdreht zum Aufkleber steht, ist spin falsch.
> **In der aktuellen Homing-Pipeline (3b, 4b) wird `spin` nicht verwendet.**
> Relevant wird es, wenn der Viewer spin korrekt darstellt (→ offenes Todo unten).
---
## Typischer Workflow
```
1. ArUco-Aufkleber auf Roboter-Glied kleben
2. Position messen / schätzen → in robot.json eintragen
(position = Mittelpunkt im lokalen Link-Frame, normal = Flächen-Richtung)
3. Homing-Run starten (homing.html)
4. Kalibrierung → Tab "Marker" öffnen
│ · Tabelle zeigt alle Marker und ihren aktuellen spin
│ · Viewer zeigt Modell-Marker (Vierecke) + beobachtete Punkte (Kugeln)
│ · Fehler-Linien: Modell-Marker → beobachteter Punkt
5. Prüfen:
· Linie kurz → Position stimmt gut
· Linie lang oder in falscher Richtung → position in robot.json korrigieren
· Viewer-Grafik verdreht → spin korrigieren (+90 / -90 / 180)
6. Spin / Normal korrigieren → Viewer lädt neu → erneut prüfen
```
---
## UI-Tab „Marker" (implementiert 2026-06-15)
### Datei: `public/calibration_marker.html`
Drei Abschnitte:
| Abschnitt | Inhalt |
|-----------|--------|
| Aktuelle Marker | Tabelle aller Arm-Marker (Link, ID, Name, Position, Normal, Size, **Spin**) aus robot.json |
| Aktionen | Link-Dropdown → Marker-ID-Dropdown → Spin-Buttons (90°, +90°, 180°) |
| Viewer | `boardViewer.html?mode=homing` — Modell + letzter Homing-Run |
### JS: `initMarker()` in `public/calibration.js`
| Funktion | Beschreibung |
|----------|-------------|
| `loadRobot()` | Holt `/api/robot`, rendert Tabelle, befüllt ID-Dropdown |
| `renderTable(robot)` | Tabelle aller Arm-Marker mit farbig hervorgehobenem Spin |
| `updateMarkerDropdown()` | Link-Dropdown-Wechsel → ID-Dropdown neu befüllen |
| `applySpin(delta)` | `POST /api/robot/set-arm-marker-spin` → Viewer `reload`-Message |
### Backend: `POST /api/robot/set-arm-marker-spin`
```
Body: { linkName: string, markerId: number, spin: number }
Return: { changed, linkName, markerId, oldSpin, newSpin }
```
Implementiert in `server/server.js` → delegiert an `setArmMarkerSpin()` in `server/editRobot.js`.
---
## Offene Punkte (Programmier-Roadmap)
### [P1] Spin-Rendering im boardViewer
**Status:** ✅ implementiert (2026-06-15)
**Datei:** `public/boardViewer.html``buildSkeletonFK()`, Abschnitt „4. Arm-Marker"
`spin` wird als zusätzliche Rotation um die Marker-Normale (in Welt-Koordinaten)
auf das orientierte Quadrat angewendet via `premultiply`:
```javascript
const normalW = new THREE.Vector3(nx, nz, -ny).transformDirection(childFrame).normalize();
const markerMesh = makeMarkerSquareOriented(posWorld, normalW, markerSizeM, col);
const spinRad = ((m.spin ?? 0) * Math.PI) / 180;
if (Math.abs(spinRad) > 1e-6) {
markerMesh.quaternion.premultiply(
new THREE.Quaternion().setFromAxisAngle(normalW, spinRad)
);
}
```
`premultiply(Q_spin)` setzt `quaternion = Q_spin * Q_normal` → zuerst Normal-Alignment,
dann Spin-Rotation in Welt-Koordinaten um `normalW`.
---
### [P2] Marker-Position verifizieren: Positions-Residuum
**Status:** ❌ noch nicht implementiert
Nach einem Homing-Run kennen wir für jeden erkannten Arm-Marker:
- `model_pos_world` = Modell-Mittelpunkt im Welt-Frame (via FK)
- `obs_pos_world` = triangulierter beobachteter Mittelpunkt
Das **Positions-Residuum** `|model_pos_world obs_pos_world|` zeigt, wie gut die
eingetragene `position` (Mittelpunkt im lokalen Frame) stimmt.
> **⚠ Wichtig: Spin-Fehler sind damit NICHT erkennbar.**
> Der Mittelpunkt eines Markers ist spin-unabhängig — egal wie ein Aufkleber gedreht
> ist, das Zentrum bleibt dasselbe. Ein falscher `spin`-Wert erzeugt daher kein
> Positions-Residuum. Spin-Fehler erfordern → P3 (visuell) oder P4 (automatisch).
**Erweiterung im Marker-Tab:** Tabelle um Spalten ergänzen:
- `Δ mm` = Positions-Residuum des letzten Homing-Runs
- `Status` = ✅ < 5 mm / ⚠ 520 mm / ❌ > 20 mm
**Datenquelle:** `/api/homing/run-data?run={ts}``finalState` + `measuredMarkers`
---
### [P3] Python: Ecken-Position in aruco_marker_poses.json ausgeben
**Status:** ❌ noch nicht implementiert
**Datei:** `scripts/3b_corner_marker_poses.py`
Voraussetzung für P3b. Aktuell enthält `aruco_marker_poses.json` pro Marker nur
`position_mm` (Mittelpunkt) und `normal`. Die Spin-Orientierung geht verloren.
`3b` trianguliert den Mittelpunkt aus den Ecken der Kamera-Beobachtungen.
Dieselbe Triangulierung auf **Ecke 0** anwenden und zusätzlich ausgeben:
```python
# Zusätzliches Feld pro Marker in aruco_marker_poses.json:
"corner0_mm": [x, y, z] # triangulierte Welt-Position von Ecke 0
```
Ecke 0 = top-left in OpenCVs ArUco-Konvention (Index 0 in `corners[0]`).
Zusammen mit `position_mm` (Mittelpunkt) definiert `corner0_mm` eindeutig die
Orientierung des Markers in 3D.
> Der Mittelpunkt bleibt spin-unabhängig. `corner0_mm` ist das einzige Feld,
> das den Spin kodiert.
---
### [P3b] boardViewer: Orientierungszeiger zeichnen
**Status:** ✅ Modell-Seite implementiert (2026-06-15) · Beobachtungs-Seite offen (→ P3)
**Datei:** `public/boardViewer.html``buildSkeletonFK()`
**Voraussetzungen:** P1 ✅, P3 (corner0_mm) für Beobachtungs-Zeiger noch offen
Für jeden Marker werden zwei kurze Linien vom Mittelpunkt zu Ecke 0 gezeichnet —
eine für das Modell, eine für die Beobachtung. Der Winkel zwischen beiden = Spin-Fehler.
**Modell-Zeiger** (implementiert — nutzt `markerMesh.quaternion` direkt):
`markerMesh.quaternion` kodiert bereits `Q_spin * Q_normal`, daher reicht:
```javascript
// Richtung zur Ecke 0: (1,1,0) normalisiert im lokalen Marker-Frame,
// transformiert durch die bereits berechnete Mesh-Rotation (Q_normal ∘ Q_spin)
const ptrDir = new THREE.Vector3(1, 1, 0).normalize().applyQuaternion(markerMesh.quaternion);
const corner0W = posWorld.clone().add(ptrDir.multiplyScalar(markerSizeM * Math.SQRT1_2));
gArmMarkers.add(makeLine(posWorld, corner0W, col, 0.9)); // Zeiger (Link-Farbe)
gArmMarkers.add(makeSphere(corner0W, 0.0008, col)); // Ecke 0 (Punkt)
```
`Math.SQRT1_2 = 1/√2` weil der Abstand Mittelpunkt→Ecke bei einem Quadrat mit
Kantenlänge `size` genau `size/√2` beträgt (`(size/2)·√2 = size/√2`).
**Beobachtungs-Zeiger** (aus `corner0_mm` in `aruco_marker_poses.json`,
sobald Python das Feld liefert → P3):
```javascript
// obs = beobachteter Marker aus _measuredMarkers
if (obs.corner0_mm) {
const corner0Obs = r2vArr(obs.corner0_mm); // robot→Three.js
gArmMarkers.add(makeLine(obsPosW, corner0Obs, 0xffffff, 0.6)); // Beobachtungs-Zeiger (dünn)
gArmMarkers.add(makeSphere(corner0Obs, 0.0006, 0xffffff)); // Beobachtungs-Ecke
}
```
**Ergebnis im Viewer:**
```
Modell-Mittelpunkt ●——▶ Modell-Ecke 0 (Link-Farbe, voll)
Obs-Mittelpunkt ●··▶ Obs-Ecke 0 (weiß, dünn)
```
Zeigen beide Zeiger in dieselbe Richtung → spin korrekt.
90°-Unterschied → spin um 90° falsch → +90° oder 90° klicken.
---
### [P3c] Homing-Check direkt im Marker-Tab starten
**Status:** ❌ noch nicht implementiert
Button „Homing-Check starten" im Marker-Tab triggert die vollständige Homing-Pipeline
(`POST /api/homing/run`) und zeigt:
- SSE-Log im Tab-internen Textfeld
- Fortschritt im Viewer via `postMessage({ type: 'homing-state', state })`
Kein struktureller Unterschied zu `homing.html` — Code-Duplizierung vermeiden
durch Extraktion eines gemeinsamen `runHomingStream(sendFn, frameFn)` aus `client.js`.
> Für Spin-Verifikation ist P3c nötig, damit frische Beobachtungsdaten im Viewer landen.
> Ohne P3 (corner0_mm) sieht man trotzdem nur Mittelpunkt-Fehlerlinien, keine Zeiger.
---
### [P4] Spin automatisch berechnen und korrigieren
**Status:** ❌ noch nicht implementiert
**Voraussetzungen:** P3 (corner0_mm), P3b (Zeiger im Viewer)
Sobald `corner0_mm` aus Python vorliegt und der Viewer die Zeiger anzeigt (P3b),
kann der tatsächliche Spin-Wert rechnerisch bestimmt werden — ohne manuelles Raten.
**Berechnung im Browser** (in `initMarker()` oder `buildSkeletonFK()`):
```javascript
// Modell-Richtung zu Ecke 0 bei spin=0 (im Welt-Frame, via FK):
const modelCorner0Dir = /* corner0World posWorld, normalisiert */;
// Beobachtete Richtung zu Ecke 0:
const obsCorner0Dir = r2vArr(obs.corner0_mm).sub(obsPosW).normalize();
// Winkel zwischen beiden (um die Marker-Normale):
const cross = new THREE.Vector3().crossVectors(modelCorner0Dir, obsCorner0Dir);
const sinA = cross.dot(normalWorld); // Vorzeichen = Drehrichtung
const cosA = modelCorner0Dir.dot(obsCorner0Dir);
const deltaDeg = Math.round(Math.atan2(sinA, cosA) * 180 / Math.PI / 90) * 90;
// → rundet auf nächste 90°: 0 / 90 / -90 / 180
```
**UI-Erweiterung im Marker-Tab:**
- Tabelle bekommt Spalte „Gemessener Spin" und „Soll-Spin (robot.json)"
- Unterschied → Badge „⚠ +90°" → Klick übernimmt die Korrektur direkt
---
### [P5] Marker-Position aus Homing übernehmen
**Status:** ❌ offen
Wenn ein Arm-Marker nur grob eingemessen wurde, kann die triangulierte Welt-Position
aus dem Homing-Run dazu genutzt werden, die `position` in robot.json zu verfeinern:
```
position_local = inverse_FK(obs_world, current_state)
```
Setzt voraus, dass der Gelenk-Winkel für diesen Link bereits korrekt bestimmt wurde.
Iterativ einsetzbar: grobe Startposition → erster Homing → verfeinerte Position → …
---
## Abhängigkeits-Kette Spin-Verifikation
```
P1 boardViewer rendert spin korrekt → Modell-Viereck zeigt echte Orientierung
P3 3b gibt corner0_mm aus → Beobachtungs-Orientierung verfügbar
P3b boardViewer zeichnet Orientierungszeiger → Spin-Fehler sichtbar als Winkel
P3c Marker-Tab: Homing-Check-Button → frische Daten ohne Tab-Wechsel
P4 JS berechnet Δspin, schlägt Korrektur vor → kein manuelles Raten mehr
```
---
## Dateiübersicht
| Datei | Rolle |
|-------|-------|
| `public/calibration.html` | Tab-Button „Marker" |
| `public/calibration_marker.html` | Panel-HTML (Tabelle, Aktionen, Viewer) |
| `public/calibration.js``initMarker()` | Frontend-Logik des Tabs |
| `server/server.js``POST /api/robot/set-arm-marker-spin` | Spin-Endpoint ✅ |
| `server/editRobot.js``setArmMarkerSpin()` | robot.json schreiben ✅ |
| `public/boardViewer.html``buildSkeletonFK()` | Spin-Rendering (→ P1) + Zeiger (→ P3b) |
| `scripts/3b_corner_marker_poses.py` | corner0_mm ausgeben (→ P3) |
| `scripts/robot_1781069752019.json``links.*.markers` | Marker-Daten |

View File

@@ -1,149 +0,0 @@
# appRobotBodyTrack
3D-Body-Tracking für Roboter aus Mehrkamera-ArUco-Bildern.
**Input**
- Bilder: `render_*.png`
- Intrinsics: `render_*.npz`
- Konfiguration: `robot.json`
**Output**
- Gelenke **R⁷**`{x, y, z, a, b, c, e}` (mm / Grad)
---
## Interfaces
Eine Logik, drei Zugänge:
- **Python**
- **CLI**
- **REST (FastAPI)**
---
## Quickstart
### Python
```python
from scripts import estimate_from_dir
result = estimate_from_dir("data/Scene8", robot_json="robot.json")
print(result.joints)
print(result.confidence)
```
---
### CLI
```bash
pip install -e .
python -m scripts data/Scene8 --robot robot.json
python -m scripts data/Scene8 --robot robot.json --cameras a,b,d
```
---
### REST API
```bash
docker compose up
```
**Request:**
```python
import requests
resp = requests.post(
"http://localhost:8446/v1/estimate",
files=[
("images", ("render_a.png", open("render_a.png", "rb"))),
("intrinsics", ("render_a.npz", open("render_a.npz", "rb"))),
],
)
print(resp.json()["joints"])
```
---
## API
| Endpoint | Methode | Zweck |
|----------|--------|------|
| `/v1/estimate` | POST | Bilder → Gelenke |
| `/v1/health` | GET | Status |
| `/v1/config` | GET | aktive Konfiguration |
**Response:**
```json
{
"joints": {"x": 50.2, "y": -2.1, "z": 94.8, "a": 20.1},
"confidence": {"x": "high", "b": "low"},
"residual_rms": 1.45,
"n_markers": 56,
"processing_ms": 1240
}
```
---
## Struktur
```
.
├── scripts/
├── config/robot.json
├── tests/
└── docker-compose.yaml
```
---
## Deployment (Docker / Portainer)
**Volume:**
```yaml
- /opt/approbot/config/robot.json:/config/robot.json:ro
```
**Healthcheck:**
```bash
curl http://<host>:8446/v1/health
```
---
## Konfiguration
Zentrale Datei: **`robot.json`**
Verwendete Bereiche:
- `links`
- `pose_estimation`
- `vision_config`
- `movements`
- `units`
---
## Stack (minimal)
- numpy
- scipy
- opencv (aruco)
- fastapi + uvicorn
---
## Naming
- **BodyTrack** → Tracking (dynamisch) ✅
- **BodyMap** → Modell / Repräsentation
- **BodySense** → Wahrnehmung (low-level)

255
doc/accessRobotAPI.md Normal file
View File

@@ -0,0 +1,255 @@
# robot.json Zugriff via appRobotDriver
> Stand: 2026-06-15
> Beschreibt die geplante Umstellung: robot.json kommt vom appRobotDriver, nicht
> mehr aus einer lokalen Datei.
---
## Ist-Zustand
`appRobotHoming` liest und schreibt die Roboter-Konfiguration direkt aus einer
lokalen Datei:
```
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.
**Problem:** Der appRobotDriver besitzt die maßgebliche Konfiguration — nicht das
Homing-System. Nach einem Neustart könnten Konfiguration und Driver auseinanderlaufen.
---
## Ziel-Zustand
```
Startup GET {ROBOT_URL}/api/robot/config → robot.json laden
Kalibrierung schreiben → lokal anpassen → POST {ROBOT_URL}/api/robot/config
Python-Skripte → weiterhin lokale Datei (Cache) (unverändert)
```
`appRobotDriver` ist die **Single Source of Truth**.
`appRobotHoming` hält eine **lokale Kopie** (Cache-Datei) nur für die Dauer eines
Laufs — Python-Skripte müssen nicht angepasst werden.
---
## appRobotDriver API (Platzhalter)
Die genaue API ist noch zu klären. Annahmen:
| Aktion | Endpoint | Body / Antwort |
|--------|----------|----------------|
| Konfiguration lesen | `GET {ROBOT_URL}/api/robot/config` | → JSON (robot.json-Inhalt) |
| Konfiguration schreiben | `POST {ROBOT_URL}/api/robot/config` | Body: JSON (robot.json-Inhalt), → `{ ok: true }` |
Sobald die echten Endpoints bekannt sind, diese Tabelle und die Implementierung
(`server/robotConfig.js`) entsprechend anpassen.
---
## Implementierungsplan
### Schritt 1 — `server/robotConfig.js` (neu)
Kapselt den gesamten robot.json-Zugriff. `server.js` und `editRobot.js` importieren
nur noch diese Funktionen — kein direktes `fsPromises.readFile` / `writeFile` mehr.
```javascript
// server/robotConfig.js (ESM)
import fsPromises from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROBOT_URL = process.env.ROBOT_URL || '';
// Lokale Cache-Datei: bleibt als Fallback und für Python-Skripte
const ROBOT_JSON = process.env.ROBOT_JSON
|| path.join(__dirname, '..', 'scripts', 'robot_1781069752019.json');
/**
* Lädt robot.json.
* Reihenfolge: (1) ROBOT_URL/api/robot/config, (2) lokale Datei als Fallback.
* Schreibt das Ergebnis immer in die lokale Cache-Datei (für Python-Skripte).
*/
export async function fetchRobot() {
if (ROBOT_URL) {
const res = await fetch(new URL('/api/robot/config', ROBOT_URL));
if (!res.ok) throw new Error(`Driver ${res.status}: ${await res.text()}`);
const data = await res.json();
// Cache für Python-Skripte aktualisieren
await fsPromises.writeFile(ROBOT_JSON, JSON.stringify(data, null, 2), 'utf8');
return data;
}
// Fallback: lokale Datei (Entwicklung ohne Driver)
return JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8'));
}
/**
* Speichert robot.json.
* Schreibt immer in lokale Cache-Datei; sendet zusätzlich an Driver wenn konfiguriert.
*/
export async function pushRobot(data) {
await fsPromises.writeFile(ROBOT_JSON, JSON.stringify(data, null, 2), 'utf8');
if (ROBOT_URL) {
const res = await fetch(new URL('/api/robot/config', ROBOT_URL), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(`Driver ${res.status}: ${await res.text()}`);
}
}
/** Pfad zur lokalen Cache-Datei wird an Python-Skripte als -robot-Argument übergeben. */
export const robotCachePath = ROBOT_JSON;
```
---
### Schritt 2 — `server/editRobot.js` anpassen
`readRobot()` und `writeRobot()` sind die einzigen I/O-Primitiven in `editRobot.js`.
Sie müssen auf `fetchRobot()` / `pushRobot()` umgestellt werden.
**Aktuell:**
```javascript
async function readRobot(robotPath) {
return JSON.parse(await fsPromises.readFile(robotPath, 'utf8'));
}
async function writeRobot(robotPath, data) {
await fsPromises.writeFile(robotPath, JSON.stringify(data, null, 2), 'utf8');
}
```
**Neu:**
```javascript
import { fetchRobot, pushRobot } from './robotConfig.js';
async function readRobot(_robotPath) { // _robotPath ignoriert Quelle ist Driver
return fetchRobot();
}
async function writeRobot(_robotPath, data) {
return pushRobot(data);
}
```
Alle exportierten Funktionen (`assignByZRange`, `setArmMarkerSpin`, `adoptXAxis`,
`setJointOriginYZ`, …) bleiben **unverändert** — sie rufen intern `readRobot` /
`writeRobot` auf.
> Der `robotPath`-Parameter bleibt in den Signaturen erhalten (Kompatibilität),
> wird aber ignoriert. Alternativ: alle Aufrufer in `server.js` bereinigen und
> Parameter entfernen (Folgeschritt).
---
### Schritt 3 — `server/server.js` anpassen
#### 3a — Python-Skripte erhalten weiterhin die Cache-Datei
```javascript
// Vorher:
import { ROBOT_JSON } from './config.js'; // oder const direkt
// '-robot', ROBOT_JSON
// Nachher:
import { robotCachePath } from './robotConfig.js';
// '-robot', robotCachePath
```
Die Pipeline (`runBoardPipeline`, `runHoming`) fetcht robot.json **einmal vor dem
Lauf** via `fetchRobot()`, um den Cache zu aktualisieren:
```javascript
import { fetchRobot, robotCachePath } from './robotConfig.js';
async function runBoardPipeline(runDir, send, refSet) {
// Cache aktualisieren bevor Python startet
await fetchRobot();
// Python-Skripte erhalten robotCachePath wie bisher
const script1Args = [..., '-robot', robotCachePath, ...];
// …
}
```
#### 3b — `GET /api/robot` liest via `fetchRobot()`
```javascript
// Vorher:
app.get('/api/robot', async (req, res) => {
const robot = JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8'));
return res.json(robot);
});
// Nachher:
import { fetchRobot } from './robotConfig.js';
app.get('/api/robot', async (req, res) => {
try {
const robot = await fetchRobot();
return res.json(robot);
} catch (err) {
return res.status(502).json({ error: `Driver nicht erreichbar: ${err.message}` });
}
});
```
#### 3c — Kalibrierungs-Endpoints: kein Änderungsbedarf
Da `editRobot.js` intern `readRobot` / `writeRobot` verwendet und diese umgestellt
werden (Schritt 2), propagieren sich alle Kalibrierungs-Schreibvorgänge automatisch
zum Driver. Kein Änderungsbedarf in den einzelnen Endpoints.
---
## Startup-Verhalten
Beim Start von `server.js` einmalig robot.json laden und cachen:
```javascript
// server.js nach HTTPS-Server-Start
try {
await fetchRobot();
console.log('✅ robot.json vom Driver geladen und gecacht.');
} catch (err) {
console.warn(`⚠ Driver nicht erreichbar nutze lokale Datei: ${err.message}`);
}
```
---
## Fallback-Verhalten
| Szenario | Verhalten |
|----------|-----------|
| `ROBOT_URL` nicht gesetzt | Nur lokale Datei — Entwicklungsmodus, Driver nicht nötig |
| Driver beim Start nicht erreichbar | Warnung, lokale Cache-Datei wird verwendet |
| Driver während Lauf nicht erreichbar | `pushRobot()` wirft Fehler → Kalibrierungs-Endpoint antwortet 502 |
| Python-Skript schlägt fehl | Kein push nötig (Python schreibt nicht in robot.json) |
---
## Datei-Übersicht nach Umbau
| Datei | Rolle |
|-------|-------|
| `server/robotConfig.js` *(neu)* | `fetchRobot()`, `pushRobot()`, `robotCachePath` |
| `server/editRobot.js` | `readRobot` / `writeRobot` delegieren an `robotConfig.js` |
| `server/server.js` | importiert `robotCachePath` statt lokalem `ROBOT_JSON`; ruft `fetchRobot()` vor Pipelines |
| `scripts/robot_1781069752019.json` | Bleibt als lokale Cache-Datei; **nicht** mehr primäre Quelle der Wahrheit |
---
## Offene Fragen
- [ ] Genaue Endpoints des appRobotDriver für GET / POST robot.json bestätigen
- [ ] Soll der Driver eine Versions-/Konflikterkennung haben (z.B. ETag / `updatedAt`)?
- [ ] Soll `pushRobot()` bei Driver-Fehler still auf lokal-only zurückfallen, oder hard fail?
- [ ] Authentifizierung zwischen appRobotHoming und appRobotDriver nötig?