'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 += `
${label} ${valStr} ${unit}
`; } resultTree.innerHTML = html || ''; } /** 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 = `${hdrs.map(h => `${h}`).join('')}`; for (const m of markers) { const pos = m.position_mm ?? [null, null, null]; const fmt = v => (v != null ? Number(v).toFixed(1) : '–'); html += ` ${m.marker_id} ${m.link ?? '–'} ${m.set ?? '–'} ${fmt(pos[0])} ${fmt(pos[1])} ${fmt(pos[2])} ${m.num_cameras ?? '–'} `; } html += ''; 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 = 'Keine Bilder vorhanden.'; } } 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 = '(Ergebnis erscheint hier)'; csvInfo.textContent = ''; csvTable.innerHTML = ''; snapshots.innerHTML = ''; _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);