headlight rework and laser addition

This commit is contained in:
legop3
2026-06-27 15:57:47 -04:00
parent 3c1b1dab6e
commit 8e322ed57b
45 changed files with 596 additions and 373 deletions
+39 -23
View File
@@ -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,
+39 -15
View File
@@ -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,
],
+2 -1
View File
@@ -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))) {
+6 -3
View File
@@ -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,
},
};
}