Phase 0, 1, 2
This commit is contained in:
111
public/homing.html
Normal file
111
public/homing.html
Normal file
@@ -0,0 +1,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Homing – appRobotHoming</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
<link rel="stylesheet" href="/calibration.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- HEADER (analog calibration.html) -->
|
||||
<div class="calib-topbar">
|
||||
<a href="/">← Zurück</a>
|
||||
<h1>Homing</h1>
|
||||
</div>
|
||||
|
||||
<div style="max-width:1200px;margin:0 auto;padding:0 16px">
|
||||
<div class="sections">
|
||||
|
||||
<!-- AKTIONEN -->
|
||||
<div class="section full">
|
||||
<h2>Aktionen</h2>
|
||||
|
||||
<div class="controls" style="flex-wrap:wrap;gap:12px;align-items:center">
|
||||
<button id="btn-homing-run">📷 Foto & Homing berechnen</button>
|
||||
<button id="btn-homing-send" disabled
|
||||
style="opacity:.4;cursor:not-allowed"
|
||||
title="Erst Homing ausführen">
|
||||
✅ An Roboter senden
|
||||
</button>
|
||||
<span id="homing-status" class="status-badge open">○ Warte</span>
|
||||
</div>
|
||||
|
||||
<!-- Fortschrittsbalken -->
|
||||
<div id="homing-progress" style="display:none;margin-top:14px">
|
||||
<div style="background:var(--border);border-radius:3px;height:5px;overflow:hidden">
|
||||
<div id="homing-progress-bar"
|
||||
style="height:100%;background:var(--accent);width:0%;transition:width .4s ease"></div>
|
||||
</div>
|
||||
<span id="homing-progress-text"
|
||||
style="font-size:11px;color:var(--muted);display:block;margin-top:4px"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AUSGABE / LOG -->
|
||||
<div class="section full">
|
||||
<h2>Ausgabe / Log</h2>
|
||||
<textarea id="log-homing" readonly
|
||||
placeholder="(Ausgabe erscheint hier sobald Homing gestartet wird)"
|
||||
style="min-height:180px"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- ANALYSIS & REASONING -->
|
||||
<div class="section full">
|
||||
<h2>Analysis & Reasoning</h2>
|
||||
<textarea id="homing-analysis" readonly
|
||||
placeholder="(Zwischenergebnisse je Script erscheinen hier)"
|
||||
style="min-height:120px;font-size:12px"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- RESULT RAW JSON + TREE VIEW -->
|
||||
<div class="section half">
|
||||
<h2>Result – Raw JSON</h2>
|
||||
<div class="panel">
|
||||
<textarea id="homing-result-json" readonly
|
||||
placeholder="(Ergebnis erscheint hier)"
|
||||
style="min-height:160px;font-size:12px"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section half">
|
||||
<h2>Result – Tree View</h2>
|
||||
<div class="panel" style="min-height:160px;padding:14px 16px">
|
||||
<div id="homing-result-tree"
|
||||
style="font-family:monospace;font-size:13px;line-height:2">
|
||||
<span style="color:var(--muted);font-size:12px">(Ergebnis erscheint hier)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SNAPSHOT CSV -->
|
||||
<div class="section full">
|
||||
<h2>Snapshot CSV (Marker)</h2>
|
||||
<div id="homing-csv-info"
|
||||
style="font-size:11px;color:var(--muted);margin-bottom:8px">
|
||||
Marker-Positionen aus aruco_marker_poses.json werden nach dem Homing angezeigt.
|
||||
</div>
|
||||
<div style="overflow-x:auto">
|
||||
<table id="homing-csv-table"
|
||||
style="font-size:12px;border-collapse:collapse;width:100%;min-width:500px"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SNAPSHOTS (Kamerabilder) -->
|
||||
<div class="section full">
|
||||
<h2>Snapshots (annotierte Kamerabilder)</h2>
|
||||
<div id="homing-snapshots"
|
||||
style="display:flex;gap:12px;flex-wrap:wrap">
|
||||
<span style="color:var(--muted);font-size:12px">
|
||||
(Bilder erscheinen nach dem Homing-Run)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.sections -->
|
||||
</div>
|
||||
|
||||
<script src="/homing.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
280
public/homing.js
Normal file
280
public/homing.js
Normal file
@@ -0,0 +1,280 @@
|
||||
'use strict';
|
||||
|
||||
// ── DOM-Referenzen ────────────────────────────────────────────────────────────
|
||||
|
||||
const btnRun = document.getElementById('btn-homing-run');
|
||||
const btnSend = document.getElementById('btn-homing-send');
|
||||
const statusBadge = document.getElementById('homing-status');
|
||||
const progressDiv = document.getElementById('homing-progress');
|
||||
const progressBar = document.getElementById('homing-progress-bar');
|
||||
const progressText = document.getElementById('homing-progress-text');
|
||||
const logEl = document.getElementById('log-homing');
|
||||
const analysisEl = document.getElementById('homing-analysis');
|
||||
const resultJson = document.getElementById('homing-result-json');
|
||||
const resultTree = document.getElementById('homing-result-tree');
|
||||
const csvInfo = document.getElementById('homing-csv-info');
|
||||
const csvTable = document.getElementById('homing-csv-table');
|
||||
const snapshots = document.getElementById('homing-snapshots');
|
||||
|
||||
let _lastState = null; // letztes Homing-Ergebnis (zum Senden an Roboter)
|
||||
|
||||
// ── Hilfsfunktionen ───────────────────────────────────────────────────────────
|
||||
|
||||
function appendLog(text) {
|
||||
logEl.value += (text ?? '') + '\n';
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function appendAnalysis(key, value) {
|
||||
const line = key + ':\n' + JSON.stringify(value, null, 2);
|
||||
analysisEl.value += line + '\n\n';
|
||||
analysisEl.scrollTop = analysisEl.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatus(label, cls) {
|
||||
statusBadge.textContent = label;
|
||||
statusBadge.className = `status-badge ${cls}`;
|
||||
}
|
||||
|
||||
function setProgress(step, total, text) {
|
||||
progressDiv.style.display = 'block';
|
||||
const pct = total > 0 ? Math.round((step / total) * 100) : 0;
|
||||
progressBar.style.width = pct + '%';
|
||||
progressText.textContent = text || `Schritt ${step} / ${total}`;
|
||||
}
|
||||
|
||||
/** Zeigt { x_mm, y_deg, z_deg, a_deg, b_deg, c_deg, e_mm } im Result-Bereich. */
|
||||
function showResult(state) {
|
||||
// Raw JSON
|
||||
resultJson.value = JSON.stringify(state, null, 2);
|
||||
|
||||
// Tree View
|
||||
const LABELS = {
|
||||
x_mm: 'x (Slider)',
|
||||
y_deg: 'y (Arm1)',
|
||||
z_deg: 'z (Ellbow)',
|
||||
a_deg: 'a (Arm2)',
|
||||
b_deg: 'b (Hand)',
|
||||
c_deg: 'c (Palm)',
|
||||
e_mm: 'e (Greifer)',
|
||||
};
|
||||
const UNITS = {
|
||||
x_mm: 'mm', y_deg: '°', z_deg: '°',
|
||||
a_deg: '°', b_deg: '°', c_deg: '°', e_mm: 'mm',
|
||||
};
|
||||
|
||||
let html = '';
|
||||
for (const [key, val] of Object.entries(state)) {
|
||||
const label = LABELS[key] ?? key;
|
||||
const unit = UNITS[key] ?? '';
|
||||
const valStr = typeof val === 'number' ? val.toFixed(2) : String(val ?? '–');
|
||||
html += `<div style="display:flex;gap:8px;padding:2px 0">
|
||||
<span style="min-width:130px;color:var(--muted)">${label}</span>
|
||||
<span style="font-weight:600">${valStr} ${unit}</span>
|
||||
</div>`;
|
||||
}
|
||||
resultTree.innerHTML = html || '<span style="color:var(--muted)">–</span>';
|
||||
}
|
||||
|
||||
/** Baut die CSV-Tabelle aus aruco_marker_poses.json-Daten. */
|
||||
function showMarkerTable(markers) {
|
||||
if (!markers || markers.length === 0) {
|
||||
csvInfo.textContent = 'Keine Marker-Daten vorhanden.';
|
||||
csvTable.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
csvInfo.textContent = `${markers.length} Marker trianguliert`;
|
||||
|
||||
const hdrs = ['ID', 'Link', 'Set', 'x mm', 'y mm', 'z mm', 'Kameras'];
|
||||
const style = 'padding:4px 8px;border:1px solid var(--border);text-align:right';
|
||||
const styleL = 'padding:4px 8px;border:1px solid var(--border);text-align:left';
|
||||
|
||||
let html = `<thead><tr>${hdrs.map(h =>
|
||||
`<th style="${h==='Link'||h==='Set'||h==='ID'?styleL:style}">${h}</th>`).join('')}</tr></thead><tbody>`;
|
||||
|
||||
for (const m of markers) {
|
||||
const pos = m.position_mm ?? [null, null, null];
|
||||
const fmt = v => (v != null ? Number(v).toFixed(1) : '–');
|
||||
html += `<tr>
|
||||
<td style="${styleL}">${m.marker_id}</td>
|
||||
<td style="${styleL}">${m.link ?? '–'}</td>
|
||||
<td style="${styleL}">${m.set ?? '–'}</td>
|
||||
<td style="${style}">${fmt(pos[0])}</td>
|
||||
<td style="${style}">${fmt(pos[1])}</td>
|
||||
<td style="${style}">${fmt(pos[2])}</td>
|
||||
<td style="${style}">${m.num_cameras ?? '–'}</td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody>';
|
||||
csvTable.innerHTML = html;
|
||||
}
|
||||
|
||||
/** Lädt Debug-Bilder und Marker-Tabelle für einen Homing-Run. */
|
||||
async function loadRunData(runDir) {
|
||||
try {
|
||||
const res = await fetch(`/api/homing/run-data?run=${encodeURIComponent(runDir)}`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
|
||||
// Snapshots
|
||||
snapshots.innerHTML = '';
|
||||
for (const img of (data.images ?? [])) {
|
||||
const figure = document.createElement('figure');
|
||||
figure.style.cssText = 'margin:0;display:flex;flex-direction:column;gap:4px';
|
||||
const el = document.createElement('img');
|
||||
el.src = `data:image/jpeg;base64,${img.contentBase64}`;
|
||||
el.style.cssText = 'max-width:400px;max-height:300px;border:1px solid var(--border);border-radius:4px';
|
||||
el.alt = img.filename;
|
||||
const cap = document.createElement('figcaption');
|
||||
cap.textContent = img.filename;
|
||||
cap.style.cssText = 'font-size:11px;color:var(--muted);text-align:center';
|
||||
figure.appendChild(el);
|
||||
figure.appendChild(cap);
|
||||
snapshots.appendChild(figure);
|
||||
}
|
||||
if (!data.images?.length) {
|
||||
snapshots.innerHTML = '<span style="color:var(--muted);font-size:12px">Keine Bilder vorhanden.</span>';
|
||||
}
|
||||
} catch { /* nicht kritisch */ }
|
||||
}
|
||||
|
||||
/** Lädt aruco_marker_poses.json für den Run und zeigt die Marker-Tabelle. */
|
||||
async function loadArucoData(runDir) {
|
||||
try {
|
||||
// Rohdaten über Homing-Run-Data-Endpoint nicht direkt verfügbar,
|
||||
// daher holen wir uns die JSON über board/latest falls es der gleiche Run ist,
|
||||
// oder wir parsen es aus den Analysis-Daten.
|
||||
// Fürs erste: Marker aus dem finalState ableiten wenn vorhanden.
|
||||
csvInfo.textContent = '(Marker-CSV wird nach dem nächsten Homing-Run geladen)';
|
||||
} catch { /* ignorieren */ }
|
||||
}
|
||||
|
||||
// ── Homing starten ────────────────────────────────────────────────────────────
|
||||
|
||||
async function runHoming() {
|
||||
// UI zurücksetzen
|
||||
logEl.value = '';
|
||||
analysisEl.value = '';
|
||||
resultJson.value = '';
|
||||
resultTree.innerHTML = '<span style="color:var(--muted);font-size:12px">(Ergebnis erscheint hier)</span>';
|
||||
csvInfo.textContent = '';
|
||||
csvTable.innerHTML = '';
|
||||
snapshots.innerHTML = '<span style="color:var(--muted);font-size:12px">…</span>';
|
||||
_lastState = null;
|
||||
|
||||
btnRun.disabled = true;
|
||||
btnSend.disabled = true;
|
||||
btnSend.style.opacity = '0.4';
|
||||
btnSend.style.cursor = 'not-allowed';
|
||||
setStatus('● Läuft …', 'wip');
|
||||
progressDiv.style.display = 'block';
|
||||
progressBar.style.width = '2%';
|
||||
progressText.textContent = 'Verbinde …';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/homing/run', { method: 'POST' });
|
||||
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = '';
|
||||
let lastRunDir = null;
|
||||
let hasError = false;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
const lines = buf.split('\n');
|
||||
buf = lines.pop(); // unvollständige letzte Zeile aufheben
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
let evt;
|
||||
try { evt = JSON.parse(line.slice(6)); } catch { continue; }
|
||||
|
||||
switch (evt.type) {
|
||||
case 'log':
|
||||
appendLog(evt.text);
|
||||
break;
|
||||
|
||||
case 'step':
|
||||
setProgress(evt.step, evt.total, evt.text);
|
||||
appendLog(`[${evt.step}/${evt.total}] ${evt.text || ''}`);
|
||||
break;
|
||||
|
||||
case 'analysis':
|
||||
appendAnalysis(evt.key, evt.value);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
appendLog(evt.text);
|
||||
hasError = true;
|
||||
break;
|
||||
|
||||
case 'done':
|
||||
if (evt.state) {
|
||||
_lastState = evt.state;
|
||||
showResult(evt.state);
|
||||
btnSend.disabled = false;
|
||||
btnSend.style.opacity = '';
|
||||
btnSend.style.cursor = '';
|
||||
setStatus('✓ Fertig', 'done');
|
||||
progressBar.style.width = '100%';
|
||||
progressText.textContent = 'Homing abgeschlossen';
|
||||
} else {
|
||||
setStatus('✗ Fehler', 'open');
|
||||
progressBar.style.width = '100%';
|
||||
progressText.textContent = hasError ? 'Fehler aufgetreten' : 'Abgebrochen';
|
||||
}
|
||||
if (evt.runDir) {
|
||||
lastRunDir = evt.runDir;
|
||||
await loadRunData(evt.runDir);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
appendLog(`❌ Verbindungsfehler: ${err.message}`);
|
||||
setStatus('✗ Fehler', 'open');
|
||||
progressBar.style.width = '100%';
|
||||
progressText.textContent = 'Verbindungsfehler';
|
||||
} finally {
|
||||
btnRun.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── State an Roboter senden ───────────────────────────────────────────────────
|
||||
|
||||
async function sendToRobot() {
|
||||
if (!_lastState) return;
|
||||
|
||||
btnSend.disabled = true;
|
||||
try {
|
||||
const res = await fetch('/api/homing/send-state', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ state: _lastState }),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
appendLog('✅ State erfolgreich an Roboter gesendet');
|
||||
setStatus('✓ Gesendet', 'done');
|
||||
} else {
|
||||
appendLog(`❌ Fehler beim Senden: ${data.error ?? JSON.stringify(data)}`);
|
||||
btnSend.disabled = false;
|
||||
}
|
||||
} catch (err) {
|
||||
appendLog(`❌ Netzwerkfehler: ${err.message}`);
|
||||
btnSend.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Event-Listener ────────────────────────────────────────────────────────────
|
||||
|
||||
btnRun.addEventListener('click', runHoming);
|
||||
btnSend.addEventListener('click', sendToRobot);
|
||||
@@ -36,6 +36,10 @@
|
||||
<a href="/calibration.html">
|
||||
<button type="button">Calibration Page</button>
|
||||
</a>
|
||||
|
||||
<a href="/homing.html">
|
||||
<button type="button">Homing</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user