Standalone 3-file diagnostics dashboard (ui/diagnostics_panel.{html,js,css}).
No build step — serve the ui/ directory directly. roslib.js via CDN.
Panels:
- Battery: voltage (V), SOC (%), current (A) with large readouts + gauge bars
+ 2-minute sparkline history canvas, 4S LiPo thresholds
- Temperatures: CPU/GPU (Jetson tegrastats) + Board/STM32 + Motor L/R
color-coded temp boxes with mini progress bars (green<60 amber<75 red>75°C)
- Motor current: per-wheel current gauge bars + CMD value + balance_state label
Thresholds: warn 8A / crit 12A
- Resources: RAM / GPU memory / Disk — gauge bars with used/total display
Thresholds: warn 80% / crit 95%
- WiFi / Network: RSSI signal bars (5-level) + dBm readout + latency (ms)
MQTT broker status via mqtt_connected KeyValue
- ROS2 node health: full DiagnosticArray node list with OK/WARN/ERROR/STALE
badges, per-node message preview, MutationObserver count badge
Features:
- Auto 2 Hz refresh via rosbridge subscriptions (throttle_rate: 500ms)
- Pulsing refresh indicator dot on each update
- System status bar: HEALTHY/DEGRADED/FAULT/STALE badge + battery/thermal/net
- Alert thresholds: red/amber/green for every metric
- Responsive CSS grid: 3-col → 2-col → 1-col via media queries
- WS URL persisted in localStorage
ROS topics:
SUB /diagnostics diagnostic_msgs/DiagnosticArray
SUB /saltybot/balance_state std_msgs/String (JSON)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
268 lines
11 KiB
HTML
268 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||
<title>Saltybot — System Diagnostics</title>
|
||
<link rel="stylesheet" href="diagnostics_panel.css">
|
||
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ── Header ── -->
|
||
<div id="header">
|
||
<div class="logo">⚡ SALTYBOT — DIAGNOSTICS</div>
|
||
|
||
<div id="conn-bar">
|
||
<div id="conn-dot"></div>
|
||
<input id="ws-input" type="text" value="ws://localhost:9090" placeholder="ws://robot-ip:9090" />
|
||
<button id="btn-connect" class="hdr-btn">CONNECT</button>
|
||
<span id="conn-label" style="color:#4b5563;font-size:10px">Not connected</span>
|
||
</div>
|
||
|
||
<div id="refresh-info">
|
||
<div id="refresh-dot"></div>
|
||
<span>auto 2 Hz</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── System status bar ── -->
|
||
<div id="status-bar">
|
||
<span style="color:#6b7280;font-size:10px">SYSTEM</span>
|
||
<span class="sys-badge badge-stale" id="system-badge">STALE</span>
|
||
<span style="color:#4b5563">│</span>
|
||
<span style="color:#6b7280;font-size:10px">BATTERY</span>
|
||
<span class="sys-badge badge-stale" id="batt-badge">—</span>
|
||
<span style="color:#4b5563">│</span>
|
||
<span style="color:#6b7280;font-size:10px">THERMAL</span>
|
||
<span class="sys-badge badge-stale" id="temp-badge">—</span>
|
||
<span style="color:#4b5563">│</span>
|
||
<span style="color:#6b7280;font-size:10px">NETWORK</span>
|
||
<span class="sys-badge badge-stale" id="net-badge">—</span>
|
||
<span id="last-update">Awaiting data…</span>
|
||
</div>
|
||
|
||
<!-- ── Dashboard grid ── -->
|
||
<div id="dashboard">
|
||
|
||
<!-- ╔═══════════════════ BATTERY ═══════════════════╗ -->
|
||
<div class="card span2">
|
||
<div class="card-title">
|
||
BATTERY — 4S LiPo (12.0–16.8 V)
|
||
<span class="badge badge-stale" id="batt-badge-2" style="font-size:9px">—</span>
|
||
</div>
|
||
|
||
<!-- Big readout row -->
|
||
<div style="display:flex;gap:16px;align-items:flex-end;flex-wrap:wrap">
|
||
<div>
|
||
<div style="font-size:9px;color:#6b7280;margin-bottom:2px">VOLTAGE</div>
|
||
<div class="big-metric">
|
||
<span class="big-num" id="batt-voltage" style="color:#22c55e">—</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size:9px;color:#6b7280;margin-bottom:2px">SOC</div>
|
||
<div class="big-metric">
|
||
<span class="big-num" id="batt-soc" style="color:#22c55e">—</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size:9px;color:#6b7280;margin-bottom:2px">CURRENT</div>
|
||
<div class="big-metric">
|
||
<span class="big-num" id="batt-current" style="color:#06b6d4;font-size:20px">—</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Gauge bars -->
|
||
<div class="gauge-row">
|
||
<div class="gauge-label-row">
|
||
<span class="gauge-label">Voltage</span>
|
||
<span class="gauge-value" id="batt-voltage-2" style="color:#6b7280">12.0–16.8 V</span>
|
||
</div>
|
||
<div class="gauge-track"><div class="gauge-fill" id="batt-volt-bar" style="width:0%"></div></div>
|
||
</div>
|
||
<div class="gauge-row">
|
||
<div class="gauge-label-row">
|
||
<span class="gauge-label">State of Charge</span>
|
||
<span class="gauge-value" style="color:#6b7280">0–100%</span>
|
||
</div>
|
||
<div class="gauge-track"><div class="gauge-fill" id="batt-soc-bar" style="width:0%"></div></div>
|
||
</div>
|
||
|
||
<!-- Sparkline history -->
|
||
<div>
|
||
<div style="font-size:9px;color:#6b7280;margin-bottom:4px">VOLTAGE HISTORY (last 2 min)</div>
|
||
<canvas id="batt-sparkline" height="56"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ╔═══════════════════ TEMPERATURES ═══════════════════╗ -->
|
||
<div class="card">
|
||
<div class="card-title">TEMPERATURES</div>
|
||
<div class="temp-grid">
|
||
<div class="temp-box" id="cpu-temp-box">
|
||
<div class="temp-label">CPU (Jetson)</div>
|
||
<div class="temp-value" id="cpu-temp-val">—</div>
|
||
<div class="temp-bar-track"><div class="temp-bar-fill" id="cpu-temp-bar" style="width:0%"></div></div>
|
||
</div>
|
||
<div class="temp-box" id="gpu-temp-box">
|
||
<div class="temp-label">GPU (Jetson)</div>
|
||
<div class="temp-value" id="gpu-temp-val">—</div>
|
||
<div class="temp-bar-track"><div class="temp-bar-fill" id="gpu-temp-bar" style="width:0%"></div></div>
|
||
</div>
|
||
<div class="temp-box" id="board-temp-box">
|
||
<div class="temp-label">Board / STM32</div>
|
||
<div class="temp-value" id="board-temp-val">—</div>
|
||
<div class="temp-bar-track"><div class="temp-bar-fill" id="board-temp-bar" style="width:0%"></div></div>
|
||
</div>
|
||
<div class="temp-box" id="motor-temp-l-box">
|
||
<div class="temp-label">Motor L</div>
|
||
<div class="temp-value" id="motor-temp-l-val">—</div>
|
||
<div class="temp-bar-track"><div class="temp-bar-fill" id="motor-temp-l-bar" style="width:0%"></div></div>
|
||
</div>
|
||
<!-- spacer to keep 2-col grid balanced if motor-temp-r is alone -->
|
||
<div class="temp-box" id="motor-temp-r-box">
|
||
<div class="temp-label">Motor R</div>
|
||
<div class="temp-value" id="motor-temp-r-val">—</div>
|
||
<div class="temp-bar-track"><div class="temp-bar-fill" id="motor-temp-r-bar" style="width:0%"></div></div>
|
||
</div>
|
||
</div>
|
||
<div style="font-size:9px;color:#374151">
|
||
Zones: <60°C green · 60–75°C amber · >75°C red
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ╔═══════════════════ MOTOR CURRENT ═══════════════════╗ -->
|
||
<div class="card">
|
||
<div class="card-title">MOTOR CURRENT & CMD</div>
|
||
<div class="motor-grid">
|
||
<div class="motor-box">
|
||
<div class="motor-label">LEFT WHEEL</div>
|
||
<div style="font-size:20px;font-weight:bold;font-family:monospace" id="motor-cur-l">—</div>
|
||
<div class="gauge-track" style="margin-top:4px">
|
||
<div class="gauge-fill" id="motor-bar-l" style="width:0%"></div>
|
||
</div>
|
||
<div style="font-size:9px;color:#4b5563;margin-top:4px">
|
||
CMD: <span id="motor-cmd-l" style="color:#6b7280">—</span>
|
||
</div>
|
||
</div>
|
||
<div class="motor-box">
|
||
<div class="motor-label">RIGHT WHEEL</div>
|
||
<div style="font-size:20px;font-weight:bold;font-family:monospace" id="motor-cur-r">—</div>
|
||
<div class="gauge-track" style="margin-top:4px">
|
||
<div class="gauge-fill" id="motor-bar-r" style="width:0%"></div>
|
||
</div>
|
||
<div style="font-size:9px;color:#4b5563;margin-top:4px">
|
||
CMD: <span id="motor-cmd-r" style="color:#6b7280">—</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="font-size:10px;color:#6b7280">
|
||
Balance state: <span id="balance-state" style="color:#9ca3af;font-family:monospace">—</span>
|
||
</div>
|
||
<div style="font-size:9px;color:#374151">Thresholds: warn 8A · crit 12A</div>
|
||
</div>
|
||
|
||
<!-- ╔═══════════════════ MEMORY / DISK ═══════════════════╗ -->
|
||
<div class="card">
|
||
<div class="card-title">RESOURCES</div>
|
||
<div class="gauge-row">
|
||
<div class="gauge-label-row">
|
||
<span class="gauge-label">RAM</span>
|
||
<span class="gauge-value" id="ram-val" style="color:#9ca3af">—</span>
|
||
</div>
|
||
<div class="gauge-track"><div class="gauge-fill" id="ram-bar" style="width:0%"></div></div>
|
||
</div>
|
||
<div class="gauge-row">
|
||
<div class="gauge-label-row">
|
||
<span class="gauge-label">GPU Memory</span>
|
||
<span class="gauge-value" id="gpu-val" style="color:#9ca3af">—</span>
|
||
</div>
|
||
<div class="gauge-track"><div class="gauge-fill" id="gpu-bar" style="width:0%"></div></div>
|
||
</div>
|
||
<div class="gauge-row">
|
||
<div class="gauge-label-row">
|
||
<span class="gauge-label">Disk</span>
|
||
<span class="gauge-value" id="disk-val" style="color:#9ca3af">—</span>
|
||
</div>
|
||
<div class="gauge-track"><div class="gauge-fill" id="disk-bar" style="width:0%"></div></div>
|
||
</div>
|
||
<div style="font-size:9px;color:#374151">Warn ≥80% · Critical ≥95%</div>
|
||
</div>
|
||
|
||
<!-- ╔═══════════════════ WIFI / LATENCY ═══════════════════╗ -->
|
||
<div class="card">
|
||
<div class="card-title">NETWORK</div>
|
||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
||
<div id="rssi-bars" class="rssi-bars"></div>
|
||
<div>
|
||
<div style="font-size:9px;color:#6b7280">RSSI</div>
|
||
<div style="font-size:20px;font-weight:bold;font-family:monospace" id="rssi-val">—</div>
|
||
<div style="font-size:9px" id="rssi-label" style="color:#6b7280">—</div>
|
||
</div>
|
||
</div>
|
||
<div class="gauge-row">
|
||
<div class="gauge-label-row">
|
||
<span class="gauge-label">Rosbridge Latency</span>
|
||
<span class="gauge-value" id="latency-val" style="color:#9ca3af">—</span>
|
||
</div>
|
||
</div>
|
||
<!-- MQTT status -->
|
||
<div style="margin-top:4px;padding-top:8px;border-top:1px solid #0c2a3a">
|
||
<div style="font-size:9px;color:#6b7280;margin-bottom:4px">MQTT BROKER</div>
|
||
<div class="mqtt-status">
|
||
<div class="mqtt-dot" id="mqtt-dot"></div>
|
||
<span id="mqtt-label" style="color:#6b7280">No data</span>
|
||
</div>
|
||
<div style="font-size:9px;color:#374151;margin-top:4px">
|
||
Via /diagnostics KeyValue: mqtt_connected
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ╔═══════════════════ ROS2 NODE HEALTH ═══════════════════╗ -->
|
||
<div class="card span3">
|
||
<div class="card-title">
|
||
ROS2 NODE HEALTH — /diagnostics
|
||
<span style="font-size:9px;color:#4b5563" id="node-count">0 nodes</span>
|
||
</div>
|
||
<div id="node-list">
|
||
<div style="color:#374151;font-size:10px;text-align:center;padding:12px">
|
||
Waiting for /diagnostics data…
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ── Footer ── -->
|
||
<div id="footer">
|
||
<span>rosbridge: <code id="footer-ws">ws://localhost:9090</code></span>
|
||
<span>diagnostics dashboard — issue #562 · auto 2Hz</span>
|
||
</div>
|
||
|
||
<script src="diagnostics_panel.js"></script>
|
||
<script>
|
||
// Sync footer WS URL
|
||
document.getElementById('ws-input').addEventListener('input', (e) => {
|
||
document.getElementById('footer-ws').textContent = e.target.value;
|
||
});
|
||
|
||
// Keep node count updated after render
|
||
const origRenderNodes = window.renderNodes;
|
||
const nodeCountEl = document.getElementById('node-count');
|
||
const _origOnDiag = window.onDiagnostics;
|
||
|
||
// Node count updates via MutationObserver on the node-list div
|
||
const nl = document.getElementById('node-list');
|
||
if (nl) {
|
||
new MutationObserver(() => {
|
||
const rows = nl.querySelectorAll('.node-row');
|
||
if (nodeCountEl) nodeCountEl.textContent = rows.length + ' node' + (rows.length !== 1 ? 's' : '');
|
||
}).observe(nl, { childList: true, subtree: false });
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|