This commit is contained in:
legop3
2026-09-08 23:11:03 -04:00
parent 038ae0f45a
commit d3fd3946e6
22 changed files with 792 additions and 177 deletions
+26
View File
@@ -66,6 +66,7 @@ const CONTROL_ACTION_NAMES = [
'sendSong',
'startHorn',
'stopHorn',
'setPeripheralControl',
'setMicPttActive',
];
@@ -724,6 +725,29 @@ export function ControlSystemProvider({ children }) {
dispatch({ type: 'control/set-mic-ptt', payload: Boolean(active) });
}, []);
const setPeripheralControl = useCallback(
(peripheralId, controlId, value) => {
const sent = pipeline.sendPeripheralControl(peripheralId, controlId, value);
if (sent) {
// The generic protocol is currently command-only. Recording the value
// here lets every renderer instance share the browser's latest intent
// without pretending that it is device-reported telemetry.
dispatch({
type: 'control/set-peripheral-value',
payload: {
roverId: pipeline.roverId,
peripheralId,
controlId,
value,
},
});
recordControlIntent();
}
return sent;
},
[pipeline, recordControlIntent],
);
const actionImplementations = useMemo(
() => ({
setMode,
@@ -751,6 +775,7 @@ export function ControlSystemProvider({ children }) {
sendSong,
startHorn,
stopHorn,
setPeripheralControl,
setMicPttActive,
}),
[
@@ -779,6 +804,7 @@ export function ControlSystemProvider({ children }) {
sendSong,
startHorn,
stopHorn,
setPeripheralControl,
setMicPttActive,
],
);
+26
View File
@@ -44,6 +44,11 @@ export function useCommandPipeline(options = {}) {
return rosterEntry.horn;
}, [rosterEntry]);
const peripherals = useMemo(
() => (Array.isArray(rosterEntry?.peripherals) ? rosterEntry.peripherals : []),
[rosterEntry],
);
const headlightState = useMemo(() => rosterEntry?.headlight?.state ?? null, [rosterEntry]);
const laserState = useMemo(() => rosterEntry?.laser?.state ?? null, [rosterEntry]);
const emitCommand = useCallback(
@@ -208,6 +213,22 @@ export function useCommandPipeline(options = {}) {
[emitCommand, roverId],
);
const sendPeripheralControl = useCallback(
(peripheralId, controlId, value) => {
if (!roverId || !peripheralId || !controlId) return null;
const peripheral = { id: peripheralId, control: controlId, value };
// Peripheral commands deliberately use the same command envelope as all
// other rover actuation. This keeps turn authorization, acknowledgements,
// and rover WebSocket routing in the server's existing command boundary.
emitCommand({
type: 'peripheral',
data: { peripheral },
});
return peripheral;
},
[emitCommand, roverId],
);
const sendSong = useCallback(
(notes = [], options = {}) => {
if (!roverId) return null;
@@ -244,6 +265,7 @@ export function useCommandPipeline(options = {}) {
laser,
laserState,
horn,
peripherals,
emitCommand,
enableSensorStream,
sendDriveDirect,
@@ -253,6 +275,7 @@ export function useCommandPipeline(options = {}) {
sendHeadlight,
sendLaser,
sendHorn,
sendPeripheralControl,
sendSong,
runMacroSteps,
}),
@@ -265,6 +288,7 @@ export function useCommandPipeline(options = {}) {
laser,
laserState,
horn,
peripherals,
emitCommand,
enableSensorStream,
sendDriveDirect,
@@ -274,6 +298,8 @@ export function useCommandPipeline(options = {}) {
sendHeadlight,
sendLaser,
sendHorn,
sendPeripheralControl,
sendSong,
runMacroSteps,
],
);
+25
View File
@@ -69,6 +69,10 @@ export const initialControlState = {
macros: DEFAULT_MACROS,
keymap: DEFAULT_KEYMAP,
inputs: {},
// Generic accessory controls do not currently report state back from roverd.
// Keep the last browser-issued value per rover/device/control so closing a
// drawer or changing responsive layouts does not make the UI lie about it.
peripheralValues: {},
};
export function controlReducer(state, action) {
@@ -226,6 +230,27 @@ export function controlReducer(state, action) {
active: Boolean(action.payload),
},
};
case 'control/set-peripheral-value': {
const roverId = String(action.payload?.roverId || '');
const peripheralId = String(action.payload?.peripheralId || '');
const controlId = String(action.payload?.controlId || '');
if (!roverId || !peripheralId || !controlId) return state;
const roverValues = state.peripheralValues?.[roverId] || {};
const peripheralValues = roverValues[peripheralId] || {};
return {
...state,
peripheralValues: {
...(state.peripheralValues || {}),
[roverId]: {
...roverValues,
[peripheralId]: {
...peripheralValues,
[controlId]: action.payload.value,
},
},
},
};
}
default:
return state;
}