mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
239 lines
9.4 KiB
React
239 lines
9.4 KiB
React
// 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 (
|
|
<CardFrame title='Roomba sensor values' clipOverflow={false} bodyClassName="space-y-0.5 text-base text-slate-100">
|
|
{/* <div className="text-sm text-slate-400">
|
|
<span>{connected ? 'online' : 'offline'}</span>
|
|
<span> · role {session?.role || 'unknown'}</span>
|
|
<span> · mode {session?.mode || '--'}</span>
|
|
{updated && <span> · sensors {updated}</span>}
|
|
<span> · driver {driverLabel}</span>
|
|
</div> */}
|
|
{!roverId ? (
|
|
<p className="text-sm text-slate-500">You are not assigned to a rover!!!!!!</p>
|
|
) : !frame ? (
|
|
<p className="text-sm text-slate-500">Waiting for sensor frames…</p>
|
|
) : (
|
|
<>
|
|
<TelemetrySummary sensors={sensors} voltage={voltage} current={current} batteryTemp={batteryTemp} charge={charge} capacity={capacity} />
|
|
<SensorDetails sensors={sensors} dockState={dockIr} />
|
|
</>
|
|
)}
|
|
{rawSnippet && (
|
|
<pre className="surface whitespace-pre-wrap break-words text-xs text-lime-300">{rawSnippet}</pre>
|
|
)}
|
|
</CardFrame>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div className="grid grid-cols-2 gap-0.5 md:grid-cols-3">
|
|
<Metric label="Voltage" value={voltage ?? '--'} />
|
|
<Metric label="Current" value={current ?? '--'} />
|
|
<Metric label="Battery temp" value={batteryTemp ?? '--'} />
|
|
<Metric label="Charge" value={charge != null ? `${charge} mAh` : '--'} />
|
|
<Metric label="Capacity" value={capacity != null ? `${capacity} mAh` : '--'} />
|
|
<Metric label="Charge" value={chargePct} />
|
|
<Metric label="OI mode" value={oiMode} />
|
|
<Metric label="Docked" value={docked} />
|
|
<Metric label="Charging state" value={charging} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Metric({ label, value }) {
|
|
return (
|
|
<div className="surface flex items-center justify-between gap-0.5 px-1 py-0.5 text-sm">
|
|
<span className="text-slate-400">{label}</span>
|
|
<span className="text-slate-100">{value}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div className="grid gap-0.5 md:grid-cols-2">
|
|
<DetailCard title="Dock IR">
|
|
<DockMiniStatus sensors={sensors} />
|
|
<DockDebug state={dockState} />
|
|
</DetailCard>
|
|
|
|
<DetailCard title="Bumps & drops">
|
|
<ValueRow label="Bump L" value={<Pill active={bumps.bumpLeft} />} />
|
|
<ValueRow label="Bump R" value={<Pill active={bumps.bumpRight} />} />
|
|
<ValueRow label="Wheel drop L" value={<Pill active={bumps.wheelDropLeft} tone="amber" />} />
|
|
<ValueRow label="Wheel drop R" value={<Pill active={bumps.wheelDropRight} tone="amber" />} />
|
|
</DetailCard>
|
|
|
|
<DetailCard title="Light bumps (raw)">
|
|
{lightSignals.map((entry) => (
|
|
<ValueRow key={entry.label} label={entry.label} value={formatNumber(entry.value)} />
|
|
))}
|
|
</DetailCard>
|
|
|
|
<DetailCard title="Cliff sensors">
|
|
{cliffSignals.map((entry) => (
|
|
<ValueRow key={entry.label} label={entry.label} value={`${formatNumber(entry.value)} · ${entry.active ? 'bool: true' : 'bool: false'}`} />
|
|
))}
|
|
</DetailCard>
|
|
|
|
<DetailCard title="Currents">
|
|
{currents.map((entry) => (
|
|
<ValueRow
|
|
key={entry.label}
|
|
label={entry.label}
|
|
value={
|
|
<span className="flex items-center gap-0.5">
|
|
<span>{formatNumber(entry.value, 'mA')}</span>
|
|
{entry.over ? <Pill active tone="red" label="over" /> : null}
|
|
</span>
|
|
}
|
|
/>
|
|
))}
|
|
</DetailCard>
|
|
|
|
<DetailCard title="Dirt detect">
|
|
<ValueRow label="Left" value={formatNumber(sensors?.dirtDetectLeft)} />
|
|
<ValueRow label="Right" value={formatNumber(sensors?.dirtDetect)} />
|
|
</DetailCard>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DetailCard({ title, children }) {
|
|
return (
|
|
<div className="surface space-y-0.5 p-1 text-sm">
|
|
<div className="text-[0.78rem] font-semibold leading-none text-slate-200">{title}</div>
|
|
<div className="space-y-0.5">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ValueRow({ label, value }) {
|
|
return (
|
|
<div className="flex items-center justify-between gap-0.5">
|
|
<span className="text-slate-300">{label}</span>
|
|
<span className="text-slate-100">{value}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 <span className={`${base} ${colors}`}>{label || (active ? 'true' : 'false')}</span>;
|
|
}
|
|
|
|
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) => (
|
|
<span className={`rounded px-1 py-0.25 text-[0.7rem] font-semibold ${code ? 'bg-emerald-600 text-white' : 'bg-slate-700 text-slate-300'}`}>
|
|
{code || '--'}
|
|
</span>
|
|
);
|
|
return (
|
|
<div className="flex items-center justify-between gap-0.5 text-xs text-slate-200">
|
|
<div className="flex items-center gap-0.5">
|
|
<span className="text-slate-400">L</span>
|
|
{badge(left)}
|
|
</div>
|
|
<div className="flex items-center gap-0.5">
|
|
<span className="text-slate-400">O</span>
|
|
{badge(omni)}
|
|
</div>
|
|
<div className="flex items-center gap-0.5">
|
|
<span className="text-slate-400">R</span>
|
|
{badge(right)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div className="text-[0.7rem] text-slate-400">
|
|
<div>{`Left: ${state.left.code ?? '--'} (${label(state.left)}) · age ${age(state.left)}`}</div>
|
|
<div>{`Omni: ${state.omni.code ?? '--'} (${label(state.omni)}) · age ${age(state.omni)}`}</div>
|
|
<div>{`Right: ${state.right.code ?? '--'} (${label(state.right)}) · age ${age(state.right)}`}</div>
|
|
</div>
|
|
);
|
|
}
|