Roadmap planen

This commit is contained in:
chk
2026-06-14 16:23:18 +02:00
parent 2e622de801
commit 275ab083fa
13 changed files with 251 additions and 3339 deletions

View File

@@ -8,374 +8,322 @@
## Was ist Homing?
Homing = der Roboter weiss **nicht**, wo er ist.
Homing = der Roboter weiss **nicht**, wo er ist.
Die Kameras schauen auf das Board + die ArUco-Marker am Roboter und berechnen
daraus die vollständige Pose aller Gelenke — ohne mechanische Endschalter.
Homing läuft bei **jedem** Einschalten ab, also muss es schnell und robust sein.
Homing läuft bei **jedem** Einschalten ab: schnell, robust, vollautomatisch.
Kalibrierung hingegen läuft nur nach mechanischen Änderungen (≈ einmalig).
---
## Voraussetzungen (Kalibrierung muss abgeschlossen sein)
## Kinematik-Kette (aus `robot.json → links`)
| Was | Wo gespeichert | Status |
|-----|----------------|--------|
| Kamera-Intrinsik (NPZ) | `data/calibration/camX_calibration.npz` | ✅ |
| Board-Marker-Positionen | `robot.json → links.Board.markers[]` | ✅ |
| X-Achsen-Richtung | `robot.json → links.*.position` (rotiert) | ✅ |
| **Arm1-Gelenkursprung Y/Z** | `robot.json → links.Arm1.jointToParent.origin[1,2]` | 🔶 in Arbeit |
| Arm-Marker-Zuordnung | `robot.json → links.Arm1/Ellbow/Arm2/Hand.markers[]` | ❌ offen |
```
Board (ROOT, fest) ← Referenz aller Kameras
├── Base linear x axis=[1,0,0] ← Slider-Position
├── Arm1 revolute y axis=[-1,0,0] ← Schultergelenk
├── Ellbow revolute z axis=[-1,0,0] ← Ellbogen
├── Arm2 revolute a axis=[0,-1,0] ← Unterarm-Drehung
├── Hand revolute b axis=[1,0,0] ← Handgelenk
├── Palm revolute c axis=[0,-1,0] ← Handfläche
└── FingerA/B linear e axis=±[1,0,0] ← Greifer (symmetrisch)
```
> **Schlüssel-Erkenntnis:** Sobald `Arm1.jointToParent.origin` korrekt gesetzt ist (aus
> der Y-Achsen-Kalibrierung), ist die gesamte Kinematikkette in `robot.json` geometrisch
> definiert. Dann kann Homing starten.
**Resultat-State:** `{ x_mm, y_deg, z_deg, a_deg, b_deg, c_deg, e_mm }`
---
## Kinematik-Kette (aus `robot.json`)
## Voraussetzungen (Kalibrierung)
```
Board (ROOT, fest)
├── Base linear variable=x axis=[1,0,0] origin=[0,0,16]
│ → Slider-Position entlang Board-X
├── Arm1 revolute variable=y axis=[-1,0,0] origin=[110, 108*, 45*]
│ → Schultergelenk (heben/senken) *aus Y-Achsen-Kalib.
├── Ellbow revolute variable=z axis=[-1,0,0] origin=[0,-250,0]
│ → Ellbogen (relativ zu Arm1-Ende)
├── Arm2 revolute variable=a axis=[0,-1,0] origin=[90,0,0]
│ → Unterarm-Drehung
├── Hand revolute variable=b axis=[1,0,0] origin=[0,-250,0]
│ → Handgelenk
├── Palm revolute variable=c axis=[0,-1,0] origin=[0,0,0]
│ → Handflächen-Rotation
└── FingerA/B linear variable=e axis=±[1,0,0] origin=[±4,-35,0]
→ Greifer (symmetrisch)
```
| Was | Mechanismus | Status |
|-----|-------------|--------|
| Kamera-Intrinsik (NPZ) | `calibration.html` → Tab Camera NPZ | ✅ fertig |
| Board-Marker-Positionen | `calibration.html` → Tab Board | ✅ fertig |
| X-Achsen-Richtung | `calibration.html` → Tab Robot X-Axis | ✅ fertig |
| **Arm1 Joint-Origin Y/Z** | `calibration.html` → Tab Arm1 → Button „Joint-Origin Y/Z übernehmen" | ✅ **Button vorhanden, ausführbar** |
| Arm-Marker in robot.json | Manuell eintragen (links.Arm1/Ellbow/Arm2/Hand.markers) | 🔶 Nutzer trägt ein |
**Gelenkvariablen:** `x` (mm), `y z a b c` (Grad), `e` (mm)
> **Kalibrierung gilt als abgeschlossen** sobald der Arm1-Button geklickt und
> die Arm-Marker eingetragen sind.
---
## Homing-Ablauf (Schritt für Schritt)
## robot.json Ladestrategie
### Schritt 1 — Snapshot aufnehmen
**Was:** Alle Kameras gleichzeitig ein Foto.
**Script:** — (direkt via Webcam-API)
**UI-Aktion:** Button `[Foto aufnehmen]`
**Ausgabe:**
- `cam0.jpg`, `cam1.jpg`, `cam2.jpg` im aktuellen Run-Verzeichnis
- **Snapshots-Sektion:** Kamerabilder erscheinen
---
### Schritt 2 — ArUco-Marker erkennen
**Was:** Pixel-Koordinaten aller sichtbaren Marker in jedem Kamerabild.
**Script:** `1_detect_aruco_observations.py`
```bash
python 1_detect_aruco_observations.py \
-i cam0.jpg cam1.jpg cam2.jpg \
-robot robot.json \
-outDir data/homing/TIMESTAMP/
### Aktuell: lokale Datei
```
ROBOT_JSON = process.env.ROBOT_JSON || 'scripts/robot_1781069752019.json'
```
**Output-Dateien:**
- `cam0_aruco_detection.json` — Marker-IDs + Pixel-Ecken + Kamera-Pose-Kandidaten
### Geplant: vom Driver per API
Der Driver-Service (ROBOT_URL) kennt die aktuelle Roboter-Konfiguration.
Lademechanismus (bereits implementiertes Muster aus `ROBOT_URL`/`BODYTRACKER_URL`):
**Snapshot CSV:** Tabelle: MarkerID | Kamera | px-Koordinaten | confidence
**Fehlerfälle:**
- Marker verdeckt → weniger Marker in CSV sichtbar
- Schlechte Beleuchtung → confidence niedrig
---
### Schritt 3 — Kamera-Posen schätzen
**Was:** Aus den Board-Markern (fix im Raum) die extrinsische Lage jeder Kamera
berechnen.
**Script:** `2_estimate_camera_from_observations.py`
```bash
python 2_estimate_camera_from_observations.py \
-i data/homing/TIMESTAMP/ \
-robot robot.json \
-outDir data/homing/TIMESTAMP/
```
GET ROBOT_URL/api/robot/config → robot.json Inhalt
```
**Output-Dateien:**
- `cam0_camera_pose.json` — 4×4 Transformationsmatrix Kamera→Welt
**Analysis & Reasoning:** Reprojektions-Fehler pro Kamera (sollte < 3 px)
**Fehlerfälle:**
- Zu wenig Board-Marker sichtbar (< 3) → Kamera-Pose nicht berechenbar
- Hoher Reprojektions-Fehler → Kalibrierung möglicherweise veraltet
---
### Schritt 3b — 3D-Marker-Positionen triangulieren
**Was:** Aus den Kamera-Posen und den Pixel-Koordinaten die echten 3D-Positionen
aller Marker berechnen (inkl. Arm-Marker).
**Script:** `3b_corner_marker_poses.py`
```bash
python 3b_corner_marker_poses.py \
-i data/homing/TIMESTAMP/ \
-robot robot.json \
--outDir data/homing/TIMESTAMP/
```
**Output-Dateien:**
- `aruco_marker_poses.json` — für jeden Marker: `position_mm`, `normal`, `corners_m`, `link`
**Snapshot CSV:** Vollständige Marker-Tabelle mit triangulierten 3D-Positionen
**Fehlerfälle:**
- Marker nur in 1 Kamera → keine Triangulation möglich (braucht ≥ 2)
---
### Schritt 4 — Gelenkwinkel bestimmen
Zwei Methoden — **4b** ist sequenziell und robuster:
#### Methode A — Vollständige State-Schätzung (schnell)
**Script:** `4_robotState_estimation_v6.py`
```bash
python 4_robotState_estimation_v6.py \
aruco_marker_poses.json \
--robot robot.json \
--output homing_state.json
```
Schätzt alle Gelenke in einem Durchlauf mit geometrischen Gleichungen
(Kabsch-Fit, Projektions-Residuen, 2D-Winkel).
#### Methode B — Sequenziell Gelenk für Gelenk (robuster)
**Script:** `4b_revolute_angle.py` — einmal pro Gelenk, von root nach tip:
```bash
# Slider-Position (x) aus Board-Markern bekannt → manuell oder aus 4a
python 4b_revolute_angle.py --robot robot.json --aruco aruco_marker_poses.json \
--link Arm1 --x-mm 180 --output state_arm1.json
python 4b_revolute_angle.py --robot robot.json --aruco aruco_marker_poses.json \
--link Ellbow --from-state state_arm1.json --output state_ellbow.json
python 4b_revolute_angle.py --robot robot.json --aruco aruco_marker_poses.json \
--link Arm2 --from-state state_ellbow.json --output state_arm2.json
python 4b_revolute_angle.py --robot robot.json --aruco aruco_marker_poses.json \
--link Hand --from-state state_arm2.json --output state_hand.json
```
Jedes `--from-state` enthält den akkumulierten Gelenk-State aus allen
vorherigen Schritten.
**Warum sequenziell?** Arm2-Winkel kann nur korrekt berechnet werden wenn
Arm1 und Ellbow bereits bekannt sind (der Weltachsenvektor des Arm2-Joints
ändert sich mit den vorherigen Gelenkwinkeln).
**Output-JSON-Struktur (pro Schritt):**
```json
{
"link": "Arm1",
"joint": "y",
"mean_angle_deg": 23.4,
"circular_std_deg": 0.8,
"num_pairs": 6,
"joint_origin_world_mm": [290, 108, 45],
"joint_axis_world": [-1, 0, 0],
"accumulated_state": { "x": 180, "y": 23.4, "z": null, "a": null, ... }
**Implementierung (Backend `server.js`):**
```javascript
async function loadRobotConfig() {
if (ROBOT_URL) {
// Vom Driver holen
const res = await fetch(new URL('/api/robot/config', ROBOT_URL));
return res.json();
}
// Fallback: lokale Datei
return JSON.parse(await fs.readFile(ROBOT_JSON, 'utf8'));
}
```
**Analysis & Reasoning:** Gelenk-für-Gelenk-Ergebnis als JSON-Baum
**Konsequenz für Homing:** Das Homing-Script bekommt robot.json als
temporäre Datei (bereits vorhandenes Muster: `ROBOT_JSON` als Pfad an Python).
Falls ROBOT_URL konfiguriert: zuerst fetch → temp-Datei schreiben → Script aufrufen.
**Fehlerfälle:**
- `circular_std_deg` > 5° → Marker-Konfiguration fragwürdig, Messung wiederholen
- `num_pairs` < 2 → Arm-Marker nicht genug sichtbar
**Priorität:** Kann nach dem restlichen Homing implementiert werden.
Solange ROBOT_URL nicht konfiguriert, läuft alles mit der lokalen Datei.
---
### Schritt 5 (optional) — Kamera-Z verfeinern
## X-Position (Slider) Bestimmung
**Was:** Kamera-Höhe (Z) ist aus Board-Markern schlecht bestimmt (Board ist flach).
Wenn Ellbow-Winkel bekannt → FK-Vorhersage als Z-Referenz nutzen.
Die Slider-Position `x` wird **nicht** manuell eingegeben, sondern aus den
triangulierten Marker-Positionen berechnet (nach Schritt 3b).
**Script:** `5_camera_z_refine.py`
**Ansatz:** Die absolute X-Position eines bekannten Arm-Markers im
Board-Koordinatensystem enthält direkt die Slider-Information —
alle anderen Gelenke sind rotatorisch und verschieben den Marker nicht
entlang der X-Achse des Boards.
```bash
python 5_camera_z_refine.py \
--angle state_ellbow.json \
--robot robot.json \
--aruco aruco_marker_poses.json \
-pose cam0_camera_pose.json \
-pose cam1_camera_pose.json \
--outDir data/homing/TIMESTAMP/
Alternativ: Schwerpunkt der Board-nahen A0-Marker projiziert auf die X-Achse
(robust, braucht keine Arm-Marker).
**In `4b_revolute_angle.py`:** `--x-mm` wird aus `aruco_marker_poses.json`
berechnet und als erstes Argument übergeben. Alle weiteren 4b-Aufrufe
nutzen `--from-state` des vorherigen Schritts.
---
## Homing-Ablauf (Script-Kette)
```
[Foto] → [1_detect] → [2_camera] → [3b_poses]
[4b Arm1]
[4b Ellbow]
[4b Arm2]
[4b Hand]
State-JSON
{x,y,z,a,b,c,e}
POST ROBOT_URL/api/state
```
**Output:** Korrigierte `*_camera_pose_v8.json` + `z_correction.json`
**Wann nötig:** Wenn Residuen in Schritt 4 systematisch in Z-Richtung driften.
**Strategie:** `4b_revolute_angle.py` sequenziell, Link für Link von root nach tip.
X-Position (`--x-mm`) wird aus den triangulierten Board-Marker-Positionen bestimmt.
---
### Schritt 6 — Ergebnis senden
## Implementierungsplan: Homing-UI
**Was:** Vollständigen Joint-State an Roboter-Controller übermitteln.
### Phase 1 — Backend-Route `POST /api/homing/run`
**UI-Aktion:** Button `[An Roboter senden]`
**Datei:** `server/server.js` (neue Route) + `server/homingOrchestrator.js` (neue Datei)
**Body:** `{ x, y, z, a, b, c, e }`
`POST ROBOT_URL/api/state` (oder Motion-Controller-Protokoll)
**Ablauf als SSE-Stream:**
**Result Raw JSON:**
```json
{
"status": "ok",
"timestamp": "2026-06-13T19:00:00",
"state": { "x": 180.0, "y": 23.4, "z": -12.1, "a": 5.0, "b": 0.0, "c": 0.0, "e": 0.0 },
"confidence": { "y_std_deg": 0.8, "z_std_deg": 1.2 }
```javascript
// server/homingOrchestrator.js
export async function runHoming({ robotJsonPath, boardDataDir, send, options }) {
// 1. Snapshot (Webcam-API)
send({ type: 'step', step: 1, text: 'Foto aufnehmen …' });
const runDir = await takeSnapshot(); // WEBCAM_URL
// 2. Marker erkennen
send({ type: 'step', step: 2, text: 'Marker erkennen …' });
await runScript(['1_detect_aruco_observations.py',
'-i', ...camImages, '-robot', robotJsonPath, '-outDir', runDir], send);
// 3. Kamera-Posen
send({ type: 'step', step: 3, text: 'Kamera-Posen schätzen …' });
await runScript(['2_estimate_camera_from_observations.py',
'-i', runDir, '-robot', robotJsonPath, '-outDir', runDir], send);
// 3b. 3D-Triangulation
send({ type: 'step', step: 4, text: '3D-Positionen triangulieren …' });
await runScript(['3b_corner_marker_poses.py',
'-i', runDir, '-robot', robotJsonPath, '--outDir', runDir], send);
// 4. X-Position aus triangulierten Board-Markern bestimmen
const xMm = estimateXFromMarkers(arucoJson); // aus position_mm der A0-Marker
// 4b. Gelenkwinkel sequenziell, Link für Link
const links = ['Arm1', 'Ellbow', 'Arm2', 'Hand'];
let fromState = null;
for (const link of links) {
send({ type: 'step', text: `Gelenkwinkel ${link}` });
const args = ['4b_revolute_angle.py',
'--robot', robotJsonPath, '--aruco', arucoJson,
'--link', link,
'--output', path.join(runDir, `state_${link}.json`),
];
if (fromState) args.push('--from-state', fromState);
else args.push('--x-mm', String(xMm));
fromState = await runScript(args, send);
}
// Ergebnis
const finalState = JSON.parse(fs.readFileSync(fromState));
send({ type: 'done', state: finalState.accumulated_state, runDir });
}
```
**Result Tree View:** Gelenk-Werte als lesbare Tabelle
**Route:**
```javascript
app.post('/api/homing/run', async (req, res) => {
// SSE-Header
res.setHeader('Content-Type', 'text/event-stream');
const send = (data) => res.write(`data: ${JSON.stringify(data)}\n\n`);
await runHoming({ robotJsonPath: ROBOT_JSON, boardDataDir, send });
res.end();
});
---
## UI-Seitenstruktur (analog index.html)
```
┌─────────────────────────────────────────┐
│ AKTIONEN │
│ [Foto aufnehmen] [Homing berechnen] │
│ [An Roboter senden] │
├─────────────────────────────────────────┤
│ AUSGABE / LOG │
│ Schritt-für-Schritt Log aller Scripts │
├─────────────────────────────────────────┤
│ ANALYSIS & REASONING │
│ Kamera-Posen, Reprojektionsfehler, │
│ Gelenk-Schätzungen je Schritt als JSON │
├──────────────────┬──────────────────────┤
│ RESULT RAW JSON │ RESULT TREE │
│ Vollst. State-JSON │ Gelenk-Tabelle │
├─────────────────────────────────────────┤
│ SNAPSHOT CSV │
│ Tabelle aller Marker: ID, Position, │
│ Link, Residual, num_cameras │
├─────────────────────────────────────────┤
│ SNAPSHOTS (Bilder) │
│ Annotierte Kamerabilder mit Markern │
└─────────────────────────────────────────┘
app.post('/api/homing/send-state', async (req, res) => {
// Sendet { x, y, z, a, b, c, e } an ROBOT_URL/api/state
});
```
---
## Offene Punkte (Voraussetzungen für Homing)
### Phase 2 — Frontend `public/homing.html`
### 🔶 A — Arm-Marker in robot.json eintragen
Neue Seite (zugänglich von `index.html` via Link-Button, wie `calibration.html`).
Die Arm-Links haben noch **keine Marker** in `robot.json` (links.Arm1/Ellbow/Arm2/Hand.markers).
**Sektionen (identisches Muster wie `index.html`):**
Mögliche Quellen:
- Aus der Y-Achsen-Kalibrierung: Marker 197, 218, 219 wurden als rotierend erkannt
→ diese können mit relativen Positionen in den jeweiligen Links eingetragen werden
- Mit `4b_revolute_angle.py` lässt sich pro Link prüfen, welche Marker
"zu diesem Link gehören" (paarweise Baseline-Methode)
```
┌──────────────────────────────────────────────────────┐
AKTIONEN │
│ [📷 Foto & Homing berechnen] │
│ [✅ An Roboter senden] (disabled bis Ergebnis) │
│ Status-Badge: ○ Warte ● Läuft ✓ Fertig ✗ Fehler │
├──────────────────────────────────────────────────────┤
│ AUSGABE / LOG │
│ Schritt-für-Schritt Log aller Scripts (SSE-Stream) │
│ Fortschritt: ──── Schritt 3/6 ──── │
├──────────────────────────────────────────────────────┤
│ ANALYSIS & REASONING │
│ Zwischenergebnisse je Script als JSON │
│ { camera_reprojection_px, arm1_std_deg, … } │
├──────────────────┬───────────────────────────────────┤
│ RESULT RAW JSON │ RESULT TREE VIEW │
│ { │ x (Slider): 180.0 mm │
│ "x": 180.0, │ y (Arm1): +23.4° │
│ "y": 23.4, │ z (Ellbow): -12.1° │
│ ... │ a (Arm2): +5.0° │
│ } │ b (Hand): 0.0° │
│ │ c (Palm): 0.0° │
│ │ e (Greifer): 0.0 mm │
├──────────────────┴───────────────────────────────────┤
│ SNAPSHOT CSV (Marker-Tabelle) │
│ ID │ Link │ x mm │ y mm │ z mm │ Residual │ │
│ 218 │ Arm2 │ 229.1 │ 118.5 │ 48.3 │ 2.1 mm │ │
├──────────────────────────────────────────────────────┤
│ SNAPSHOTS (annotierte Kamerabilder) │
│ [cam0] [cam1] [cam2] │
└──────────────────────────────────────────────────────┘
```
**Aktion:** Für jedes Arm-Segment mind. 2 Marker bestimmen und in robot.json eintragen.
**Schlüssel-Implementierungsdetails:**
### 🔶 B — Arm1.jointToParent.origin[Y, Z] finalisieren
```javascript
// homing.js (client)
Aus der Arm1-Y-Achsen-Kalibrierung (calibration_arm.html):
- Berechneter Wert: Y ≈ 115.6 mm, Z ≈ 50.3 mm
- Aktueller Wert in robot.json: origin = [110, 108, 45]
- **Aktion:** Button „Joint-Origin Y/Z übernehmen" klicken → schreibt in robot.json
// SSE-Stream vom Backend empfangen
async function runHoming() {
const response = await fetch('/api/homing/run', { method: 'POST' });
await readSseStream(response, appendLog, (evt) => {
if (evt.type === 'step') { updateProgress(evt); }
if (evt.type === 'analysis') { showAnalysis(evt.data); }
if (evt.type === 'done') {
showResult(evt.state);
enableSendButton(evt.state);
}
});
}
### 🔶 C — X-Position (Slider) im Homing
Die Slider-Position `x` ist für `4b_revolute_angle.py --x-mm` nötig.
Optionen:
1. Mechanischer Anschlag / Encoder → immer bekannt
2. Aus Board-Markern schätzen (funktioniert wenn A0-Marker sichtbar)
3. Manuell eingeben (Fallback)
### 🔶 D — Homing-Seite implementieren
Eine neue `homing.html`-Seite (oder Tab in `calibration.html`) die:
- Die Script-Kette 1 → 2 → 3b → 4b(je Link) → (5) als SSE-Stream orchestriert
- Alle Ausgabe-Sektionen wie in index.html befüllt
- „An Roboter senden" Button mit finalem State-JSON verdrahtet
// Ergebnis an Roboter senden
async function sendToRobot(state) {
await fetch('/api/homing/send-state', {
method: 'POST',
body: JSON.stringify({ state }),
});
}
```
---
## Script-Abhängigkeitsgraph
### Phase 3 — robot.json via Driver-API
**Voraussetzung:** ROBOT_URL ist konfiguriert und der Driver hat `GET /api/robot/config`.
**Implementierung in `server.js`:**
```javascript
// Beim Start oder on-demand: robot.json vom Driver laden
async function fetchRobotConfig() {
if (!ROBOT_URL) return; // lokale Datei reicht
const res = await fetch(new URL('/api/robot/config', ROBOT_URL));
if (!res.ok) return; // Fallback auf lokale Datei
const data = await res.json();
// Temporär in data/robot/robot_live.json cachen
await fs.writeFile(ROBOT_JSON_LIVE, JSON.stringify(data, null, 2));
}
```
**Auswirkung:** Nur `ROBOT_JSON` Variable ändern — alle Scripts bekommen
automatisch die aktuelle Konfiguration.
---
## Reihenfolge der Implementierung
```
Kamera-Snapshot
[1] detect_aruco → cam*_aruco_detection.json
[2] estimate_camera → cam*_camera_pose.json
│ (aus Board-Markern)
[3b] corner_marker_poses → aruco_marker_poses.json
│ (alle Marker trianguliert)
├──────────────────────────────────────┐
▼ ▼
[4b] revolute Arm1 [4] state_v6 (alternativ)
[4b] revolute Ellbow ──► [5] camera_z_refine (optional, nutzt Ellbow)
[4b] revolute Arm2
[4b] revolute Hand
State-JSON {x, y, z, a, b, c, e}
→ Roboter-Controller
[Jetzt] Arm-Marker eintragen (Nutzer)
→ Arm1 Joint-Origin Button klicken (bereits ausführbar)
[1] POST /api/homing/run + homingOrchestrator.js
→ Script-Kette als SSE orchestrieren
→ Minimale UI: nur Log + Raw JSON
[2] public/homing.html
→ Vollständige UI mit allen Sektionen
→ Link von index.html
[3] POST /api/homing/send-state
→ ROBOT_URL/api/state aufrufen (analog zu robotActions.js)
[4] robot.json via Driver-API (wenn ROBOT_URL verfügbar)
→ Nur wenn Driver den Endpunkt implementiert
```
---
## Status-Tabelle
| Schritt | Script | Status | Blockiert durch |
|---------|--------|--------|-----------------|
| 1 Snapshot | Webcam-API | ✅ vorhanden | — |
| 2 Marker erkennen | `1_detect_aruco.py` | ✅ vorhanden | — |
| 3 Kamera-Pose | `2_estimate_camera.py` | ✅ vorhanden | — |
| 3b Triangulation | `3b_corner_marker_poses.py` | ✅ vorhanden | — |
| 4 State-Schätzung | `4_robotState_estimation_v6.py` | ✅ vorhanden | Arm-Marker fehlen |
| 4b Sequenziell | `4b_revolute_angle.py` | ✅ vorhanden | Arm-Marker fehlen |
| 5 Z-Verfeinern | `5_camera_z_refine.py` | ✅ vorhanden | Ellbow-Marker fehlen |
| **Arm1-Origin** | calibration_arm.html | 🔶 bereit | Joint-Origin Y/Z übernehmen |
| **Arm-Marker** | robot.json | ❌ fehlen | manuelle Zuordnung |
| **Homing-UI** | homing.html | ❌ fehlt | erst nach Markern sinnvoll |
| Schritt | Was | Status |
|---------|-----|--------|
| Scripts 1, 2, 3b, 4b | Homing-Scripts | ✅ vorhanden |
| Kalibrierung | Kamera, Board, X-Achse | ✅ fertig |
| Arm1 Joint-Origin | Button in calibration_arm.html | ✅ **ausführbar** |
| Arm-Marker | robot.json links.Arm1/… .markers | 🔶 Nutzer trägt ein |
| `/api/homing/run` | Backend-Orchestrierung | ❌ zu implementieren |
| `homing.html` | Frontend-UI | ❌ zu implementieren |
| `/api/homing/send-state` | State an Roboter | ❌ zu implementieren |
| robot.json via API | Driver-Integration | ⏳ nach allem anderen |