# 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 1–3b 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 520–606). Sie erzeugt `/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 /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 }) { // 1–3b: 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 |