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 }) => (
{label}
{value}
);
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 | null => {
if (value && typeof value === 'object') {
return value as Record;
}
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 (
Payload
Unable to decode payload; showing raw bytes only.
{bytesToHex(packet.payload)}
);
}
if (typeof payloadObj?.flags === 'number' && payloadObj?.data instanceof Uint8Array) {
return (
Control
{bytesToHex(payloadObj.data)}} />
);
}
if (packet.payloadType === PayloadType.GROUP_TEXT && typeof packet.decrypted !== 'undefined') {
const payload = packet.decodedPayload as GroupTextPayload;
const decrypted = packet.decrypted as DecryptedGroupMessage;
return (
{payloadNameByValue[packet.payloadType] ?? packet.payloadType} (Encrypted)
{payload.channelHash}{decrypted.group ? ` (${decrypted.group})` : ''}>} />
);
}
if (
typeof payloadObj.channelHash === 'string'
&& payloadObj.cipherText instanceof Uint8Array
&& payloadObj.cipherMAC instanceof Uint8Array
) {
return (
{payloadNameByValue[packet.payloadType] ?? packet.payloadType} (Encrypted)
{bytesToHex(payloadObj.cipherMAC)}} />
);
}
if (
typeof payloadObj.dstHash === 'string'
&& typeof payloadObj.srcHash === 'string'
&& payloadObj.cipherText instanceof Uint8Array
&& payloadObj.cipherMAC instanceof Uint8Array
) {
return (
{payloadNameByValue[packet.payloadType] ?? packet.payloadType} (Encrypted)
{payloadObj.dstHash}} />
{payloadObj.srcHash}} />
{bytesToHex(payloadObj.cipherMAC)}} />
);
}
if (payloadObj.data instanceof Uint8Array) {
return (
{payloadNameByValue[packet.payloadType] ?? packet.payloadType} (Raw)
{bytesToHex(payloadObj.data)}} />
);
}
if (packet.payloadType === PayloadType.ADVERT) {
const advert = payload as AdvertPayload;
return (
Advert Payload
{bytesToHex(advert.publicKey)}} />
{advert.timestamp && (
{advert.timestamp.toISOString()}} />
)}
{advert.appdata?.name && (
)}
{advert.appdata?.latitude && (
)}
{advert.appdata?.longitude && (
)}
);
}
if (packet.payloadType === PayloadType.ANON_REQ) {
const anonReq = payload as AnonReqPayload;
return (
Anonymous Request
{bytesToHex(anonReq.publicKey)}} />
);
}
return (
Payload
{JSON.stringify(payloadObj)}
);
};
interface MeshCorePacketDetailsPaneProps {
packet: Packet | null;
streamReady: boolean;
}
const MeshCorePacketDetailsPane: React.FC = ({ packet, streamReady }) => {
if (!packet) {
return (
Select a packet
Click any hash in the table to inspect MeshCore header and payload details.
Stream prepared: {streamReady ? 'yes' : 'no'}
);
}
return (
{/*
{payloadNameByValue[packet.payloadType] ?? packet.payloadType}
{packet.hash}} />
{packet.snr !== undefined && }
{packet.rssi !== undefined && }
{packet.radioName && }
{bytesToHex(packet.path)}} />
{bytesToHex(packet.raw)}} />
*/}
Stream Preparation
MeshCore stream service is initialized and ready for topic subscriptions.
Ready: {streamReady ? 'yes' : 'no'}
);
};
export default MeshCorePacketDetailsPane;