saltylab-firmware/ui/gamepad_panel.html
sl-webui cbcae34b79 feat: WebUI gamepad teleoperation panel (Issue #598)
- Standalone ui/gamepad_panel.{html,js,css} — no build step
- Web Gamepad API integration: L-stick=linear, R-stick=angular
  - LT trigger scales speed down (fine control)
  - B/Circle button toggles E-stop; Start button resumes
  - Live raw axis bars and button state in sidebar
- Virtual dual joystick (left=drive, right=steer) via Pointer Capture API
  - Deadzone ring drawn on canvas; configurable 0–40%
  - Touch and mouse support
- WASD/Arrow keyboard input (W/S=forward/reverse, A/D=turn, Space=E-stop)
- Speed limiter sliders: linear (0–1.0 m/s), angular (0–2.0 rad/s)
- Configurable deadzone slider (0–40%)
- E-stop: latches zero-velocity command, blinking overlay, resume button
- Publishes geometry_msgs/Twist to /cmd_vel at 20 Hz via rosbridge WebSocket
- Input priority: gamepad > keyboard > virtual sticks
- Live command display (m/s, rad/s) with color feedback
- Pub rate display (Hz) in sidebar
- localStorage WS URL persistence, auto-reconnect on load
- Mobile-responsive: sidebar hidden ≤800px, right stick hidden ≤560px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 14:58:41 -04:00

198 lines
6.4 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 — Gamepad Teleop</title>
<link rel="stylesheet" href="gamepad_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 — GAMEPAD TELEOP</div>
<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="hbtn">CONNECT</button>
<span id="conn-label" style="color:#4b5563;font-size:10px">Not connected</span>
<div style="flex:1"></div>
<div id="gp-indicator">
<div id="gp-dot"></div>
<span id="gp-label">No gamepad</span>
</div>
</div>
<!-- ── Toolbar ── -->
<div id="toolbar">
<!-- Speed limiter -->
<span class="tlbl">LINEAR</span>
<input id="slider-linear" type="range" min="10" max="100" value="50" class="tslider">
<span id="val-linear" class="tval">0.50 m/s</span>
<div class="tsep"></div>
<span class="tlbl">ANGULAR</span>
<input id="slider-angular" type="range" min="10" max="100" value="50" class="tslider">
<span id="val-angular" class="tval">1.00 rad/s</span>
<div class="tsep"></div>
<span class="tlbl">DEADZONE</span>
<input id="slider-dz" type="range" min="0" max="40" value="10" class="tslider" style="width:70px">
<span id="val-dz" class="tval">10%</span>
<div class="tsep"></div>
<button id="btn-estop" class="hbtn estop-btn">⛔ E-STOP</button>
<button id="btn-resume" class="hbtn resume-btn" style="display:none">▶ RESUME</button>
</div>
<!-- ── Main ── -->
<div id="main">
<!-- Virtual joystick area -->
<div id="joystick-area">
<div class="stick-wrap" id="left-wrap">
<div class="stick-label">LEFT — DRIVE</div>
<canvas id="left-stick" width="200" height="200"></canvas>
<div class="stick-vals" id="left-vals">↕ 0.00 m/s</div>
</div>
<!-- Center info -->
<div id="center-panel">
<div class="cp-block">
<div class="cp-title">COMMAND</div>
<div class="cp-row">
<span class="cp-lbl">Linear</span>
<span class="cp-val" id="disp-linear">0.00 m/s</span>
</div>
<div class="cp-row">
<span class="cp-lbl">Angular</span>
<span class="cp-val" id="disp-angular">0.00 rad/s</span>
</div>
</div>
<div class="cp-block">
<div class="cp-title">LIMITS</div>
<div class="cp-row">
<span class="cp-lbl">Max lin</span>
<span class="cp-val" id="lim-linear">0.50 m/s</span>
</div>
<div class="cp-row">
<span class="cp-lbl">Max ang</span>
<span class="cp-val" id="lim-angular">1.00 rad/s</span>
</div>
<div class="cp-row">
<span class="cp-lbl">Deadzone</span>
<span class="cp-val" id="lim-dz">10%</span>
</div>
</div>
<div class="cp-block">
<div class="cp-title">KEYBOARD</div>
<div class="key-grid">
<div></div>
<div class="key" id="key-w">W</div>
<div></div>
<div class="key" id="key-a">A</div>
<div class="key" id="key-s">S</div>
<div class="key" id="key-d">D</div>
<div></div>
<div class="key key-wide" id="key-space">SPC</div>
<div></div>
</div>
<div style="font-size:9px;color:#4b5563;text-align:center;margin-top:4px">
W/S=fwd/rev · A/D=turn · SPC=stop
</div>
</div>
<!-- Gamepad layout diagram -->
<div class="cp-block">
<div class="cp-title">GAMEPAD MAPPING</div>
<div style="font-size:9px;color:#6b7280;line-height:1.8">
<div>L-stick ↕ → linear vel</div>
<div>R-stick ↔ → angular vel</div>
<div>LT/RT → fine speed ctrl</div>
<div>B/Circle → E-stop toggle</div>
<div>Start → Resume</div>
</div>
</div>
<div id="estop-panel" style="display:none">
<div class="estop-text">⛔ E-STOP</div>
<div class="estop-sub">ACTIVE</div>
</div>
</div>
<div class="stick-wrap" id="right-wrap">
<div class="stick-label">RIGHT — STEER</div>
<canvas id="right-stick" width="200" height="200"></canvas>
<div class="stick-vals" id="right-vals">↔ 0.00 rad/s</div>
</div>
</div>
<!-- Sidebar -->
<aside id="sidebar">
<!-- Status -->
<div class="sb-card">
<div class="sb-title">Status</div>
<div class="sb-row">
<span class="sb-lbl">E-stop</span>
<span class="sb-val" id="sb-estop" style="color:#6b7280">OFF</span>
</div>
<div class="sb-row">
<span class="sb-lbl">Input</span>
<span class="sb-val" id="sb-input"></span>
</div>
<div class="sb-row">
<span class="sb-lbl">Pub rate</span>
<span class="sb-val" id="sb-rate"></span>
</div>
<div class="sb-row">
<span class="sb-lbl">Topic</span>
<span class="sb-val" style="font-size:9px">/cmd_vel</span>
</div>
</div>
<!-- Gamepad raw -->
<div class="sb-card">
<div class="sb-title">Gamepad Raw</div>
<div id="gp-raw">
<div style="color:#374151;font-size:10px;text-align:center;padding:12px 0">
No gamepad connected.<br>Press any button to activate.
</div>
</div>
</div>
<!-- Topics -->
<div class="sb-card">
<div class="sb-title">Topics</div>
<div style="font-size:9px;color:#374151;line-height:1.9">
<div>PUB <code style="color:#4b5563">/cmd_vel</code></div>
<div style="color:#1e3a5f;padding-left:8px">geometry_msgs/Twist</div>
<div style="margin-top:4px;color:#6b7280">20 Hz publish rate</div>
<div style="margin-top:4px;color:#6b7280">Sends zero on E-stop</div>
</div>
</div>
</aside>
</div>
<!-- ── Footer ── -->
<div id="footer">
<span>virtual sticks · WASD keyboard · Web Gamepad API</span>
<span>rosbridge: <code id="footer-ws">ws://localhost:9090</code></span>
<span>gamepad teleop — issue #598</span>
</div>
<script src="gamepad_panel.js"></script>
<script>
document.getElementById('ws-input').addEventListener('input', (e) => {
document.getElementById('footer-ws').textContent = e.target.value;
});
</script>
</body>
</html>