Phase 0, 1, 2
This commit is contained in:
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);
|
||||
Reference in New Issue
Block a user