// Aux Column // Purpose: Assembles the mobile auxiliary controls column, which is the left column by default. // Scope: Owns mobile aux/camera/headlight/laser/horn wiring while reusing desktop variation components where intended. import { useCallback, useEffect, useRef, useState } from 'react'; import { FaBullhorn, FaCrosshairs, FaLightbulb } from 'react-icons/fa'; import './mobileControls.css'; import { useControlActions, useControlSelector } from '../../controls/index.js'; import { useTelemetrySelector } from '../../context/TelemetryContext.jsx'; import { dockTelemetryEqual, selectDockTelemetry } from '../../context/telemetryViews.js'; import useCanControlRover from '../../hooks/useCanControlRover.js'; import HornControl from '../HornControl/index.jsx'; import GPIOToggleControl from '../GPIOToggleControl/index.jsx'; import { AUX_ZERO } from './constants.js'; import VacuumControls from './VacuumControls.jsx'; import VerticalCameraTilt from './VerticalCameraTilt.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx'; import RoverAccessoryControls from '../RoverAccessoryControls/index.jsx'; import AccessoriesToggle from '../RoverAccessoryControls/AccessoriesToggle.jsx'; import useRoverAccessories from '../RoverAccessoryControls/useRoverAccessories.js'; const CAMERA_TILT_STEP_DEGREES = 0.5; const CAMERA_TILT_PRECISION_STEP_DEGREES = 0.1; function AuxColumnContent({ accessoriesAvailable, onShowAccessories }) { const roverId = useControlSelector((control) => control.state.roverId); const roomLightsLockedOn = useSessionSelector((state) => Boolean(state.session?.homeAssistant?.lightPolicy?.lockedOn)); const camera = useControlSelector((control) => control.state.camera); const horn = useControlSelector((control) => control.state.horn); const headlight = useControlSelector((control) => control.pipeline?.headlight); const headlightState = useControlSelector((control) => control.pipeline?.headlightState); const laser = useControlSelector((control) => control.pipeline?.laser); const laserState = useControlSelector((control) => control.pipeline?.laserState); const pipelineHorn = useControlSelector((control) => control.pipeline?.horn); const { setServoAngle, setHeadlight, setLaser, setAuxMotors, startHorn, stopHorn } = useControlActions(); const dockTelemetry = useTelemetrySelector(roverId, selectDockTelemetry, dockTelemetryEqual); const canControl = useCanControlRover(roverId); // Turn ownership is the common mutation boundary for every control in this // column. Dock and OI state are applied separately only where the hardware // command itself depends on the Roomba being able to drive. const controlsDisabled = !roverId || !canControl; const docked = Boolean(dockTelemetry?.homeBase); const drivingMode = String(dockTelemetry?.oiModeLabel || '').toLowerCase() === 'full'; const vacuumDisabled = controlsDisabled || docked || dockTelemetry.movementLocked || (!drivingMode && !dockTelemetry.cameraLocked); const activeAuxButtonRef = useRef(null); const cameraConfig = camera?.config; const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig); const headlightAvailable = Boolean(roverId && headlight); const laserAvailable = Boolean(roverId && laser); const hornAvailable = Boolean(roverId && pipelineHorn); const hornBlocked = horn?.overheated; const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45; const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45; const cameraValue = typeof camera?.angle === 'number' ? camera.angle : typeof cameraConfig?.homeAngle === 'number' ? cameraConfig.homeAngle : (cameraMin + cameraMax) / 2; const cameraDisabled = Boolean(controlsDisabled || dockTelemetry.cameraLocked); /* The mobile tilt track shares the same precision flag as desktop tilt. This keeps the servo fine-step behavior tied to the selected movement mode rather than inventing a separate mobile-only camera setting. */ const cameraTiltStep = camera?.precisionMode ? CAMERA_TILT_PRECISION_STEP_DEGREES : CAMERA_TILT_STEP_DEGREES; const handleHeadlightToggle = useCallback( (nextOn) => { if (!headlightAvailable || controlsDisabled) return; setHeadlight(nextOn); }, [controlsDisabled, headlightAvailable, setHeadlight], ); const handleLaserToggle = useCallback( (nextOn) => { if (!laserAvailable || controlsDisabled) return; setLaser(nextOn); }, [controlsDisabled, laserAvailable, setLaser], ); const handleHornStart = useCallback(() => { if (controlsDisabled) return false; return startHorn(); }, [controlsDisabled, startHorn]); const handleAuxPress = useCallback( (id, values) => { if (vacuumDisabled) return; activeAuxButtonRef.current = id; setAuxMotors(values); }, [setAuxMotors, vacuumDisabled], ); const handleAuxRelease = useCallback( (id) => { if (activeAuxButtonRef.current === id) { activeAuxButtonRef.current = null; // Neutral commands must remain available after ownership or OI state is // lost; otherwise disabling a held control could preserve its last output. setAuxMotors(AUX_ZERO); } }, [setAuxMotors], ); useEffect(() => { if (!vacuumDisabled || activeAuxButtonRef.current === null) return; // Pointer cancellation is browser-dependent when a held button becomes // disabled. Explicitly neutralize the Roomba motors at the state boundary. activeAuxButtonRef.current = null; setAuxMotors(AUX_ZERO); }, [setAuxMotors, vacuumDisabled]); return (
{accessoriesAvailable ? ( ) : null}
{cameraEnabled ? ( ) : null} {(headlightAvailable || laserAvailable) ? (
{headlightAvailable ? ( ) : null} {laserAvailable ? ( ) : null}
) : null}
{hornAvailable ? ( ) : null}
); } function RoverAuxColumn({ roverId, layout, className }) { const [showAccessories, setShowAccessories] = useState(false); const { hasAccessories } = useRoverAccessories(roverId); return (
{showAccessories && hasAccessories ? (
setShowAccessories(false)} /> )} />
) : ( setShowAccessories(true)} /> )}
); } export default function AuxColumn({ layout, className = '' }) { const roverId = useControlSelector((control) => control.state.roverId); // Keying the stateful view by assignment makes every newly selected rover // start in the familiar Aux view. It also guarantees that an Accessories // view cannot remain open after changing to a rover without accessories. return ( ); }