Phase 0, 1, 2

This commit is contained in:
chk
2026-06-14 16:58:45 +02:00
parent 0cb2bae554
commit dd8de5674d
6 changed files with 773 additions and 119 deletions

280
public/homing.js Normal file
View 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}&thinsp;${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);