mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
guh
This commit is contained in:
@@ -2,9 +2,9 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useReducer,
|
||||
import { controlReducer, initialControlState } from './controlReducer.js';
|
||||
import { computeDifferentialSpeeds, clamp } from './controlMath.js';
|
||||
import { useCommandPipeline } from './commandPipeline.js';
|
||||
import { loadControlSettings, saveControlSettings } from './persistence.js';
|
||||
import { DEFAULT_KEYMAP } from './constants.js';
|
||||
import { DEFAULT_KEYMAP, DEFAULT_MACROS } from './constants.js';
|
||||
import { canonicalizeKeyInput } from './keymapUtils.js';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
|
||||
const ControlSystemContext = createContext(null);
|
||||
|
||||
@@ -25,16 +25,23 @@ export function ControlSystemProvider({ children }) {
|
||||
const pipeline = useCommandPipeline();
|
||||
const [state, dispatch] = useReducer(controlReducer, initialControlState);
|
||||
const servoAngleRef = useRef(initialControlState.camera.angle);
|
||||
const {
|
||||
value: controlSettings,
|
||||
save: saveControlSettings,
|
||||
} = useSettingsNamespace('controls', { keymap: DEFAULT_KEYMAP, macros: DEFAULT_MACROS });
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'control/set-rover', payload: pipeline.roverId });
|
||||
}, [pipeline.roverId]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'control/settings/loading' });
|
||||
const data = loadControlSettings();
|
||||
dispatch({ type: 'control/settings/loaded', payload: data });
|
||||
}, []);
|
||||
if (controlSettings?.keymap) {
|
||||
dispatch({ type: 'control/set-keymap', payload: controlSettings.keymap });
|
||||
}
|
||||
if (controlSettings?.macros) {
|
||||
dispatch({ type: 'control/set-macros', payload: controlSettings.macros });
|
||||
}
|
||||
}, [controlSettings?.keymap, controlSettings?.macros]);
|
||||
|
||||
useEffect(() => {
|
||||
const config = pipeline.servoConfig;
|
||||
@@ -98,23 +105,6 @@ export function ControlSystemProvider({ children }) {
|
||||
[pipeline],
|
||||
);
|
||||
|
||||
const persistSettings = useCallback(
|
||||
(partial) => {
|
||||
const merged = { ...(state.settings.data ?? {}), ...(partial ?? {}) };
|
||||
const success = saveControlSettings(merged);
|
||||
if (success) {
|
||||
dispatch({ type: 'control/settings/loaded', payload: merged });
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'control/settings/error',
|
||||
payload: new Error('Failed to save control settings'),
|
||||
});
|
||||
}
|
||||
return success;
|
||||
},
|
||||
[state.settings.data],
|
||||
);
|
||||
|
||||
const updateKeyBinding = useCallback(
|
||||
(bindingId, keyValue) => {
|
||||
if (!bindingId) return false;
|
||||
@@ -123,17 +113,23 @@ export function ControlSystemProvider({ children }) {
|
||||
const next = cloneKeymap(state.keymap);
|
||||
next[bindingId] = [canonical];
|
||||
dispatch({ type: 'control/set-keymap', payload: next });
|
||||
persistSettings({ keymap: next });
|
||||
saveControlSettings((current) => ({
|
||||
...(current ?? {}),
|
||||
keymap: next,
|
||||
}));
|
||||
return true;
|
||||
},
|
||||
[state.keymap, persistSettings],
|
||||
[state.keymap, saveControlSettings],
|
||||
);
|
||||
|
||||
const resetKeyBindings = useCallback(() => {
|
||||
const defaults = cloneKeymap(DEFAULT_KEYMAP);
|
||||
dispatch({ type: 'control/set-keymap', payload: defaults });
|
||||
persistSettings({ keymap: defaults });
|
||||
}, [persistSettings]);
|
||||
saveControlSettings((current) => ({
|
||||
...(current ?? {}),
|
||||
keymap: defaults,
|
||||
}));
|
||||
}, [saveControlSettings]);
|
||||
|
||||
const setServoAngle = useCallback(
|
||||
(value) => {
|
||||
@@ -218,12 +214,6 @@ export function ControlSystemProvider({ children }) {
|
||||
dispatch({ type: 'control/register-input-state', payload: { source, state: data } });
|
||||
}, []);
|
||||
|
||||
const reloadSettings = useCallback(() => {
|
||||
dispatch({ type: 'control/settings/loading' });
|
||||
const next = loadControlSettings();
|
||||
dispatch({ type: 'control/settings/loaded', payload: next });
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
state,
|
||||
@@ -243,8 +233,6 @@ export function ControlSystemProvider({ children }) {
|
||||
updateKeyBinding,
|
||||
resetKeyBindings,
|
||||
registerInputState,
|
||||
reloadSettings,
|
||||
persistSettings,
|
||||
},
|
||||
}),
|
||||
[
|
||||
@@ -263,8 +251,6 @@ export function ControlSystemProvider({ children }) {
|
||||
updateKeyBinding,
|
||||
resetKeyBindings,
|
||||
registerInputState,
|
||||
reloadSettings,
|
||||
persistSettings,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -62,6 +62,3 @@ export const DEFAULT_MACROS = [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const CONTROL_SETTINGS_COOKIE = 'roverControlSettings';
|
||||
export const CONTROL_SETTINGS_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
||||
|
||||
@@ -30,33 +30,8 @@ export const initialControlState = {
|
||||
macros: DEFAULT_MACROS,
|
||||
keymap: DEFAULT_KEYMAP,
|
||||
inputs: {},
|
||||
settings: {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
lastLoadedAt: null,
|
||||
lastSavedAt: null,
|
||||
data: null,
|
||||
},
|
||||
};
|
||||
|
||||
function mergeSettings(state, payload) {
|
||||
const nextSettings = {
|
||||
status: 'ready',
|
||||
error: null,
|
||||
lastLoadedAt: Date.now(),
|
||||
lastSavedAt: state.settings.lastSavedAt,
|
||||
data: payload ?? state.settings.data,
|
||||
};
|
||||
const nextState = { ...state, settings: nextSettings };
|
||||
if (payload?.keymap) {
|
||||
nextState.keymap = { ...state.keymap, ...payload.keymap };
|
||||
}
|
||||
if (payload?.macros) {
|
||||
nextState.macros = payload.macros;
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
|
||||
export function controlReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'control/set-rover':
|
||||
@@ -124,26 +99,6 @@ export function controlReducer(state, action) {
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'control/settings/loading':
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
status: 'loading',
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
case 'control/settings/loaded':
|
||||
return mergeSettings(state, action.payload);
|
||||
case 'control/settings/error':
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
status: 'error',
|
||||
error: action.payload,
|
||||
},
|
||||
};
|
||||
case 'control/set-keymap':
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { CONTROL_SETTINGS_COOKIE, CONTROL_SETTINGS_MAX_AGE } from './constants.js';
|
||||
|
||||
function parseCookieValue(raw) {
|
||||
try {
|
||||
const decoded = decodeURIComponent(raw ?? '');
|
||||
return JSON.parse(decoded);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse control settings cookie', error); // eslint-disable-line no-console
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadControlSettings() {
|
||||
if (typeof document === 'undefined') return null;
|
||||
const cookiePrefix = `${CONTROL_SETTINGS_COOKIE}=`;
|
||||
const entry = document.cookie
|
||||
.split(';')
|
||||
.map((part) => part.trim())
|
||||
.find((part) => part.startsWith(cookiePrefix));
|
||||
if (!entry) return null;
|
||||
const raw = entry.substring(cookiePrefix.length);
|
||||
return parseCookieValue(raw);
|
||||
}
|
||||
|
||||
export function saveControlSettings(settings) {
|
||||
if (typeof document === 'undefined') return false;
|
||||
try {
|
||||
const serialized = encodeURIComponent(JSON.stringify(settings ?? {}));
|
||||
const cookie = `${CONTROL_SETTINGS_COOKIE}=${serialized}; path=/; max-age=${CONTROL_SETTINGS_MAX_AGE}; samesite=strict`;
|
||||
document.cookie = cookie;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to write control settings cookie', error); // eslint-disable-line no-console
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user