// 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 { useSessionSelector } from '../../context/SessionContext.jsx';
import { useVisualTelemetrySelector } from '../../context/TelemetryContext.jsx';
import { selectFrameForDisplay } from '../../context/telemetryViews.js';
import { useDockIr } from '../../hooks/useDockIr.js';
import CardFrame from '../CardFrame/index.jsx';
export default function TelemetryPanel() {
const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const frame = useVisualTelemetrySelector(roverId, selectFrameForDisplay);
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 rawSnippet = frame?.raw ? frame.raw : null;
return (
{/*
{connected ? 'online' : 'offline'}
· role {session?.role || 'unknown'}
· mode {session?.mode || '--'}
{updated && · sensors {updated}}
· driver {driverLabel}
*/}
{!roverId ? (
You are not assigned to a rover!!!!!!
) : !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)}`}
);
}