diff --git a/doc/04_y_achse.md b/doc/04_y_achse.md deleted file mode 100644 index 68d9bf6..0000000 --- a/doc/04_y_achse.md +++ /dev/null @@ -1,127 +0,0 @@ -# Bestimmung der Y-Rotationsachse aus Marker-Positionen - -## Problem - -Wenn der Roboter um seine Y-Achse rotiert, bewegt sich jeder erkannte Marker auf einem **Kreisbogen** im 3D-Raum. Ziel ist es, Lage und Richtung dieser Rotationsachse aus den beobachteten Marker-Positionen (x, y, z) zu mehreren Zeitstempeln zu berechnen. - ---- - -## Reichen zwei Positionen? - -**Nein.** Zwei Punkte P₁, P₂ desselben Markers definieren nur eine Strecke. Die Rotationsachse muss durch die senkrechte Mittelebene dieser Strecke laufen – aber *wo* genau in dieser Ebene und in welcher Richtung bleibt unbestimmt. Das System ist unterbestimmt. - -| Beobachtungen | Bestimmbar? | Begründung | -|---|---|---| -| 1 Marker, 2 Positionen | Nein | Achse liegt irgendwo auf einer Halbebene | -| 1 Marker, 3 Positionen | **Ja** | Eindeutiger Umkreis → eindeutige Achse | -| N Marker, je ≥ 3 Positionen | Ja, überbestimmt | Least-Squares, robuster gegenüber Messrauschen | - ---- - -## Mathematik: 1 Marker, 3 Positionen - -Gegeben: P₁, P₂, P₃ ∈ ℝ³ (derselbe Marker zu drei verschiedenen Rotationswinkeln). - -Da die Rotation starr ist, liegen P₁, P₂, P₃ auf einem **Kreis**, dessen Ebene senkrecht zur Rotationsachse steht. - -### Schritt 1 – Achsenrichtung (Normalenvektor der Kreisebene) - -``` -v₁ = P₂ − P₁ -v₂ = P₃ − P₁ -n = (v₁ × v₂) / |v₁ × v₂| ← Einheitsvektor entlang der Rotationsachse -``` - -### Schritt 2 – Mittelpunkt des Kreises (Umkreismittelpunkt) - -Der Umkreismittelpunkt C liegt in der Ebene der drei Punkte und ist von allen drei Punkten gleich weit entfernt. Er ist ein Punkt **auf der Rotationsachse**. - -Berechnung über Baryzentrischen Koordinaten: - -``` -a² = |P₂ − P₃|² -b² = |P₁ − P₃|² -c² = |P₁ − P₂|² - -w₁ = a²·(b² + c² − a²) -w₂ = b²·(a² + c² − b²) -w₃ = c²·(a² + b² − c²) - -C = (w₁·P₁ + w₂·P₂ + w₃·P₃) / (w₁ + w₂ + w₃) -``` - -### Schritt 3 – Rotationsachse - -``` -Achse: r(t) = C + t·n, t ∈ ℝ -``` - ---- - -## Mathematik: N Marker, je ≥ 3 Positionen (Least Squares) - -Jeder Marker i liefert: -- einen Schätzwert **Cᵢ** (Umkreismittelpunkt, Punkt auf der Achse) -- einen Schätzwert **nᵢ** (Normalenvektor, Achsenrichtung) - -### Achsenrichtung (gemittelt / PCA) - -Wegen möglichem Vorzeichenambiguität alle nᵢ auf dasselbe Halbraum ausrichten, dann: - -``` -n̄ = mean(nᵢ) / |mean(nᵢ)| -``` - -Robuster: **PCA** über die Matrix der nᵢ-Vektoren → erster Hauptkomponentenvektor. - -### Achsenposition (Least Squares) - -Minimiere die quadratischen Abstände aller Umkreismittelpunkte Cᵢ zur gesuchten Geraden `r(t) = A + t·n̄`: - -``` -Abstand² von Cᵢ zur Achse = |Cᵢ − A|² − ((Cᵢ − A)·n̄)² -``` - -Ableitung null setzen liefert: - -``` -A = C̄ − (C̄·n̄)·n̄ + (C̄·n̄)·n̄ = C̄ (Referenzpunkt = Schwerpunkt der Cᵢ) -``` - -d. h., der **Schwerpunkt der Umkreismittelpunkte** ist der optimale Referenzpunkt auf der Achse. Die Achsenrichtung n̄ bleibt unverändert. - -**Verbleibender Fehler** (Residuum pro Marker): - -``` -εᵢ = |(Cᵢ − C̄) − ((Cᵢ − C̄)·n̄)·n̄| -``` - -Große εᵢ deuten auf einen fehlerhaften Marker oder eine nicht-rotatorische Bewegungskomponente hin. - ---- - -## Praktische Empfehlung - -- **Mindestens 3 Zeitstempel** pro Marker, verteilt über einen Winkelbereich von ≥ 30° (sonst ist der Kreis numerisch schlecht konditioniert). -- **3–4 Marker** mit je 3 Positionen sind ausreichend für eine stabile Schätzung. -- Die Rotationswinkel müssen nicht bekannt sein – nur die 3D-Koordinaten der Marker. -- Bei sehr kleinen Winkeln (< 10°) ist die Bestimmung numerisch instabil; größere Drehungen bevorzugen. - ---- - -## Zusammenfassung - -``` -Eingabe: Marker-Positionen P[marker][timestamp] ∈ ℝ³ - (mind. 3 Timestamps pro Marker, mind. 1 Marker) - -Pro Marker: - 1. n_i = normalize((P2-P1) × (P3-P1)) ← Achsenrichtung - 2. C_i = Umkreismittelpunkt(P1, P2, P3) ← Punkt auf Achse - -Kombination: - 3. n̄ = normalize(mean(n_i)) ← beste Achsenrichtung - 4. C̄ = mean(C_i) ← bester Referenzpunkt - -Ergebnis: Rotationsachse r(t) = C̄ + t·n̄ -``` diff --git a/doc/Callibration_ROADMAP.md b/doc/Callibration_ROADMAP.md deleted file mode 100644 index 685b8f0..0000000 --- a/doc/Callibration_ROADMAP.md +++ /dev/null @@ -1,123 +0,0 @@ -# Calibration Roadmap – appRobotHoming - -> Stand: 2026-06-10 -> Ziel: Vor dem Homing muss das System einmalig (oder nach mechanischen Änderungen) kalibriert werden. - ---- - -## Übersicht der Kalibrierungsschritte - -``` -[1] Camera NPZ → [2] Board → [3] Robot X-Axis → [4] Arm1 / Arm2 -``` - -Jede Stufe baut auf der vorherigen auf. Die Ergebnisse werden als Dateien gespeichert und vom Homing-Prozess geladen. - ---- - -## [1] Camera NPZ – Kamerakalibrierung - -**Ziel:** Intrinsische Kameraparameter (Brennweite, Verzerrungskoeffizienten, Kameramatrix) für jede Kamera ermitteln und als `.npz`-Datei speichern. - -**Vorgehen (noch offen):** -- Schachbrettmuster / ChArUco-Board aus verschiedenen Winkeln fotografieren -- OpenCV `calibrateCamera()` ausführen -- Ergebnis speichern: `cam0_calib.npz`, `cam1_calib.npz`, `cam2_calib.npz` - -**Aktionen auf der Seite (geplant):** -- Fotos aufnehmen (mehrere Posen) -- Kalibrierung berechnen -- `.npz`-Datei herunterladen / auf Server speichern -- Reprojektionsfehler anzeigen - -**Offene Fragen:** -- Welches Muster wird verwendet (Schachbrett vs. ChArUco)? -- Automatische Erfassung oder manueller Upload der Bilder? - ---- - -## [2] Board – Referenz-Board-Kalibrierung - -**Ziel:** Die extrinsische Position des Marker-Boards im Kamera-Koordinatensystem bestimmen. - -**Vorgehen (noch offen):** -- Board in definierter Position aufstellen -- Foto aufnehmen, Marker erkennen (`aruco.detectMarkers`) -- `solvePnP` → Rotations- und Translationsvektor berechnen -- Transformation Board → Kamera speichern - -**Aktionen auf der Seite (geplant):** -- Kamerabild anzeigen mit erkannten Markern -- Pose berechnen und anzeigen -- Kalibrierungsdatei speichern - -**Offene Fragen:** -- Feste Board-Position oder Referenzpunkte einmessen? - ---- - -## [3] Robot X-Axis – Achsenrichtung kalibrieren - -**Ziel:** Die X-Achse des Roboters im Weltkoordinatensystem verorten (Ausrichtung und Nullpunkt). - -**Vorgehen (noch offen):** -- Roboter an bekannte X-Positionen fahren (z. B. X=0 und X=max) -- Kamera beobachtet den Endeffektor / Marker am Roboter -- Achsvektor aus zwei Messpunkten berechnen -- Ergebnis speichern - -**Aktionen auf der Seite (geplant):** -- Roboter zu Referenzposition 1 fahren → Foto → Marker-Position merken -- Roboter zu Referenzposition 2 fahren → Foto → Marker-Position merken -- Achsvektor berechnen und anzeigen -- Speichern - -**Offene Fragen:** -- Wie viele Referenzpunkte werden benötigt? -- Wird die Z-Achse separat kalibriert? - ---- - -## [4] Arm1 / Arm2 – Gelenk-Kalibrierung - -**Ziel:** Nullposition und Kinematikparameter von Arm1 und Arm2 einmessen. - -**Vorgehen (noch offen):** -- Arm in mechanische Nullposition fahren (physischer Anschlag oder Markierung) -- Kamera prüft die tatsächliche Arm-Pose -- Offset zwischen Soll und Ist berechnen und speichern - -**Aktionen auf der Seite (geplant):** -- Arm1 auf Nullposition → Foto → Winkel ablesen -- Arm2 auf Nullposition → Foto → Winkel ablesen -- Offset-Korrektur berechnen und speichern - -**Offene Fragen:** -- Separater Marker pro Armgelenk? -- Kalibrierung bei jedem Start oder nur nach Umbau? - ---- - -## Dateistruktur (geplant) - -``` -calibration/ - cam0_calib.npz - cam1_calib.npz - cam2_calib.npz - board_pose.json - robot_xaxis.json - arm_offsets.json -``` - ---- - -## Status - -| Schritt | Status | Anmerkung | -|------------------|--------------|----------------------------------| -| Camera NPZ | offen | Konzept unklar | -| Board | offen | Konzept unklar | -| Robot X-Axis | offen | Konzept unklar | -| Arm1 / Arm2 | offen | Konzept unklar | -| Calibration-UI | in Arbeit | HTML-Seite angelegt | diff --git a/doc/Homing_ROADMAP.md b/doc/Homing_ROADMAP.md index 645f795..0efd70b 100644 --- a/doc/Homing_ROADMAP.md +++ b/doc/Homing_ROADMAP.md @@ -1,23 +1,22 @@ -# Homing Roadmap – appRobotHoming +# Homing – appRobotHoming -> Stand: 2026-06-13 -> Ziel: Aus einem einzigen Kamera-Snapshot die aktuellen Gelenkwinkel/-positionen -> des Roboters bestimmen und an den Controller senden. +> Stand: 2026-06-14 +> Homing läuft bei **jedem Einschalten** — schnell, vollautomatisch, ohne mechanische Endschalter. --- ## 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. +Der Roboter weiss beim Einschalten nicht, wo er steht. +Die Kameras schauen auf die ArUco-Marker am Roboter und berechnen daraus +die vollständige Pose aller Gelenke. -Homing läuft bei **jedem** Einschalten ab: schnell, robust, vollautomatisch. -Kalibrierung hingegen läuft nur nach mechanischen Änderungen (≈ einmalig). +**Homing** (dieser Prozess): bei jedem Einschalten, automatisch. +**Kalibrierung** (`doc/Kalibrierung.md`): nur nach mechanischen Änderungen. --- -## Kinematik-Kette (aus `robot.json → links`) +## Kinematik-Kette ``` Board (ROOT, fest) ← Referenz aller Kameras @@ -31,376 +30,121 @@ Board (ROOT, fest) ← Referenz aller Kameras └── 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 }` +**Ergebnis-State:** `{ x_mm, y_deg, z_deg, a_deg, b_deg, c_deg, e_mm }` --- -## Voraussetzungen (Kalibrierung) +## Voraussetzungen -| 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 | +Homing setzt eine abgeschlossene Kalibrierung voraus: -> **Kalibrierung gilt als abgeschlossen** sobald der Arm1-Button geklickt und -> die Arm-Marker eingetragen sind. +| Was | Status | +|-----|--------| +| Kamera-Intrinsik (NPZ) | ✅ | +| Board-Marker-Positionen | ✅ | +| X-Achsen-Richtung | ✅ | +| Arm1 Joint-Origin Y/Z | ✅ Button vorhanden und ausführbar | +| Arm-Marker in robot.json | 🔶 Nutzer trägt ein (`links.Arm1/Ellbow/Arm2/Hand.markers`) | --- -## robot.json – Ladestrategie +## Ablauf -### Aktuell: lokale Datei ``` +Foto alle Kameras + │ + ▼ +1_detect_aruco_observations.py (pro Kamera, mit NPZ) + │ + ▼ +2_estimate_camera_from_observations.py (pro Kamera) + │ + ▼ +3b_corner_marker_poses.py (einmal, benötigt ≥2 Kamera-Posen) + │ → aruco_marker_poses.json + ▼ +X-Position aus Marker-Positionen schätzen + │ → x_mm (Durchschnitt x der Nicht-Board-Marker) + ▼ +4b_revolute_angle.py --link Arm1 --x-mm {x_mm} + │ → state_Arm1.json (accumulated_state) + ▼ +4b_revolute_angle.py --link Ellbow --from-state state_Arm1.json + │ → state_Ellbow.json + ▼ +4b_revolute_angle.py --link Arm2 --from-state state_Ellbow.json + │ → state_Arm2.json + ▼ +4b_revolute_angle.py --link Hand --from-state state_Arm2.json + │ → state_Hand.json ← accumulated_state enthält x,y,z,a,b,c,e + ▼ +POST ROBOT_URL/api/state +``` + +**Schritte 1–3b** sind dieselbe Board-Pipeline wie in der Kalibrierung. +Sie sind in `runBoardPipeline()` (`server/server.js`) als gemeinsame Funktion ausgelagert. + +**4b-Schleife**: sequenziell von root nach tip; jedes Script bekommt den Zustand des +vorherigen Schritts über `--from-state`. Der erste Aufruf erhält die geschätzte +X-Slider-Position über `--x-mm`. + +--- + +## Implementierung + +| Komponente | Datei | Beschreibung | +|-----------|-------|--------------| +| Board-Pipeline | `server/server.js` → `runBoardPipeline(runDir, send)` | Foto + Scripts 1, 2, 3b; von Board-Run und Homing genutzt | +| X-Schätzung | `server/homingOrchestrator.js` → `estimateXFromMarkers()` | Mittelwert x der Nicht-Board-Marker aus `aruco_marker_poses.json` | +| Homing-Orchestrator | `server/homingOrchestrator.js` → `runHoming()` | Kompletter Ablauf als SSE-Stream | +| Backend-Route | `POST /api/homing/run` | SSE-Stream, startet `runHoming()` | +| State senden | `POST /api/homing/send-state` | Weiterleitung an `ROBOT_URL/api/state` | +| Run-Daten | `GET /api/homing/run-data?run=ts` | Debug-Bilder (base64) + finalState | +| Frontend | `public/index.html` + `public/client.js` | Homing-Buttons, Fortschrittsbalken, Tree View | + +**Lauf-Verzeichnisse:** `data/homing/{timestamp}/` + +--- + +## SSE-Event-Typen + +Das Backend streamt während des Homing-Laufs folgende Events: + +| `type` | Felder | Bedeutung | +|--------|--------|-----------| +| `log` | `text` | Zeile aus Script-Ausgabe | +| `step` | `step`, `total`, `text` | Fortschritt (1–6) | +| `analysis` | `key`, `value` | Zwischenergebnis (x_mm, state_Arm1, …) | +| `error` | `text` | Fehler (Script-Exit ≠ 0 o.ä.) | +| `done` | `exitCode`, `state?`, `runDir` | Abschluss; `state` nur bei Erfolg | + +--- + +## robot.json — Ladestrategie + +**Aktuell:** Lokale Datei +```javascript 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`):** +**Geplant** (wenn Driver `GET ROBOT_URL/api/robot/config` implementiert): ```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. +Auswirkung: nur `ROBOT_JSON`-Variable ändern — alle Scripts bekommen automatisch +die aktuelle Konfiguration. --- -## X-Position (Slider) – Bestimmung +## Offene Punkte -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. - ---- - -## To-Do / Fortschritt - -### Voraussetzungen (Kalibrierung abschliessen) - -- [x] Kamera-Intrinsik (NPZ) kalibriert -- [x] Board-Marker-Positionen kalibriert -- [x] X-Achsen-Richtung kalibriert -- [x] Arm1 Joint-Origin Y/Z — Button in `calibration_arm.html` ausführbar -- [ ] **Arm-Marker eintragen** (Nutzer): `links.Arm1.markers`, `links.Ellbow.markers`, `links.Arm2.markers`, `links.Hand.markers` in `scripts/robot_*.json` -- [ ] Arm1 Joint-Origin Y/Z Button klicken + in robot.json gespeichert - ---- - -### Phase 0 — Refactor: Board-Pipeline auslagern - -- [x] Funktion `runBoardPipeline(runDir, send)` in `server/server.js` extrahieren - - [x] Bestehende Logik aus `POST /api/board/run` in Funktion verschieben - - [x] `POST /api/board/run` ruft `runBoardPipeline()` auf (Verhalten unverändert) - - [ ] Test: Board-Run funktioniert weiterhin identisch - ---- - -### Phase 1 — Backend `POST /api/homing/run` - -- [x] Konstante `SCRIPT_4B` in `server/server.js` ergänzen -- [x] `server/homingOrchestrator.js` erstellen - - [x] `estimateXFromMarkers(arucoJsonPath)` implementieren - - [x] `runHoming({ robotJsonPath, homingDir, send, runScript, runBoardPipeline, SCRIPT_4B })` implementieren - - [x] `runBoardPipeline()` aufrufen → `aruco_marker_poses.json` - - [x] X-Position berechnen - - [x] 4b-Schleife: Arm1 → Ellbow → Arm2 → Hand (sequenziell, `--from-state`) - - [x] `send({ type: 'done', state: accumulated_state, runDir })` -- [x] Route `POST /api/homing/run` in `server/server.js` (SSE-Stream) -- [x] Route `POST /api/homing/send-state` in `server/server.js` -- [x] Route `GET /api/homing/run-data` (Bilder + State für Frontend) -- [x] `ROBOT_URL` Konstante in `server/server.js` ergänzen - ---- - -### Phase 2 — Frontend `public/homing.html` - -- [x] Datei `public/homing.html` erstellen - - [x] Sektion **Aktionen**: Button „Foto & Homing berechnen", Button „An Roboter senden" - - [x] Sektion **Ausgabe / Log**: SSE-Stream-Ausgabe, Schritt-Fortschritt - - [x] Sektion **Analysis & Reasoning**: Zwischenergebnisse je Script als JSON - - [x] Sektion **Result Raw JSON** + **Result Tree View**: `{ x, y, z, a, b, c, e }` - - [x] Sektion **Snapshot CSV**: Marker-Tabelle (ID, Link, x, y, z) - - [x] Sektion **Snapshots**: annotierte Kamerabilder -- [x] `public/homing.js` erstellen - - [x] `runHoming()`: POST + SSE-Stream lesen, Log befüllen, Fortschrittsbalken - - [x] `showResult(state)`: Tree View + Raw JSON befüllen, Send-Button aktivieren - - [x] `sendToRobot(state)`: POST `/api/homing/send-state` - - [x] `loadRunData(runDir)`: Debug-Bilder nach dem Run laden -- [x] Link-Button von `public/index.html` zu `homing.html` ergänzen - ---- - -### Phase 3 — State an Roboter senden - -- [ ] Route `POST /api/homing/send-state` in `server/server.js` registrieren - - [ ] Body: `{ state: { x, y, z, a, b, c, e } }` - - [ ] Weiterleitung an `ROBOT_URL/api/state` (analog zu `robotActions.js`) - - [ ] Fehler wenn `ROBOT_URL` nicht konfiguriert: JSON-Fehler zurückgeben -- [ ] Frontend: Fehler-Feedback wenn kein ROBOT_URL - ---- - -### Phase 4 — robot.json via Driver-API *(nach allem anderen)* - -- [ ] `loadRobotConfig()` Funktion in `server/server.js` - - [ ] Wenn `ROBOT_URL` gesetzt: `GET ROBOT_URL/api/robot/config` → temp-Datei cachen - - [ ] Fallback: lokale Datei (Verhalten unverändert) -- [ ] Homing und Board-Run nutzen `loadRobotConfig()` statt direkt `ROBOT_JSON` -- [ ] *(Voraussetzung: Driver implementiert `GET /api/robot/config`)* - ---- - -## Status-Übersicht - -| Bereich | Status | -|---------|--------| -| Python-Scripts (1, 2, 3b, 4b) | ✅ vorhanden | -| Kalibrierung (Kamera, Board, X-Achse) | ✅ fertig | -| Arm1 Joint-Origin Button | ✅ ausführbar | -| Arm-Marker in robot.json | 🔶 Nutzer | -| Phase 0 – runBoardPipeline() | ✅ fertig | -| Phase 1 – /api/homing/run | ✅ fertig | -| Phase 2 – homing.html | ✅ fertig | -| Phase 3 – /api/homing/send-state | ✅ Route implementiert | -| Phase 4 – robot.json via Driver-API | ⏳ später | +- [ ] **Arm-Marker eintragen** (Nutzer): `links.Arm1/Ellbow/Arm2/Hand.markers` in `robot.json` +- [ ] **Erstmals testen**: Homing-Run mit echtem Roboter und eingetragenen Markern durchführen +- [ ] **X-Schätzung verfeinern** (optional): `estimateXFromMarkers()` könnte gelenk-spezifischere Logik nutzen statt einfachem Mittelwert +- [ ] **robot.json via Driver-API** (optional): wenn Driver `GET ROBOT_URL/api/robot/config` bereitstellt diff --git a/doc/Kalibrierung.md b/doc/Kalibrierung.md new file mode 100644 index 0000000..97e5147 --- /dev/null +++ b/doc/Kalibrierung.md @@ -0,0 +1,303 @@ +# Kalibrierung – appRobotHoming + +> Stand: 2026-06-14 +> Einmaliger Vorgang — nur nach mechanischen Änderungen wiederholen. +> Jede Stufe baut auf der vorherigen auf. + +--- + +## Übersicht + +``` +[1] Camera NPZ → [2] Board → [3] X-Achse → [4] Arm1 Y-Achse +``` + +| Schritt | UI-Tab | Ergebnis | Status | +|---------|--------|----------|--------| +| [1] Camera NPZ | Camera NPZ | `data/calibration/{session}/{cam}_calibration.npz` | ✅ | +| [2] Board | Board | `links.Board.markers[].position` in `robot.json` | ✅ | +| [3] X-Achse | Robot X Axis | Alle Marker-Positionen in `robot.json` rotiert | ✅ | +| [4] Arm1 Y-Achse | Arm1 – Y | `links.Arm1.jointToParent.origin[1,2]` in `robot.json` | ✅ | +| Arm-Marker eintragen | — | `links.Arm1/Ellbow/Arm2/Hand.markers` | 🔶 Nutzer | + +--- + +## [1] Camera NPZ — Kameraparameter + +**Ziel:** Intrinsische Parameter (Brennweite, Verzerrungskoeffizienten, Kameramatrix) +für jede Kamera als `.npz`-Datei speichern. + +**Ablauf:** +1. ChArUco-Board aus verschiedenen Winkeln fotografieren (mehrere Posen) +2. OpenCV `calibrateCamera()` berechnet Kameraparameter +3. Ergebnis als `.npz` speichern und an Webcam-Service übertragen + +**Speichert:** `data/calibration/{session}/{cam}_calibration.npz` + +**Wird verwendet von:** Script `1_detect_aruco_observations.py` beim Board-Run und Homing +(Argument `-npz`). + +--- + +## [2] Board — Referenz-Marker-Positionen + +**Ziel:** Absolute 3D-Positionen aller Board-ArUco-Marker im Welt-Koordinatensystem +bestimmen und in `robot.json` speichern. + +**Ablauf:** +1. Foto mit allen Kameras (Snapshot) +2. Script `1_detect_aruco_observations.py` → `{cam}_aruco_detection.json` +3. Script `2_estimate_camera_from_observations.py` → `{cam}_camera_pose.json` +4. Script `3b_corner_marker_poses.py` → `aruco_marker_poses.json` (Triangulierung) +5. Positionen in `robot.json` unter `links.Board.markers` eintragen / bestätigen + +**Aktionen im Board-Tab:** +- **Board erkennen**: startet den vollständigen Foto+Script-Durchlauf (SSE-Stream) +- **Marker zuordnen** (Z-Bereich, Set, Link): Marker manuell kategorisieren +- **Sets justieren** (Kabsch 2D+Z): Zwei Marker-Sets aufeinander ausrichten +- **Marker ID zuordnen / entfernen**: einzelne Marker-Korrekturen + +**Speichert:** `links.Board.markers` in `robot.json` + +--- + +## [3] X-Achse — Schieberichtung des Sliders + +**Ziel:** X-Achse des Roboters (Slider-Richtung `Base → Arm1`) im Board-Koordinatensystem +ausrichten, sodass `[1, 0, 0]` der realen Bewegungsrichtung entspricht. + +**Ablauf:** +1. Roboter auf Position A fahren → Board erkennen +2. Roboter auf Position B fahren (mind. 20 mm Unterschied) → Board erkennen +3. Board-Viewer berechnet den Richtungsvektor der Markerbewegungen +4. „X-Achse übernehmen": alle Marker-Positionen in `robot.json` werden rotiert, + sodass die gemessene Richtung zur neuen X-Achse `[1, 0, 0]` wird + +**Implementierung:** `editRobot.js` → `adoptXAxis()` — rotiert alle `links.*.markers[].position` +um den A0-Schwerpunkt als Ursprung. + +**Speichert:** alle `links.*.markers[].position` in `robot.json` (rotiert) + +--- + +## [4] Arm1 — Y-Rotationsachse (Schultergelenk) + +**Ziel:** Lage und Richtung der Rotationsachse zwischen `Base` und `Arm1` bestimmen +und als `jointToParent.origin` in `robot.json` speichern. + +**Ablauf:** +1. Board erkennen mit Arm in **Position A** +2. Arm1 um ≥ 15° drehen +3. Board erkennen (**Position B**) +4. Arm1 nochmals ≥ 15° drehen +5. Board erkennen (**Position C**) +6. Board-Viewer berechnet automatisch die Rotationsachse (magenta Linie im 3D-Viewer) +7. Aktion **„Joint-Origin Y/Z übernehmen"**: speichert Y/Z des Achspunkts in `robot.json` + +**Optional — Aktion „Fixe Marker → Link Base":** +Marker mit Bewegung < 10 mm über alle drei Positionen sind physisch am Basis-Körper +befestigt. Diese werden in `links.Base.markers` eingetragen. + +**Speichert:** +- `links.Arm1.jointToParent.origin[1]` (Y) und `[2]` (Z) in `robot.json` +- Optional: `links.Base.markers` ergänzt + +--- + +### Mathematik: Bestimmung der Rotationsachse + +Bei einer Rotation um die Y-Achse bewegt sich jeder erkannte Marker auf einem +**Kreisbogen** im 3D-Raum. Aus den beobachteten Marker-Positionen zu mehreren +Zeitstempeln lassen sich Lage und Richtung der Rotationsachse berechnen. + +#### Wie viele Beobachtungen sind nötig? + +Entscheidend ist nicht „2 oder 3 Positionen" allein, sondern die Kombination aus +Anzahl Marker und Anzahl Positionen: + +| Beobachtungen | Bestimmbar? | Begründung | +|---|---|---| +| 1 Marker, 2 Positionen | **Nein** | Achse liegt *irgendwo* in der Mittelsenkrechten-Ebene der Sehne; Richtung und Lage bleiben unterbestimmt | +| **2 Marker, je 2 Positionen** | **Ja** | **Verfahren A**: Richtung aus d₁ × d₂, Lage aus Schnitt zweier Mittelsenkrechten-Ebenen | +| 1 Marker, 3 Positionen | **Ja** | **Verfahren B**: drei Punkte definieren einen eindeutigen Umkreis → eindeutige Achse | +| N Marker, je ≥ 2 bzw. ≥ 3 Positionen | Ja, überbestimmt | Least-Squares, robust gegenüber Messrauschen, erlaubt Fehlerabschätzung | + +Die häufige Kurzbehauptung „zwei Positionen reichen nicht" gilt nur für **einen +einzigen** Marker. Sobald **zwei** (nicht-entartete) Marker vorliegen, genügen +zwei Positionen — siehe Verfahren A. + +> Die aktuelle Implementierung (`public/yAxisCompute.js`) verwendet **Verfahren B**. + +#### Verfahren A — Zwei Marker, je zwei Positionen + +Gegeben: M₁ₐ, M₁ᵦ (Marker 1 zu Zeit a, b) und M₂ₐ, M₂ᵦ (Marker 2 zu Zeit a, b). +Beide Marker sitzen am selben starren Körper, der zwischen a und b um die gesuchte +Achse rotiert. Verschiebungsvektoren (Sehnen der Kreisbögen): + +``` +d₁ = M₁ᵦ − M₁ₐ +d₂ = M₂ᵦ − M₂ₐ +``` + +**Schritt 1 – Achsenrichtung.** Jeder Punkt bewegt sich in einer Ebene **senkrecht** +zur Achse; die Sehne liegt in dieser Ebene, also `d₁ ⊥ n` und `d₂ ⊥ n`: + +``` +n = (d₁ × d₂) / |d₁ × d₂| ← Einheitsvektor entlang der Rotationsachse +``` + +**Schritt 2 – Achsenlage.** Der Kreismittelpunkt eines Markers ist von Start- und +Endposition gleich weit entfernt, liegt also in der **Mittelsenkrechten-Ebene** der +Sehne. Da zusätzlich `n ⊥ dᵢ`, liegt die **gesamte Achse** in dieser Ebene. + +``` +Ebene 1: d₁ · (x − P₁) = 0, P₁ = (M₁ₐ + M₁ᵦ) / 2 +Ebene 2: d₂ · (x − P₂) = 0, P₂ = (M₂ₐ + M₂ᵦ) / 2 +``` + +Die Achse ist die **Schnittgerade** beider Ebenen mit Richtung `n`. Ein Punkt A auf +ihr (Ansatz A = α·d₁ + β·d₂, Komponente entlang n zu 0 gesetzt): + +``` +c₁ = d₁ · P₁, c₂ = d₂ · P₂ +D = |d₁|²·|d₂|² − (d₁ · d₂)² (= |d₁ × d₂|²) + +α = (c₁·|d₂|² − c₂·(d₁ · d₂)) / D +β = (c₂·|d₁|² − c₁·(d₁ · d₂)) / D + +A = α·d₁ + β·d₂ ← Referenzpunkt auf der Achse +``` + +Ergebnis: `r(t) = A + t·n`. + +**Entartung:** `D = 0` ⟺ `d₁ ∥ d₂` ⟺ beide Marker liegen **mit der Achse in einer +gemeinsamen Ebene**. Dann sind die Mittelsenkrechten-Ebenen parallel und schneiden +sich nicht eindeutig. Erkennung über `|d₁ × d₂| / (|d₁|·|d₂|) ≈ 0`; auch ein Marker +direkt auf der Achse liefert `dᵢ ≈ 0`. → anderen Marker wählen oder Verfahren B. + +#### Verfahren B — Ein Marker, drei Positionen *(implementiert)* + +Gegeben P₁, P₂, P₃ desselben Markers zu drei Drehwinkeln. Die drei Punkte liegen +auf einem Kreis, dessen Ebene senkrecht zur Achse steht. + +**Schritt 1 — Achsenrichtung** (Normalenvektor der Kreisebene): +``` +v₁ = P₂ − P₁ +v₂ = P₃ − P₁ +n = normalize(v₁ × v₂) +``` + +**Schritt 2 — Umkreismittelpunkt** (Punkt auf der Achse), über baryzentrische Gewichte: +``` +a² = |P₂ − P₃|², b² = |P₁ − P₃|², c² = |P₁ − P₂|² + +w₁ = a²·(b² + c² − a²) +w₂ = b²·(a² + c² − b²) +w₃ = c²·(a² + b² − c²) + +C = (w₁·P₁ + w₂·P₂ + w₃·P₃) / (w₁ + w₂ + w₃) +``` + +Ergebnis: `r(t) = C + t·n`. **Entartung:** P₁,P₂,P₃ kollinear (zu kleiner Drehwinkel) +→ `|v₁ × v₂| ≈ 0`, Umkreis numerisch schlecht bestimmt. + +#### Kombination über N Marker (Least Squares) + +Beide Verfahren lassen sich über mehrere Marker mitteln — robuster und mit +Fehlerabschätzung. + +**Achsenrichtung gemeinsam.** Aus `dᵢ ⊥ n` (bzw. Ebenen-Normalen) minimiert die beste +Richtung `Σ (dᵢ · n)²` unter `|n| = 1`: +``` +M = Σ dᵢ · dᵢᵀ (3×3-Matrix) +n = Eigenvektor von M zum kleinsten Eigenwert +``` +(Verallgemeinert das Kreuzprodukt. Verfahren B alternativ: alle Normalen nᵢ aufs +gleiche Halbraum ausrichten und mitteln.) + +**Achsenlage gemeinsam.** Für Verfahren B (Umkreismittelpunkte Cᵢ) ergibt sich der +einfache Schwerpunkt: +``` +axisDir = normalize( mean(nᵢ) ) ← Vorzeichen vorher angleichen +axisPoint = mean(Cᵢ) ← Schwerpunkt der Umkreismittelpunkte +``` + +**Residuum pro Marker** (Verfahren B, Fehler- / Ausreißer-Erkennung): +``` +εᵢ = |(Cᵢ − C̄) − ((Cᵢ − C̄)·n̄)·n̄| +``` +Große εᵢ deuten auf einen fehlerhaften Marker oder eine nicht-rotatorische +Bewegungskomponente hin. + +Die Y/Z-Koordinaten von `axisPoint` geben an, wo die Rotationsachse durch die +YZ-Ebene geht — das ist der gesuchte `jointToParent.origin`. + +#### Genauigkeit & Verfahrenswahl + +Beide Verfahren sind im **rauschfreien** Fall exakt; die Unterschiede liegen in +Robustheit und Aufwand: + +| Kriterium | Verfahren A (2 Marker, 2 Bilder) | Verfahren B (1 Marker, 3 Bilder) | +|---|---|---| +| Mindest-Aufnahmen | 2 Bilder | 3 Bilder | +| Mindest-Marker | 2 (gut separiert) | 1 | +| Eigenständige Fehler-Schätzung | nur über mehrere Marker | pro Marker (Residuum εᵢ) | +| Kritische Entartung | d₁ ∥ d₂ (Marker koplanar mit Achse) | P₁,P₂,P₃ kollinear (kleiner Winkel) | +| Empfindlichkeit | hoch, wenn Sehnen fast parallel oder Drehwinkel klein | hoch, wenn Drehwinkel klein (Bogen fast gerade) | + +**Beide** brauchen einen ausreichend großen Drehwinkel: kleine Winkel liefern kurze +Sehnen bzw. fast gerade Bögen, in denen das Messrauschen dominiert. + +- **Verfahren A** ist schneller (zwei statt drei Aufnahmen), sinnvoll bei ≥ 2 gut + getrennten Markern. +- **Verfahren B** ist robuster für die **Fehlerrechnung**: jeder Marker liefert + unabhängig eine vollständige Achsen-Schätzung, das Residuum εᵢ erlaubt + Ausreißer-Erkennung, keine Entartung zwischen Markern. Für belastbare Genauigkeit + daher vorzuziehen — und der Grund, weshalb die Implementierung Verfahren B nutzt. + +**Praktische Werte (beide Verfahren):** +- Drehwinkel zwischen den Positionen ≥ 15° (besser ≥ 30°), sonst numerisch instabil. +- 3–4 Marker mit guter räumlicher Verteilung um die Achse. +- Die Rotationswinkel müssen **nicht** bekannt sein – nur die 3D-Koordinaten. + +**Marker-Filter (`min_movement_mm = 10`):** +Marker, die sich zwischen A/B/C weniger als 10 mm bewegen, sind nicht am rotierenden +Teil befestigt und werden aus der Achsenberechnung ausgeschlossen. Ihre Position A +wird für die optionale Base-Link-Zuweisung gespeichert. + +--- + +### Implementierungsnachweis + +Die Implementierung in `public/yAxisCompute.js` setzt **Verfahren B** um: + +| Schritt | Implementierung | Verifikation | +|---------|----------------|--------------| +| Normalenvektor | `yAxisCompute.js` Z. 51–61 | Kreuzprodukt v₁×v₂, normiert ✓ | +| Umkreismittelpunkt | `yAxisCompute.js` Z. 63–76 | Baryzentrische Gewichte ✓ | +| Mittelung Normalen | `yAxisCompute.js` Z. 153–162 | Vorzeichen-Alignment + mean + renormieren ✓ | +| Mittelung Achspunkte | `yAxisCompute.js` Z. 164–167 | Schwerpunkt der Cᵢ ✓ | +| Origin speichern | `calibration.js` → `setOriginBtn` sendet `axisPoint[1,2]` | `editRobot.js` schreibt `origin[1,2]` ✓ | + +**Ergebnis: Implementierung entspricht der beschriebenen Mathematik (Verfahren B) vollständig.** + +--- + +## Dateistruktur + +``` +data/ + calibration/ + {session}/ + meta.json + {cam}_calibration.npz ← Kameraparameter + {cam}_aruco_detection.json ← Marker-Erkennung + {cam}_camera_pose.json ← Kamera-Pose +scripts/ + robot_1781069752019.json ← robot.json (Haupt-Konfiguration) + links.Board.markers ← Board-Marker-Positionen + links.Base.markers ← fixe Basis-Marker (nach Y-Kalibrierung) + links.Arm1.jointToParent.origin ← Schultergelenk-Position (nach Y-Kalibrierung) + links.Arm1.markers ← Arm1-Marker (Nutzer eingetragen) + ... +``` diff --git a/public/index.html b/public/index.html index 9f7c923..013a548 100755 --- a/public/index.html +++ b/public/index.html @@ -23,7 +23,7 @@ title="Erst Homing ausführen"> ✅ An Roboter senden - ○ Warte + ○ Warte diff --git a/public/styles.css b/public/styles.css index 4ccfb68..45808de 100755 --- a/public/styles.css +++ b/public/styles.css @@ -237,8 +237,9 @@ textarea { background: #1e293b; color: #94a3b8; } +.status-badge.idle { color: #60a5fa; } +.status-badge.wip { color: #93c5fd; } .status-badge.open { color: #f59e0b; } -.status-badge.wip { color: #60a5fa; } .status-badge.done { color: #34d399; background: #064e3b; } /* ===== HOMING FORTSCHRITTSBALKEN ===== */ diff --git a/public/yAxisCompute.js b/public/yAxisCompute.js index b4bb0e6..cc1244a 100644 --- a/public/yAxisCompute.js +++ b/public/yAxisCompute.js @@ -60,7 +60,7 @@ const n = cross.map(c => c / crossLen); - // Baryzentrische Gewichte → Umkreismittelpunkt (doc/04_y_achse.md) + // Baryzentrische Gewichte → Umkreismittelpunkt (doc/Kalibrierung.md → [4] Arm1, Verfahren B) const a2 = dist2(P2, P3), b2 = dist2(P1, P3), c2 = dist2(P1, P2); const w1 = a2 * (b2 + c2 - a2); const w2 = b2 * (a2 + c2 - b2); diff --git a/scripts/4_yAxis_rotation_reconstruction.py b/scripts/4_yAxis_rotation_reconstruction.py index 09e015c..3fde47b 100644 --- a/scripts/4_yAxis_rotation_reconstruction.py +++ b/scripts/4_yAxis_rotation_reconstruction.py @@ -10,7 +10,7 @@ Gegeben drei Messungen (Pos A, B, C) – in denen dieselben fremd-Marker - Referenzpunkt auf der Achse (axisPoint_mm) - Residuen pro Punkt (Qualitätsmass) -Methode (doc/04_y_achse.md): +Methode (doc/Kalibrierung.md → [4] Arm1, Verfahren B): Jeder Marker bewegt sich auf einem Kreisbogen. Die Ebene des Kreises steht senkrecht zur Rotationsachse → Normalenvektor = Achsenrichtung. Der Umkreismittelpunkt des Dreiecks P1-P2-P3 liegt auf der Achse.