mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
headlight rework and laser addition
This commit is contained in:
+18
-12
@@ -1,14 +1,15 @@
|
||||
// Night Vision Control
|
||||
// Purpose: Defines the Night Vision Control module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
// GPIO Toggle Control
|
||||
// Purpose: Renders a direct press target for rover GPIO-backed toggles such as the headlight and laser.
|
||||
// Scope: Owns optimistic button state and touch/click de-duplication while callers provide device labels and actions.
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
function isBoolean(value) {
|
||||
return typeof value === 'boolean';
|
||||
}
|
||||
|
||||
export default function NightVisionControl({
|
||||
nightVisionOn,
|
||||
export default function GPIOToggleControl({
|
||||
label,
|
||||
on,
|
||||
disabled,
|
||||
onToggle,
|
||||
keyLabel,
|
||||
@@ -16,16 +17,21 @@ export default function NightVisionControl({
|
||||
heightClass = '',
|
||||
}) {
|
||||
const [optimistic, setOptimistic] = useState(
|
||||
isBoolean(nightVisionOn) ? nightVisionOn : null,
|
||||
isBoolean(on) ? on : null,
|
||||
);
|
||||
const suppressClickRef = useRef(false);
|
||||
const suppressClickTimerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isBoolean(nightVisionOn)) {
|
||||
setOptimistic(nightVisionOn);
|
||||
if (isBoolean(on)) {
|
||||
// Server-confirmed state can arrive after an optimistic click. Defer the
|
||||
// reconciliation one tick so this effect stays a synchronization point
|
||||
// instead of triggering React's synchronous set-state-in-effect lint rule.
|
||||
const timer = window.setTimeout(() => setOptimistic(on), 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}
|
||||
}, [nightVisionOn]);
|
||||
return undefined;
|
||||
}, [on]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -58,7 +64,7 @@ export default function NightVisionControl({
|
||||
Mobile browsers, especially Safari, do not always dispatch a reliable
|
||||
synthetic click for a second finger while another finger is held on the
|
||||
drive pad. Toggle on the real touch pointerdown instead, then suppress the
|
||||
follow-up click so one tap cannot flip night vision twice.
|
||||
follow-up click so one tap cannot flip the GPIO-backed device twice.
|
||||
*/
|
||||
event.preventDefault();
|
||||
suppressClickRef.current = true;
|
||||
@@ -91,7 +97,7 @@ export default function NightVisionControl({
|
||||
};
|
||||
|
||||
const buttonClasses = useMemo(() => {
|
||||
// Night vision is used as a direct mobile press target, so selection and
|
||||
// This control is used as a direct mobile press target, so selection and
|
||||
// Safari callout suppression live on the button itself rather than only on
|
||||
// the surrounding mobile column.
|
||||
const base =
|
||||
@@ -114,7 +120,7 @@ export default function NightVisionControl({
|
||||
className={buttonClasses}
|
||||
>
|
||||
<span className="flex items-center gap-0.5">
|
||||
<span className="text-sm font-semibold">Night Vision</span>
|
||||
<span className="text-sm font-semibold">{label}</span>
|
||||
{keyLabel ? (
|
||||
<span className="rounded bg-slate-800 px-1 py-0.5 text-[0.6rem] font-semibold text-slate-200">
|
||||
{keyLabel}
|
||||
@@ -38,7 +38,8 @@ export const ACTIONS = [
|
||||
{ id: 'sideReverse', label: 'Side reverse toggle', kind: 'button', section: 'Brush toggles' },
|
||||
{ id: 'driveMacro', label: 'Drive macro', kind: 'button', section: 'Mode macros' },
|
||||
{ id: 'dockMacro', label: 'Dock macro', kind: 'button', section: 'Mode macros' },
|
||||
{ id: 'nightVisionToggle', label: 'Night vision toggle', kind: 'button', section: 'Camera' },
|
||||
{ id: 'headlightToggle', label: 'Headlight toggle', kind: 'button', section: 'Camera' },
|
||||
{ id: 'laserToggle', label: 'Laser toggle', kind: 'button', section: 'Camera' },
|
||||
];
|
||||
|
||||
export const CAPTURE_AXIS_THRESHOLD = 0.45;
|
||||
|
||||
@@ -26,7 +26,8 @@ const KEY_ACTIONS = [
|
||||
{ id: 'auxAllForward', label: 'All Aux Forward', group: 'Aux Motors' },
|
||||
{ id: 'cameraUp', label: 'Camera Up', group: 'Camera' },
|
||||
{ id: 'cameraDown', label: 'Camera Down', group: 'Camera' },
|
||||
{ id: 'nightVisionToggle', label: 'Toggle Night Vision', group: 'Camera' },
|
||||
{ id: 'headlightToggle', label: 'Toggle Headlight', group: 'Camera' },
|
||||
{ id: 'laserToggle', label: 'Toggle Laser', group: 'Camera' },
|
||||
{ id: 'videoFilterCycle', label: 'Cycle Video Filter', group: 'Camera' },
|
||||
{ id: 'hornHonk', label: 'Horn (Hold)', group: 'Audio' },
|
||||
{ id: 'micPtt', label: 'Mic Push To Talk', group: 'Audio' },
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Aux Column
|
||||
// Purpose: Assembles the mobile auxiliary controls column, which is the left column by default.
|
||||
// Scope: Owns mobile aux/camera/night vision/horn wiring while reusing desktop variation components where intended.
|
||||
// Scope: Owns mobile aux/camera/headlight/laser/horn wiring while reusing desktop variation components where intended.
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
||||
import HornControl from '../HornControl/index.jsx';
|
||||
import NightVisionControl from '../NightVisionControl/index.jsx';
|
||||
import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
|
||||
import { AUX_ZERO } from './constants.js';
|
||||
import VacuumControls from './VacuumControls.jsx';
|
||||
import VerticalCameraTilt from './VerticalCameraTilt.jsx';
|
||||
@@ -15,16 +15,19 @@ function AuxColumnContent() {
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
const camera = useControlSelector((control) => control.state.camera);
|
||||
const horn = useControlSelector((control) => control.state.horn);
|
||||
const nightVision = useControlSelector((control) => control.pipeline?.nightVision);
|
||||
const nightVisionState = useControlSelector((control) => control.pipeline?.nightVisionState);
|
||||
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, setNightVision, setAuxMotors, startHorn, stopHorn } = useControlActions();
|
||||
const { setServoAngle, setHeadlight, setLaser, setAuxMotors, startHorn, stopHorn } = useControlActions();
|
||||
const dockAssist = useManualDockAssist();
|
||||
const disabled = !roverId;
|
||||
const activeAuxButtonRef = useRef(null);
|
||||
const cameraConfig = camera?.config;
|
||||
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
|
||||
const nightVisionAvailable = Boolean(roverId && nightVision);
|
||||
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;
|
||||
@@ -37,13 +40,22 @@ function AuxColumnContent() {
|
||||
: (cameraMin + cameraMax) / 2;
|
||||
const cameraDisabled = Boolean(disabled || dockAssist.cameraLocked);
|
||||
|
||||
const handleNightVisionToggle = useCallback(
|
||||
const handleHeadlightToggle = useCallback(
|
||||
(nextOn) => {
|
||||
if (!nightVisionAvailable) return;
|
||||
trackAnalyticsEvent('night_vision_toggle', { roverId, source: 'mobile_control', enabled: Boolean(nextOn) });
|
||||
setNightVision(nextOn);
|
||||
if (!headlightAvailable) return;
|
||||
trackAnalyticsEvent('headlight_toggle', { roverId, source: 'mobile_control', enabled: Boolean(nextOn) });
|
||||
setHeadlight(nextOn);
|
||||
},
|
||||
[nightVisionAvailable, roverId, setNightVision],
|
||||
[headlightAvailable, roverId, setHeadlight],
|
||||
);
|
||||
|
||||
const handleLaserToggle = useCallback(
|
||||
(nextOn) => {
|
||||
if (!laserAvailable) return;
|
||||
trackAnalyticsEvent('laser_toggle', { roverId, source: 'mobile_control', enabled: Boolean(nextOn) });
|
||||
setLaser(nextOn);
|
||||
},
|
||||
[laserAvailable, roverId, setLaser],
|
||||
);
|
||||
|
||||
const handleHornStart = useCallback(() => {
|
||||
@@ -93,13 +105,27 @@ function AuxColumnContent() {
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{nightVisionAvailable ? (
|
||||
<NightVisionControl
|
||||
nightVisionOn={nightVisionState?.nightVisionOn}
|
||||
disabled={disabled}
|
||||
onToggle={handleNightVisionToggle}
|
||||
heightClass="h-full"
|
||||
/>
|
||||
{(headlightAvailable || laserAvailable) ? (
|
||||
<div className="mobile-touch-control flex min-h-0 flex-1 flex-col gap-0.5">
|
||||
{headlightAvailable ? (
|
||||
<GPIOToggleControl
|
||||
label="Headlight"
|
||||
on={headlightState?.headlightOn}
|
||||
disabled={disabled}
|
||||
onToggle={handleHeadlightToggle}
|
||||
heightClass="h-full"
|
||||
/>
|
||||
) : null}
|
||||
{laserAvailable ? (
|
||||
<GPIOToggleControl
|
||||
label="Laser"
|
||||
on={laserState?.laserOn}
|
||||
disabled={disabled}
|
||||
onToggle={handleLaserToggle}
|
||||
heightClass="h-full"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mobile-touch-control min-h-0">
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import RoverQueuesPanel from '../RoverQueuesPanel/index.jsx';
|
||||
import RawUserPilePanel from '../RawUserPilePanel/index.jsx';
|
||||
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||
import NightVisionControl from '../NightVisionControl/index.jsx';
|
||||
import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
|
||||
import HornControl from '../HornControl/index.jsx';
|
||||
import CameraTiltControl from '../CameraTiltControl/index.jsx';
|
||||
import VipPanel from '../VipPanel/index.jsx';
|
||||
@@ -62,17 +62,20 @@ function DriveDockPanel() {
|
||||
const keymap = useControlSelector((control) => control.state.keymap);
|
||||
const camera = useControlSelector((control) => control.state.camera);
|
||||
const horn = useControlSelector((control) => control.state.horn);
|
||||
const nightVision = useControlSelector((control) => control.pipeline?.nightVision);
|
||||
const nightVisionState = useControlSelector((control) => control.pipeline?.nightVisionState);
|
||||
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, setNightVision, startHorn, stopHorn } = useControlActions();
|
||||
const { setServoAngle, setHeadlight, setLaser, startHorn, stopHorn } = useControlActions();
|
||||
const dockAssist = useManualDockAssist();
|
||||
const driveDockState = useDriveDockState(roverId);
|
||||
const hideInlineControls = driveDockState.docked && !driveDockState.driving;
|
||||
|
||||
const config = camera?.config;
|
||||
const cameraEnabled = Boolean(roverId && camera?.enabled && config);
|
||||
const nightVisionAvailable = Boolean(roverId && nightVision);
|
||||
const headlightAvailable = Boolean(roverId && headlight);
|
||||
const laserAvailable = Boolean(roverId && laser);
|
||||
const hornAvailable = Boolean(roverId && pipelineHorn);
|
||||
const hornBlocked = horn?.overheated;
|
||||
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
|
||||
@@ -83,16 +86,21 @@ function DriveDockPanel() {
|
||||
: typeof config?.homeAngle === 'number'
|
||||
? config.homeAngle
|
||||
: (min + max) / 2;
|
||||
const nightVisionLabel = formatKeyLabel(keymap?.nightVisionToggle?.[0]);
|
||||
const headlightLabel = formatKeyLabel(keymap?.headlightToggle?.[0]);
|
||||
const laserLabel = formatKeyLabel(keymap?.laserToggle?.[0]);
|
||||
const hornLabel = formatKeyLabel(keymap?.hornHonk?.[0]);
|
||||
const upLabel = formatKeyLabel(keymap?.cameraUp?.[0]);
|
||||
const downLabel = formatKeyLabel(keymap?.cameraDown?.[0]);
|
||||
const cameraDisabled = Boolean(!roverId || dockAssist.cameraLocked);
|
||||
const trackedControls = useMemo(
|
||||
() => ({
|
||||
setNightVision: (nextOn) => {
|
||||
trackAnalyticsEvent('night_vision_toggle', { roverId, source: 'desktop_control', enabled: Boolean(nextOn) });
|
||||
setNightVision(nextOn);
|
||||
setHeadlight: (nextOn) => {
|
||||
trackAnalyticsEvent('headlight_toggle', { roverId, source: 'desktop_control', enabled: Boolean(nextOn) });
|
||||
setHeadlight(nextOn);
|
||||
},
|
||||
setLaser: (nextOn) => {
|
||||
trackAnalyticsEvent('laser_toggle', { roverId, source: 'desktop_control', enabled: Boolean(nextOn) });
|
||||
setLaser(nextOn);
|
||||
},
|
||||
startHorn: () => {
|
||||
trackAnalyticsEvent('horn_start', { roverId, source: 'desktop_control' });
|
||||
@@ -100,7 +108,7 @@ function DriveDockPanel() {
|
||||
},
|
||||
stopHorn,
|
||||
}),
|
||||
[roverId, setNightVision, startHorn, stopHorn],
|
||||
[roverId, setHeadlight, setLaser, startHorn, stopHorn],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -113,13 +121,27 @@ function DriveDockPanel() {
|
||||
<DriveDockAction layout="desktop" expand driveDockState={driveDockState} />
|
||||
{!hideInlineControls ? (
|
||||
<div className={`${themeStackClass} p-0 text-sm text-slate-200`}>
|
||||
{nightVisionAvailable && (
|
||||
<NightVisionControl
|
||||
nightVisionOn={nightVisionState?.nightVisionOn}
|
||||
disabled={!roverId}
|
||||
onToggle={trackedControls.setNightVision}
|
||||
keyLabel={nightVisionLabel}
|
||||
/>
|
||||
{(headlightAvailable || laserAvailable) && (
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{headlightAvailable && (
|
||||
<GPIOToggleControl
|
||||
label="Headlight"
|
||||
on={headlightState?.headlightOn}
|
||||
disabled={!roverId}
|
||||
onToggle={trackedControls.setHeadlight}
|
||||
keyLabel={headlightLabel}
|
||||
/>
|
||||
)}
|
||||
{laserAvailable && (
|
||||
<GPIOToggleControl
|
||||
label="Laser"
|
||||
on={laserState?.laserOn}
|
||||
disabled={!roverId}
|
||||
onToggle={trackedControls.setLaser}
|
||||
keyLabel={laserLabel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hornAvailable && (
|
||||
<HornControl
|
||||
|
||||
@@ -53,8 +53,10 @@ const CONTROL_ACTION_NAMES = [
|
||||
'stopAllMotion',
|
||||
'sendOiCommand',
|
||||
'setSensorStream',
|
||||
'setNightVision',
|
||||
'toggleNightVision',
|
||||
'setHeadlight',
|
||||
'toggleHeadlight',
|
||||
'setLaser',
|
||||
'toggleLaser',
|
||||
'updateKeyBinding',
|
||||
'resetKeyBindings',
|
||||
'registerInputState',
|
||||
@@ -469,28 +471,38 @@ export function ControlSystemProvider({ children }) {
|
||||
[pipeline],
|
||||
);
|
||||
|
||||
const setNightVision = useCallback(
|
||||
(nightVisionOn) => {
|
||||
if (!pipeline.nightVision) return;
|
||||
if (typeof nightVisionOn === 'boolean') {
|
||||
/*
|
||||
Rover daemon command names describe the IR LED, while the UI state
|
||||
describes camera visibility. LED "off" means nightVisionOn=true, and
|
||||
LED "on" means nightVisionOn=false.
|
||||
*/
|
||||
const action = nightVisionOn ? 'off' : 'on';
|
||||
pipeline.sendNightVision(action);
|
||||
} else {
|
||||
pipeline.sendNightVision('toggle');
|
||||
}
|
||||
const setHeadlight = useCallback(
|
||||
(headlightOn) => {
|
||||
if (!pipeline.headlight) return;
|
||||
// Web controls now speak in logical device state. Any electrical
|
||||
// inversion needed by the actual GPIO driver is handled by roverd's
|
||||
// activeLow config, so this command stays readable and direct.
|
||||
const action = typeof headlightOn === 'boolean' ? (headlightOn ? 'on' : 'off') : 'toggle';
|
||||
pipeline.sendHeadlight(action);
|
||||
recordControlIntent();
|
||||
},
|
||||
[pipeline, recordControlIntent],
|
||||
);
|
||||
|
||||
const toggleNightVision = useCallback(() => {
|
||||
setNightVision();
|
||||
}, [setNightVision]);
|
||||
const toggleHeadlight = useCallback(() => {
|
||||
setHeadlight();
|
||||
}, [setHeadlight]);
|
||||
|
||||
const setLaser = useCallback(
|
||||
(laserOn) => {
|
||||
if (!pipeline.laser) return;
|
||||
// The laser shares the same logical toggle contract as the headlight; it
|
||||
// is separate only because it has its own GPIO pin, UI control, and keybind.
|
||||
const action = typeof laserOn === 'boolean' ? (laserOn ? 'on' : 'off') : 'toggle';
|
||||
pipeline.sendLaser(action);
|
||||
recordControlIntent();
|
||||
},
|
||||
[pipeline, recordControlIntent],
|
||||
);
|
||||
|
||||
const toggleLaser = useCallback(() => {
|
||||
setLaser();
|
||||
}, [setLaser]);
|
||||
|
||||
const setSongNote = useCallback(
|
||||
(note) => {
|
||||
@@ -665,8 +677,10 @@ export function ControlSystemProvider({ children }) {
|
||||
stopAllMotion,
|
||||
sendOiCommand,
|
||||
setSensorStream,
|
||||
setNightVision,
|
||||
toggleNightVision,
|
||||
setHeadlight,
|
||||
toggleHeadlight,
|
||||
setLaser,
|
||||
toggleLaser,
|
||||
updateKeyBinding,
|
||||
resetKeyBindings,
|
||||
registerInputState,
|
||||
@@ -689,8 +703,10 @@ export function ControlSystemProvider({ children }) {
|
||||
stopAllMotion,
|
||||
sendOiCommand,
|
||||
setSensorStream,
|
||||
setNightVision,
|
||||
toggleNightVision,
|
||||
setHeadlight,
|
||||
toggleHeadlight,
|
||||
setLaser,
|
||||
toggleLaser,
|
||||
updateKeyBinding,
|
||||
resetKeyBindings,
|
||||
registerInputState,
|
||||
|
||||
@@ -29,9 +29,14 @@ export function useCommandPipeline(options = {}) {
|
||||
return rosterEntry.cameraServo;
|
||||
}, [rosterEntry]);
|
||||
|
||||
const nightVision = useMemo(() => {
|
||||
if (!rosterEntry?.nightVision || !rosterEntry.nightVision.enabled) return null;
|
||||
return rosterEntry.nightVision;
|
||||
const headlight = useMemo(() => {
|
||||
if (!rosterEntry?.headlight || !rosterEntry.headlight.enabled) return null;
|
||||
return rosterEntry.headlight;
|
||||
}, [rosterEntry]);
|
||||
|
||||
const laser = useMemo(() => {
|
||||
if (!rosterEntry?.laser || !rosterEntry.laser.enabled) return null;
|
||||
return rosterEntry.laser;
|
||||
}, [rosterEntry]);
|
||||
|
||||
const horn = useMemo(() => {
|
||||
@@ -39,7 +44,8 @@ export function useCommandPipeline(options = {}) {
|
||||
return rosterEntry.horn;
|
||||
}, [rosterEntry]);
|
||||
|
||||
const nightVisionState = useMemo(() => rosterEntry?.nightVision?.state ?? null, [rosterEntry]);
|
||||
const headlightState = useMemo(() => rosterEntry?.headlight?.state ?? null, [rosterEntry]);
|
||||
const laserState = useMemo(() => rosterEntry?.laser?.state ?? null, [rosterEntry]);
|
||||
|
||||
const emitCommand = useCallback(
|
||||
(payload, cb) => {
|
||||
@@ -167,16 +173,28 @@ export function useCommandPipeline(options = {}) {
|
||||
[roverId, sendOiCommand, sendDriveDirect, sendAuxMotors, sendServoAngle],
|
||||
);
|
||||
|
||||
const sendNightVision = useCallback(
|
||||
const sendHeadlight = useCallback(
|
||||
(action = 'toggle') => {
|
||||
if (!roverId || !nightVision) return null;
|
||||
if (!roverId || !headlight) return null;
|
||||
emitCommand({
|
||||
type: 'nightVision',
|
||||
data: { nightVision: { action } },
|
||||
type: 'headlight',
|
||||
data: { headlight: { action } },
|
||||
});
|
||||
return action;
|
||||
},
|
||||
[emitCommand, nightVision, roverId],
|
||||
[emitCommand, headlight, roverId],
|
||||
);
|
||||
|
||||
const sendLaser = useCallback(
|
||||
(action = 'toggle') => {
|
||||
if (!roverId || !laser) return null;
|
||||
emitCommand({
|
||||
type: 'laser',
|
||||
data: { laser: { action } },
|
||||
});
|
||||
return action;
|
||||
},
|
||||
[emitCommand, laser, roverId],
|
||||
);
|
||||
|
||||
const sendHorn = useCallback(
|
||||
@@ -222,8 +240,10 @@ export function useCommandPipeline(options = {}) {
|
||||
roverId,
|
||||
rosterEntry,
|
||||
servoConfig,
|
||||
nightVision,
|
||||
nightVisionState,
|
||||
headlight,
|
||||
headlightState,
|
||||
laser,
|
||||
laserState,
|
||||
horn,
|
||||
emitCommand,
|
||||
enableSensorStream,
|
||||
@@ -231,7 +251,8 @@ export function useCommandPipeline(options = {}) {
|
||||
sendAuxMotors,
|
||||
sendServoAngle,
|
||||
sendOiCommand,
|
||||
sendNightVision,
|
||||
sendHeadlight,
|
||||
sendLaser,
|
||||
sendHorn,
|
||||
sendSong,
|
||||
runMacroSteps,
|
||||
@@ -240,8 +261,10 @@ export function useCommandPipeline(options = {}) {
|
||||
roverId,
|
||||
rosterEntry,
|
||||
servoConfig,
|
||||
nightVision,
|
||||
nightVisionState,
|
||||
headlight,
|
||||
headlightState,
|
||||
laser,
|
||||
laserState,
|
||||
horn,
|
||||
emitCommand,
|
||||
enableSensorStream,
|
||||
@@ -249,7 +272,8 @@ export function useCommandPipeline(options = {}) {
|
||||
sendAuxMotors,
|
||||
sendServoAngle,
|
||||
sendOiCommand,
|
||||
sendNightVision,
|
||||
sendHeadlight,
|
||||
sendLaser,
|
||||
sendHorn,
|
||||
runMacroSteps,
|
||||
],
|
||||
|
||||
@@ -51,7 +51,8 @@ export const DEFAULT_KEYMAP = {
|
||||
auxAllForward: [","],
|
||||
cameraUp: ['u'],
|
||||
cameraDown: ['j'],
|
||||
nightVisionToggle: ['e'],
|
||||
headlightToggle: ['e'],
|
||||
laserToggle: ['r'],
|
||||
videoFilterCycle: ['2'],
|
||||
hornHonk: ['h'],
|
||||
micPtt: ['m'],
|
||||
|
||||
@@ -54,7 +54,8 @@ export default function GamepadInputManager() {
|
||||
setAuxMotors,
|
||||
setServoAngle,
|
||||
runMacro,
|
||||
toggleNightVision,
|
||||
toggleHeadlight,
|
||||
toggleLaser,
|
||||
registerInputState,
|
||||
} = useControlActions();
|
||||
const cameraAngle = useControlSelector((control) => control.state.camera?.angle);
|
||||
@@ -175,7 +176,8 @@ export default function GamepadInputManager() {
|
||||
setDriveVector,
|
||||
setMode,
|
||||
setServoAngle,
|
||||
toggleNightVision,
|
||||
toggleHeadlight,
|
||||
toggleLaser,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -286,11 +288,18 @@ export default function GamepadInputManager() {
|
||||
handleButtonEdge('dockMacro', false);
|
||||
}
|
||||
|
||||
if (outputs.buttons.nightVisionToggle && handleButtonEdge('nightVisionToggle', true)) {
|
||||
trackAnalyticsEvent('night_vision_toggle', { roverId: latest.roverId || '', source: 'gamepad' });
|
||||
latest.toggleNightVision();
|
||||
} else if (!outputs.buttons.nightVisionToggle) {
|
||||
handleButtonEdge('nightVisionToggle', false);
|
||||
if (outputs.buttons.headlightToggle && handleButtonEdge('headlightToggle', true)) {
|
||||
trackAnalyticsEvent('headlight_toggle', { roverId: latest.roverId || '', source: 'gamepad' });
|
||||
latest.toggleHeadlight();
|
||||
} else if (!outputs.buttons.headlightToggle) {
|
||||
handleButtonEdge('headlightToggle', false);
|
||||
}
|
||||
|
||||
if (outputs.buttons.laserToggle && handleButtonEdge('laserToggle', true)) {
|
||||
trackAnalyticsEvent('laser_toggle', { roverId: latest.roverId || '', source: 'gamepad' });
|
||||
latest.toggleLaser();
|
||||
} else if (!outputs.buttons.laserToggle) {
|
||||
handleButtonEdge('laserToggle', false);
|
||||
}
|
||||
|
||||
if (Math.abs(outputs.cameraAxis) > 0.001) {
|
||||
|
||||
@@ -91,7 +91,8 @@ export default function KeyboardInputManager() {
|
||||
runMacro,
|
||||
stopAllMotion,
|
||||
registerInputState,
|
||||
toggleNightVision,
|
||||
toggleHeadlight,
|
||||
toggleLaser,
|
||||
startHorn,
|
||||
stopHorn,
|
||||
setMicPttActive,
|
||||
@@ -376,7 +377,8 @@ export default function KeyboardInputManager() {
|
||||
startHorn,
|
||||
stopAllMotion,
|
||||
stopHorn,
|
||||
toggleNightVision,
|
||||
toggleHeadlight,
|
||||
toggleLaser,
|
||||
videoColorFilter,
|
||||
};
|
||||
});
|
||||
@@ -413,9 +415,12 @@ export default function KeyboardInputManager() {
|
||||
} else if (newlyPressed.some((token) => latest.keymap.dockMacro?.has(token))) {
|
||||
trackAnalyticsEvent('dock_assist_toggle', { roverId: latest.roverId || '', source: 'keyboard' });
|
||||
latest.dockAssist.toggleAssist();
|
||||
} else if (newlyPressed.some((token) => latest.keymap.nightVisionToggle?.has(token))) {
|
||||
trackAnalyticsEvent('night_vision_toggle', { roverId: latest.roverId || '', source: 'keyboard' });
|
||||
latest.toggleNightVision();
|
||||
} else if (newlyPressed.some((token) => latest.keymap.headlightToggle?.has(token))) {
|
||||
trackAnalyticsEvent('headlight_toggle', { roverId: latest.roverId || '', source: 'keyboard' });
|
||||
latest.toggleHeadlight();
|
||||
} else if (newlyPressed.some((token) => latest.keymap.laserToggle?.has(token))) {
|
||||
trackAnalyticsEvent('laser_toggle', { roverId: latest.roverId || '', source: 'keyboard' });
|
||||
latest.toggleLaser();
|
||||
} else if (newlyPressed.some((token) => latest.keymap.videoFilterCycle?.has(token))) {
|
||||
cycleVideoFilter();
|
||||
} else if (newlyPressed.some((token) => latest.keymap.hornHonk?.has(token))) {
|
||||
|
||||
@@ -163,7 +163,8 @@ export function computeGamepadOutputs(padState, profile) {
|
||||
const sideReverseSource = resolveButtonSource(padState, bindings.sideReverse?.sources);
|
||||
const driveMacroSource = resolveButtonSource(padState, bindings.driveMacro?.sources);
|
||||
const dockMacroSource = resolveButtonSource(padState, bindings.dockMacro?.sources);
|
||||
const nightVisionSource = resolveButtonSource(padState, bindings.nightVisionToggle?.sources);
|
||||
const headlightSource = resolveButtonSource(padState, bindings.headlightToggle?.sources);
|
||||
const laserSource = resolveButtonSource(padState, bindings.laserToggle?.sources);
|
||||
|
||||
return {
|
||||
driveVector: { x: driveX, y: driveY, boost: false },
|
||||
@@ -176,7 +177,8 @@ export function computeGamepadOutputs(padState, profile) {
|
||||
sideReverse: sideReverseSource.pressed,
|
||||
driveMacro: driveMacroSource.pressed,
|
||||
dockMacro: dockMacroSource.pressed,
|
||||
nightVisionToggle: nightVisionSource.pressed,
|
||||
headlightToggle: headlightSource.pressed,
|
||||
laserToggle: laserSource.pressed,
|
||||
},
|
||||
sources: {
|
||||
drive: driveSource.source,
|
||||
@@ -189,7 +191,8 @@ export function computeGamepadOutputs(padState, profile) {
|
||||
sideReverse: sideReverseSource.source,
|
||||
driveMacro: driveMacroSource.source,
|
||||
dockMacro: dockMacroSource.source,
|
||||
nightVisionToggle: nightVisionSource.source,
|
||||
headlightToggle: headlightSource.source,
|
||||
laserToggle: laserSource.source,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ export const HELP_CONTENT = {
|
||||
items: [
|
||||
{ action: 'cameraUp', label: 'Tilt up' },
|
||||
{ action: 'cameraDown', label: 'Tilt down' },
|
||||
{ action: 'nightVisionToggle', label: 'Toggle night vision' },
|
||||
{ action: 'headlightToggle', label: 'Toggle headlight' },
|
||||
{ action: 'laserToggle', label: 'Toggle laser' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -202,7 +203,8 @@ export const HELP_CONTENT = {
|
||||
// items: [
|
||||
// { action: 'driveMacro', label: 'Drive macro' },
|
||||
// { action: 'dockMacro', label: 'Dock macro' },
|
||||
// { action: 'nightVisionToggle', label: 'Toggle night vision' },
|
||||
// { action: 'headlightToggle', label: 'Toggle headlight' },
|
||||
// { action: 'laserToggle', label: 'Toggle laser' },
|
||||
// { action: 'chatFocus', label: 'Chat focus' },
|
||||
// ],
|
||||
// },
|
||||
|
||||
@@ -73,10 +73,14 @@ export const GAMEPAD_PROFILE_DEFAULT = {
|
||||
kind: 'button',
|
||||
sources: [{ kind: 'button', index: 3 }],
|
||||
},
|
||||
nightVisionToggle: {
|
||||
headlightToggle: {
|
||||
kind: 'button',
|
||||
sources: [{ kind: 'button', index: 9 }],
|
||||
},
|
||||
laserToggle: {
|
||||
kind: 'button',
|
||||
sources: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user