Adds a full gimbal control panel with live camera preview: Standalone page (ui/gimbal_panel.html + .js + .css): - Self-contained HTML page, no build step, served directly - roslib.js via CDN, connects to rosbridge WebSocket - 2-D canvas pan/tilt pad: click-drag + touch pointer capture - Live camera stream (front/rear/left/right selector, base64 CompressedImage) - FPS badge + angle overlay on video feed - Preset positions: CENTER / LEFT / RIGHT / UP / DOWN - Home button (0° / 0°) - Person-tracking toggle → /gimbal/tracking_enabled - Current angle display from /gimbal/state feedback - WS URL persisted in localStorage React component (GimbalPanel.jsx) + App.jsx integration: - Same features in dashboard — TELEOP group → Gimbal tab - Shares rosbridge connection from parent - Mobile-responsive: stacks vertically on mobile, side-by-side on lg+ ROS topics: PUB /gimbal/cmd geometry_msgs/Vector3 SUB /gimbal/state geometry_msgs/Vector3 PUB /gimbal/tracking_enabled std_msgs/Bool SUB /camera/*/image_raw/compressed sensor_msgs/CompressedImage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
6.4 KiB
HTML
184 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 — Gimbal Control</title>
|
|
<link rel="stylesheet" href="gimbal_panel.css">
|
|
<!-- roslib from CDN -->
|
|
<script src="https://cdn.jsdelivr.net/npm/roslib@1.3.0/build/roslib.min.js"></script>
|
|
<style>
|
|
/* Cam button active state (can't use CSS-only with JS-toggled class without Tailwind) */
|
|
.cam-btn { padding: 3px 10px; border-radius: 4px; border: 1px solid #1e3a5f;
|
|
background: #070712; color: #4b5563; font-family: monospace;
|
|
font-size: 10px; font-weight: bold; cursor: pointer; transition: all 0.15s; }
|
|
.cam-btn:hover { color: #d1d5db; }
|
|
.cam-btn.active { background: #083344; border-color: #155e75; color: #67e8f9; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ── Header ── -->
|
|
<div id="header">
|
|
<div class="logo">⚡ SALTYBOT — GIMBAL</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="btn btn-cyan">CONNECT</button>
|
|
<span id="conn-status" style="color:#4b5563;font-size:10px;">Not connected</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Main ── -->
|
|
<div id="main">
|
|
|
|
<!-- ── Left: Camera feed ── -->
|
|
<section id="camera-section">
|
|
|
|
<!-- Camera selector toolbar -->
|
|
<div id="cam-toolbar">
|
|
<button class="cam-btn active" data-cam="front">FRONT</button>
|
|
<button class="cam-btn" data-cam="rear" >REAR</button>
|
|
<button class="cam-btn" data-cam="left" >LEFT</button>
|
|
<button class="cam-btn" data-cam="right">RIGHT</button>
|
|
<span id="cam-topic">/camera/front/image_raw/compressed</span>
|
|
</div>
|
|
|
|
<!-- Video frame -->
|
|
<div id="camera-frame">
|
|
<img id="camera-img" alt="gimbal camera feed" />
|
|
<div id="no-signal">
|
|
<div class="icon">📷</div>
|
|
<div>NO SIGNAL</div>
|
|
</div>
|
|
<div id="fps-badge">— fps</div>
|
|
<div id="angle-overlay">PAN 0.0° TILT 0.0°</div>
|
|
</div>
|
|
|
|
</section>
|
|
|
|
<!-- ── Right: Controls ── -->
|
|
<aside id="controls">
|
|
|
|
<!-- Pan/Tilt pad -->
|
|
<div class="card">
|
|
<div class="card-title">PAN / TILT CONTROL</div>
|
|
<div id="pad-wrap">
|
|
<div class="pad-labels">
|
|
<span>← PAN</span>
|
|
<span style="color:#155e75">DRAG</span>
|
|
<span>PAN →</span>
|
|
</div>
|
|
<canvas id="gimbal-pad" width="200" height="150"></canvas>
|
|
<div style="font-size:9px;color:#374151;text-align:center">
|
|
↑ TILT UP | ↓ TILT DOWN
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Current angles -->
|
|
<div class="card">
|
|
<div class="card-title">CURRENT ANGLES (feedback)</div>
|
|
<div id="angle-display">
|
|
<div class="angle-box">
|
|
<div class="angle-label">PAN</div>
|
|
<div class="angle-value" id="pan-val">0.0</div>
|
|
<div class="angle-unit">degrees</div>
|
|
</div>
|
|
<div class="angle-box">
|
|
<div class="angle-label">TILT</div>
|
|
<div class="angle-value" id="tilt-val">0.0</div>
|
|
<div class="angle-unit">degrees</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preset positions -->
|
|
<div class="card">
|
|
<div class="card-title">PRESET POSITIONS</div>
|
|
<div id="presets">
|
|
<button class="btn btn-cyan" data-preset="0">
|
|
<span class="preset-name">CENTER</span>
|
|
<span class="preset-val">0° / 0°</span>
|
|
</button>
|
|
<button class="btn btn-cyan" data-preset="1">
|
|
<span class="preset-name">LEFT</span>
|
|
<span class="preset-val">-45° / 0°</span>
|
|
</button>
|
|
<button class="btn btn-cyan" data-preset="2">
|
|
<span class="preset-name">RIGHT</span>
|
|
<span class="preset-val">+45° / 0°</span>
|
|
</button>
|
|
<button class="btn btn-cyan" data-preset="3">
|
|
<span class="preset-name">UP</span>
|
|
<span class="preset-val">0° / +45°</span>
|
|
</button>
|
|
<button class="btn btn-cyan" data-preset="4">
|
|
<span class="preset-name">DOWN</span>
|
|
<span class="preset-val">0° / -30°</span>
|
|
</button>
|
|
</div>
|
|
<button id="btn-home" style="margin-top:8px">⌂ HOME (0° / 0°)</button>
|
|
</div>
|
|
|
|
<!-- Person tracking -->
|
|
<div class="card">
|
|
<div class="card-title">PERSON TRACKING</div>
|
|
<div id="tracking-row">
|
|
<span class="toggle-label" id="tracking-label">OFF — manual control</span>
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="tracking-toggle">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
<div style="font-size:9px;color:#4b5563;margin-top:6px">
|
|
Publishes to <code>/gimbal/tracking_enabled</code>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Topic info -->
|
|
<div class="card" style="font-size:9px;color:#4b5563;line-height:1.8">
|
|
<div class="card-title">ROS TOPICS</div>
|
|
<div>PUB <code style="color:#374151">/gimbal/cmd</code> → geometry_msgs/Vector3</div>
|
|
<div>SUB <code style="color:#374151">/gimbal/state</code> → geometry_msgs/Vector3</div>
|
|
<div>PUB <code style="color:#374151">/gimbal/tracking_enabled</code> → std_msgs/Bool</div>
|
|
<div>SUB <code style="color:#374151">/camera/*/image_raw/compressed</code></div>
|
|
</div>
|
|
|
|
</aside>
|
|
</div>
|
|
|
|
<!-- ── Footer ── -->
|
|
<div id="footer">
|
|
<span>rosbridge: <code id="footer-ws">ws://localhost:9090</code></span>
|
|
<span>gimbal control panel — issue #551</span>
|
|
</div>
|
|
|
|
<script src="gimbal_panel.js"></script>
|
|
<script>
|
|
// Sync footer ws URL
|
|
document.getElementById('ws-input').addEventListener('input', (e) => {
|
|
document.getElementById('footer-ws').textContent = e.target.value;
|
|
});
|
|
|
|
// Tracking label update
|
|
document.getElementById('tracking-toggle').addEventListener('change', (e) => {
|
|
document.getElementById('tracking-label').textContent =
|
|
e.target.checked ? 'ON — following person' : 'OFF — manual control';
|
|
});
|
|
|
|
// Camera topic label update
|
|
document.querySelectorAll('.cam-btn').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
const cam = btn.dataset.cam;
|
|
document.getElementById('cam-topic').textContent =
|
|
`/camera/${cam}/image_raw/compressed`;
|
|
document.getElementById('footer-ws').textContent =
|
|
document.getElementById('ws-input').value;
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|