Files
appRobotHoming/public/homing.js
2026-06-14 16:58:45 +02:00

281 lines
10 KiB
JavaScript
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.
'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);