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