mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
slopfixing / issue 002
This commit is contained in:
+2
-3
@@ -11,7 +11,7 @@ import {
|
||||
ControlSystemProvider,
|
||||
KeyboardInputManager,
|
||||
GamepadInputManager,
|
||||
useControlSystem,
|
||||
useControlSelector,
|
||||
} from './controls/index.js';
|
||||
import RoomCameraPanel from './components/RoomCameraPanel/index.jsx';
|
||||
import KinectPanel from './components/KinectPanel/index.jsx';
|
||||
@@ -105,10 +105,9 @@ function MobileFeatureTabs({
|
||||
const roverId = String(state.session?.assignment?.roverId || '').trim();
|
||||
return roverId ? state.session?.audioForward?.[roverId] || null : null;
|
||||
});
|
||||
const { state: controlState } = useControlSystem();
|
||||
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
|
||||
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
|
||||
const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-amber-400';
|
||||
const pttActive = Boolean(controlState?.mic?.pttActive);
|
||||
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
|
||||
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
|
||||
const vipMicActive = Boolean(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Purpose: Defines the Drive Dock Action module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
|
||||
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
||||
@@ -103,10 +103,9 @@ export default function DriveDockAction({
|
||||
compactHeightClass = '',
|
||||
}) {
|
||||
const isMobile = layout === 'mobile';
|
||||
const {
|
||||
state: { roverId, keymap },
|
||||
actions,
|
||||
} = useControlSystem();
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
const keymap = useControlSelector((control) => control.state.keymap);
|
||||
const actions = useControlActions();
|
||||
const dockAssist = useManualDockAssist();
|
||||
const frame = useTelemetryFrame(roverId);
|
||||
const state = driveDockState ?? deriveDriveDockState(frame);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import RoverMediaPlayer from '../RoverMediaPlayer/index.jsx';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlSelector } from '../../controls/index.js';
|
||||
import { useDriverVideoModePolicy } from '../../hooks/useDriverVideoModePolicy.js';
|
||||
import TurnsOverlay from '../HudOverlays/TurnsOverlay/index.jsx';
|
||||
import HudOverlay from '../HudOverlays/HudOverlay/index.jsx';
|
||||
@@ -15,9 +15,7 @@ import CardFrame from '../CardFrame/index.jsx';
|
||||
export default function DriverVideo({ layoutFormat = 'desktop' }) {
|
||||
const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
|
||||
const videoMode = useDriverVideoModePolicy(roverId);
|
||||
const {
|
||||
state: { lastControlIntentAt },
|
||||
} = useControlSystem();
|
||||
const lastControlIntentAt = useControlSelector((control) => control.state.lastControlIntentAt);
|
||||
|
||||
if (!roverId) {
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Help Overlay
|
||||
// Purpose: Defines the Help Overlay module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlSelector } from '../../controls/index.js';
|
||||
import HelpContentView from '../HelpContentView/index.jsx';
|
||||
|
||||
export default function HelpOverlay({ visible, layout, onClose, showOnLoad, onToggleShowOnLoad }) {
|
||||
if (!visible) return null;
|
||||
const keymap = useControlSelector((control) => control.state.keymap);
|
||||
|
||||
const { state } = useControlSystem();
|
||||
if (!visible) return null;
|
||||
|
||||
const handleCheckbox = (event) => {
|
||||
const keepShowing = !event.target.checked;
|
||||
@@ -39,7 +39,7 @@ export default function HelpOverlay({ visible, layout, onClose, showOnLoad, onTo
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-[85vh] overflow-y-auto p-0.5">
|
||||
<HelpContentView layout={layout} keymap={state?.keymap || {}} />
|
||||
<HelpContentView layout={layout} keymap={keymap || {}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Help Panel
|
||||
// Purpose: Defines the Help Panel module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlSelector } from '../../controls/index.js';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
import HelpContentView from '../HelpContentView/index.jsx';
|
||||
|
||||
export default function HelpPanel({ layout, onOpenOverlay }) {
|
||||
const { state } = useControlSystem();
|
||||
const keymap = useControlSelector((control) => control.state.keymap);
|
||||
|
||||
const actions = (
|
||||
<button
|
||||
@@ -20,7 +20,7 @@ export default function HelpPanel({ layout, onOpenOverlay }) {
|
||||
|
||||
return (
|
||||
<CardFrame title="Help" actions={actions} bodyClassName="space-y-0.5 text-sm">
|
||||
<HelpContentView layout={layout} keymap={state?.keymap || {}} />
|
||||
<HelpContentView layout={layout} keymap={keymap || {}} />
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useMemo } from 'react';
|
||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlSelector } from '../../controls/index.js';
|
||||
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
|
||||
@@ -169,9 +169,7 @@ function LampTile({ entity, connected, controlsLocked, onToggle, onSetColor, onS
|
||||
}
|
||||
|
||||
export default function HomeAssistantControls() {
|
||||
const {
|
||||
state: { keymap },
|
||||
} = useControlSystem();
|
||||
const keymap = useControlSelector((control) => control.state.keymap);
|
||||
const ha = useSessionSelector((state) => state.session?.homeAssistant || null);
|
||||
const { homeAssistantToggle, homeAssistantSetLightColor, homeAssistantSetLightWhite } =
|
||||
useSessionActions();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSessionSelector } from '../../../context/SessionContext.jsx';
|
||||
import { useControlSystem } from '../../../controls/index.js';
|
||||
import { useControlSelector } from '../../../controls/index.js';
|
||||
import SocialButton from '../../SocialButton/index.jsx';
|
||||
|
||||
function TurnsOverlay({
|
||||
@@ -15,9 +15,7 @@ function TurnsOverlay({
|
||||
const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
|
||||
const socketId = useSessionSelector((state) => state.session?.socketId || null);
|
||||
const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {});
|
||||
const {
|
||||
state: { lastControlIntentAt },
|
||||
} = useControlSystem();
|
||||
const lastControlIntentAt = useControlSelector((control) => control.state.lastControlIntentAt);
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
const [showTurnCue, setShowTurnCue] = useState(false);
|
||||
const [turnCueStartAt, setTurnCueStartAt] = useState(null);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Purpose: Defines the Keymap Settings module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { DEFAULT_KEYMAP } from '../../controls/constants.js';
|
||||
import { canonicalizeKeyInput, formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||
import { setKeyboardCaptureLocked } from '../../controls/inputs/keyboardCaptureLock.js';
|
||||
@@ -191,10 +191,8 @@ function BindingRow({ action, value, isActive, onCaptureStart, onCancel, onCaptu
|
||||
}
|
||||
|
||||
export default function KeymapSettings() {
|
||||
const {
|
||||
state: { keymap },
|
||||
actions: { updateKeyBinding, resetKeyBindings },
|
||||
} = useControlSystem();
|
||||
const keymap = useControlSelector((control) => control.state.keymap);
|
||||
const { updateKeyBinding, resetKeyBindings } = useControlActions();
|
||||
const { value: inputSettings, save: saveInputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||
const keyboardSettings = {
|
||||
...INPUT_SETTINGS_DEFAULTS.keyboard,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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.
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
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';
|
||||
@@ -11,19 +11,20 @@ import VacuumControls from './VacuumControls.jsx';
|
||||
import VerticalCameraTilt from './VerticalCameraTilt.jsx';
|
||||
|
||||
function AuxColumnContent() {
|
||||
const {
|
||||
state: { roverId, camera, horn },
|
||||
pipeline,
|
||||
actions: { setServoAngle, setNightVision, setAuxMotors, startHorn, stopHorn },
|
||||
} = useControlSystem();
|
||||
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 pipelineHorn = useControlSelector((control) => control.pipeline?.horn);
|
||||
const { setServoAngle, setNightVision, 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 && pipeline?.nightVision);
|
||||
const nightVisionState = pipeline?.nightVisionState;
|
||||
const hornAvailable = Boolean(roverId && pipeline?.horn);
|
||||
const nightVisionAvailable = Boolean(roverId && nightVision);
|
||||
const hornAvailable = Boolean(roverId && pipelineHorn);
|
||||
const hornBlocked = horn?.overheated;
|
||||
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
|
||||
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Purpose: Provides the mobile movement control pad and speed mode selector.
|
||||
// Scope: Converts touch pad cells into keyboard-style drive vectors; movement column owns drive/dock placement.
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { normalizeKeymapEntries } from '../../controls/keymapUtils.js';
|
||||
import {
|
||||
computeKeyboardDriveVector,
|
||||
@@ -29,10 +29,8 @@ function getSpeedModeConfig(speedMode) {
|
||||
}
|
||||
|
||||
export default function ControlPadPanel({ disabled = false }) {
|
||||
const {
|
||||
state: { keymap: rawKeymap },
|
||||
actions: { setDriveVector, registerInputState },
|
||||
} = useControlSystem();
|
||||
const rawKeymap = useControlSelector((control) => control.state.keymap);
|
||||
const { setDriveVector, registerInputState } = useControlActions();
|
||||
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||
const [speedMode, setSpeedMode] = useState('normal');
|
||||
const [activeInputLabel, setActiveInputLabel] = useState('stop');
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Movement Column
|
||||
// Purpose: Assembles the mobile movement column, which is the right column by default.
|
||||
// Scope: Integrates drive/dock actions with the mobile control pad without hiding that dependency inside the pad.
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlSelector } from '../../controls/index.js';
|
||||
import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx';
|
||||
import ControlPadPanel from './ControlPadPanel.jsx';
|
||||
|
||||
function MovementColumnContent({ layout }) {
|
||||
const {
|
||||
state: { roverId },
|
||||
} = useControlSystem();
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
const driveDockState = useDriveDockState(roverId);
|
||||
const dockedNotDriving = driveDockState.docked && !driveDockState.driving;
|
||||
const expandAction = dockedNotDriving || driveDockState.dockingInProgress;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Purpose: Defines the Overcurrent Limiter Panel module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useMemo } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlSelector } from '../../controls/index.js';
|
||||
import { OVERCURRENT_GROUPS } from '../../controls/overcurrentLimiter.js';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
|
||||
@@ -26,10 +26,8 @@ function ProgressBar({ value, color = 'bg-emerald-500' }) {
|
||||
}
|
||||
|
||||
export default function OvercurrentLimiterPanel() {
|
||||
const {
|
||||
state: { roverId },
|
||||
overcurrentLimiter,
|
||||
} = useControlSystem();
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
const overcurrentLimiter = useControlSelector((control) => control.overcurrentLimiter);
|
||||
const groups = useMemo(() => OVERCURRENT_GROUPS.map((group) => group.key), []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlSelector } from '../../controls/index.js';
|
||||
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||
import NicknameForm from '../NicknameForm/index.jsx';
|
||||
import SocialButton from '../SocialButton/index.jsx';
|
||||
@@ -53,9 +53,9 @@ export default function QuickstartOverlay({
|
||||
onOpenHelp,
|
||||
onClose,
|
||||
}) {
|
||||
const { state } = useControlSystem();
|
||||
const rawKeymap = useControlSelector((control) => control.state.keymap);
|
||||
const isDesktop = layout === 'desktop';
|
||||
const keymap = useMemo(() => state?.keymap || {}, [state?.keymap]);
|
||||
const keymap = useMemo(() => rawKeymap || {}, [rawKeymap]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import Tabs, { Tab, TabList, TabPanel, TabPanels } from '../Tabs/index.jsx';
|
||||
import TopDownMap from '../TopDownMap/index.jsx';
|
||||
import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx';
|
||||
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
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';
|
||||
@@ -36,9 +36,7 @@ const CHAT_DOCK_MAX_HEIGHT = 300;
|
||||
const CHAT_DOCK_BOTTOM_INSET = 8;
|
||||
|
||||
function TopDownMapPanel() {
|
||||
const {
|
||||
state: { roverId },
|
||||
} = useControlSystem();
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
const frame = useTelemetryFrame(roverId);
|
||||
const sensors = frame?.sensors || {};
|
||||
|
||||
@@ -54,20 +52,22 @@ function TopDownMapPanel() {
|
||||
}
|
||||
|
||||
function DriveDockPanel() {
|
||||
const {
|
||||
state: { roverId, keymap, camera, horn },
|
||||
pipeline,
|
||||
actions: { setServoAngle, setNightVision, startHorn, stopHorn },
|
||||
} = useControlSystem();
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
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 pipelineHorn = useControlSelector((control) => control.pipeline?.horn);
|
||||
const { setServoAngle, setNightVision, 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 && pipeline?.nightVision);
|
||||
const nightVisionState = pipeline?.nightVisionState;
|
||||
const hornAvailable = Boolean(roverId && pipeline?.horn);
|
||||
const nightVisionAvailable = Boolean(roverId && nightVision);
|
||||
const hornAvailable = Boolean(roverId && pipelineHorn);
|
||||
const hornBlocked = horn?.overheated;
|
||||
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
|
||||
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
||||
@@ -146,10 +146,9 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
const roverId = String(state.session?.assignment?.roverId || '').trim();
|
||||
return roverId ? state.session?.audioForward?.[roverId] || null : null;
|
||||
});
|
||||
const { state: controlState } = useControlSystem();
|
||||
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
|
||||
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
|
||||
const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-red-600';
|
||||
const pttActive = Boolean(controlState?.mic?.pttActive);
|
||||
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
|
||||
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
|
||||
const vipMicActive = Boolean(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Purpose: Defines the Settings Panel module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useMemo } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import AuthPanel from '../AuthPanel/index.jsx';
|
||||
import AdminPanel from '../AdminPanel/index.jsx';
|
||||
import KeymapSettings from '../KeymapSettings/index.jsx';
|
||||
@@ -91,10 +91,9 @@ function RangeSetting({ label, value, disabled = false, onChange }) {
|
||||
}
|
||||
|
||||
export default function SettingsPanel() {
|
||||
const {
|
||||
state: { keymap, roverId },
|
||||
actions: { sendOiCommand, setSensorStream },
|
||||
} = useControlSystem();
|
||||
const keymap = useControlSelector((control) => control.state.keymap);
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
const { sendOiCommand, setSensorStream } = useControlActions();
|
||||
const canControl = Boolean(roverId);
|
||||
const [hudMapDesktop, setHudMapDesktop] = useHudMapSetting();
|
||||
const socket = useSocket();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { fieldClass } from '../constants.js';
|
||||
import { useControlSystem } from '../../../controls/index.js';
|
||||
import { useControlSelector } from '../../../controls/index.js';
|
||||
import { formatKeyLabel } from '../../../controls/keymapUtils.js';
|
||||
import { useSettingsNamespace } from '../../../settings/index.js';
|
||||
import { MAX_UPLOAD_BYTES, TARGET_SAMPLE_RATE, RTC_CONFIG } from './constants.js';
|
||||
@@ -28,7 +28,8 @@ export default function VipAudioUploadCard({
|
||||
readyMicWhip,
|
||||
stopMicWhip,
|
||||
}) {
|
||||
const { state: controlState } = useControlSystem();
|
||||
const keymap = useControlSelector((control) => control.state.keymap);
|
||||
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
|
||||
const { value: vipAudio, save: saveVipAudio } = useSettingsNamespace('vipAudio', {
|
||||
openMicEnabled: false,
|
||||
pttMode: 'live',
|
||||
@@ -60,7 +61,6 @@ export default function VipAudioUploadCard({
|
||||
const clipRecordingRef = useRef(false);
|
||||
const clipSampleRateRef = useRef(TARGET_SAMPLE_RATE);
|
||||
|
||||
const pttActive = Boolean(controlState?.mic?.pttActive);
|
||||
|
||||
const selectedForwardState = useMemo(
|
||||
() => (roverId ? audioForwardByRover?.[roverId] || null : null),
|
||||
@@ -81,7 +81,7 @@ export default function VipAudioUploadCard({
|
||||
const whipLinkActive = !clipMode && (micState === 'live' || micState === 'starting');
|
||||
const clipRecording = clipMode && clipState === 'recording';
|
||||
const clipSending = clipMode && clipState === 'sending';
|
||||
const pttKeyLabel = formatKeyLabel(controlState?.keymap?.micPtt?.[0]) || 'M';
|
||||
const pttKeyLabel = formatKeyLabel(keymap?.micPtt?.[0]) || 'M';
|
||||
|
||||
const setPttMode = useCallback(
|
||||
(nextMode) => {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
// Control Context Provider
|
||||
// Purpose: Exposes control-system state/actions to control-capable components. Scope: Owns reducer wiring, pipeline integration, and top-level provider hooks.
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from 'react';
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { controlReducer, initialControlState } from './controlReducer.js';
|
||||
import { computeDifferentialSpeeds, clamp } from './controlMath.js';
|
||||
import { useCommandPipeline } from './commandPipeline.js';
|
||||
@@ -27,6 +38,35 @@ import {
|
||||
|
||||
const ControlSystemContext = createContext(null);
|
||||
|
||||
function normalizeSelector(selector) {
|
||||
return typeof selector === 'function' ? selector : (snapshot) => snapshot;
|
||||
}
|
||||
|
||||
const CONTROL_ACTION_NAMES = [
|
||||
'setMode',
|
||||
'setDriveVector',
|
||||
'setAuxMotors',
|
||||
'setServoAngle',
|
||||
'nudgeServo',
|
||||
'goServoHome',
|
||||
'runMacro',
|
||||
'stopAllMotion',
|
||||
'sendOiCommand',
|
||||
'setSensorStream',
|
||||
'setNightVision',
|
||||
'toggleNightVision',
|
||||
'updateKeyBinding',
|
||||
'resetKeyBindings',
|
||||
'registerInputState',
|
||||
'setManualDockAssistActive',
|
||||
'toggleManualDockAssist',
|
||||
'setSongNote',
|
||||
'sendSong',
|
||||
'startHorn',
|
||||
'stopHorn',
|
||||
'setMicPttActive',
|
||||
];
|
||||
|
||||
function cloneKeymap(map) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(map || {}).map(([key, values]) => [key, Array.isArray(values) ? [...values] : []]),
|
||||
@@ -59,6 +99,53 @@ function removeDriveSequenceBackoff(steps = []) {
|
||||
|
||||
export function ControlSystemProvider({ children }) {
|
||||
const [state, dispatch] = useReducer(controlReducer, initialControlState);
|
||||
const snapshotRef = useRef(null);
|
||||
const subscribersRef = useRef(new Set());
|
||||
const actionImplementationsRef = useRef({});
|
||||
const [stableActions] = useState(() => {
|
||||
/*
|
||||
Public action functions intentionally keep stable identities. Each wrapper
|
||||
reads the current implementation at call time, so consumers can depend on
|
||||
actions without re-rendering just because the real implementation now
|
||||
closes over a new state slice, pipeline object, or settings value.
|
||||
*/
|
||||
return Object.fromEntries(
|
||||
CONTROL_ACTION_NAMES.map((name) => [
|
||||
name,
|
||||
(...args) => actionImplementationsRef.current[name]?.(...args),
|
||||
]),
|
||||
);
|
||||
}, []);
|
||||
const getControlSnapshot = useCallback(() => snapshotRef.current, []);
|
||||
const subscribeControlSnapshot = useCallback((selector, listener, equalityFn = Object.is) => {
|
||||
const normalizedSelector = normalizeSelector(selector);
|
||||
const currentSnapshot = snapshotRef.current;
|
||||
const subscriber = {
|
||||
selector: normalizedSelector,
|
||||
equalityFn,
|
||||
listener,
|
||||
current: normalizedSelector(currentSnapshot),
|
||||
};
|
||||
subscribersRef.current.add(subscriber);
|
||||
return () => {
|
||||
subscribersRef.current.delete(subscriber);
|
||||
};
|
||||
}, []);
|
||||
const notifyControlSubscribers = useCallback((nextSnapshot) => {
|
||||
/*
|
||||
The provider context value below is stable, so React will not fan out an
|
||||
update to every consumer automatically. This loop is the replacement: it
|
||||
asks each subscribed component whether the exact value it selected changed
|
||||
and only wakes that component when its selected value is different.
|
||||
*/
|
||||
subscribersRef.current.forEach((subscriber) => {
|
||||
const nextSelected = subscriber.selector(nextSnapshot);
|
||||
if (!subscriber.equalityFn(subscriber.current, nextSelected)) {
|
||||
subscriber.current = nextSelected;
|
||||
subscriber.listener(nextSelected);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
const prevModeRef = useRef(null);
|
||||
const pendingLightsRef = useRef(false);
|
||||
const servoAngleRef = useRef(initialControlState.camera.angle);
|
||||
@@ -166,7 +253,7 @@ export function ControlSystemProvider({ children }) {
|
||||
if (pipeline.roverId) {
|
||||
pipeline.enableSensorStream();
|
||||
}
|
||||
}, [pipeline.roverId, pipeline.enableSensorStream]);
|
||||
}, [pipeline]);
|
||||
|
||||
const setMode = useCallback(
|
||||
(mode) => {
|
||||
@@ -430,6 +517,8 @@ export function ControlSystemProvider({ children }) {
|
||||
return { waveform, freqs: normalized };
|
||||
}, [hornSettings]);
|
||||
|
||||
const stopHornRef = useRef(null);
|
||||
|
||||
const startHorn = useCallback(() => {
|
||||
if (!pipeline.horn) return;
|
||||
if (state.horn?.overheated) return false;
|
||||
@@ -445,7 +534,7 @@ export function ControlSystemProvider({ children }) {
|
||||
hornAutoStopRef.current = null;
|
||||
}
|
||||
hornAutoStopRef.current = setTimeout(() => {
|
||||
stopHorn();
|
||||
stopHornRef.current?.();
|
||||
}, HORN_MAX_MS);
|
||||
recordControlIntent();
|
||||
return true;
|
||||
@@ -474,15 +563,13 @@ export function ControlSystemProvider({ children }) {
|
||||
}
|
||||
dispatch({ type: 'control/set-horn-heat', payload: { heat: nextHeat, overheated: nextOverheated } });
|
||||
}
|
||||
}, [dispatch, pipeline, state.horn?.heat, state.horn?.overheated, HORN_HEAT_UP_PER_SEC]);
|
||||
}, [dispatch, pipeline, state.horn?.heat, state.horn?.overheated]);
|
||||
|
||||
const hornStateRef = useRef({
|
||||
active: Boolean(state.horn?.active),
|
||||
heat: state.horn?.heat ?? 0,
|
||||
overheated: Boolean(state.horn?.overheated),
|
||||
});
|
||||
const stopHornRef = useRef(stopHorn);
|
||||
|
||||
useEffect(() => {
|
||||
hornStateRef.current = {
|
||||
active: Boolean(state.horn?.active),
|
||||
@@ -525,7 +612,7 @@ export function ControlSystemProvider({ children }) {
|
||||
}
|
||||
}, tickMs);
|
||||
return () => clearInterval(interval);
|
||||
}, [dispatch, hornNeedsTick, HORN_HEAT_COOL_PER_SEC, HORN_HEAT_RESUME_THRESHOLD, HORN_HEAT_UP_PER_SEC]);
|
||||
}, [dispatch, hornNeedsTick]);
|
||||
|
||||
const setManualDockAssistActive = useCallback(
|
||||
(active) => {
|
||||
@@ -561,41 +648,32 @@ export function ControlSystemProvider({ children }) {
|
||||
dispatch({ type: 'control/set-mic-ptt', payload: Boolean(active) });
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
const actionImplementations = useMemo(
|
||||
() => ({
|
||||
state,
|
||||
dispatch,
|
||||
pipeline,
|
||||
overcurrentLimiter,
|
||||
actions: {
|
||||
setMode,
|
||||
setDriveVector,
|
||||
setAuxMotors,
|
||||
setServoAngle,
|
||||
nudgeServo,
|
||||
goServoHome,
|
||||
runMacro,
|
||||
stopAllMotion,
|
||||
sendOiCommand,
|
||||
setSensorStream,
|
||||
setNightVision,
|
||||
toggleNightVision,
|
||||
updateKeyBinding,
|
||||
resetKeyBindings,
|
||||
registerInputState,
|
||||
setManualDockAssistActive,
|
||||
toggleManualDockAssist,
|
||||
setSongNote,
|
||||
sendSong,
|
||||
startHorn,
|
||||
stopHorn,
|
||||
setMicPttActive,
|
||||
},
|
||||
setMode,
|
||||
setDriveVector,
|
||||
setAuxMotors,
|
||||
setServoAngle,
|
||||
nudgeServo,
|
||||
goServoHome,
|
||||
runMacro,
|
||||
stopAllMotion,
|
||||
sendOiCommand,
|
||||
setSensorStream,
|
||||
setNightVision,
|
||||
toggleNightVision,
|
||||
updateKeyBinding,
|
||||
resetKeyBindings,
|
||||
registerInputState,
|
||||
setManualDockAssistActive,
|
||||
toggleManualDockAssist,
|
||||
setSongNote,
|
||||
sendSong,
|
||||
startHorn,
|
||||
stopHorn,
|
||||
setMicPttActive,
|
||||
}),
|
||||
[
|
||||
state,
|
||||
pipeline,
|
||||
overcurrentLimiter,
|
||||
setMode,
|
||||
setDriveVector,
|
||||
setAuxMotors,
|
||||
@@ -621,13 +699,106 @@ export function ControlSystemProvider({ children }) {
|
||||
],
|
||||
);
|
||||
|
||||
return <ControlSystemContext.Provider value={contextValue}>{children}</ControlSystemContext.Provider>;
|
||||
const snapshot = useMemo(
|
||||
() => ({
|
||||
state,
|
||||
dispatch,
|
||||
pipeline,
|
||||
overcurrentLimiter,
|
||||
actions: stableActions,
|
||||
}),
|
||||
[state, pipeline, overcurrentLimiter, stableActions],
|
||||
);
|
||||
|
||||
if (snapshotRef.current == null) {
|
||||
/*
|
||||
The first render has to make a snapshot available synchronously because
|
||||
descendants can call selector hooks during that same render pass. Later
|
||||
renders publish updates from the layout effect below, after React has
|
||||
committed the provider's new state.
|
||||
*/
|
||||
snapshotRef.current = snapshot;
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
actionImplementationsRef.current = actionImplementations;
|
||||
}, [actionImplementations]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (snapshotRef.current === snapshot) return;
|
||||
snapshotRef.current = snapshot;
|
||||
notifyControlSubscribers(snapshot);
|
||||
}, [notifyControlSubscribers, snapshot]);
|
||||
|
||||
const store = useMemo(
|
||||
() => ({
|
||||
getSnapshot: getControlSnapshot,
|
||||
subscribe: subscribeControlSnapshot,
|
||||
actions: stableActions,
|
||||
}),
|
||||
[getControlSnapshot, stableActions, subscribeControlSnapshot],
|
||||
);
|
||||
|
||||
return <ControlSystemContext.Provider value={store}>{children}</ControlSystemContext.Provider>;
|
||||
}
|
||||
|
||||
export function useControlSelector(selector, equalityFn = Object.is) {
|
||||
const store = useContext(ControlSystemContext);
|
||||
if (!store) {
|
||||
throw new Error('useControlSelector must be used within ControlSystemProvider');
|
||||
}
|
||||
const selectorRef = useRef(selector);
|
||||
const equalityRef = useRef(equalityFn);
|
||||
|
||||
const [selected, setSelected] = useState(() => selector(store.getSnapshot()));
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
Selector functions are often declared inline at the call site. Refreshing
|
||||
these refs from an effect keeps the subscription callback pointed at the
|
||||
newest selector/equality pair without making render mutate refs.
|
||||
*/
|
||||
selectorRef.current = selector;
|
||||
equalityRef.current = equalityFn;
|
||||
}, [equalityFn, selector]);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
Recheck once after subscribing because the provider may have published a
|
||||
newer snapshot between this component's render and its effect. The equality
|
||||
guard keeps that synchronization from causing a redundant render.
|
||||
*/
|
||||
setSelected((prev) => {
|
||||
const next = selectorRef.current(store.getSnapshot());
|
||||
return equalityRef.current(prev, next) ? prev : next;
|
||||
});
|
||||
|
||||
return store.subscribe(
|
||||
(snapshot) => selectorRef.current(snapshot),
|
||||
(nextSelected) => {
|
||||
setSelected((prev) => (equalityRef.current(prev, nextSelected) ? prev : nextSelected));
|
||||
},
|
||||
(a, b) => equalityRef.current(a, b),
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function useControlActions() {
|
||||
const store = useContext(ControlSystemContext);
|
||||
if (!store) {
|
||||
throw new Error('useControlActions must be used within ControlSystemProvider');
|
||||
}
|
||||
return store.actions;
|
||||
}
|
||||
|
||||
export function useControlSystem() {
|
||||
const context = useContext(ControlSystemContext);
|
||||
if (!context) {
|
||||
throw new Error('useControlSystem must be used within ControlSystemProvider');
|
||||
}
|
||||
return context;
|
||||
/*
|
||||
This compatibility hook intentionally selects the whole snapshot, so it has
|
||||
the same broad update behavior as the old context API. New and migrated
|
||||
consumers should prefer useControlSelector/useControlActions so they only
|
||||
re-render for data they actually read.
|
||||
*/
|
||||
return useControlSelector((snapshot) => snapshot);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// Control Module Exports
|
||||
// Purpose: Re-exports control context/providers and input managers from one entrypoint. Scope: Keeps control imports stable and concise for app/module consumers.
|
||||
export { ControlSystemProvider, useControlSystem } from './ControlContext.jsx';
|
||||
export {
|
||||
ControlSystemProvider,
|
||||
useControlActions,
|
||||
useControlSelector,
|
||||
useControlSystem,
|
||||
} from './ControlContext.jsx';
|
||||
export { default as KeyboardInputManager } from './inputs/KeyboardInputManager.jsx';
|
||||
export { default as GamepadInputManager } from './inputs/GamepadInputManager.jsx';
|
||||
export { useOvercurrentLimiter } from './overcurrentLimiter.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Gamepad Input Manager
|
||||
// Purpose: Converts polled gamepad state into normalized control actions/commands. Scope: Integrates bindings, deadzone math, and dispatch callbacks for driving.
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { useControlSystem } from '../ControlContext.jsx';
|
||||
import { useControlActions, useControlSelector } from '../ControlContext.jsx';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import { GAMEPAD_SETTINGS_DEFAULTS, GAMEPAD_PROFILE_DEFAULT } from '../../settings/namespaces.js';
|
||||
import {
|
||||
@@ -48,17 +48,16 @@ function pickActivePad(pads, activeSignature) {
|
||||
|
||||
export default function GamepadInputManager() {
|
||||
const {
|
||||
state,
|
||||
actions: {
|
||||
setMode,
|
||||
setDriveVector,
|
||||
setAuxMotors,
|
||||
setServoAngle,
|
||||
runMacro,
|
||||
toggleNightVision,
|
||||
registerInputState,
|
||||
},
|
||||
} = useControlSystem();
|
||||
setMode,
|
||||
setDriveVector,
|
||||
setAuxMotors,
|
||||
setServoAngle,
|
||||
runMacro,
|
||||
toggleNightVision,
|
||||
registerInputState,
|
||||
} = useControlActions();
|
||||
const cameraAngle = useControlSelector((control) => control.state.camera?.angle);
|
||||
const cameraConfig = useControlSelector((control) => control.state.camera?.config);
|
||||
const dockAssist = useManualDockAssist();
|
||||
const { value: gamepadSettings, save: saveGamepadSettings } = useSettingsNamespace(
|
||||
'gamepad',
|
||||
@@ -162,8 +161,8 @@ export default function GamepadInputManager() {
|
||||
// with the newest settings and control actions without resubscribing to the hub.
|
||||
latestRef.current = {
|
||||
activeSignature,
|
||||
cameraAngle: state.camera?.angle,
|
||||
cameraConfig: state.camera?.config,
|
||||
cameraAngle,
|
||||
cameraConfig,
|
||||
dockAssist,
|
||||
gamepadSettings,
|
||||
registerInputState,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Keyboard Input Manager
|
||||
// Purpose: Captures and translates keyboard events into normalized control intents. Scope: Owns keydown/keyup listeners and dispatch coordination for drive controls.
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { useControlSystem } from '../ControlContext.jsx';
|
||||
import { useControlActions, useControlSelector } from '../ControlContext.jsx';
|
||||
import { useChat } from '../../context/ChatContext.jsx';
|
||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
|
||||
@@ -83,30 +83,32 @@ function shouldIgnoreEvent(event) {
|
||||
|
||||
export default function KeyboardInputManager() {
|
||||
const {
|
||||
state,
|
||||
actions: {
|
||||
setMode,
|
||||
setDriveVector,
|
||||
setAuxMotors,
|
||||
nudgeServo,
|
||||
runMacro,
|
||||
stopAllMotion,
|
||||
registerInputState,
|
||||
toggleNightVision,
|
||||
startHorn,
|
||||
stopHorn,
|
||||
setMicPttActive,
|
||||
setSongNote,
|
||||
sendSong,
|
||||
},
|
||||
} = useControlSystem();
|
||||
setMode,
|
||||
setDriveVector,
|
||||
setAuxMotors,
|
||||
nudgeServo,
|
||||
runMacro,
|
||||
stopAllMotion,
|
||||
registerInputState,
|
||||
toggleNightVision,
|
||||
startHorn,
|
||||
stopHorn,
|
||||
setMicPttActive,
|
||||
setSongNote,
|
||||
sendSong,
|
||||
} = useControlActions();
|
||||
const rawKeymap = useControlSelector((control) => control.state.keymap);
|
||||
const cameraNudgeDegrees = useControlSelector((control) => control.state.camera?.config?.nudgeDegrees);
|
||||
const songNote = useControlSelector((control) => control.state.song?.note);
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
const hornActive = useControlSelector((control) => Boolean(control.state.horn?.active));
|
||||
const homeAssistant = useSessionSelector((state) => state.session?.homeAssistant || null);
|
||||
const dockAssist = useManualDockAssist();
|
||||
const { homeAssistantSetState } = useSessionActions();
|
||||
const { focusChat, isChatFocused } = useChat();
|
||||
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||
const { save: saveVideoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS);
|
||||
const keymap = useMemo(() => normalizeKeymapEntries(state.keymap), [state.keymap]);
|
||||
const keymap = useMemo(() => normalizeKeymapEntries(rawKeymap), [rawKeymap]);
|
||||
const actionTokens = useMemo(() => {
|
||||
const tokens = new Set();
|
||||
Object.values(keymap).forEach((bindingSet) => {
|
||||
@@ -132,8 +134,8 @@ export default function KeyboardInputManager() {
|
||||
return mapTiltSpeedToInterval(tiltSpeed);
|
||||
}, [inputSettings?.keyboard]);
|
||||
const servoStep = useMemo(
|
||||
() => Math.abs(state.camera?.config?.nudgeDegrees || 1),
|
||||
[state.camera?.config?.nudgeDegrees],
|
||||
() => Math.abs(cameraNudgeDegrees || 1),
|
||||
[cameraNudgeDegrees],
|
||||
);
|
||||
|
||||
const activeTokensRef = useRef(new Set());
|
||||
@@ -352,7 +354,7 @@ export default function KeyboardInputManager() {
|
||||
setMicPttActive,
|
||||
setMode,
|
||||
setSongNote,
|
||||
songNote: state.song?.note,
|
||||
songNote,
|
||||
startHorn,
|
||||
stopAllMotion,
|
||||
stopHorn,
|
||||
@@ -455,11 +457,11 @@ export default function KeyboardInputManager() {
|
||||
|
||||
useEffect(() => {
|
||||
latestResetAllRef.current();
|
||||
}, [state.roverId]);
|
||||
}, [roverId]);
|
||||
|
||||
useEffect(() => {
|
||||
hornActiveRef.current = Boolean(state.horn?.active);
|
||||
}, [state.horn?.active]);
|
||||
hornActiveRef.current = hornActive;
|
||||
}, [hornActive]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
|
||||
|
||||
export function useManualDockAssist(options = {}) {
|
||||
const { manageLifecycle = false } = options;
|
||||
const {
|
||||
state: { roverId, manualDockAssist },
|
||||
actions,
|
||||
} = useControlSystem();
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
const active = useControlSelector((control) => Boolean(control.state.manualDockAssist?.active));
|
||||
const actions = useControlActions();
|
||||
const frame = useTelemetryFrame(roverId);
|
||||
const sensors = frame?.sensors || {};
|
||||
const chargingLabel = sensors?.chargingState?.label || '';
|
||||
const docked = Boolean(sensors?.chargingSources?.homeBase);
|
||||
const charging = docked && chargingLabel.toLowerCase() !== 'not charging' && chargingLabel !== '';
|
||||
const active = Boolean(manualDockAssist?.active);
|
||||
const wasDockedRef = useRef(false);
|
||||
|
||||
const enterAssist = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user