diff --git a/public/calibration.css b/public/calibration.css new file mode 100644 index 0000000..af28daa --- /dev/null +++ b/public/calibration.css @@ -0,0 +1,173 @@ + +/* ===== LAYOUT ===== */ + +body { + display: flex; + flex-direction: column; + height: 100vh; + margin: 0; + padding: 0; + overflow: hidden; +} + +/* ===== HEADER (gleicher Stil wie .section auf index.html) ===== */ + +.calib-topbar { + display: flex; + align-items: center; + gap: 16px; + padding: 14px 20px; + background: var(--panel); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.calib-topbar a { + color: var(--muted); + text-decoration: none; + font-size: 13px; + border: 1px solid #334155; + border-radius: 6px; + padding: 6px 14px; + transition: border-color 0.2s, color 0.2s; +} + +.calib-topbar a:hover { + border-color: var(--accent); + color: var(--accent); +} + +.calib-topbar h1 { + margin: 0; + font-size: 15px; + font-weight: 500; + color: var(--accent); +} + +/* ===== BODY: SIDEBAR + CONTENT ===== */ + +.calib-body { + display: flex; + flex: 1; + overflow: hidden; + padding: 16px; + gap: 0; +} + +/* ===== LESEZEICHEN-TABS LINKS ===== */ + +.tab-sidebar { + display: flex; + flex-direction: column; + gap: 3px; + padding-top: 4px; + flex-shrink: 0; + width: 148px; +} + +.tab-btn { + background: #0e1a2b; + border: 1px solid #1f2937; + border-right: none; + border-radius: 6px 0 0 6px; + padding: 11px 16px; + color: var(--muted); + font-size: 13px; + text-align: left; + cursor: pointer; + transition: color 0.15s, background 0.15s, border-color 0.15s; + white-space: nowrap; +} + +.tab-btn:hover { + color: var(--text); + background: #132c44; + border-color: #334155; +} + +.tab-btn.active { + color: var(--accent); + background: var(--panel); /* gleiche Farbe wie Content-Area */ + border-color: #334155; + border-right: none; + border-left: 3px solid var(--accent); + padding-left: 13px; /* kompensiert den dickeren linken Rand */ + z-index: 1; +} + +/* ===== CONTENT AREA ===== */ + +.tab-content { + flex: 1; + background: var(--panel); + border: 1px solid #334155; + border-radius: 0 8px 8px 8px; + overflow-y: auto; + padding: 20px 24px; +} + +/* ===== TAB PANELS ===== */ + +.tab-panel { display: none; } +.tab-panel.active { display: block; } + +/* ===== SECTION OVERRIDE: kein äusserer Rand in der Content-Area ===== */ + +.tab-content .section { + background: #0e1a2b; +} + +/* ===== PLACEHOLDER NOTE ===== */ + +.placeholder-note { + margin-top: 14px; + padding: 14px 18px; + background: #0b1220; + border: 1px dashed #334155; + border-radius: 8px; + color: var(--muted); + font-size: 13px; + line-height: 1.7; +} + +/* ===== STATUS BADGE ===== */ + +.status-badge { + display: inline-block; + font-size: 11px; + padding: 2px 8px; + border-radius: 4px; + background: #1e293b; + color: var(--muted); + margin-left: 8px; + vertical-align: middle; + font-weight: normal; +} +.status-badge.open { color: #f59e0b; } +.status-badge.done { color: #34d399; background: #064e3b; } +.status-badge.wip { color: #60a5fa; } + +/* ===== INFO GRID ===== */ + +.info-grid { + display: grid; + grid-template-columns: 160px 1fr; + gap: 6px 12px; + margin-top: 14px; + font-size: 13px; +} + +.info-label { + color: var(--muted); +} + +.info-value { + color: var(--text); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +/* Buttons: aktiv vs. deaktiviert visuell unterscheiden */ +.controls button:disabled { + opacity: 0.35; + cursor: not-allowed; +} diff --git a/public/calibration.html b/public/calibration.html index 66f20cb..055902b 100644 --- a/public/calibration.html +++ b/public/calibration.html @@ -5,182 +5,7 @@ Kalibrierung – appRobotHoming - + @@ -195,430 +20,22 @@ - +
+
+
+
+
+
- -
-
- - -
-

Aktuelle Kalibrierung

-
- Timestamp - - - Erstellt am - - - Bilder / Kameras - -
-
- - -
-

Aktionen

-
- - - - - - - -
-
- - -
-

Ausgabe / Log

- -
- -
-
- - -
-
- -
-

Board – ArUco & Kamera-Pose

-
- Ablauf - - Foto aufnehmen → ArUco erkennen → Kamera-Pose schätzen - - Schritte - - 1_detect_aruco_observations  →  2_estimate_camera_from_observations - - Letzter Run - -
-
- - -
-
- -
-

Ausgabe / Log

- -
- -
-
- - -
-
- -
-

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

Ausgabe / Log

- -
- -
-
- - -
-
- -
-

Arm1 / Arm2 offen

-
- Ziel: Nullposition und Kinematikparameter von Arm1 und Arm2 einmessen. - Arm fährt in mechanische Nullposition, Kamera prüft die tatsächliche Pose, - Offset wird berechnet und gespeichert.

- Geplante Aktionen: Arm1 → Nullpos → Foto → Winkel · Arm2 → Nullpos → Foto → - Winkel · Offset-Korrektur speichern.

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

Ausgabe / Log

- -
- -
-
- - - - + diff --git a/public/calibration.js b/public/calibration.js new file mode 100644 index 0000000..9297412 --- /dev/null +++ b/public/calibration.js @@ -0,0 +1,264 @@ +/* calibration.js – Kalibrierungs-Frontend */ + +// ── Panel-Loading (lazy) ─────────────────────────────────────────────────────── + +const _panelLoaded = new Set(); + +async function loadPanel(tab, src) { + if (_panelLoaded.has(tab)) return; + try { + const r = await fetch(src); + if (!r.ok) throw new Error(`HTTP ${r.status}`); + document.getElementById('tab-' + tab).innerHTML = await r.text(); + _panelLoaded.add(tab); + + // h2-Klick → Section ein-/ausklappen + document.getElementById('tab-' + tab).querySelectorAll('.section h2').forEach(h2 => { + h2.addEventListener('click', () => h2.closest('.section').classList.toggle('collapsed')); + }); + + // Tab-spezifische Initialisierung + if (tab === 'camera-npz') initCameraNpz(); + else if (tab === 'board') initBoard(); + + } catch (err) { + document.getElementById('tab-' + tab).innerHTML = + `

Panel konnte nicht geladen werden: ${err}

`; + } +} + +// ── Tab-Switching ───────────────────────────────────────────────────────────── + +document.getElementById('tabSidebar').addEventListener('click', e => { + const btn = e.target.closest('.tab-btn'); + if (!btn) return; + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); + btn.classList.add('active'); + document.getElementById('tab-' + btn.dataset.tab).classList.add('active'); + loadPanel(btn.dataset.tab, btn.dataset.src); +}); + +// Erstes Tab sofort laden +(function () { + const first = document.querySelector('.tab-btn.active'); + if (first) loadPanel(first.dataset.tab, first.dataset.src); +})(); + +// ── Shared: SSE-Stream lesen ────────────────────────────────────────────────── + +async function readSseStream(response, logFn, onDone) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + + const parts = buffer.split('\n\n'); + buffer = parts.pop(); // unvollständiges letztes Fragment behalten + + for (const part of parts) { + for (const line of part.split('\n')) { + if (!line.startsWith('data: ')) continue; + try { + const evt = JSON.parse(line.slice(6)); + if (evt.type === 'log') { if (evt.text !== '') logFn(evt.text); } + else if (evt.type === 'done') { onDone(evt); } + } catch { /* ungültiges JSON überspringen */ } + } + } + } +} + +// ── Camera NPZ ──────────────────────────────────────────────────────────────── + +function initCameraNpz() { + const logCamera = document.getElementById('log-camera'); + + function logC(msg) { + const ts = new Date().toLocaleTimeString('de-CH'); + logCamera.value += `[${ts}] ${msg}\n`; + logCamera.scrollTop = logCamera.scrollHeight; + } + + function formatDate(isoString) { + if (!isoString) return '–'; + return new Date(isoString).toLocaleString('de-CH'); + } + + function updateCalibInfo(meta) { + const sel = document.getElementById('cam-select-calib'); + if (!meta) { + document.getElementById('info-timestamp').textContent = '(keine Session vorhanden)'; + document.getElementById('info-created').textContent = '–'; + document.getElementById('info-images').textContent = '–'; + sel.innerHTML = ''; + return; + } + document.getElementById('info-timestamp').textContent = meta.timestamp ?? '–'; + document.getElementById('info-created').textContent = formatDate(meta.createdAt); + const cameras = meta.cameras ?? []; + const imgTxt = meta.imageCount != null + ? `${meta.imageCount} Bilder total. ${cameras.length} Kamera(s) verwendet.` + : '–'; + document.getElementById('info-images').textContent = imgTxt; + + const prev = sel.value; + sel.innerHTML = ''; + for (const cam of cameras) { + const opt = document.createElement('option'); + opt.value = cam; opt.textContent = cam; + if (cam === prev) opt.selected = true; + sel.appendChild(opt); + } + if (cameras.length === 1) sel.value = cameras[0]; + } + + async function loadCalibCurrent() { + try { + const r = await fetch('/api/calibration/current'); + const d = await r.json(); + updateCalibInfo(d.meta); + if (d.session) logC(`Session geladen: ${d.session}`); + else logC('Noch keine Kalibrierungs-Session vorhanden.'); + } catch (err) { + logC(`Fehler beim Laden: ${err}`); + } + } + + async function safeJson(r) { + const raw = await r.text().catch(() => ''); + try { return { ok: r.ok, status: r.status, data: JSON.parse(raw) }; } + catch { return { ok: r.ok, status: r.status, data: null, raw: raw.slice(0, 300) }; } + } + + // Aktuelle Session laden + loadCalibCurrent(); + + // "Neue Kalibrierung anlegen" + document.getElementById('btn-new-calib').addEventListener('click', async () => { + logC('Neue Kalibrierung wird angelegt …'); + try { + const r = await fetch('/api/calibration/new', { method: 'POST' }); + const d = await r.json(); + if (d.error) { logC(`Fehler: ${d.error}`); return; } + updateCalibInfo(d.meta); + if (d.warning) logC(`Warnung: ${d.warning}`); + else logC(`Session angelegt: ${d.session} | Fotos: ${(d.savedFiles ?? []).join(', ')}`); + } catch (err) { + logC(`Fehler: ${err}`); + } + }); + + // "Foto aufnehmen" + document.getElementById('btn-foto-calib').addEventListener('click', async () => { + logC('Fotos werden aufgenommen …'); + try { + const r = await fetch('/api/calibration/foto', { method: 'POST' }); + const d = await r.json(); + if (d.error) { logC(`Fehler: ${d.error}`); return; } + updateCalibInfo(d.meta); + logC(`Gespeichert: ${(d.savedFiles ?? []).join(', ')}`); + } catch (err) { + logC(`Fehler: ${err}`); + } + }); + + // "Kalibrierung berechnen" – SSE + document.getElementById('btn-compute-calib').addEventListener('click', async () => { + const camera = document.getElementById('cam-select-calib').value; + if (!camera) { logC('⚠ Bitte zuerst eine Kamera auswählen.'); return; } + logC(`Starte Kalibrierung für ${camera} …`); + const btn = document.getElementById('btn-compute-calib'); + btn.disabled = true; + try { + const response = await fetch('/api/calibration/compute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ camera }), + }); + if (!response.ok) { + const raw = await response.text().catch(() => ''); + let msg; + try { msg = JSON.parse(raw).error || raw; } + catch { msg = raw.slice(0, 300) || response.statusText; } + logC(`❌ HTTP ${response.status}: ${msg || '(kein Fehlertext – evtl. Server neu starten?)'}`); + return; + } + await readSseStream(response, logC, (evt) => { + if (evt.exitCode === 0) logC('✅ Kalibrierung abgeschlossen.'); + else logC(`❌ Script beendet mit Exit-Code ${evt.exitCode}`); + }); + } catch (err) { + logC(`Fehler: ${err}`); + } finally { + btn.disabled = false; + } + }); + + // "NPZ speichern" → an Webcam-Service übertragen + document.getElementById('btn-upload-npz').addEventListener('click', async () => { + const camera = document.getElementById('cam-select-calib').value; + if (!camera) { logC('⚠ Bitte zuerst eine Kamera auswählen.'); return; } + logC(`NPZ wird an Webcam-Service übertragen (${camera}) …`); + try { + const { ok, status, data, raw } = await safeJson(await fetch('/api/calibration/upload-npz', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ camera }), + })); + if (!ok || data?.error) { + logC(`❌ HTTP ${status}: ${data?.error ?? raw ?? '(kein Fehlertext)'}`); + return; + } + logC(`✅ Gespeichert: ${data.webcam?.saved} (${data.size} Bytes)`); + logC(` calibrationUrl: ${data.webcam?.calibrationUrl}`); + } catch (err) { + logC(`❌ Fehler: ${err}`); + } + }); +} + +// ── Board ───────────────────────────────────────────────────────────────────── + +function initBoard() { + const logBoard = document.getElementById('log-board'); + + function logB(msg) { + const ts = new Date().toLocaleTimeString('de-CH'); + logBoard.value += `[${ts}] ${msg}\n`; + logBoard.scrollTop = logBoard.scrollHeight; + } + + document.getElementById('btn-board-run').addEventListener('click', async () => { + logB('Board-Erkennung wird gestartet …'); + const btn = document.getElementById('btn-board-run'); + btn.disabled = true; + try { + const response = await fetch('/api/board/run', { method: 'POST' }); + 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}`; } + logB(`❌ HTTP ${response.status}: ${msg}`); + return; + } + await readSseStream(response, logB, (evt) => { + if (evt.exitCode === 0) { + logB('✅ Board-Run abgeschlossen.'); + if (evt.runDir) document.getElementById('board-last-run').textContent = evt.runDir; + } else { + logB(`❌ Beendet mit Exit-Code ${evt.exitCode}`); + } + }); + } catch (err) { + logB(`❌ Fehler: ${err}`); + } finally { + btn.disabled = false; + } + }); +} diff --git a/public/calibration_arm.html b/public/calibration_arm.html new file mode 100644 index 0000000..91e599d --- /dev/null +++ b/public/calibration_arm.html @@ -0,0 +1,28 @@ +
+ +
+

Arm1 / Arm2 offen

+
+ Ziel: Nullposition und Kinematikparameter von Arm1 und Arm2 einmessen. + Arm fährt in mechanische Nullposition, Kamera prüft die tatsächliche Pose, + Offset wird berechnet und gespeichert.

+ Geplante Aktionen: Arm1 → Nullpos → Foto → Winkel · Arm2 → Nullpos → Foto → + Winkel · Offset-Korrektur speichern.

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

Ausgabe / Log

+ +
+ +
diff --git a/public/calibration_board.html b/public/calibration_board.html new file mode 100644 index 0000000..d1d7d88 --- /dev/null +++ b/public/calibration_board.html @@ -0,0 +1,28 @@ +
+ +
+

Board – ArUco & Kamera-Pose

+
+ Ablauf + + Foto aufnehmen → ArUco erkennen → Kamera-Pose schätzen + + Schritte + + 1_detect_aruco_observations  →  2_estimate_camera_from_observations + + Letzter Run + +
+
+ + +
+
+ +
+

Ausgabe / Log

+ +
+ +
diff --git a/public/calibration_camera.html b/public/calibration_camera.html new file mode 100644 index 0000000..169216a --- /dev/null +++ b/public/calibration_camera.html @@ -0,0 +1,40 @@ +
+ + +
+

Aktuelle Kalibrierung

+
+ Timestamp + + + Erstellt am + + + Bilder / Kameras + +
+
+ + +
+

Aktionen

+
+ + + + + + + +
+
+ + +
+

Ausgabe / Log

+ +
+ +
diff --git a/public/calibration_xaxis.html b/public/calibration_xaxis.html new file mode 100644 index 0000000..da3c413 --- /dev/null +++ b/public/calibration_xaxis.html @@ -0,0 +1,28 @@ +
+ +
+

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. +
+
+ + + + + + +
+
+ +
+

Ausgabe / Log

+ +
+ +
diff --git a/public/sceneViewer.html b/public/sceneViewer.html new file mode 100644 index 0000000..e74c287 --- /dev/null +++ b/public/sceneViewer.html @@ -0,0 +1,995 @@ + + + + +Robot FK Viewer + + + + + + + + +
+ +
+ + + +