pass 1 (broken)

This commit is contained in:
legop3
2026-05-02 15:39:38 -04:00
parent 53cf39e7fb
commit ba4b6cfc78
17 changed files with 247 additions and 182 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-BhTaDZYU.js"></script> <script type="module" crossorigin src="/assets/index-CduNi74O.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CGvUXLso.css"> <link rel="stylesheet" crossorigin href="/assets/index-CGvUXLso.css">
</head> </head>
<body> <body>
+3 -2
View File
@@ -93,6 +93,7 @@ function MobileFeatureTabs({
roomPanelId, roomPanelId,
showTelemetry = true, showTelemetry = true,
}) { }) {
const [activeTab, setActiveTab] = useState('chat');
const session = useSessionSelector((state) => state.session); const session = useSessionSelector((state) => state.session);
const { state: controlState } = useControlSystem(); const { state: controlState } = useControlSystem();
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' }); const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
@@ -116,7 +117,7 @@ function MobileFeatureTabs({
); );
return ( return (
<section className="panel text-base"> <section className="panel text-base">
<Tabs defaultTab="chat"> <Tabs defaultTab="chat" currentTab={activeTab} onTabChange={setActiveTab}>
<TabList> <TabList>
<Tab id="chat">Chat</Tab> <Tab id="chat">Chat</Tab>
<Tab id="vip" highlight={vipClipPlaying ? 'green' : vipMicActive ? 'pink' : 'none'}> <Tab id="vip" highlight={vipClipPlaying ? 'green' : vipMicActive ? 'pink' : 'none'}>
@@ -141,7 +142,7 @@ function MobileFeatureTabs({
</div> </div>
</TabPanel> </TabPanel>
<TabPanel id="vip" keepMounted> <TabPanel id="vip" keepMounted>
<VipPanel /> <VipPanel isActive={activeTab === 'vip'} />
</TabPanel> </TabPanel>
<TabPanel id="roomcontrols"> <TabPanel id="roomcontrols">
<div className="space-y-0.5"> <div className="space-y-0.5">
@@ -2,14 +2,14 @@
// Purpose: Defines the Global Objective Banner module and the local helpers/components used in this file. // Purpose: Defines the Global Objective Banner module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useSession } from '../../context/SessionContext.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx';
const MOBILE_DISMISS_MS = 10000; const MOBILE_DISMISS_MS = 10000;
const MAX_FONT_PX = 28; const MAX_FONT_PX = 28;
const MIN_FONT_PX = 14; const MIN_FONT_PX = 14;
export default function GlobalObjectiveBanner({ layout = 'desktop', className = '', dismissable = true }) { export default function GlobalObjectiveBanner({ layout = 'desktop', className = '', dismissable = true }) {
const { session } = useSession(); const session = useSessionSelector((state) => state.session);
const goalText = session?.globalObjective?.text ? String(session.globalObjective.text).trim() : ''; const goalText = session?.globalObjective?.text ? String(session.globalObjective.text).trim() : '';
const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape' || layout === 'mobile'; const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape' || layout === 'mobile';
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
+3 -4
View File
@@ -2,9 +2,11 @@
// Purpose: Defines the Log Panel module and the local helpers/components used in this file. // Purpose: Defines the Log 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. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useSessionSelector } from '../../context/SessionContext.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useMemo } from 'react';
export default function LogPanel() { export default function LogPanel() {
const logs = useSessionSelector((state) => state.logs); const logs = useSessionSelector((state) => state.logs);
const rendered = useMemo(() => logs.slice().reverse(), [logs]);
return ( return (
<div className="panel-section space-y-0.5 text-base"> <div className="panel-section space-y-0.5 text-base">
<div className="flex items-center justify-between text-sm text-slate-400"> <div className="flex items-center justify-between text-sm text-slate-400">
@@ -15,10 +17,7 @@ export default function LogPanel() {
{logs.length === 0 ? ( {logs.length === 0 ? (
<p>No logs yet.</p> <p>No logs yet.</p>
) : ( ) : (
logs rendered.map((entry) => (
.slice()
.reverse()
.map((entry) => (
<div key={entry.id} className="surface"> <div key={entry.id} className="surface">
<span className="text-amber-400">{entry.timestamp}</span>{' '} <span className="text-amber-400">{entry.timestamp}</span>{' '}
<span className="text-lime-400">[{entry.level}]</span>{' '} <span className="text-lime-400">[{entry.level}]</span>{' '}
@@ -2,7 +2,7 @@
// Purpose: Defines the Raw User Pile Panel module and the local helpers/components used in this file. // Purpose: Defines the Raw User Pile 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. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useSession } from '../../context/SessionContext.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx';
import NicknameForm from '../NicknameForm/index.jsx'; import NicknameForm from '../NicknameForm/index.jsx';
import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx'; import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx';
@@ -35,7 +35,7 @@ export default function RawUserPilePanel({
fillHeight = false, fillHeight = false,
compact = false, compact = false,
}) { }) {
const { session } = useSession(); const session = useSessionSelector((state) => state.session);
const canSetNickname = session?.role !== 'spectator'; const canSetNickname = session?.role !== 'spectator';
const users = session?.users ?? []; const users = session?.users ?? [];
const selfId = session?.socketId || null; const selfId = session?.socketId || null;
+4 -2
View File
@@ -23,6 +23,7 @@ import VipPanel from '../VipPanel/index.jsx';
import { useSessionSelector } from '../../context/SessionContext.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx'; import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
import { useState } from 'react';
function TopDownMapPanel() { function TopDownMapPanel() {
const { const {
@@ -115,6 +116,7 @@ function DriveDockPanel() {
} }
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) { export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
const [activeTab, setActiveTab] = useState('telemetry');
const session = useSessionSelector((state) => state.session); const session = useSessionSelector((state) => state.session);
const { state: controlState } = useControlSystem(); const { state: controlState } = useControlSystem();
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' }); const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
@@ -138,7 +140,7 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
); );
return ( return (
<section className="panel text-base"> <section className="panel text-base">
<Tabs defaultTab="telemetry"> <Tabs defaultTab="telemetry" currentTab={activeTab} onTabChange={setActiveTab}>
<TabList> <TabList>
<Tab id="telemetry">Controls</Tab> <Tab id="telemetry">Controls</Tab>
<Tab id="vip" highlight={vipClipPlaying ? 'green' : vipMicActive ? 'pink' : 'none'}> <Tab id="vip" highlight={vipClipPlaying ? 'green' : vipMicActive ? 'pink' : 'none'}>
@@ -179,7 +181,7 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
</div> </div>
</TabPanel> </TabPanel>
<TabPanel id="vip" keepMounted> <TabPanel id="vip" keepMounted>
<VipPanel /> <VipPanel isActive={activeTab === 'vip'} />
</TabPanel> </TabPanel>
<TabPanel id="help"> <TabPanel id="help">
<HelpPanel layout={layout} onOpenOverlay={onOpenHelpOverlay} /> <HelpPanel layout={layout} onOpenOverlay={onOpenHelpOverlay} />
@@ -2,7 +2,7 @@
// Purpose: Defines the Room Camera Panel module and the local helpers/components used in this file. // Purpose: Defines the Room Camera 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. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useSession } from '../../context/SessionContext.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
import { useRoomCameraSnapshots } from '../../hooks/useRoomCameraSnapshots.js'; import { useRoomCameraSnapshots } from '../../hooks/useRoomCameraSnapshots.js';
import RoomCameraFeed from '../RoomCameraFeed/index.jsx'; import RoomCameraFeed from '../RoomCameraFeed/index.jsx';
@@ -32,7 +32,7 @@ export default function RoomCameraPanel({
hideHeader = false, hideHeader = false,
panelId = null, panelId = null,
}) { }) {
const { session } = useSession(); const session = useSessionSelector((state) => state.session);
const cameras = session?.roomCameras || []; const cameras = session?.roomCameras || [];
const feedMap = useRoomCameraSnapshots(cameras.map((camera) => ({ id: camera.id }))); const feedMap = useRoomCameraSnapshots(cameras.map((camera) => ({ id: camera.id })));
const { value: orientationSettings, save: saveOrientationSettings } = useSettingsNamespace('roomCameraPanels', {}); const { value: orientationSettings, save: saveOrientationSettings } = useSettingsNamespace('roomCameraPanels', {});
@@ -13,7 +13,7 @@ import {
cliffColor, cliffColor,
} from './visuals.jsx'; } from './visuals.jsx';
export default function TopDownMapContent({ sensors = {}, variant = 'full', size: overrideSize, overlay = false }) { function TopDownMapContent({ sensors = {}, variant = 'full', size: overrideSize, overlay = false }) {
const size = overrideSize || (variant === 'mini' ? 190 : 260); const size = overrideSize || (variant === 'mini' ? 190 : 260);
const center = size / 2; const center = size / 2;
const offsetY = size * 0.07; const offsetY = size * 0.07;
@@ -147,3 +147,5 @@ export default function TopDownMapContent({ sensors = {}, variant = 'full', size
</div> </div>
); );
} }
export default React.memo(TopDownMapContent);
@@ -5,7 +5,7 @@ import React from 'react';
import TopDownMap from '../TopDownMap/index.jsx'; import TopDownMap from '../TopDownMap/index.jsx';
import { roverNameChromeStyle } from '../../lib/roverColor.js'; import { roverNameChromeStyle } from '../../lib/roverColor.js';
export default function HudOverlay({ function HudOverlay({
sensors, sensors,
label, label,
roverColor = null, roverColor = null,
@@ -174,3 +174,5 @@ export default function HudOverlay({
</div> </div>
); );
} }
export default React.memo(HudOverlay);
@@ -3,7 +3,7 @@
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import React from 'react'; import React from 'react';
export default function LightBumpBars({ sensors }) { function LightBumpBars({ sensors }) {
const values = [ const values = [
sensors?.lightBumpLeftSignal, sensors?.lightBumpLeftSignal,
sensors?.lightBumpFrontLeftSignal, sensors?.lightBumpFrontLeftSignal,
@@ -47,3 +47,5 @@ export default function LightBumpBars({ sensors }) {
</div> </div>
); );
} }
export default React.memo(LightBumpBars);
+12 -4
View File
@@ -100,10 +100,18 @@ export default function VideoTile({
.map(([key]) => key); .map(([key]) => key);
const limiterCaps = overcurrentLimiter?.caps || null; const limiterCaps = overcurrentLimiter?.caps || null;
const limiterGroups = overcurrentLimiter?.overcurrent?.groups || null; const limiterGroups = overcurrentLimiter?.overcurrent?.groups || null;
const debugAudio = const debugFlags = useMemo(() => {
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugAudio'); if (typeof window === 'undefined') {
const debugHud = return { debugAudio: false, debugHud: false };
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugHud'); }
const params = new URLSearchParams(window.location.search);
return {
debugAudio: params.has('debugAudio'),
debugHud: params.has('debugHud'),
};
}, []);
const debugAudio = debugFlags.debugAudio;
const debugHud = debugFlags.debugHud;
const limiterFill = useMemo(() => { const limiterFill = useMemo(() => {
if (!limiterCaps) return null; if (!limiterCaps) return null;
const driveCap = Number.isFinite(limiterCaps?.drive?.cap) ? limiterCaps.drive.cap : 1; const driveCap = Number.isFinite(limiterCaps?.drive?.cap) ? limiterCaps.drive.cap : 1;
+6 -5
View File
@@ -2,7 +2,7 @@
// Purpose: Defines the Vip Panel module and the local helpers/components used in this file. // Purpose: Defines the Vip 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. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useSession } from '../../context/SessionContext.jsx'; import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
import { COOKIE_KEY_REGEX, flowWrapClass } from '../vip/constants.js'; import { COOKIE_KEY_REGEX, flowWrapClass } from '../vip/constants.js';
import VipAudioUploadCard from '../vip/VipAudioUploadCard/index.jsx'; import VipAudioUploadCard from '../vip/VipAudioUploadCard/index.jsx';
@@ -12,10 +12,10 @@ import VipPrivateRoverAccessCard from '../vip/VipPrivateRoverAccessCard.jsx';
import VipNeatoCard from '../vip/VipNeatoCard.jsx'; import VipNeatoCard from '../vip/VipNeatoCard.jsx';
import VipLiftCard from '../vip/VipLiftCard.jsx'; import VipLiftCard from '../vip/VipLiftCard.jsx';
export default function VipPanel() { export default function VipPanel({ isActive = true }) {
const session = useSessionSelector((state) => state.session);
const neatoLidar = useSessionSelector((state) => state.neatoLidar);
const { const {
session,
neatoLidar,
identifySession, identifySession,
requestVerification, requestVerification,
requestPrivateRoverAccess, requestPrivateRoverAccess,
@@ -31,7 +31,7 @@ export default function VipPanel() {
neatoPowerCycle, neatoPowerCycle,
liftUp, liftUp,
liftDown, liftDown,
} = useSession(); } = useSessionActions();
const { value: identity, save: saveIdentity } = useSettingsNamespace('identity', { cookieUserId: '' }); const { value: identity, save: saveIdentity } = useSettingsNamespace('identity', { cookieUserId: '' });
const { value: profile } = useSettingsNamespace('profile', { nickname: '' }); const { value: profile } = useSettingsNamespace('profile', { nickname: '' });
@@ -80,6 +80,7 @@ export default function VipPanel() {
<VipNeatoCard <VipNeatoCard
neato={session?.neato || null} neato={session?.neato || null}
lidar={neatoLidar} lidar={neatoLidar}
lidarActive={isActive}
onStart={neatoStart} onStart={neatoStart}
onSendHome={neatoSendHome} onSendHome={neatoSendHome}
onLocate={neatoLocate} onLocate={neatoLocate}
+8 -3
View File
@@ -1,7 +1,7 @@
// Vip Neato Card // Vip Neato Card
// Purpose: Defines the Vip Neato Card module and the local helpers/components used in this file. // Purpose: Defines the Vip Neato Card module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
function normalizeState(value) { function normalizeState(value) {
return String(value || '').trim(); return String(value || '').trim();
@@ -114,6 +114,7 @@ function buildLidarRenderPoints(points = []) {
export default function VipNeatoCard({ export default function VipNeatoCard({
neato, neato,
lidar, lidar,
lidarActive = true,
onStart, onStart,
onSendHome, onSendHome,
onLocate, onLocate,
@@ -140,7 +141,10 @@ export default function VipNeatoCard({
const robotError = normalizeState(neato?.telemetry?.robotError) || '--'; const robotError = normalizeState(neato?.telemetry?.robotError) || '--';
const robotAlert = normalizeState(neato?.telemetry?.robotAlert) || '--'; const robotAlert = normalizeState(neato?.telemetry?.robotAlert) || '--';
const lidarPoints = Array.isArray(lidar?.points) ? lidar.points : []; const lidarPoints = Array.isArray(lidar?.points) ? lidar.points : [];
const lidarRenderPoints = buildLidarRenderPoints(lidarPoints); const lidarRenderPoints = useMemo(
() => (lidarActive ? buildLidarRenderPoints(lidarPoints) : []),
[lidarActive, lidarPoints],
);
const lidarStatus = normalizeState(lidar?.status) || '--'; const lidarStatus = normalizeState(lidar?.status) || '--';
const lidarDebug = lidar?.debug && typeof lidar.debug === 'object' ? lidar.debug : null; const lidarDebug = lidar?.debug && typeof lidar.debug === 'object' ? lidar.debug : null;
const lidarReason = normalizeState(lidarDebug?.reason) || '--'; const lidarReason = normalizeState(lidarDebug?.reason) || '--';
@@ -180,13 +184,14 @@ export default function VipNeatoCard({
const primaryState = docked ? 'Docked' : uiStateLabel !== '--' ? uiStateLabel : 'Away from dock'; const primaryState = docked ? 'Docked' : uiStateLabel !== '--' ? uiStateLabel : 'Away from dock';
useEffect(() => { useEffect(() => {
if (!lidarActive) return undefined;
if (!lidar || !Array.isArray(lidar.points)) return undefined; if (!lidar || !Array.isArray(lidar.points)) return undefined;
setLidarFlash(true); setLidarFlash(true);
const timer = setTimeout(() => { const timer = setTimeout(() => {
setLidarFlash(false); setLidarFlash(false);
}, 120); }, 120);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [lidar]); }, [lidar, lidarActive]);
return ( return (
<section className={`surface text-sm text-slate-200 ${wrapClass}`}> <section className={`surface text-sm text-slate-200 ${wrapClass}`}>
+69 -12
View File
@@ -2,27 +2,75 @@
// Purpose: Maintains shared telemetry snapshots and rover status streams for UI consumers. Scope: Subscribes to telemetry events and exposes normalized read APIs to components. // Purpose: Maintains shared telemetry snapshots and rover status streams for UI consumers. Scope: Subscribes to telemetry events and exposes normalized read APIs to components.
/* eslint-disable react-refresh/only-export-components */ /* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useMemo, useState, useEffect } from 'react'; import { createContext, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
import { useSocket } from './SocketContext.jsx'; import { useSocket } from './SocketContext.jsx';
const TelemetryContext = createContext({ frames: {} }); const EMPTY_FRAMES = Object.freeze({});
const EMPTY_FRAME = null;
const TelemetryContext = createContext(null);
export function TelemetryProvider({ children }) { export function TelemetryProvider({ children }) {
const socket = useSocket(); const socket = useSocket();
const [frames, setFrames] = useState({}); const framesRef = useRef({});
const roverSubscribersRef = useRef(new Map());
const allSubscribersRef = useRef(new Set());
const notifyRover = (roverId) => {
const listeners = roverSubscribersRef.current.get(roverId);
if (listeners) {
listeners.forEach((listener) => listener());
}
allSubscribersRef.current.forEach((listener) => listener());
};
const store = useMemo(
() => ({
getFrames: () => framesRef.current,
getFrame: (roverId) => {
if (!roverId) return EMPTY_FRAME;
return framesRef.current[roverId] ?? EMPTY_FRAME;
},
subscribeAll: (listener) => {
allSubscribersRef.current.add(listener);
return () => {
allSubscribersRef.current.delete(listener);
};
},
subscribeRover: (roverId, listener) => {
if (!roverId) return () => {};
let listeners = roverSubscribersRef.current.get(roverId);
if (!listeners) {
listeners = new Set();
roverSubscribersRef.current.set(roverId, listeners);
}
listeners.add(listener);
return () => {
const current = roverSubscribersRef.current.get(roverId);
if (!current) return;
current.delete(listener);
if (!current.size) {
roverSubscribersRef.current.delete(roverId);
}
};
},
}),
[],
);
useEffect(() => { useEffect(() => {
function handleSensorFrame({ roverId, sensors = {}, frame = {} }) { function handleSensorFrame({ roverId, sensors = {}, frame = {} }) {
if (!roverId) return; if (!roverId) return;
setFrames((prev) => ({ framesRef.current = {
...prev, ...framesRef.current,
[roverId]: { [roverId]: {
roverId, roverId,
sensors, sensors,
raw: frame?.data || null, raw: frame?.data || null,
receivedAt: Date.now(), receivedAt: Date.now(),
}, },
})); };
notifyRover(roverId);
} }
socket.on('sensorFrame', handleSensorFrame); socket.on('sensorFrame', handleSensorFrame);
@@ -31,16 +79,25 @@ export function TelemetryProvider({ children }) {
}; };
}, [socket]); }, [socket]);
const value = useMemo(() => ({ frames }), [frames]); return <TelemetryContext.Provider value={store}>{children}</TelemetryContext.Provider>;
return <TelemetryContext.Provider value={value}>{children}</TelemetryContext.Provider>;
} }
export function useTelemetryFrames() { export function useTelemetryFrames() {
return useContext(TelemetryContext).frames; const store = useContext(TelemetryContext);
if (!store) {
throw new Error('useTelemetryFrames must be used within TelemetryProvider');
}
return useSyncExternalStore(store.subscribeAll, store.getFrames, () => EMPTY_FRAMES);
} }
export function useTelemetryFrame(roverId) { export function useTelemetryFrame(roverId) {
const frames = useTelemetryFrames(); const store = useContext(TelemetryContext);
if (!roverId) return null; if (!store) {
return frames[roverId] ?? null; throw new Error('useTelemetryFrame must be used within TelemetryProvider');
}
return useSyncExternalStore(
(listener) => store.subscribeRover(roverId, listener),
() => store.getFrame(roverId),
() => EMPTY_FRAME,
);
} }
-14
View File
@@ -53,20 +53,6 @@ export function useDockIr(sensors, options = {}) {
})); }));
}, [sensors?.infraredCharacterLeft, sensors?.infraredCharacterRight, sensors?.infraredCharacterOmni]); }, [sensors?.infraredCharacterLeft, sensors?.infraredCharacterRight, sensors?.infraredCharacterOmni]);
useEffect(() => {
// Debug: log when new codes appear
const { left, right, omni } = state;
const haveAny = left?.ts || right?.ts || omni?.ts;
if (!haveAny) return;
const stamp = new Date().toISOString();
// eslint-disable-next-line no-console
console.debug('[DockIR]', stamp, {
left: left?.code ?? 0,
omni: omni?.code ?? 0,
right: right?.code ?? 0,
});
}, [state.left?.code, state.right?.code, state.omni?.code]);
return useMemo(() => { return useMemo(() => {
const now = Date.now(); const now = Date.now();
const withWindow = (entry) => { const withWindow = (entry) => {