import { useEffect, useRef, useState } from 'react'; import { useControlSystem } from '../controls/index.js'; import { formatKeyLabel } from '../controls/keymapUtils.js'; import NightVisionControl from './NightVisionControl.jsx'; import HornControl from './HornControl.jsx'; const SLIDER_THROTTLE_MS = 150; function formatDegrees(value) { if (typeof value !== 'number' || Number.isNaN(value)) return '—'; return `${value > 0 ? '+' : ''}${value.toFixed(1)}°`; } export default function CameraServoPanel() { const { state: { roverId, camera, keymap, horn }, pipeline, actions: { setServoAngle, nudgeServo, goServoHome, 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 nightVisionKey = formatKeyLabel(keymap?.nightVisionToggle?.[0]); const hornKey = formatKeyLabel(keymap?.hornHonk?.[0]); 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; if (!enabled && !nightVisionAvailable && !hornAvailable) return null; 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 step = typeof config?.nudgeDegrees === 'number' && config.nudgeDegrees > 0 ? config.nudgeDegrees : 1; const scheduleSend = (next) => { if (throttleRef.current) { clearTimeout(throttleRef.current); } throttleRef.current = setTimeout(() => { setServoAngle(next); }, SLIDER_THROTTLE_MS); }; 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); }; const handleNudge = (direction) => { const delta = step * direction; nudgeServo(delta); }; const handleNightVisionToggle = (nextOn) => { if (!nightVisionAvailable) return; setNightVision(nextOn); }; return (
{nightVisionAvailable && ( )} {hornAvailable && ( )} {enabled && ( <>
Camera Tilt {formatDegrees(value)}
{formatDegrees(min)} {formatDegrees(max)}
)} {/*
*/} {/*

Step: {formatDegrees(step)} · Pin GPIO {config?.pin}

*/}
); }