From 1032c53630d1dd13ac7c204d9bdfa9744a13799a Mon Sep 17 00:00:00 2001
From: chk <79915315+ChKendel@users.noreply.github.com>
Date: Wed, 10 Jun 2026 14:41:55 +0200
Subject: [PATCH] Viewer
---
public/boardViewer.html | 344 ++++++++++++++++++++++++++++++++++
public/calibration.js | 9 +-
public/calibration_board.html | 13 ++
server/server.js | 67 +++++++
4 files changed, 432 insertions(+), 1 deletion(-)
create mode 100644 public/boardViewer.html
diff --git a/public/boardViewer.html b/public/boardViewer.html
new file mode 100644
index 0000000..a138a40
--- /dev/null
+++ b/public/boardViewer.html
@@ -0,0 +1,344 @@
+
+
+
+
+Board Viewer
+
+
+
+
+
+
+
+
+ Erkannt
+ Nicht erkannt
+ Kamera
+
+
+
Laden …
+
+
+
+
+
+
Orbit · Scroll · Rechte Taste = Pan
+
+
+
+
+
diff --git a/public/calibration.js b/public/calibration.js
index 9297412..a852a62 100644
--- a/public/calibration.js
+++ b/public/calibration.js
@@ -250,7 +250,14 @@ function initBoard() {
await readSseStream(response, logB, (evt) => {
if (evt.exitCode === 0) {
logB('✅ Board-Run abgeschlossen.');
- if (evt.runDir) document.getElementById('board-last-run').textContent = evt.runDir;
+ if (evt.runDir) {
+ document.getElementById('board-last-run').textContent = evt.runDir;
+ // Board-Viewer im iframe nach dem Run neu laden
+ const frame = document.getElementById('board-viewer-frame');
+ if (frame?.contentWindow) {
+ frame.contentWindow.postMessage({ type: 'reload' }, '*');
+ }
+ }
} else {
logB(`❌ Beendet mit Exit-Code ${evt.exitCode}`);
}
diff --git a/public/calibration_board.html b/public/calibration_board.html
index d1d7d88..a72baa9 100644
--- a/public/calibration_board.html
+++ b/public/calibration_board.html
@@ -25,4 +25,17 @@
+
+
Board-Viewer
+
+ Wird nach jedem Board-Run automatisch aktualisiert.
+
+
+
+
diff --git a/server/server.js b/server/server.js
index 9215907..1f7396d 100755
--- a/server/server.js
+++ b/server/server.js
@@ -572,6 +572,73 @@ app.post('/api/board/run', async (req, res) => {
}
});
+/** Neuestes Board-Run-Verzeichnis (Timestamp-Name) oder null */
+async function findLatestBoardRun() {
+ 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;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * GET /api/board/latest
+ * Gibt Daten des letzten Board-Runs zurück: robot.json + Detection-Ergebnisse + Kamera-Posen.
+ * Wird vom Board-Viewer (boardViewer.html) abgefragt.
+ */
+app.get('/api/board/latest', async (req, res) => {
+ try {
+ const runName = await findLatestBoardRun();
+ if (!runName) return res.json({ runDir: null, robot: null, detections: [], cameraPoses: [] });
+
+ const runDir = path.join(boardDataDir, runName);
+
+ let robot = null;
+ try { robot = JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8')); } catch {}
+
+ let files = [];
+ try { files = await fsPromises.readdir(runDir); } catch {}
+
+ const detections = [];
+ const cameraPoses = [];
+
+ for (const f of files.sort()) {
+ if (f.endsWith('_aruco_detection.json')) {
+ try {
+ const data = JSON.parse(await fsPromises.readFile(path.join(runDir, f), 'utf8'));
+ detections.push({
+ file: f,
+ cameraId: data.camera?.camera_id ?? f.replace('_aruco_detection.json', ''),
+ detectedMarkerIds: (data.detections ?? []).map(d => d.marker_id),
+ numDetected: data.aruco?.num_detected_markers ?? 0,
+ numRejected: data.aruco?.num_rejected_candidates ?? 0,
+ });
+ } catch {}
+ } else if (f.endsWith('_camera_pose.json')) {
+ try {
+ const data = JSON.parse(await fsPromises.readFile(path.join(runDir, f), 'utf8'));
+ const cp = data.camera_pose;
+ cameraPoses.push({
+ file: f,
+ cameraId: data.camera?.camera_id ?? f.replace('_camera_pose.json', ''),
+ position_mm: cp?.camera_in_world?.position_mm ?? null,
+ rotation_matrix: cp?.world_to_camera?.rotation_matrix ?? null,
+ usedMarkerIds: data.estimation?.used_marker_ids ?? [],
+ rms_px: data.estimation?.residual_rms_px ?? null,
+ });
+ } catch {}
+ }
+ }
+
+ return res.json({ runDir: runName, robot, detections, cameraPoses });
+ } catch (err) {
+ return res.status(500).json({ error: String(err) });
+ }
+});
+
/**
* POST /api/calibration/upload-npz
* Liest {camera}_calibration.npz aus der aktuellen Session und