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 @@
↺
+
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 = '
⟳ aktuellster ' +
+ runs5.map(r => `
${r} `).join('');
+ }
+ if (selC) {
+ selC.innerHTML = '
– keiner – ' +
+ runs10.map(r => `
${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 = '
alle ' +
+ sets.map(s => `
${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
+ –
-
-
Pos 1 anfahren
-
Foto Pos 1
-
Pos 2 anfahren
-
Foto Pos 2
-
Achse berechnen
-
Speichern
+
+ Board erkennen
+
+ Referenz:
+
+ alle
+
+
+
+
+
Aktionen
+
+
+ ⬅ Links
+
+ Roboter-X-Achse bewegen (Schrittweite folgt)
+
+ Rechts ➡
+
+
+
+
+
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);