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

12 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

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, 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 });
}

Route:

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();
});

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)

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