Files
hamview/ui/src/pages/meshcore/MeshCorePacketDetailsPane.tsx
2026-03-08 22:22:51 +01:00

289 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { bytesToHex } from '@noble/hashes/utils.js';
import { Card, Stack } from 'react-bootstrap';
import PacketDissectionViewer from '../../components/PacketDissectionViewer';
import type { Segment } from '../../types/protocol/dissection.types';
import {
payloadNameByValue,
routeDisplayByValue,
} from './MeshCoreData';
import { PayloadType, type AdvertPayload, type AnonReqPayload, type DecryptedGroupMessage, type GroupTextPayload, type Payload } from '../../types/protocol/meshcore.types';
import type { Packet } from '../../protocols/meshcore';
const HeaderFact: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => (
<div className="meshcore-fact-row">
<span className="meshcore-fact-label">{label}</span>
<span className="meshcore-fact-value">{value}</span>
</div>
);
const buildMeshCoreSegments = (packet: Packet): Segment[] => {
const { pathHashSize, pathHashCount } = packet;
const pathLength = pathHashSize * pathHashCount;
const segments: Segment[] = [
{
name: 'Header',
offset: 0,
byteCount: 1,
bitfields: [
{
offset: 6,
length: 2,
name: 'Version',
},
{
offset: 2,
length: 4,
name: `Payload Type (${payloadNameByValue[packet.payloadType] ?? 'Unknown'})`,
},
{
offset: 0,
length: 2,
name: `Route Type (${routeDisplayByValue[packet.routeType] ?? 'Unknown'})`,
},
],
},
{
name: 'Path Descriptor',
offset: 1,
byteCount: 1,
bitfields: [
{
offset: 6,
length: 2,
name: `Hash Size Selector (size=${pathHashSize})`,
},
{
offset: 0,
length: 6,
name: `Hash Count (count=${pathHashCount})`,
},
],
},
];
if (pathLength > 0) {
segments.push({
name: 'Path Data',
offset: 2,
byteCount: pathLength,
attributes: [
{
byteWidth: pathLength,
type: 'bytes',
name: `Path Hashes (${pathHashCount} × ${pathHashSize} bytes)`,
},
],
});
}
const payloadOffset = 2 + (packet.transportCodes ? packet.transportCodes.length * 2 : 0) + pathLength;
if (packet.payload.length > 0) {
segments.push({
name: 'Payload',
offset: payloadOffset,
byteCount: packet.payload.length,
attributes: [
{
byteWidth: packet.payload.length,
type: 'bytes',
name: `Payload Data (${packet.payload.length} bytes)`,
},
],
});
}
if (packet.segments) {
segments.push(...packet.segments);
}
return segments;
};
const asRecord = (value: unknown): Record<string, unknown> | null => {
if (value && typeof value === 'object') {
return value as Record<string, unknown>;
}
return null;
};
const PayloadDetails: React.FC<{ packet: Packet }> = ({ packet }) => {
const payload = packet.decodedPayload as Payload | undefined;
const payloadObj = asRecord(payload);
if (typeof payload === 'undefined' || !payload || !payloadObj) {
return (
<Card body className="data-table-card">
<h6 className="mb-2">Payload</h6>
<div>Unable to decode payload; showing raw bytes only.</div>
<code>{bytesToHex(packet.payload)}</code>
</Card>
);
}
if (typeof payloadObj?.flags === 'number' && payloadObj?.data instanceof Uint8Array) {
return (
<Card body className="data-table-card">
<h6 className="mb-2">Control</h6>
<HeaderFact label="Flags" value={`0x${payloadObj.flags.toString(16)}`} />
<HeaderFact label="Data Length" value={payloadObj.data.length} />
<HeaderFact label="Data" value={<code>{bytesToHex(payloadObj.data)}</code>} />
</Card>
);
}
if (packet.payloadType === PayloadType.GROUP_TEXT && typeof packet.decrypted !== 'undefined') {
const payload = packet.decodedPayload as GroupTextPayload;
const decrypted = packet.decrypted as DecryptedGroupMessage;
return (
<Card body className="data-table-card">
<h6 className="mb-2">{payloadNameByValue[packet.payloadType] ?? packet.payloadType} (Encrypted)</h6>
<HeaderFact label="Channel Hash" value={<><span className="meshcore-hash">{payload.channelHash}</span>{decrypted.group ? ` (${decrypted.group})` : ''}</>} />
<HeaderFact label="Timestamp" value={decrypted.timestamp.toLocaleString()} />
<HeaderFact label="Message" value={decrypted.message} />
</Card>
);
}
if (
typeof payloadObj.channelHash === 'string'
&& payloadObj.cipherText instanceof Uint8Array
&& payloadObj.cipherMAC instanceof Uint8Array
) {
return (
<Card body className="data-table-card">
<h6 className="mb-2">{payloadNameByValue[packet.payloadType] ?? packet.payloadType} (Encrypted)</h6>
<HeaderFact label="Channel Hash" value={payloadObj.channelHash} />
<HeaderFact label="Cipher Text" value={payloadObj.cipherText.length + ' bytes'} />
<HeaderFact label="Cipher MAC" value={<code>{bytesToHex(payloadObj.cipherMAC)}</code>} />
</Card>
);
}
if (
typeof payloadObj.dstHash === 'string'
&& typeof payloadObj.srcHash === 'string'
&& payloadObj.cipherText instanceof Uint8Array
&& payloadObj.cipherMAC instanceof Uint8Array
) {
return (
<Card body className="data-table-card">
<h6 className="mb-2">{payloadNameByValue[packet.payloadType] ?? packet.payloadType} (Encrypted)</h6>
<HeaderFact label="Destination" value={<span className="meshcore-hash">{payloadObj.dstHash}</span>} />
<HeaderFact label="Source" value={<span className="meshcore-hash">{payloadObj.srcHash}</span>} />
<HeaderFact label="Cipher Text Length" value={payloadObj.cipherText.length} />
<HeaderFact label="Cipher MAC" value={<code>{bytesToHex(payloadObj.cipherMAC)}</code>} />
</Card>
);
}
if (payloadObj.data instanceof Uint8Array) {
return (
<Card body className="data-table-card">
<h6 className="mb-2">{payloadNameByValue[packet.payloadType] ?? packet.payloadType} (Raw)</h6>
<HeaderFact label="Data Length" value={payloadObj.data.length} />
<HeaderFact label="Data" value={<code>{bytesToHex(payloadObj.data)}</code>} />
</Card>
);
}
if (packet.payloadType === PayloadType.ADVERT) {
const advert = payload as AdvertPayload;
return (
<Card body className="data-table-card">
<h6 className="mb-2">Advert Payload</h6>
<HeaderFact label="Public Key" value={<code>{bytesToHex(advert.publicKey)}</code>} />
<HeaderFact label="Signature" value={advert.signature.length + ' Bytes'} />
{advert.timestamp && (
<HeaderFact label="Time" value={<code>{advert.timestamp.toISOString()}</code>} />
)}
{advert.appdata?.name && (
<HeaderFact label="Name" value={advert.appdata.name} />
)}
{advert.appdata?.latitude && (
<HeaderFact label="Latitude" value={advert.appdata.latitude} />
)}
{advert.appdata?.longitude && (
<HeaderFact label="Longitude" value={advert.appdata.longitude} />
)}
</Card>
);
}
if (packet.payloadType === PayloadType.ANON_REQ) {
const anonReq = payload as AnonReqPayload;
return (
<Card body className="data-table-card">
<h6 className="mb-2">Anonymous Request</h6>
<HeaderFact label="Destination" value={anonReq.dstHash} />
<HeaderFact label="Public Key" value={<code>{bytesToHex(anonReq.publicKey)}</code>} />
</Card>
);
}
return (
<Card body className="data-table-card">
<h6 className="mb-2">Payload</h6>
<code>{JSON.stringify(payloadObj)}</code>
</Card>
);
};
interface MeshCorePacketDetailsPaneProps {
packet: Packet | null;
streamReady: boolean;
}
const MeshCorePacketDetailsPane: React.FC<MeshCorePacketDetailsPaneProps> = ({ packet, streamReady }) => {
if (!packet) {
return (
<Card body className="data-table-card h-100">
<h6>Select a packet</h6>
<div>Click any hash in the table to inspect MeshCore header and payload details.</div>
<div className="mt-2 text-secondary">Stream prepared: {streamReady ? 'yes' : 'no'}</div>
</Card>
);
}
return (
<Stack gap={2} className="h-100 meshcore-detail-stack">
{/*
<Card body className="data-table-card">
<Stack direction="horizontal" gap={2} className="mb-2">
<h6 className="mb-0">{payloadNameByValue[packet.payloadType] ?? packet.payloadType}</h6>
</Stack>
<HeaderFact label="Time" value={packet.timestamp.toISOString()} />
<HeaderFact label="Hash" value={<code>{packet.hash}</code>} />
{packet.snr !== undefined && <HeaderFact label="SNR" value={`${packet.snr.toFixed(1)} dB`} />}
{packet.rssi !== undefined && <HeaderFact label="RSSI" value={`${packet.rssi} dBm`} />}
{packet.radioName && <HeaderFact label="Radio" value={packet.radioName} />}
<HeaderFact label="Version" value={packet.version} />
<HeaderFact label="Payload Type" value={`${payloadNameByValue[packet.payloadType] ?? 'Unknown'} (${packet.payloadType})`} />
<HeaderFact label="Route Type" value={routeDisplayByValue[packet.routeType] ?? packet.routeType} />
<HeaderFact label="Raw Length" value={`${packet.raw.length} bytes`} />
<HeaderFact label="Path" value={<code>{bytesToHex(packet.path)}</code>} />
<HeaderFact label="Raw Packet" value={<code>{bytesToHex(packet.raw)}</code>} />
</Card>
*/}
<PayloadDetails packet={packet} />
<Card body className="data-table-card">
<PacketDissectionViewer
rawPacket={packet.toBytes()}
segments={packet.segments || buildMeshCoreSegments(packet)}
title={`${payloadNameByValue[packet.payloadType] ?? packet.payloadType} Packet Dissection`}
/>
</Card>
<Card body className="data-table-card">
<h6 className="mb-2">Stream Preparation</h6>
<div>MeshCore stream service is initialized and ready for topic subscriptions.</div>
<div className="text-secondary">Ready: {streamReady ? 'yes' : 'no'}</div>
</Card>
</Stack>
);
};
export default MeshCorePacketDetailsPane;