Files
appRobotHoming/public/calibration.html
2026-06-10 11:45:41 +02:00

517 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kalibrierung appRobotHoming</title>
<link rel="stylesheet" href="/styles.css">
<style>
/* ===== 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;
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="calib-topbar">
<a href="/">← Zurück</a>
<h1>Kalibrierung</h1>
</div>
<!-- SIDEBAR + CONTENT -->
<div class="calib-body">
<!-- LESEZEICHEN-TABS -->
<nav class="tab-sidebar" id="tabSidebar">
<button class="tab-btn active" data-tab="camera-npz">Camera NPZ</button>
<button class="tab-btn" data-tab="board">Board</button>
<button class="tab-btn" data-tab="robot-x-axis">Robot X Axis</button>
<button class="tab-btn" data-tab="arm">Arm1 / Arm2</button>
</nav>
<!-- CONTENT -->
<div class="tab-content">
<!-- PANEL: Camera NPZ -->
<div class="tab-panel active" id="tab-camera-npz">
<div class="sections">
<!-- Info-Box: Aktuelle Kalibrierung -->
<div class="section full">
<h2>Aktuelle Kalibrierung</h2>
<div id="calib-info" class="info-grid">
<span class="info-label">Timestamp</span>
<span class="info-value" id="info-timestamp"></span>
<span class="info-label">Erstellt am</span>
<span class="info-value" id="info-created"></span>
<span class="info-label">Bilder / Kameras</span>
<span class="info-value" id="info-images"></span>
</div>
</div>
<!-- Aktionen -->
<div class="section full">
<h2>Aktionen</h2>
<div class="controls" style="margin-top: 14px;">
<button id="btn-new-calib">Neue Kalibrierung anlegen</button>
<button id="btn-foto-calib">Foto aufnehmen</button>
<select id="cam-select-calib" title="Kamera für Kalibrierung wählen">
<option value=""> Kamera </option>
</select>
<button id="btn-compute-calib">Kalibrierung berechnen</button>
<button disabled title="Folgt später">NPZ speichern</button>
</div>
</div>
<!-- Ausgabe -->
<div class="section full">
<h2>Ausgabe / Log</h2>
<textarea id="log-camera" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
</div>
</div>
</div>
<!-- PANEL: Board -->
<div class="tab-panel" id="tab-board">
<div class="sections">
<div class="section full">
<h2>Board <span class="status-badge open">offen</span></h2>
<div class="placeholder-note">
Ziel: Extrinsische Position des Marker-Boards im Kamera-Koordinatensystem bestimmen
(Rotations- und Translationsvektor via <code>solvePnP</code>).<br><br>
Geplante Aktionen: Kamerabild mit erkannten Markern anzeigen · Pose berechnen ·
Kalibrierungsdatei speichern.<br><br>
<em>Aktionen werden ergänzt sobald das Konzept feststeht.</em>
</div>
<div class="controls" style="margin-top: 14px;">
<button disabled>Foto aufnehmen</button>
<button disabled>Marker erkennen</button>
<button disabled>Pose berechnen</button>
<button disabled>Board-Pose speichern</button>
</div>
</div>
<div class="section full">
<h2>Ausgabe / Log</h2>
<textarea id="log-board" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
</div>
</div>
</div>
<!-- PANEL: Robot X Axis -->
<div class="tab-panel" id="tab-robot-x-axis">
<div class="sections">
<div class="section full">
<h2>Robot X Axis <span class="status-badge open">offen</span></h2>
<div class="placeholder-note">
Ziel: X-Achse des Roboters im Weltkoordinatensystem verorten (Richtungsvektor und
Nullpunkt). Roboter fährt zwei bekannte Positionen an, Kamera beobachtet den
Endeffektor-Marker.<br><br>
Geplante Aktionen: Referenzposition 1 anfahren · Foto · Marker merken ·
Referenzposition 2 anfahren · Foto · Achsvektor berechnen · Speichern.<br><br>
<em>Aktionen werden ergänzt sobald das Konzept feststeht.</em>
</div>
<div class="controls" style="margin-top: 14px;">
<button disabled>Pos 1 anfahren</button>
<button disabled>Foto Pos 1</button>
<button disabled>Pos 2 anfahren</button>
<button disabled>Foto Pos 2</button>
<button disabled>Achse berechnen</button>
<button disabled>Speichern</button>
</div>
</div>
<div class="section full">
<h2>Ausgabe / Log</h2>
<textarea id="log-xaxis" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
</div>
</div>
</div>
<!-- PANEL: Arm1 / Arm2 -->
<div class="tab-panel" id="tab-arm">
<div class="sections">
<div class="section full">
<h2>Arm1 / Arm2 <span class="status-badge open">offen</span></h2>
<div class="placeholder-note">
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.<br><br>
Geplante Aktionen: Arm1 → Nullpos → Foto → Winkel · Arm2 → Nullpos → Foto →
Winkel · Offset-Korrektur speichern.<br><br>
<em>Aktionen werden ergänzt sobald das Konzept feststeht.</em>
</div>
<div class="controls" style="margin-top: 14px;">
<button disabled>Arm1 → Nullpos</button>
<button disabled>Foto Arm1</button>
<button disabled>Arm2 → Nullpos</button>
<button disabled>Foto Arm2</button>
<button disabled>Offsets berechnen</button>
<button disabled>Speichern</button>
</div>
</div>
<div class="section full">
<h2>Ausgabe / Log</h2>
<textarea id="log-arm" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
</div>
</div>
</div>
</div><!-- /.tab-content -->
</div><!-- /.calib-body -->
<script>
// ── 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');
});
// ── Section collapse ───────────────────────────────────────────────────────
document.querySelectorAll('.section h2').forEach(h2 => {
h2.addEventListener('click', () => h2.closest('.section').classList.toggle('collapsed'));
});
// ── Camera NPZ ─────────────────────────────────────────────────────────────
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 = '';
// Selector leeren
sel.innerHTML = '<option value=""> Kamera </option>';
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;
// Kamera-Selector aktualisieren
const prev = sel.value;
sel.innerHTML = '<option value=""> Kamera </option>';
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);
}
// Falls nur eine Kamera vorhanden automatisch vorwählen
if (cameras.length === 1) sel.value = cameras[0];
}
// Beim Laden aktuelle Session holen
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}`);
}
}
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-Stream lesen
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 err = await response.json().catch(() => ({ error: response.statusText }));
logC(`Fehler: ${err.error}`);
return;
}
// SSE-Stream zeilenweise verarbeiten
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 });
// Vollständige SSE-Ereignisse (getrennt durch \n\n) extrahieren
const parts = buffer.split('\n\n');
buffer = parts.pop(); // letztes unvollständiges 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 !== '') logC(evt.text);
} else if (evt.type === 'done') {
logC(evt.exitCode === 0
? '✅ Kalibrierung abgeschlossen.'
: `❌ Script beendet mit Exit-Code ${evt.exitCode}`);
}
} catch { /* ungültiges JSON überspringen */ }
}
}
}
} catch (err) {
logC(`Fehler: ${err}`);
} finally {
btn.disabled = false;
}
});
</script>
</body>
</html>