// Telemetry Panel // Purpose: Defines the Telemetry 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 { useSessionSelector } from '../../context/SessionContext.jsx'; import { useTelemetryFrame } from '../../context/TelemetryContext.jsx'; import { useDockIr } from '../../hooks/useDockIr.js'; function formatMetric(value, fallback = '--') { if (value == null || value === '') return fallback; return value; } export default function TelemetryPanel() { const connected = useSessionSelector((state) => state.connected); const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null); const activeDriverId = useSessionSelector((state) => { const id = state.session?.assignment?.roverId; return id ? state.session?.activeDrivers?.[id] || null : null; }); const selfSocketId = useSessionSelector((state) => state.session?.socketId || null); const users = useSessionSelector((state) => state.session?.users ?? []); const frame = useTelemetryFrame(roverId); const sensors = frame?.sensors || {}; const dockIr = useDockIr(sensors); const voltage = sensors.voltageMv != null ? `${(sensors.voltageMv / 1000).toFixed(2)} V` : null; const current = sensors.currentMa != null ? `${sensors.currentMa} mA` : null; const batteryTemp = sensors.batteryTemperatureC != null ? `${sensors.batteryTemperatureC} °C` : null; const charge = sensors.batteryChargeMah; const capacity = sensors.batteryCapacityMah; const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null; const rawSnippet = frame?.raw ? frame.raw : null; const driverLabel = useMemo(() => { if (!roverId) return 'n/a'; if (!activeDriverId) return 'Available'; if (activeDriverId === selfSocketId) return 'You'; const user = users.find((entry) => entry.socketId === activeDriverId); return user?.nickname || activeDriverId.slice(0, 6); }, [activeDriverId, roverId, selfSocketId, users]); return (
{/*
{connected ? 'online' : 'offline'} · role {session?.role || 'unknown'} · mode {session?.mode || '--'} {updated && · sensors {updated}} · driver {driverLabel}
*/} {!roverId ? (

Assign a rover to view sensors.

) : !frame ? (

Waiting for sensor frames…

) : ( <> )} {rawSnippet && (
{rawSnippet}
)}
); } function TelemetrySummary({ sensors, voltage, current, batteryTemp, charge, capacity }) { const chargePct = charge != null && capacity ? `${Math.round((charge / capacity) * 100)}` : '--'; const oiMode = sensors?.oiMode?.label || '--'; const docked = sensors?.chargingSources?.homeBase ? 'Yes' : 'No'; const charging = sensors?.chargingState?.label || '--'; return (
); } function Metric({ label, value }) { return (
{label} {value}
); } function SensorDetails({ sensors, dockState }) { const bumps = sensors?.bumpsAndWheelDrops || {}; const over = sensors?.wheelOvercurrents || {}; const lightSignals = [ { label: 'Left', value: sensors?.lightBumpLeftSignal }, { label: 'Front Left', value: sensors?.lightBumpFrontLeftSignal }, { label: 'Center Left', value: sensors?.lightBumpCenterLeftSignal }, { label: 'Center Right', value: sensors?.lightBumpCenterRightSignal }, { label: 'Front Right', value: sensors?.lightBumpFrontRightSignal }, { label: 'Right', value: sensors?.lightBumpRightSignal }, ]; const cliffSignals = [ { label: 'Left', value: sensors?.cliffLeftSignal, active: sensors?.cliffLeft }, { label: 'Front Left', value: sensors?.cliffFrontLeftSignal, active: sensors?.cliffFrontLeft }, { label: 'Front Right', value: sensors?.cliffFrontRightSignal, active: sensors?.cliffFrontRight }, { label: 'Right', value: sensors?.cliffRightSignal, active: sensors?.cliffRight }, ]; const currents = [ { label: 'Wheel L', value: sensors?.wheelLeftCurrentMa, over: over.leftWheel }, { label: 'Wheel R', value: sensors?.wheelRightCurrentMa, over: over.rightWheel }, { label: 'Side brush', value: sensors?.sideBrushCurrentMa, over: over.sideBrush }, { label: 'Main brush', value: sensors?.mainBrushCurrentMa, over: over.mainBrush }, ]; return (
} /> } /> } /> } /> {lightSignals.map((entry) => ( ))} {cliffSignals.map((entry) => ( ))} {currents.map((entry) => ( {formatNumber(entry.value, 'mA')} {entry.over ? : null} } /> ))}
); } function DetailCard({ title, children }) { return (
{title}
{children}
); } function ValueRow({ label, value }) { return (
{label} {value}
); } function Pill({ active, tone = 'green', label }) { const base = 'rounded px-1.5 py-0.5 text-[0.7rem] font-semibold'; const colors = tone === 'red' ? active ? 'bg-red-600 text-white' : 'bg-slate-700 text-slate-300' : tone === 'amber' ? active ? 'bg-amber-500 text-slate-900' : 'bg-slate-700 text-slate-300' : active ? 'bg-emerald-600 text-white' : 'bg-slate-700 text-slate-300'; return {label || (active ? 'true' : 'false')}; } function formatNumber(value, unit) { if (value == null || Number.isNaN(value)) return '--'; return unit ? `${value} ${unit}` : value; } function DockMiniStatus({ sensors }) { const left = sensors?.infraredCharacterLeft; const right = sensors?.infraredCharacterRight; const omni = sensors?.infraredCharacterOmni; const badge = (code) => ( {code || '--'} ); return (
L {badge(left)}
O {badge(omni)}
R {badge(right)}
); } function DockDebug({ state }) { if (!state) return null; const label = (entry) => { const parts = []; if (entry?.red) parts.push('R'); if (entry?.green) parts.push('G'); if (entry?.force) parts.push('F'); return parts.length ? parts.join('') : 'none'; }; const age = (entry) => (entry?.age != null ? `${entry.age}ms` : '--'); return (
{`Left: ${state.left.code ?? '--'} (${label(state.left)}) · age ${age(state.left)}`}
{`Omni: ${state.omni.code ?? '--'} (${label(state.omni)}) · age ${age(state.omni)}`}
{`Right: ${state.right.code ?? '--'} (${label(state.right)}) · age ${age(state.right)}`}
); }