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'; import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx'; import NightVisionControl from './NightVisionControl.jsx'; import HornControl from './HornControl.jsx'; export function RoverRosterPanel({ title = 'Rovers' }) { const { session, requestControl } = useSession(); const [pending, setPending] = useState({}); const canRequest = useMemo(() => session?.role && session.role !== 'spectator', [session?.role]); 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 } /> ); } export default function ControlSummary() { const { state: { roverId, keymap }, } = useControlSystem(); const frame = useTelemetryFrame(roverId); const sensors = frame?.sensors || {}; const driveDockState = useDriveDockState(roverId); const hideInlineControls = driveDockState.docked && !driveDockState.driving; return (
{!hideInlineControls ? : null}
); } export function InlineCameraTilt({ keymap }) { const { state: { roverId, camera, horn }, pipeline, actions: { setServoAngle, setNightVision, startHorn, stopHorn }, } = useControlSystem(); const config = camera?.config; const enabled = Boolean(roverId && camera?.enabled && config); const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision); const nightVisionState = pipeline?.nightVisionState; const hornAvailable = Boolean(roverId && pipeline?.horn); const hornBlocked = horn?.overheated; 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 draggingRef = useRef(false); useEffect(() => { if (!draggingRef.current) { setPendingAngle(value); } }, [value]); const handleSlider = (event) => { const next = Number.parseFloat(event.target.value); if (Number.isNaN(next)) return; draggingRef.current = true; setPendingAngle(next); setServoAngle(next); }; const commitSlider = () => { draggingRef.current = false; }; if (!enabled && !nightVisionAvailable && !hornAvailable) return null; const upLabel = formatKeyLabel(keymap?.cameraUp?.[0]); const downLabel = formatKeyLabel(keymap?.cameraDown?.[0]); const nightVisionLabel = formatKeyLabel(keymap?.nightVisionToggle?.[0]); const hornLabel = formatKeyLabel(keymap?.hornHonk?.[0]); return (
{nightVisionAvailable && ( )} {hornAvailable && ( )} {enabled && (
Camera tilt {formatDegrees(value)}
{downLabel ? : null} {formatDegrees(min)} {formatDegrees(max)} {upLabel ? : null}
)}
); } function KeyPill({ label }) { return {label}; } function formatDegrees(value) { if (typeof value !== 'number' || Number.isNaN(value)) return '—'; return `${value > 0 ? '+' : ''}${value.toFixed(1)}°`; }