saltylab-firmware/ui/vesc_panel.html
sl-webui 89f892e5ef feat: VESC motor dashboard panel (Issue #653)
Standalone panel ui/vesc_panel.{html,js,css} with live CAN telemetry
via rosbridge. Subscribes to /vesc/left/state, /vesc/right/state
(std_msgs/String JSON) and /vesc/combined for battery voltage.

Features:
- Canvas arc gauge per motor showing RPM + direction (FWD/REV/STOP)
- Current draw bar (motor + input), duty cycle bar, temperature bars
- FET and motor temperature boxes with warn/crit colour coding
- Sparkline charts for RPM and current (last 60 s, 120 samples)
- Battery card: voltage, total draw, both RPMs, SOC progress bar
- Colour-coded health: green/amber/red at configurable thresholds
- E-stop button: publishes zero /cmd_vel + /saltybot/emergency event
- Stale detection (2 s timeout → OFFLINE state)
- Hz counter + last-stamp display in header
- Mobile-responsive layout (single-column below 640 px)
- WS URL persisted in localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:35:35 -04:00

230 lines
8.4 KiB
HTML
Raw Permalink 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.

<!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 — VESC Motor Dashboard</title>
<link rel="stylesheet" href="vesc_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 — VESC MOTORS</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="header-right">
<span id="hz-label" class="meta-label">— Hz</span>
<span style="color:#374151">|</span>
<span id="stamp-label" class="meta-label">No data</span>
</div>
</div>
<!-- ── Status bar ── -->
<div id="status-bar">
<span style="color:#6b7280;font-size:10px">LEFT</span>
<span class="sys-badge badge-stale" id="badge-left">OFFLINE</span>
<span style="color:#4b5563"></span>
<span style="color:#6b7280;font-size:10px">RIGHT</span>
<span class="sys-badge badge-stale" id="badge-right">OFFLINE</span>
<span style="color:#4b5563"></span>
<span style="color:#6b7280;font-size:10px">BATTERY</span>
<span class="sys-badge badge-stale" id="badge-batt"></span>
<span style="color:#4b5563"></span>
<span style="color:#6b7280;font-size:10px">TOTAL DRAW</span>
<span class="sys-badge badge-stale" id="badge-total"></span>
<button id="btn-estop" class="estop-btn">⛔ E-STOP</button>
</div>
<!-- ── Dashboard ── -->
<div id="dashboard">
<!-- ╔═════════ LEFT MOTOR ═════════╗ -->
<div class="card motor-card" id="card-left">
<div class="card-title">
LEFT MOTOR
<span class="fault-badge" id="fault-left">OK</span>
</div>
<!-- Arc gauge + direction -->
<div class="gauge-row-top">
<div class="arc-wrap">
<canvas id="rpm-arc-left" width="140" height="100"></canvas>
<div class="arc-dir" id="dir-left"></div>
</div>
<div class="motor-stats">
<div class="stat-row">
<span class="stat-label">CURRENT (MTR)</span>
<span class="stat-val" id="cur-left"></span>
</div>
<div class="bar-track"><div class="bar-fill" id="cur-bar-left"></div></div>
<div class="stat-row" style="margin-top:6px">
<span class="stat-label">CURRENT (IN)</span>
<span class="stat-val" id="curin-left"></span>
</div>
<div class="bar-track"><div class="bar-fill" id="curin-bar-left"></div></div>
<div class="stat-row" style="margin-top:6px">
<span class="stat-label">DUTY CYCLE</span>
<span class="stat-val" id="duty-left"></span>
</div>
<div class="bar-track"><div class="bar-fill" id="duty-bar-left"></div></div>
</div>
</div>
<!-- Temperatures -->
<div class="temp-row">
<div class="temp-box" id="tbox-fet-left">
<div class="temp-label">FET TEMP</div>
<div class="temp-val" id="tfet-left"></div>
<div class="bar-track mini"><div class="bar-fill" id="tfet-bar-left"></div></div>
</div>
<div class="temp-box" id="tbox-mot-left">
<div class="temp-label">MOTOR TEMP</div>
<div class="temp-val" id="tmot-left"></div>
<div class="bar-track mini"><div class="bar-fill" id="tmot-bar-left"></div></div>
</div>
</div>
<!-- Sparklines -->
<div class="spark-section">
<div class="spark-label">RPM · 60s</div>
<canvas class="sparkline" id="spark-rpm-left" height="40"></canvas>
<div class="spark-label" style="margin-top:4px">CURRENT · 60s</div>
<canvas class="sparkline" id="spark-cur-left" height="40"></canvas>
</div>
</div>
<!-- ╔═════════ RIGHT MOTOR ═════════╗ -->
<div class="card motor-card" id="card-right">
<div class="card-title">
RIGHT MOTOR
<span class="fault-badge" id="fault-right">OK</span>
</div>
<div class="gauge-row-top">
<div class="arc-wrap">
<canvas id="rpm-arc-right" width="140" height="100"></canvas>
<div class="arc-dir" id="dir-right"></div>
</div>
<div class="motor-stats">
<div class="stat-row">
<span class="stat-label">CURRENT (MTR)</span>
<span class="stat-val" id="cur-right"></span>
</div>
<div class="bar-track"><div class="bar-fill" id="cur-bar-right"></div></div>
<div class="stat-row" style="margin-top:6px">
<span class="stat-label">CURRENT (IN)</span>
<span class="stat-val" id="curin-right"></span>
</div>
<div class="bar-track"><div class="bar-fill" id="curin-bar-right"></div></div>
<div class="stat-row" style="margin-top:6px">
<span class="stat-label">DUTY CYCLE</span>
<span class="stat-val" id="duty-right"></span>
</div>
<div class="bar-track"><div class="bar-fill" id="duty-bar-right"></div></div>
</div>
</div>
<div class="temp-row">
<div class="temp-box" id="tbox-fet-right">
<div class="temp-label">FET TEMP</div>
<div class="temp-val" id="tfet-right"></div>
<div class="bar-track mini"><div class="bar-fill" id="tfet-bar-right"></div></div>
</div>
<div class="temp-box" id="tbox-mot-right">
<div class="temp-label">MOTOR TEMP</div>
<div class="temp-val" id="tmot-right"></div>
<div class="bar-track mini"><div class="bar-fill" id="tmot-bar-right"></div></div>
</div>
</div>
<div class="spark-section">
<div class="spark-label">RPM · 60s</div>
<canvas class="sparkline" id="spark-rpm-right" height="40"></canvas>
<div class="spark-label" style="margin-top:4px">CURRENT · 60s</div>
<canvas class="sparkline" id="spark-cur-right" height="40"></canvas>
</div>
</div>
<!-- ╔═════════ BATTERY / COMBINED ═════════╗ -->
<div class="card" id="card-batt">
<div class="card-title">
BATTERY — 4S LiPo (12.016.8 V)
<span class="meta-label" id="batt-stamp"></span>
</div>
<div class="batt-row">
<!-- Big voltage -->
<div class="batt-metric">
<div class="batt-metric-label">VOLTAGE</div>
<div class="big-num" id="batt-voltage"></div>
<div class="batt-unit">V</div>
</div>
<div class="batt-metric">
<div class="batt-metric-label">TOTAL DRAW</div>
<div class="big-num" id="batt-total-cur" style="color:#06b6d4"></div>
<div class="batt-unit">A</div>
</div>
<div class="batt-metric">
<div class="batt-metric-label">LEFT RPM</div>
<div class="big-num" id="batt-rpm-l" style="color:#a855f7;font-size:20px"></div>
</div>
<div class="batt-metric">
<div class="batt-metric-label">RIGHT RPM</div>
<div class="big-num" id="batt-rpm-r" style="color:#a855f7;font-size:20px"></div>
</div>
</div>
<!-- Voltage bar -->
<div>
<div class="stat-row">
<span class="stat-label">Voltage (12.016.8 V)</span>
<span class="stat-val" id="batt-volt-pct"></span>
</div>
<div class="bar-track"><div class="bar-fill" id="batt-volt-bar"></div></div>
</div>
<!-- Total current bar -->
<div>
<div class="stat-row">
<span class="stat-label">Total Current (0120 A)</span>
<span class="stat-val" id="batt-cur-pct"></span>
</div>
<div class="bar-track"><div class="bar-fill" id="batt-cur-bar"></div></div>
</div>
<div style="font-size:9px;color:#374151">
Voltage zones: &lt;13.2V warn · &lt;12.4V critical · FET &gt;70°C warn · Motor &gt;85°C warn
</div>
</div>
</div>
<!-- ── Footer ── -->
<div id="footer">
<span>rosbridge: <code id="footer-ws">ws://localhost:9090</code></span>
<span>topics: /vesc/left/state · /vesc/right/state · /vesc/combined (std_msgs/String JSON)</span>
<span>vesc motor dashboard — issue #653</span>
</div>
<script src="vesc_panel.js"></script>
<script>
document.getElementById('ws-input').addEventListener('input', (e) => {
document.getElementById('footer-ws').textContent = e.target.value;
});
</script>
</body>
</html>