Add RSSI/SNR to received messages. Closes #148.

This commit is contained in:
Jack Kingsman
2026-04-03 15:20:44 -07:00
parent 08238aa464
commit 7ad1ee26a4
10 changed files with 167 additions and 27 deletions
+17 -1
View File
@@ -300,6 +300,9 @@ export function MessageList({
const [resendableIds, setResendableIds] = useState<Set<number>>(new Set());
const resendTimersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());
const packetCacheRef = useRef<Map<number, RawPacket>>(new Map());
const packetSignalOverrideRef = useRef<{ rssi: number | null; snr: number | null } | undefined>(
undefined
);
const [packetInspectorSource, setPacketInspectorSource] = useState<
| { kind: 'packet'; packet: RawPacket }
| { kind: 'loading'; message: string }
@@ -325,6 +328,13 @@ export function MessageList({
const prevConvKeyRef = useRef<string | null>(null);
const handleAnalyzePacket = useCallback(async (message: Message) => {
// Extract signal from the first path if available
const firstPath = message.paths?.[0];
packetSignalOverrideRef.current =
firstPath && (firstPath.rssi != null || firstPath.snr != null)
? { rssi: firstPath.rssi ?? null, snr: firstPath.snr ?? null }
: undefined;
if (message.packet_id == null) {
setPacketInspectorSource({
kind: 'unavailable',
@@ -1180,12 +1190,18 @@ export function MessageList({
{packetInspectorSource && (
<RawPacketInspectorDialog
open={packetInspectorSource !== null}
onOpenChange={(isOpen) => !isOpen && setPacketInspectorSource(null)}
onOpenChange={(isOpen) => {
if (!isOpen) {
setPacketInspectorSource(null);
packetSignalOverrideRef.current = undefined;
}
}}
channels={channels}
source={packetInspectorSource}
title="Analyze Packet"
description="On-demand raw packet analysis for a message-backed archival packet."
notice={ANALYZE_PACKET_NOTICE}
signalOverride={packetSignalOverrideRef.current}
/>
)}
</div>
+14 -3
View File
@@ -103,14 +103,25 @@ export function PathModal({
) : null}
{/* Raw path summary */}
<div className="text-sm">
<div className="text-sm space-y-1">
{paths.map((p, index) => {
const hops = parsePathHops(p.path, p.path_len);
const rawPath = hops.length > 0 ? hops.join('->') : 'direct';
const hasSignal = p.rssi != null || p.snr != null;
return (
<div key={index}>
<span className="text-foreground/70 font-semibold">Path {index + 1}:</span>{' '}
<span className="font-mono text-muted-foreground">{rawPath}</span>
<div>
<span className="text-foreground/70 font-semibold">Path {index + 1}:</span>{' '}
<span className="font-mono text-muted-foreground">{rawPath}</span>
</div>
{hasSignal && (
<div className="text-[0.6875rem] text-muted-foreground ml-4">
Last hop (as heard by you):{' '}
{p.rssi != null && <span>{p.rssi} dBm RSSI</span>}
{p.rssi != null && p.snr != null && <span> · </span>}
{p.snr != null && <span>{p.snr.toFixed(1)} dB SNR</span>}
</div>
)}
</div>
);
})}
@@ -35,6 +35,11 @@ type RawPacketInspectorDialogSource =
message: string;
};
interface SignalOverride {
rssi: number | null;
snr: number | null;
}
interface RawPacketInspectorDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -43,10 +48,12 @@ interface RawPacketInspectorDialogProps {
title: string;
description: string;
notice?: ReactNode;
signalOverride?: SignalOverride;
}
interface RawPacketInspectionPanelProps {
packet: RawPacket;
signalOverride?: SignalOverride;
channels: Channel[];
}
@@ -125,15 +132,21 @@ function formatTimestamp(timestamp: number): string {
});
}
function formatSignal(packet: RawPacket): string {
const parts: string[] = [];
if (packet.rssi !== null) {
parts.push(`${packet.rssi} dBm RSSI`);
}
if (packet.snr !== null) {
parts.push(`${packet.snr.toFixed(1)} dB SNR`);
}
return parts.length > 0 ? parts.join(' · ') : 'No signal sample';
function formatSignal(
packet: RawPacket,
signalOverride?: SignalOverride
): { lines: string[]; label: string } {
const rssi = signalOverride?.rssi ?? packet.rssi;
const snr = signalOverride?.snr ?? packet.snr;
const lines: string[] = [];
if (rssi !== null) lines.push(`${rssi} dBm RSSI`);
if (snr !== null) lines.push(`${snr.toFixed(1)} dB SNR`);
const isOverride =
signalOverride != null && (signalOverride.rssi != null || signalOverride.snr != null);
return {
lines: lines.length > 0 ? lines : ['No signal sample'],
label: isOverride ? 'Last Hop Signal' : 'Signal',
};
}
function formatByteRange(field: PacketByteField): string {
@@ -569,7 +582,11 @@ function FieldSection({
);
}
export function RawPacketInspectionPanel({ packet, channels }: RawPacketInspectionPanelProps) {
export function RawPacketInspectionPanel({
packet,
channels,
signalOverride,
}: RawPacketInspectionPanelProps) {
const decoderOptions = useMemo(() => createDecoderOptions(channels), [channels]);
const groupTextCandidates = useMemo(
() => buildGroupTextResolutionCandidates(channels),
@@ -641,11 +658,24 @@ export function RawPacketInspectionPanel({ packet, channels }: RawPacketInspecti
primary={`${inspection.routeTypeName} · ${inspection.payloadTypeName}`}
secondary={`${inspection.payloadVersionName} · ${formatPathMode(inspection.decoded?.pathHashSize, inspection.pathTokens.length)}`}
/>
<CompactMetaCard
label="Signal"
primary={formatSignal(packet)}
secondary={packetContext ? null : undefined}
/>
{(() => {
const sig = formatSignal(packet, signalOverride);
return (
<div className="rounded-lg border border-border/70 bg-card/70 p-2.5">
<div className="text-[0.625rem] uppercase tracking-wider text-muted-foreground">
{sig.label}
</div>
{sig.lines.map((line, i) => (
<div
key={i}
className={`${i === 0 ? 'mt-1' : 'mt-0.5'} text-sm font-medium leading-tight text-foreground`}
>
{line}
</div>
))}
</div>
);
})()}
</section>
</div>
@@ -715,6 +745,7 @@ export function RawPacketInspectorDialog({
title,
description,
notice,
signalOverride,
}: RawPacketInspectorDialogProps) {
const [packetInput, setPacketInput] = useState('');
@@ -739,7 +770,13 @@ export function RawPacketInspectorDialog({
let body: ReactNode;
if (source.kind === 'packet') {
body = <RawPacketInspectionPanel packet={source.packet} channels={channels} />;
body = (
<RawPacketInspectionPanel
packet={source.packet}
channels={channels}
signalOverride={signalOverride}
/>
);
} else if (source.kind === 'paste') {
body = (
<>