// 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 (
);
}
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)}`}
);
}