import { useEffect, useMemo, useRef, useState } from 'react'; import { useSession } from '../context/SessionContext.jsx'; import { useTelemetryFrame } from '../context/TelemetryContext.jsx'; import { useControlSystem } from '../controls/index.js'; import { formatKeyLabel } from '../controls/keymapUtils.js'; import TopDownMap from './TopDownMap.jsx'; import RoverRoster from './RoverRoster.jsx'; export default function ControlSummary() { const { session, requestControl } = useSession(); const { state: { roverId, keymap }, actions, } = useControlSystem(); const frame = useTelemetryFrame(roverId); const sensors = frame?.sensors || {}; const roster = session?.roster ?? []; const [pending, setPending] = useState({}); const canRequest = useMemo(() => session?.role && session.role !== 'spectator', [session?.role]); const driving = useMemo( () => (sensors.oiMode?.label || '').toLowerCase() === 'full', [sensors.oiMode?.label], ); const docked = Boolean(sensors.chargingSources?.homeBase); const charging = Boolean( sensors.chargingState?.label && sensors.chargingState.label.toLowerCase() !== 'not charging', ); const handleStartDrive = () => { if (!roverId) return; actions.setMode('drive'); actions.runMacro('drive-sequence'); }; const handleDock = () => { if (!roverId) return; actions.setMode('dock'); actions.runMacro('seek-dock'); }; async function handleRequest(targetRoverId) { if (!targetRoverId) return; setPending((prev) => ({ ...prev, [targetRoverId]: true })); try { await requestControl(targetRoverId); } catch (err) { alert(err.message); } finally { setPending((prev) => ({ ...prev, [targetRoverId]: false })); } } return (
canRequest ? ( ) : null } />
); } function ActionButton({ title, description, statuses, onClick, disabled, tone, keyAction, keymap }) { const colors = tone === 'indigo' ? 'bg-indigo-700 hover:bg-indigo-600 focus-visible:ring-indigo-400' : 'bg-emerald-700 hover:bg-emerald-600 focus-visible:ring-emerald-400'; const keyLabel = keyAction ? formatKeyLabel(keymap?.[keyAction]?.[0]) : ''; return ( ); } function KeyPill({ label }) { return {label}; } function StatusPill({ label, active }) { return ( {label} ); } function InlineCameraTilt({ keymap }) { const { state: { roverId, camera }, actions: { setServoAngle }, } = useControlSystem(); const config = camera?.config; const enabled = Boolean(roverId && camera?.enabled && config); const min = typeof config?.minAngle === 'number' ? config.minAngle : -30; const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30; const value = typeof camera?.angle === 'number' ? camera.angle : typeof config?.homeAngle === 'number' ? config.homeAngle : (min + max) / 2; const [pendingAngle, setPendingAngle] = useState(value); const throttleRef = useRef(null); const draggingRef = useRef(false); useEffect(() => { if (!draggingRef.current) { setPendingAngle(value); } }, [value]); useEffect( () => () => { if (throttleRef.current) { clearTimeout(throttleRef.current); } }, [], ); const scheduleSend = (next) => { if (throttleRef.current) { clearTimeout(throttleRef.current); } throttleRef.current = setTimeout(() => { setServoAngle(next); }, 150); }; const handleSlider = (event) => { const next = Number.parseFloat(event.target.value); if (Number.isNaN(next)) return; draggingRef.current = true; setPendingAngle(next); scheduleSend(next); }; const commitSlider = () => { draggingRef.current = false; if (throttleRef.current) { clearTimeout(throttleRef.current); } setServoAngle(pendingAngle); }; if (!enabled) return null; const upLabel = formatKeyLabel(keymap?.cameraUp?.[0]); const downLabel = formatKeyLabel(keymap?.cameraDown?.[0]); return (
Camera tilt {formatDegrees(value)}
{downLabel ? : null} {formatDegrees(min)} {formatDegrees(max)} {upLabel ? : null}
); } function formatDegrees(value) { if (typeof value !== 'number' || Number.isNaN(value)) return '—'; return `${value > 0 ? '+' : ''}${value.toFixed(1)}°`; }