mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 18:10:47 -04:00
258 lines
7.2 KiB
JavaScript
258 lines
7.2 KiB
JavaScript
// Control State Reducer
|
|
// Purpose: Implements reducer transitions for control-system runtime state. Scope: Centralizes deterministic state updates for input, mode, and dispatch events.
|
|
import { DEFAULT_KEYMAP, DEFAULT_MACROS, SONG_DEFAULT_NOTE } from './constants.js';
|
|
|
|
function createDriveState() {
|
|
return {
|
|
vector: { x: 0, y: 0, boost: false },
|
|
speeds: { left: 0, right: 0 },
|
|
source: null,
|
|
lastUpdatedAt: 0,
|
|
};
|
|
}
|
|
|
|
function createAuxState() {
|
|
return { main: 0, side: 0, vacuum: 0 };
|
|
}
|
|
|
|
function createCameraState() {
|
|
return {
|
|
enabled: false,
|
|
angle: null,
|
|
config: null,
|
|
/*
|
|
Servo precision is UI/control state, not rover hardware state. Keeping it
|
|
beside the camera angle lets every camera-tilt surface use the same
|
|
precision setting while still sending the normal decimal angle commands.
|
|
*/
|
|
precisionMode: false,
|
|
};
|
|
}
|
|
|
|
function createSongState() {
|
|
return {
|
|
note: SONG_DEFAULT_NOTE,
|
|
};
|
|
}
|
|
|
|
function createHornState() {
|
|
return {
|
|
active: false,
|
|
heat: 0,
|
|
overheated: false,
|
|
};
|
|
}
|
|
|
|
function createMicState() {
|
|
return {
|
|
pttActive: false,
|
|
};
|
|
}
|
|
|
|
function createManualDockAssistState() {
|
|
return {
|
|
active: false,
|
|
};
|
|
}
|
|
|
|
export const initialControlState = {
|
|
roverId: null,
|
|
mode: 'drive',
|
|
drive: createDriveState(),
|
|
aux: createAuxState(),
|
|
camera: createCameraState(),
|
|
song: createSongState(),
|
|
horn: createHornState(),
|
|
mic: createMicState(),
|
|
manualDockAssist: createManualDockAssistState(),
|
|
lastControlIntentAt: 0,
|
|
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) {
|
|
switch (action.type) {
|
|
case 'control/set-rover':
|
|
return {
|
|
...state,
|
|
roverId: action.payload ?? null,
|
|
drive: action.payload ? state.drive : createDriveState(),
|
|
aux: action.payload ? state.aux : createAuxState(),
|
|
song: action.payload ? state.song : createSongState(),
|
|
horn: action.payload ? state.horn : createHornState(),
|
|
mic: action.payload ? state.mic : createMicState(),
|
|
manualDockAssist: action.payload ? state.manualDockAssist : createManualDockAssistState(),
|
|
lastControlIntentAt: action.payload ? state.lastControlIntentAt : 0,
|
|
};
|
|
case 'control/set-mode':
|
|
return state.mode === action.payload
|
|
? state
|
|
: { ...state, mode: action.payload === 'dock' ? 'dock' : 'drive' };
|
|
case 'control/update-drive': {
|
|
const { vector, speeds, source } = action.payload;
|
|
return {
|
|
...state,
|
|
drive: {
|
|
vector: vector ?? state.drive.vector,
|
|
speeds: speeds ?? state.drive.speeds,
|
|
source: source ?? state.drive.source,
|
|
lastUpdatedAt: Date.now(),
|
|
},
|
|
};
|
|
}
|
|
case 'control/set-aux-motors':
|
|
return {
|
|
...state,
|
|
aux: {
|
|
...state.aux,
|
|
...action.payload,
|
|
},
|
|
};
|
|
case 'control/set-camera-config':
|
|
return {
|
|
...state,
|
|
camera: {
|
|
...state.camera,
|
|
enabled: Boolean(action.payload?.config),
|
|
config: action.payload?.config ?? null,
|
|
precisionMode: action.payload?.config ? state.camera.precisionMode : false,
|
|
angle:
|
|
typeof action.payload?.angle === 'number'
|
|
? action.payload.angle
|
|
: action.payload?.config
|
|
? action.payload.config.homeAngle ?? state.camera.angle
|
|
: null,
|
|
},
|
|
};
|
|
case 'control/set-camera-angle':
|
|
return {
|
|
...state,
|
|
camera: { ...state.camera, angle: action.payload },
|
|
};
|
|
case 'control/set-camera-precision-mode':
|
|
if (state.camera.precisionMode === Boolean(action.payload)) {
|
|
return state;
|
|
}
|
|
return {
|
|
...state,
|
|
camera: {
|
|
...state.camera,
|
|
/*
|
|
Movement inputs own this flag because precision camera mode is meant
|
|
to follow movement precision mode automatically instead of becoming a
|
|
separate toggle the driver has to remember to reset.
|
|
*/
|
|
precisionMode: Boolean(action.payload),
|
|
},
|
|
};
|
|
case 'control/register-input-state': {
|
|
const sourceKey = action.payload?.source || 'unknown';
|
|
return {
|
|
...state,
|
|
inputs: {
|
|
...state.inputs,
|
|
[sourceKey]: {
|
|
...(state.inputs[sourceKey] ?? {}),
|
|
...action.payload?.state,
|
|
updatedAt: Date.now(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
case 'control/set-keymap':
|
|
return {
|
|
...state,
|
|
keymap: { ...state.keymap, ...(action.payload ?? {}) },
|
|
};
|
|
case 'control/set-macros':
|
|
return {
|
|
...state,
|
|
macros: Array.isArray(action.payload) ? action.payload : state.macros,
|
|
};
|
|
case 'control/reset':
|
|
return {
|
|
...state,
|
|
drive: createDriveState(),
|
|
aux: createAuxState(),
|
|
song: createSongState(),
|
|
horn: createHornState(),
|
|
mic: createMicState(),
|
|
lastControlIntentAt: 0,
|
|
};
|
|
case 'control/set-horn-active':
|
|
return {
|
|
...state,
|
|
horn: {
|
|
...(state.horn || createHornState()),
|
|
active: Boolean(action.payload),
|
|
},
|
|
};
|
|
case 'control/set-horn-heat':
|
|
return {
|
|
...state,
|
|
horn: {
|
|
...(state.horn || createHornState()),
|
|
heat: typeof action.payload?.heat === 'number' ? action.payload.heat : 0,
|
|
overheated: Boolean(action.payload?.overheated),
|
|
},
|
|
};
|
|
case 'control/record-intent':
|
|
return {
|
|
...state,
|
|
lastControlIntentAt: Date.now(),
|
|
};
|
|
case 'control/set-song-note':
|
|
return {
|
|
...state,
|
|
song: {
|
|
...(state.song || createSongState()),
|
|
note: action.payload ?? SONG_DEFAULT_NOTE,
|
|
},
|
|
};
|
|
case 'control/set-mic-ptt':
|
|
return {
|
|
...state,
|
|
mic: {
|
|
...(state.mic || createMicState()),
|
|
pttActive: Boolean(action.payload),
|
|
},
|
|
};
|
|
case 'control/set-manual-dock-assist':
|
|
return {
|
|
...state,
|
|
manualDockAssist: {
|
|
...(state.manualDockAssist || createManualDockAssistState()),
|
|
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;
|
|
}
|
|
}
|