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

14 KiB
Raw Blame History

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).


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):

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):

// 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:

// 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:

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:

// 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:

// 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