8bitkick
Init
e7efca5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Joint stream</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 16px; }
#status { margin-bottom: 8px; }
table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
th, td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; }
th { background: #f4f4f4; position: sticky; top: 0; }
pre { background: #f8f8f8; border: 1px solid #eee; padding: 8px; overflow: auto; max-height: 30vh; }
.ok { color: #0a0; }
.err { color: #a00; }
</style>
</head>
<body>
<h3>Joint stream</h3>
<div id="status">Connecting…</div>
<table id="joint-table" aria-label="Joint table">
<thead></thead>
<tbody></tbody>
</table>
<h4>Last payload</h4>
<pre id="raw"></pre>
<script>
// Change via URL: ?ws=ws://host:port/api/state/ws/full
const WS_URL = new URLSearchParams(location.search).get("ws")
|| "ws://127.0.0.1:8000/api/state/ws/full";
const statusEl = document.getElementById("status");
const rawEl = document.getElementById("raw");
const thead = document.querySelector("#joint-table thead");
const tbody = document.querySelector("#joint-table tbody");
let ws;
let latestPayload = null;
let rafScheduled = false;
let columns = ["Joint", "position", "velocity", "torque"]; // default preferred columns
function setStatus(text, cls="") {
statusEl.className = cls;
statusEl.textContent = text;
}
function connect() {
try {
ws = new WebSocket(WS_URL);
} catch (e) {
setStatus("Invalid WebSocket URL: " + e.message, "err");
return;
}
ws.onopen = () => setStatus("Connected: " + WS_URL, "ok");
ws.onclose = (e) => setStatus(`Disconnected (${e.code}) — reconnecting in 1s…`, "err");
ws.onerror = () => setStatus("WebSocket error — see console", "err");
ws.onmessage = (event) => {
try {
latestPayload = JSON.parse(event.data);
scheduleRender();
} catch (e) {
setStatus("Bad JSON from server", "err");
}
};
// simple autoreconnect
ws.addEventListener("close", () => setTimeout(connect, 1000), { once: true });
}
function scheduleRender() {
if (rafScheduled) return;
rafScheduled = true;
requestAnimationFrame(() => {
rafScheduled = false;
if (latestPayload) render(latestPayload);
});
}
function render(payload) {
rawEl.textContent = JSON.stringify(payload, null, 2);
const joints = pickJoints(payload);
const rows = normalizeJoints(joints);
if (!rows.length) {
thead.innerHTML = "";
tbody.innerHTML = "";
return;
}
// build dynamic column set (prefer known order)
const fieldSet = new Set(["Joint", ...columns.slice(1)]);
for (const r of rows) for (const k of Object.keys(r)) fieldSet.add(k);
// ensure "Joint" first
const cols = ["Joint", ...[...fieldSet].filter(c => c !== "Joint")];
if (JSON.stringify(cols) !== JSON.stringify(columns)) {
columns = cols;
renderHeader(columns);
}
renderBody(rows, columns);
}
function renderHeader(cols) {
const tr = document.createElement("tr");
for (const c of cols) {
const th = document.createElement("th");
th.textContent = c;
tr.appendChild(th);
}
thead.innerHTML = "";
thead.appendChild(tr);
}
function renderBody(rows, cols) {
const frag = document.createDocumentFragment();
for (const r of rows) {
const tr = document.createElement("tr");
for (const c of cols) {
const td = document.createElement("td");
const v = r[c];
td.textContent = v === undefined ? "" : (typeof v === "number" ? Number(v).toFixed(4) : String(v));
tr.appendChild(td);
}
frag.appendChild(tr);
}
tbody.innerHTML = "";
tbody.appendChild(frag);
}
function pickJoints(obj) {
if (!obj || typeof obj !== "object") return null;
// Common placements
if (obj.joints) return obj.joints;
if (obj.state && obj.state.joints) return obj.state.joints;
if (obj.motors) return obj.motors;
if (obj.body && obj.body.joints) return obj.body.joints;
return obj.joints_state || null;
}
function normalizeJoints(joints) {
if (!joints) return [];
const out = [];
const preferredKeys = ["position","pos","angle","velocity","vel","speed","torque","effort","current","temperature","temp","value"];
const extractFields = (o) => {
const row = {};
for (const k of preferredKeys) {
if (o && typeof o === "object" && k in o && isFinite(o[k])) row[k] = Number(o[k]);
}
// fallback: include any top-level numeric fields
for (const [k,v] of Object.entries(o || {})) {
if (!(k in row) && isFinite(v)) row[k] = Number(v);
}
return row;
};
if (Array.isArray(joints)) {
for (const item of joints) {
if (item == null) continue;
let name = item.name ?? item.id ?? item.label ?? "";
// if the array is like [{jointName: {..}}, ...]
if (!name && typeof item === "object" && !Array.isArray(item)) {
const keys = Object.keys(item);
if (keys.length === 1) {
name = keys[0];
const fields = extractFields(item[name]);
out.push({ Joint: name, ...fields });
continue;
}
}
const fields = extractFields(item);
out.push({ Joint: String(name || out.length), ...fields });
}
return out;
}
if (typeof joints === "object") {
for (const [name, val] of Object.entries(joints)) {
if (val != null && typeof val === "object") {
out.push({ Joint: name, ...extractFields(val) });
} else if (isFinite(val)) {
out.push({ Joint: name, value: Number(val) });
}
}
return out;
}
return out;
}
connect();
</script>
</body>
</html>