Files
appRobotHoming/public/client.js
2026-06-14 18:24:12 +02:00

616 lines
19 KiB
JavaScript
Executable File
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.
// client.js
// UI: Buttons, Anzeige von Result als JSON + Baum, Fallback für Commands
function appendLog(line) {
const el = document.getElementById("log");
if (!el) return;
const now = new Date().toISOString();
el.value += `[${now}] ${line}\n`;
el.scrollTop = el.scrollHeight;
}
function clearTextarea(id) {
const el = document.getElementById(id);
if (el) el.value = "";
}
function clearElement(id) {
const el = document.getElementById(id);
if (el) el.innerHTML = "";
}
function formatScalar(value) {
if (value === null) return "null";
if (value === undefined) return "undefined";
if (typeof value === "string") return JSON.stringify(value);
if (typeof value === "number" && Number.isFinite(value)) return String(value);
if (typeof value === "boolean") return String(value);
return String(value);
}
function renderTree(container, value, key = "result", open = true) {
if (!container) return;
container.innerHTML = "";
container.appendChild(renderNode(key, value, open));
}
function renderNode(key, value, open = false) {
const isObject = value !== null && typeof value === "object";
if (!isObject) {
const leaf = document.createElement("div");
leaf.className = "tree-leaf";
leaf.textContent = `${key}: ${formatScalar(value)}`;
return leaf;
}
const details = document.createElement("details");
details.open = open;
const summary = document.createElement("summary");
summary.textContent = Array.isArray(value)
? `${key} [${value.length}]`
: key;
details.appendChild(summary);
const body = document.createElement("div");
body.style.marginLeft = "16px";
if (Array.isArray(value)) {
value.forEach((item, idx) => {
body.appendChild(renderNode(String(idx), item, false));
});
} else {
Object.entries(value).forEach(([childKey, childVal]) => {
body.appendChild(renderNode(childKey, childVal, false));
});
}
details.appendChild(body);
return details;
}
function renderResult(result) {
const jsonEl = document.getElementById("result-json");
const treeEl = document.getElementById("result-tree");
if (jsonEl) {
jsonEl.value = JSON.stringify(result, null, 2);
}
renderTree(treeEl, result, "result", true);
}
dataCache = null
headersCache = null
rowsCache = null
timeCache = null
jsonCache = null
async function fetchCSV(){
if(dataCache == null || headersCache == null || rowsCache == null || timeCache == null){
({ data: dataCache, headers: headersCache, rows: rowsCache } = await fetchCSV_fromServer());
timeCache = Date.now();
}
if (Date.now() - timeCache > 1000){
({ data: dataCache, headers: headersCache, rows: rowsCache } = await fetchCSV_fromServer());
timeCache = Date.now();
}
return {data: dataCache, headers: headersCache, rows: rowsCache}
}
async function fetchCSV_fromServer() {
const res = await fetch("/api/latest-snapshot");
if (!res.ok) throw new Error("Fehler beim Laden des Snapshots");
let data;
if (res.headers.get("content-type")?.includes("application/json")) {
data = await res.json();
} else {
const csvData = await res.text();
data = {
filename: "latest.csv",
mtime: new Date().toISOString(),
content: csvData
};
}
if (!jsonCache && data.jsonFile) {
jsonCache = JSON.parse(data.jsonFile.content);
}
const lines = data.content.trim().split(/\r?\n/).filter(Boolean);
if (lines.length < 2) {
throw new Error("Keine oder unvollständige Daten");
}
const headers = lines[0].split(",").map(h => h.trim());
const rows = lines.slice(1).map(line => {
const cells = line.split(",");
const obj = {};
headers.forEach((h, i) => {
const raw = (cells[i] ?? "").trim();
const numeric = Number(raw);
obj[h] = raw !== "" && Number.isFinite(numeric) ? numeric : raw;
});
return obj;
});
return { data, headers, rows };
}
async function fetchWebcamSnapshotData() {
const { data, headers, rows } = await fetchCSV();
return {
filename: data.filename,
mtime: data.mtime,
headers,
rows,
robotIntrinsics: jsonCache,
imageFile: data.imageFile,
image2: data.image2
};
}
async function sendToBodyTracker({imageFile, image2, robotIntrinsics}) {
const response = await fetch('/api/estimate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imageFile, image2, robotIntrinsics })
});
if (!response.ok) {
const message = await response.text();
throw new Error(`BodyTracker fehlgeschlagen (${response.status}): ${message}`);
}
return await response.json();
}
async function renderSnapshot() {
const table = document.getElementById("snapshot-table");
const pictureEl = document.getElementById("snapshot-info-picture");
if (!table) return;
try {
const { data, headers, rows } = await fetchCSV();
// Info anzeigen
const infoEl = document.getElementById("snapshot-info");
if (infoEl) {
infoEl.textContent = `Datei: ${data.filename}, Geändert: ${new Date(data.mtime).toLocaleString()}, Zeilen: ${rows.length}`;
}
// Bild anzeigen, falls vorhanden
if (pictureEl) {
let imagesHtml = '';
if (data.imageFile) {
imagesHtml += `<img src="data:${data.imageFile.mimeType};base64,${data.imageFile.contentBase64}" alt="${data.imageFile.filename}" style="max-width: calc(50% - 2.5px); height: auto;">`;
}
if (data.image2) {
imagesHtml += `<img src="data:${data.image2.mimeType};base64,${data.image2.contentBase64}" alt="${data.image2.filename}" style="max-width: calc(50% - 2.5px); height: auto;">`;
}
if (imagesHtml) {
pictureEl.innerHTML = `<div style="display: flex; gap: 5px;">${imagesHtml}</div>`;
} else {
pictureEl.innerHTML = "";
}
}
// Tabelle leeren
table.innerHTML = "";
// Header
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
headers.forEach(h => {
const th = document.createElement("th");
th.textContent = h;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Body
const tbody = document.createElement("tbody");
rows.forEach(row => {
const tr = document.createElement("tr");
headers.forEach(h => {
const td = document.createElement("td");
let value = row[h];
if (typeof value === 'number') {
if (h === 'id' || h === 'seen_by') {
value = Math.round(value);
} else {
value = value.toFixed(1);
}
}
td.textContent = value;
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
} catch (err) {
console.error("Fehler beim Rendern des Snapshots:", err);
}
}
async function onCalculateClick() {
clearTextarea("analysis-log");
clearTextarea("result-json");
clearElement("result-tree");
clearElement("snapshot-table");
appendLog("Starte Berechnung...");
try {
const response = await fetch("robot.json");
const robot = await response.json();
const snapshot = await fetchWebcamSnapshotData();
appendLog(`Snapshot geladen: ${snapshot.filename} (${snapshot.rows.length} Zeilen)`);
let result = null;
try {
result = await sendToBodyTracker(snapshot);
appendLog("BodyTracker wurde aufgerufen.");
} catch (err) {
appendLog(`BodyTracker nicht aufgerufen: ${err.message}`);
}
if (!result) {
appendLog("Fallback: lokale Pose-Berechnung mit calculateAngles.js");
result = await window.calculate(snapshot.robotIntrinsics, robot);
}
renderResult(result);
await renderSnapshot();
appendLog("Result angezeigt.");
} catch (err) {
appendLog(`Fehler: ${err.message}`);
}
}
// ── Foto ─────────────────────────────────────────────────────────────────────
/**
* Foto-Button: holt HD-JPEGs aller 3 Kameras parallel
* und zeigt sie nebeneinander im Snapshot-Bereich an.
*/
async function onFotoClick() {
const display = document.getElementById('snapshot-info-picture');
if (!display) return;
const camIds = ['cam0', 'cam1', 'cam2'];
const labels = { cam0: 'front', cam1: 'left', cam2: 'right' };
appendLog('Foto: alle Kameras …');
const t = Date.now();
// Wrapper mit 3 Spalten
const wrapper = document.createElement('div');
wrapper.style.cssText = 'display:flex; gap:6px; align-items:flex-start;';
camIds.forEach(id => {
const col = document.createElement('div');
col.style.cssText = 'flex:1 1 0; min-width:0;';
const caption = document.createElement('div');
caption.style.cssText = 'font-size:0.8em; margin-bottom:3px; color:#aaa;';
caption.textContent = `${id} (${labels[id]})`;
const img = document.createElement('img');
img.style.cssText = 'width:100%; height:auto; display:block;';
img.alt = id;
img.onload = () => appendLog(` ${id}: ${img.naturalWidth}×${img.naturalHeight}px`);
img.onerror = () => appendLog(` ${id}: Fehler`);
img.src = `/api/webcam/snapshot/${id}?t=${t}`;
col.appendChild(caption);
col.appendChild(img);
wrapper.appendChild(col);
});
display.innerHTML = '';
display.appendChild(wrapper);
}
// ── Homing ────────────────────────────────────────────────────────────────────
let _homingState = null;
function setHomingStatus(label, cls) {
const el = document.getElementById('homing-status');
if (!el) return;
el.textContent = label;
el.className = `status-badge ${cls}`;
}
function setHomingProgress(step, total, text) {
const wrap = document.getElementById('homing-progress');
const bar = document.getElementById('homing-progress-bar');
const txt = document.getElementById('homing-progress-text');
if (!wrap) return;
wrap.style.display = 'block';
if (bar) bar.style.width = (total > 0 ? Math.round(step / total * 100) : 0) + '%';
if (txt) txt.textContent = text || `Schritt ${step} / ${total}`;
}
function showHomingResult(state) {
// Raw JSON
const jsonEl = document.getElementById('result-json');
if (jsonEl) jsonEl.value = JSON.stringify(state, null, 2);
// Tree View: Labels + Einheiten statt generischem renderTree
const treeEl = document.getElementById('result-tree');
if (treeEl) {
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:3px 0;font-family:ui-monospace,monospace;font-size:13px">
<span style="min-width:130px;color:#94a3b8">${label}</span>
<span style="font-weight:600">${valStr}&thinsp;${unit}</span>
</div>`;
}
treeEl.innerHTML = html || '<span style="color:#64748b"></span>';
}
}
async function loadHomingImages(runDir) {
const display = document.getElementById('snapshot-info-picture');
if (!display || !runDir) return;
try {
const res = await fetch(`/api/homing/run-data?run=${encodeURIComponent(runDir)}`);
if (!res.ok) return;
const data = await res.json();
const debugImages = (data.images ?? []).filter(img => /debug/i.test(img.filename));
let html = '<div style="display:flex;flex-direction:column;gap:12px;margin-top:8px">';
for (const img of debugImages) {
html += `<figure style="margin:0">
<img src="data:image/jpeg;base64,${img.contentBase64}"
style="width:100%;max-width:760px;height:auto;border:1px solid #1f2937;border-radius:4px;display:block"
alt="${img.filename}">
<figcaption style="font-size:11px;color:#64748b;margin-top:3px">
${img.filename}
</figcaption>
</figure>`;
}
html += '</div>';
display.innerHTML = html;
} catch { /* nicht kritisch */ }
}
async function runHoming() {
// UI zurücksetzen
clearTextarea('log');
clearTextarea('analysis-log');
clearTextarea('result-json');
clearElement('result-tree');
clearElement('snapshot-table');
clearElement('snapshot-info-picture');
const btnRun = document.getElementById('btn-homing-run');
const btnSend = document.getElementById('btn-homing-send');
_homingState = null;
if (btnRun) btnRun.disabled = true;
if (btnSend) {
btnSend.disabled = true;
btnSend.style.opacity = '.4';
btnSend.style.cursor = 'not-allowed';
}
setHomingStatus('● Läuft …', 'wip');
setHomingProgress(0, 6, '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 = '';
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();
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':
setHomingProgress(evt.step, evt.total, evt.text);
appendLog(`[${evt.step}/${evt.total}] ${evt.text || ''}`);
break;
case 'analysis': {
const el = document.getElementById('analysis-log');
if (el) {
el.value += `${evt.key}:\n${JSON.stringify(evt.value, null, 2)}\n\n`;
el.scrollTop = el.scrollHeight;
}
break;
}
case 'error':
appendLog(evt.text ?? '');
setHomingStatus('✗ Fehler', 'open');
break;
case 'done':
if (evt.state) {
_homingState = evt.state;
showHomingResult(evt.state);
if (btnSend) {
btnSend.disabled = false;
btnSend.style.opacity = '';
btnSend.style.cursor = '';
}
setHomingStatus('✓ Fertig', 'done');
setHomingProgress(6, 6, 'Homing abgeschlossen');
} else {
setHomingStatus('✗ Fehler', 'open');
setHomingProgress(6, 6, 'Fehler aufgetreten');
}
if (evt.runDir) await loadHomingImages(evt.runDir);
const frame = document.getElementById('board-viewer-frame');
if (frame?.contentWindow) {
frame.contentWindow.postMessage({ type: 'reload' }, '*');
if (evt.state) {
frame.contentWindow.postMessage({ type: 'homing-state', state: evt.state }, '*');
}
}
break;
}
}
}
} catch (err) {
appendLog(`❌ Verbindungsfehler: ${err.message}`);
setHomingStatus('✗ Fehler', 'open');
} finally {
if (btnRun) btnRun.disabled = false;
}
}
async function sendHomingToRobot() {
if (!_homingState) return;
const btnSend = document.getElementById('btn-homing-send');
if (btnSend) btnSend.disabled = true;
try {
const res = await fetch('/api/homing/send-state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ state: _homingState }),
});
const data = await res.json();
if (res.ok) {
appendLog('✅ State erfolgreich an Roboter gesendet');
setHomingStatus('✓ Gesendet', 'done');
} else {
appendLog(`❌ Fehler beim Senden: ${data.error ?? JSON.stringify(data)}`);
if (btnSend) btnSend.disabled = false;
}
} catch (err) {
appendLog(`❌ Netzwerkfehler: ${err.message}`);
if (btnSend) btnSend.disabled = false;
}
}
async function onCommandClick(btn) {
const cmd = btn.dataset.cmd;
const payloadSelector = btn.dataset.payload;
const payload = payloadSelector
? document.querySelector(payloadSelector)?.value ?? ""
: "";
if (typeof window.sendCommand === "function") {
try {
await window.sendCommand(cmd, payload);
appendLog(`Command gesendet: ${cmd}${payload ? " " + payload : ""}`);
} catch (err) {
appendLog(`Command-Fehler: ${err.message}`);
}
return;
}
appendLog(`Command (kein Transport definiert): ${cmd}${payload ? " " + payload : ""}`);
}
function setupUi() {
// Homing-Buttons
const homingRunBtn = document.getElementById('btn-homing-run');
if (homingRunBtn) homingRunBtn.addEventListener('click', runHoming);
const homingSendBtn = document.getElementById('btn-homing-send');
if (homingSendBtn) homingSendBtn.addEventListener('click', sendHomingToRobot);
// Ältere Buttons (Fallback / Debug)
const calculateBtn = document.getElementById("btn-calculate");
if (calculateBtn) {
calculateBtn.addEventListener("click", onCalculateClick);
}
const fotoBtn = document.getElementById("btn-foto");
if (fotoBtn) {
fotoBtn.addEventListener("click", onFotoClick);
}
document.querySelectorAll("button[data-cmd]").forEach(btn => {
if (btn.id === "btn-calculate") return;
btn.addEventListener("click", () => onCommandClick(btn));
});
// ===== SECTION COLLAPSE/EXPAND =====
document.querySelectorAll(".section h2").forEach(heading => {
heading.addEventListener("click", () => {
const section = heading.closest(".section");
if (section) {
section.classList.toggle("collapsed");
}
});
heading.style.cursor = "pointer";
});
// Auto-expand when content is written to a collapsed section
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList" || mutation.type === "characterData") {
let el = mutation.target;
while (el && el !== document.body) {
const section = el.closest(".section");
if (section && section.classList.contains("collapsed")) {
section.classList.remove("collapsed");
break;
}
el = el.parentElement;
}
}
});
});
// Observer auf alle Elemente in Sections anwenden
document.querySelectorAll(".section").forEach(section => {
observer.observe(section, {
childList: true,
subtree: true,
characterData: true,
characterDataOldValue: false
});
});
}
window.addEventListener("DOMContentLoaded", setupUi);