Files
appRobotHoming/doc/Homing_ROADMAP.md
2026-06-14 16:29:01 +02:00

357 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Homing Roadmap appRobotHoming
> Stand: 2026-06-13
> Ziel: Aus einem einzigen Kamera-Snapshot die aktuellen Gelenkwinkel/-positionen
> des Roboters bestimmen und an den Controller senden.
---
## Was ist Homing?
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: schnell, robust, vollautomatisch.
Kalibrierung hingegen läuft nur nach mechanischen Änderungen (≈ einmalig).
---
## Kinematik-Kette (aus `robot.json → links`)
```
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)
```
**Resultat-State:** `{ x_mm, y_deg, z_deg, a_deg, b_deg, c_deg, e_mm }`
---
## Voraussetzungen (Kalibrierung)
| 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 |
> **Kalibrierung gilt als abgeschlossen** sobald der Arm1-Button geklickt und
> die Arm-Marker eingetragen sind.
---
## robot.json Ladestrategie
### Aktuell: lokale Datei
```
ROBOT_JSON = process.env.ROBOT_JSON || 'scripts/robot_1781069752019.json'
```
### Geplant: vom Driver per API
Der Driver-Service (ROBOT_URL) kennt die aktuelle Roboter-Konfiguration.
Lademechanismus (bereits implementiertes Muster aus `ROBOT_URL`/`BODYTRACKER_URL`):
```
GET ROBOT_URL/api/robot/config → robot.json Inhalt
```
**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'));
}
```
**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.
**Priorität:** Kann nach dem restlichen Homing implementiert werden.
Solange ROBOT_URL nicht konfiguriert, läuft alles mit der lokalen Datei.
---
## X-Position (Slider) Bestimmung
Die Slider-Position `x` wird **nicht** manuell eingegeben, sondern aus den
triangulierten Marker-Positionen berechnet (nach Schritt 3b).
**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.
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
```
**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.
---
## Implementierungsplan: Homing-UI
### ⚠ Wichtig: Schritte 13b existieren bereits
Die Kette **Foto → 1_detect → 2_camera → 3b_poses** ist bereits vollständig
implementiert und produktiv in `POST /api/board/run` (`server/server.js`,
ca. Zeile 520606). Sie erzeugt `<runDir>/aruco_marker_poses.json`.
**Die echten, funktionierenden Aufrufe (NICHT neu erfinden):**
```javascript
// Pro Kamera geloopt (jede Kamera hat eigene NPZ):
runScript([SCRIPT_1, '-i', imgPath, '-npz', npzPath, '-robot', ROBOT_JSON,
'-cameraId', camId, '-outDir', runDir, '--saveDebugImage']);
runScript([SCRIPT_2, '-i', detJson, '-robot', ROBOT_JSON, '-outDir', runDir]);
// Einmal, nach allen Kameras (braucht ≥2 _camera_pose.json):
runScript([SCRIPT_3B, '--evalDir', runDir, '--robot', ROBOT_JSON]);
// → schreibt <runDir>/aruco_marker_poses.json
```
**Empfehlung:** Die Snapshot+1+2+3b-Logik aus `/api/board/run` in eine
gemeinsame Funktion `runBoardPipeline(runDir, send)` auslagern. Homing ruft
sie auf und hängt nur den 4b-Teil an. So gibt es keine Duplikate und keine
abweichenden Argumente.
---
### Phase 1 — Backend-Route `POST /api/homing/run`
**Datei:** `server/server.js` (neue Route) + `server/homingOrchestrator.js` (neue Datei)
**Ablauf als SSE-Stream:**
```javascript
// server/homingOrchestrator.js
export async function runHoming({ robotJsonPath, send }) {
// 13b: bestehende Board-Pipeline wiederverwenden
// (Foto + 1_detect + 2_camera + 3b_poses, pro Kamera geloopt)
send({ type: 'step', step: 1, text: 'Snapshot + Marker-Triangulation …' });
const runDir = await runBoardPipeline(robotJsonPath, send); // aus server.js ausgelagert
const arucoJson = path.join(runDir, 'aruco_marker_poses.json');
// 4. X-Position aus triangulierten Markern bestimmen
const xMm = estimateXFromMarkers(arucoJson); // siehe Abschnitt „X-Position"
// 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 = [SCRIPT_4B,
'--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));
const exit = await runScript(args, send);
if (exit !== 0) { send({ type: 'error', text: `4b ${link} Exit ${exit}` }); return; }
fromState = path.join(runDir, `state_${link}.json`);
}
// Ergebnis: letztes state_*.json enthält den vollständigen accumulated_state
const finalState = JSON.parse(fs.readFileSync(fromState, 'utf8'));
send({ type: 'done', state: finalState.accumulated_state, runDir });
}
```
> **Hinweis:** `runScript()` gibt den Exit-Code zurück (nicht den Pfad).
> Der State-Pfad wird separat gemerkt (`fromState`).
**Route:**
```javascript
app.post('/api/homing/run', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
const send = (data) => res.write(`data: ${JSON.stringify(data)}\n\n`);
try {
await runHoming({ robotJsonPath: ROBOT_JSON, send });
} catch (err) {
send({ type: 'error', text: String(err) });
}
res.end();
});
app.post('/api/homing/send-state', async (req, res) => {
// Sendet { x, y, z, a, b, c, e } an ROBOT_URL/api/state
});
```
---
### Phase 2 — Frontend `public/homing.html`
Neue Seite (zugänglich von `index.html` via Link-Button, wie `calibration.html`).
**Sektionen (identisches Muster wie `index.html`):**
```
┌──────────────────────────────────────────────────────┐
│ 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] │
└──────────────────────────────────────────────────────┘
```
**Schlüssel-Implementierungsdetails:**
```javascript
// homing.js (client)
// 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);
}
});
}
// Ergebnis an Roboter senden
async function sendToRobot(state) {
await fetch('/api/homing/send-state', {
method: 'POST',
body: JSON.stringify({ state }),
});
}
```
---
### 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
```
[Jetzt] Arm-Marker eintragen (Nutzer)
→ Arm1 Joint-Origin Button klicken (bereits ausführbar)
[0] Refactor: Snapshot+1+2+3b aus /api/board/run
in runBoardPipeline(runDir, send) auslagern
→ wird von Board-Run UND Homing genutzt
[1] POST /api/homing/run + homingOrchestrator.js
→ runBoardPipeline() + 4b-Schleife als SSE
→ SCRIPT_4B-Konstante in server.js ergänzen
→ 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 | 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 |