Files
appRobotHoming/public/client.js
2026-06-10 09:39:32 +02:00

409 lines
12 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 ─────────────────────────────────────────────────────────────────────
/**
* Kameraliste vom Backend holen und <select id="cam-select"> befüllen.
* Schlägt das Laden fehl, bleiben die hardkodierten Fallback-Optionen erhalten.
*/
async function loadCameras() {
const select = document.getElementById('cam-select');
if (!select) return;
try {
const res = await fetch('/api/webcam/cameras');
if (!res.ok) return;
const { cameras } = await res.json();
select.innerHTML = cameras
.map(c => `<option value="${c.id}">${c.id} (${c.position || c.name})</option>`)
.join('');
} catch {
// Fallback: hardkodierte Optionen aus index.html bleiben erhalten
}
}
/**
* Foto-Button: holt ein HD-JPEG der gewählten Kamera und zeigt es an.
* Der Endpoint /api/webcam/snapshot/:id liefert das JPEG direkt (kein base64).
*/
async function onFotoClick() {
const camId = document.getElementById('cam-select')?.value || 'cam0';
const display = document.getElementById('foto-display');
if (!display) return;
appendLog(`Foto: ${camId}`);
// Cache-Buster über Timestamp-Parameter, damit der Browser nicht aus dem Cache lädt
const url = `/api/webcam/snapshot/${camId}?t=${Date.now()}`;
const img = document.createElement('img');
img.style.maxWidth = '100%';
img.style.height = 'auto';
img.alt = camId;
img.onload = () => appendLog(`Foto ${camId}: OK (${img.naturalWidth}×${img.naturalHeight}px)`);
img.onerror = () => appendLog(`Foto ${camId}: Fehler beim Laden`);
img.src = url;
display.innerHTML = '';
display.appendChild(img);
}
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() {
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));
});
// Kameraliste vom Backend laden (befüllt #cam-select dynamisch)
loadCameras();
// ===== 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);