From 3084324f4a47e54f78c0f6f998712fef47eba749 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:30:16 +0200 Subject: [PATCH] x-axis justierung: visualize --- public/boardViewer.html | 103 ++++++++++++++++++++++++++++++++-- public/calibration.js | 74 +++++++++++++++++++++++- public/calibration_xaxis.html | 73 +++++++++++++++++++----- server/server.js | 36 +++++++++--- 4 files changed, 255 insertions(+), 31 deletions(-) diff --git a/public/boardViewer.html b/public/boardViewer.html index 197dccd..2996e0c 100644 --- a/public/boardViewer.html +++ b/public/boardViewer.html @@ -51,6 +51,23 @@ font-size: 11px; } .btn:hover { border-color: var(--accent); color: var(--accent); } + #run-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 12px; + background: var(--panel); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + flex-wrap: wrap; + } + .run-lbl { + font-size: 10px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: .04em; + white-space: nowrap; + } #canvas-wrap { flex: 1; min-height: 360px; position: relative; overflow: hidden; } canvas { display: block; width: 100%; height: 100%; } #hint { @@ -127,6 +144,7 @@ Erkannt (nur 2D) Gemessen (3b) Fremd (3b) + Vergleich Kamera @@ -134,6 +152,19 @@ +
+ Basis + + Vergleich + (nur fremd) + + +
+
Orbit · Scroll · Rechte Taste = Pan
@@ -182,7 +213,8 @@ const gPaper = new THREE.Group(); // weißes A0-Papier const gMarkers = new THREE.Group(); // Modell-Rechtecke const gMeasured = new THREE.Group(); // gemessene Positionen (3b) const gCameras = new THREE.Group(); // Kamera-Frusta -scene.add(gPaper, gMarkers, gMeasured, gCameras); +const gCompare = new THREE.Group(); // Vergleichs-Punkte (anderer Timestamp, nur fremd) +scene.add(gPaper, gMarkers, gMeasured, gCameras, gCompare); function clearGroup(g) { while (g.children.length) { @@ -542,11 +574,17 @@ function buildTable(data) { } // ── Daten laden ─────────────────────────────────────────────────────────────── + +/** Haupt-Run laden (Basis-Dropdown). Ohne Selektion → neuester Run. */ async function loadData() { - const statusEl = document.getElementById('status'); + const statusEl = document.getElementById('status'); statusEl.textContent = 'Laden …'; + const selRun = document.getElementById('sel-run-primary')?.value ?? ''; + const url = selRun + ? `/api/board/latest?run=${encodeURIComponent(selRun)}` + : '/api/board/latest'; try { - const r = await fetch('/api/board/latest'); + const r = await fetch(url); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await r.json(); if (!data.runDir) { @@ -564,10 +602,63 @@ async function loadData() { } } -loadData(); -document.getElementById('btnReload').addEventListener('click', loadData); +/** Vergleichs-Run laden (Compare-Dropdown) – zeigt nur fremd-triangulierte Marker als orange Kugeln. */ +async function loadCompareData() { + clearGroup(gCompare); + const selRun = document.getElementById('sel-run-compare')?.value ?? ''; + if (!selRun) return; + try { + const r = await fetch(`/api/board/latest?run=${encodeURIComponent(selRun)}`); + if (!r.ok) return; + const data = await r.json(); + const markers = data.measuredMarkers?.markers ?? []; + if (!markers.length) return; + // Board-Marker-IDs aus Robot.json (für diesen Run) + const boardIds = new Set((data.robot?.links?.Board?.markers ?? []).map(m => m.id)); + for (const m of markers) { + if (!boardIds.has(m.marker_id)) { + // Nicht zugeordnet → orange Kugel (Vergleich) + gCompare.add(makeSphere(r2vArr(m.position_mm), 0.006, 0xf97316)); + } + } + } catch { /* kein 3b-Output für diesen Run */ } +} + +/** Run-Listen laden und beide Dropdowns befüllen. */ +async function initRunSelectors() { + try { + const [r5, r10] = await Promise.all([ + fetch('/api/board/runs?limit=5'), + fetch('/api/board/runs?limit=10'), + ]); + const { runs: runs5 } = r5.ok ? await r5.json() : { runs: [] }; + const { runs: runs10 } = r10.ok ? await r10.json() : { runs: [] }; + + const selP = document.getElementById('sel-run-primary'); + const selC = document.getElementById('sel-run-compare'); + const cur = selP?.value ?? ''; // aktuell gewählten Run behalten + + if (selP) { + selP.innerHTML = '' + + runs5.map(r => ``).join(''); + } + if (selC) { + selC.innerHTML = '' + + runs10.map(r => ``).join(''); + } + } catch { /* offline oder noch keine Runs */ } +} + +initRunSelectors().then(() => loadData()); +document.getElementById('btnReload').addEventListener('click', () => { + initRunSelectors().then(() => { loadData(); loadCompareData(); }); +}); +document.getElementById('sel-run-primary')?.addEventListener('change', loadData); +document.getElementById('sel-run-compare')?.addEventListener('change', loadCompareData); window.addEventListener('message', (e) => { - if (e.data?.type === 'reload') loadData(); + if (e.data?.type === 'reload') { + initRunSelectors().then(() => { loadData(); loadCompareData(); }); + } }); // ── Resize & Render-Loop ────────────────────────────────────────────────────── diff --git a/public/calibration.js b/public/calibration.js index 223572b..f8beea6 100644 --- a/public/calibration.js +++ b/public/calibration.js @@ -18,8 +18,9 @@ async function loadPanel(tab, src) { }); // Tab-spezifische Initialisierung - if (tab === 'camera-npz') initCameraNpz(); - else if (tab === 'board') initBoard(); + if (tab === 'camera-npz') initCameraNpz(); + else if (tab === 'board') initBoard(); + else if (tab === 'robot-x-axis') initXAxis(); } catch (err) { document.getElementById('tab-' + tab).innerHTML = @@ -354,6 +355,75 @@ async function loadBoardTable() { // ── Board ───────────────────────────────────────────────────────────────────── +// ── Tab: Robot X Axis ───────────────────────────────────────────────────────── + +async function populateXAxisSetDropdowns() { + let sets = []; + try { + const r = await fetch('/api/robot/board-sets'); + if (r.ok) sets = (await r.json()).sets ?? []; + } catch {} + const sel = document.getElementById('xaxis-ref-set'); + if (sel) { + sel.innerHTML = '' + + sets.map(s => ``).join(''); + } +} + +function initXAxis() { + const logEl = document.getElementById('log-xaxis'); + + function logX(msg) { + const ts = new Date().toLocaleTimeString('de-CH'); + logEl.value += `[${ts}] ${msg}\n`; + logEl.scrollTop = logEl.scrollHeight; + } + + populateXAxisSetDropdowns(); + + document.getElementById('btn-xaxis-run').addEventListener('click', async () => { + const refSet = document.getElementById('xaxis-ref-set')?.value ?? ''; + logX(`Board-Erkennung … Referenz: ${refSet || 'alle'}`); + const btn = document.getElementById('btn-xaxis-run'); + btn.disabled = true; + try { + const response = await fetch('/api/board/run', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refSet: refSet || undefined }), + }); + if (!response.ok) { + const raw = await response.text().catch(() => ''); + let msg; + try { msg = JSON.parse(raw).error || raw; } + catch { msg = raw.slice(0, 300) || `HTTP ${response.status}`; } + logX(`❌ HTTP ${response.status}: ${msg}`); + return; + } + await readSseStream(response, logX, (evt) => { + if (evt.exitCode === 0) { + logX('✅ Board-Run abgeschlossen.'); + if (evt.runDir) { + document.getElementById('xaxis-last-run').textContent = evt.runDir; + const frame = document.getElementById('xaxis-viewer-frame'); + if (frame?.contentWindow) { + frame.contentWindow.postMessage({ type: 'reload' }, '*'); + } + } + } else { + logX(`❌ Beendet mit Exit-Code ${evt.exitCode}`); + } + }); + } catch (err) { + logX(`❌ Fehler: ${err}`); + } finally { + btn.disabled = false; + } + }); +} + +// ── Tab: Board (shared helpers) ─────────────────────────────────────────────── + /** Befüllt alle Set-Dropdowns aus /api/robot/board-sets */ async function populateBoardSetDropdowns() { let sets = []; diff --git a/public/calibration_xaxis.html b/public/calibration_xaxis.html index da3c413..53572b4 100644 --- a/public/calibration_xaxis.html +++ b/public/calibration_xaxis.html @@ -1,28 +1,71 @@
+
-

Robot X Axis offen

-
- Ziel: X-Achse des Roboters im Weltkoordinatensystem verorten (Richtungsvektor und - Nullpunkt). Roboter fährt zwei bekannte Positionen an, Kamera beobachtet den - Endeffektor-Marker.

- Geplante Aktionen: Referenzposition 1 anfahren · Foto · Marker merken · - Referenzposition 2 anfahren · Foto · Achsvektor berechnen · Speichern.

- Aktionen werden ergänzt sobald das Konzept feststeht. +

Robot X Axis – Board-Erkennung

+
+ Ziel + + X-Achse des Roboters im Weltkoordinatensystem verorten (Richtungsvektor + Nullpunkt). + Der Roboter fährt entlang der X-Achse, die Kamera beobachtet das Board aus mehreren Positionen. + + Ablauf + + Board erkennen → ⬅ / ➡ Roboter bewegen → Board erkennen + → im Viewer die zwei Runs als Basis + Vergleich wählen → Achse berechnen + + Letzter Run +
-
- - - - - - +
+ +
+ +
+

Aktionen

+
+ + Roboter-X-Achse bewegen (Schrittweite folgt) + +
+
+ +

Ausgabe / Log

+ +
+

Board-Viewer

+

+ Basis-Dropdown: vollständige Anzeige eines Runs.   + Vergleich-Dropdown: zeigt nur fremd-triangulierte Punkte (orange) eines anderen Runs. +

+ +
+
diff --git a/server/server.js b/server/server.js index 419bd53..789000f 100755 --- a/server/server.js +++ b/server/server.js @@ -620,26 +620,46 @@ app.post('/api/board/run', async (req, res) => { } }); -/** Neuestes Board-Run-Verzeichnis (Timestamp-Name) oder null */ -async function findLatestBoardRun() { +/** Alle Board-Run-Verzeichnisse, neueste zuerst */ +async function listBoardRuns() { try { await fsPromises.access(boardDataDir); const entries = await fsPromises.readdir(boardDataDir, { withFileTypes: true }); - const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse(); - return dirs[0] ?? null; + return entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse(); } catch { - return null; + return []; } } +/** Neuestes Board-Run-Verzeichnis (Timestamp-Name) oder null */ +async function findLatestBoardRun() { + const dirs = await listBoardRuns(); + return dirs[0] ?? null; +} + /** - * GET /api/board/latest - * Gibt Daten des letzten Board-Runs zurück: robot.json + Detection-Ergebnisse + Kamera-Posen. + * GET /api/board/runs?limit=N + * Gibt eine Liste der vorhandenen Board-Run-Verzeichnisse zurück (neueste zuerst). + */ +app.get('/api/board/runs', async (req, res) => { + try { + const limit = Math.max(1, Math.min(50, parseInt(req.query.limit ?? '10', 10))); + const runs = await listBoardRuns(); + return res.json({ runs: runs.slice(0, limit) }); + } catch (err) { + return res.status(500).json({ error: String(err) }); + } +}); + +/** + * GET /api/board/latest?run= + * Gibt Daten eines Board-Runs zurück: robot.json + Detection-Ergebnisse + Kamera-Posen. + * Ohne ?run → neuester Run. Mit ?run= → genau dieser Run. * Wird vom Board-Viewer (boardViewer.html) abgefragt. */ app.get('/api/board/latest', async (req, res) => { try { - const runName = await findLatestBoardRun(); + const runName = req.query.run || await findLatestBoardRun(); if (!runName) return res.json({ runDir: null, robot: null, detections: [], cameraPoses: [] }); const runDir = path.join(boardDataDir, runName);