Checkpoint

This commit is contained in:
2026-03-08 22:22:51 +01:00
parent 247c827291
commit 9053ec65a6
65 changed files with 5874 additions and 708 deletions

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useMemo, useState } from 'react';
import { bytesToHex } from '@noble/hashes/utils.js';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import BrandingWatermarkIcon from '@mui/icons-material/BrandingWatermark';
import LeakAddIcon from '@mui/icons-material/LeakAdd';
import PersonIcon from '@mui/icons-material/Person';
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
import ReplyIcon from '@mui/icons-material/Reply';
@@ -10,20 +10,18 @@ import RouteIcon from '@mui/icons-material/Route';
import SensorsIcon from '@mui/icons-material/Sensors';
import SignalCellularAltIcon from '@mui/icons-material/SignalCellularAlt';
import StorageIcon from '@mui/icons-material/Storage';
import WifiTetheringIcon from '@mui/icons-material/WifiTethering';
import { Packet } from '../../protocols/meshcore';
import { NodeType, PayloadType, RouteType } from '../../types/protocol/meshcore.types';
import { MeshCoreStream } from '../../services/MeshCoreStream';
import type { MeshCoreMessage } from '../../services/MeshCoreStream';
import type { Payload, AdvertPayload } from '../../types/protocol/meshcore.types';
import API from '../../services/API';
import MeshCoreServiceImpl from '../../services/MeshCoreService';
import { base64ToBytes } from '../../util';
import MeshCoreService from '../../services/MeshCoreService';
import MeshCoreStream from '../../services/MeshCoreStream';
import {
MeshCoreDataContext,
type MeshCoreDataContextValue,
type MeshCorePacketRecord,
type MeshCoreGroupChatRecord,
type MeshCoreNodePoint,
} from './MeshCoreContext';
@@ -32,7 +30,6 @@ export {
MeshCoreDataContext,
useMeshCoreData,
type MeshCoreDataContextValue,
type MeshCorePacketRecord,
type MeshCoreGroupChatRecord,
type MeshCoreNodePoint,
} from './MeshCoreContext';
@@ -68,6 +65,21 @@ export const routeTypeNameByValue: Record<number, string> = {
[RouteType.TRANSPORT_DIRECT]: 'Transport Direct',
};
export const RouteTypeIcon: React.FC<{ routeType: number }> = ({ routeType }) => {
switch (routeType) {
case RouteType.TRANSPORT_FLOOD:
return <LeakAddIcon className="meshcore-route-icon meshcore-route-flood" titleAccess={routeTypeNameByValue[routeType]} />;
case RouteType.FLOOD:
return <LeakAddIcon className="meshcore-route-icon meshcore-route-flood" titleAccess={routeTypeNameByValue[routeType]} />;
case RouteType.DIRECT:
return <WifiTetheringIcon className="meshcore-route-icon meshcore-route-direct" titleAccess={routeTypeNameByValue[routeType]} />;
case RouteType.TRANSPORT_DIRECT:
return <WifiTetheringIcon className="meshcore-route-icon meshcore-route-direct" titleAccess={routeTypeNameByValue[routeType]} />;
default:
return <QuestionMarkIcon className="meshcore-route-icon" titleAccess="Unknown" />;
}
}
export const payloadValueByName = Object.fromEntries(
Object.entries(PayloadType).map(([name, value]) => [name, value])
) as Record<string, number>;
@@ -104,6 +116,7 @@ export const PayloadTypeIcon: React.FC<{ payloadType: number }> = ({ payloadType
return <QuestionMarkIcon className="meshcore-payload-icon" titleAccess="Unknown" />;
}
};
export const nodeTypeValueByName = Object.fromEntries(
Object.entries(NodeType).map(([name, value]) => [name, value])
) as Record<string, number>;
@@ -182,122 +195,59 @@ export const routeValueByUrl: Record<string, number> = Object.fromEntries(
const DISCARD_DUPLICATE_PATH_PACKETS = true;
const getPacketPathKey = (raw: Uint8Array): string => {
if (raw.length < 2) {
return '';
}
const pathField = raw[1];
const hashSize = (pathField >> 6) + 1;
const hashCount = pathField & 0x3f;
if (hashCount === 0 || hashSize === 4) {
return '';
}
const pathByteLength = hashCount * hashSize;
const availablePathBytes = Math.min(pathByteLength, Math.max(raw.length - 2, 0));
if (availablePathBytes <= 0) {
return '';
}
return bytesToHex(raw.slice(2, 2 + availablePathBytes));
};
const dedupeByHashAndPath = (packets: MeshCorePacketRecord[]): MeshCorePacketRecord[] => {
const dedupeByHashAndPath = (packets: Packet[]): Packet[] => {
if (!DISCARD_DUPLICATE_PATH_PACKETS) {
return packets;
}
const sortedByReceiveTime = [...packets].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
const sortedByReceiveTime = [...packets].sort((a, b) => a.receivedAt.getTime() - b.receivedAt.getTime());
const seen = new Set<string>();
const deduped: MeshCorePacketRecord[] = [];
const deduped: Packet[] = [];
sortedByReceiveTime.forEach((packet) => {
const signature = `${packet.hash}:${getPacketPathKey(packet.raw)}`;
if (seen.has(signature)) {
const hash = packet.hash();
if (seen.has(hash)) {
return;
}
seen.add(signature);
seen.add(hash);
deduped.push(packet);
});
return deduped;
};
const summarizePayload = (payloadType: number, decodedPayload: Payload | undefined, payloadBytes: Uint8Array): string => {
switch (payloadType) {
case PayloadType.REQUEST:
return `request len=${payloadBytes.length}`;
case PayloadType.RESPONSE:
return `response len=${payloadBytes.length}`;
case PayloadType.TEXT:
if (decodedPayload && 'dstHash' in decodedPayload && 'srcHash' in decodedPayload) {
return `text ${decodedPayload.srcHash}->${decodedPayload.dstHash}`;
}
return 'text message';
case PayloadType.ACK:
return 'acknowledgement';
case PayloadType.ADVERT:
const advert = decodedPayload as AdvertPayload;
console.log('advert', advert);
if (advert && decodedPayload && 'appdata' in decodedPayload && advert.appdata && 'name' in advert.appdata) {
return `advertisement: ${advert.appdata.name}`;
}
return 'advertisement';
case PayloadType.GROUP_TEXT:
if (decodedPayload && 'channelHash' in decodedPayload) {
return `group channel=${decodedPayload.channelHash}`;
}
return `group text len=${payloadBytes.length}`;
case PayloadType.GROUP_DATA:
return `group data len=${payloadBytes.length}`;
case PayloadType.ANON_REQ:
if (decodedPayload && 'dstHash' in decodedPayload) {
return `anon req dst=${decodedPayload.dstHash}`;
}
return 'anon request';
case PayloadType.PATH:
return `path len=${payloadBytes.length}`;
case PayloadType.TRACE:
return `trace len=${payloadBytes.length}`;
case PayloadType.MULTIPART:
return `multipart len=${payloadBytes.length}`;
case PayloadType.CONTROL:
if (decodedPayload && 'flags' in decodedPayload && typeof decodedPayload.flags === 'number') {
return `control flags=0x${decodedPayload.flags.toString(16)}`;
}
return `control raw=${bytesToHex(payloadBytes.slice(0, 4))}`;
case PayloadType.RAW_CUSTOM:
default:
return `raw=${bytesToHex(payloadBytes.slice(0, Math.min(6, payloadBytes.length)))}`;
}
};
// `payloadSummary` and its summarization logic were removed — summaries are rendered
// directly in the packet rows now. Keep path/key helpers above.
const toGroupChats = (packets: MeshCorePacketRecord[]): MeshCoreGroupChatRecord[] => {
const toGroupChats = (packets: Packet[]): MeshCoreGroupChatRecord[] => {
return packets
.filter((packet) => packet.payloadType === PayloadType.GROUP_TEXT || packet.payloadType === PayloadType.TEXT)
.map((packet, index) => {
const payload = packet.decodedPayload as Record<string, unknown> | undefined;
const payload = packet.decodedPayload as Record<string, unknown>;
const channel = (payload && typeof payload === 'object' && 'channelHash' in payload
? (payload.channelHash as string)
: 'general') as string;
const sender = (payload && typeof payload === 'object' && 'srcHash' in payload
? (payload.srcHash as string)
: packet.path[0].toString(16).padStart(2, '0')) as string;
: (packet.path.length > 0 ? packet.path[0].toString(16).padStart(2, '0') : 'unknown')) as string;
return {
hash: packet.hash,
timestamp: packet.timestamp,
channel,
sender,
message: `Mock message #${index + 1} (${packet.payloadSummary})`,
message: (() => {
const msg = (payload && typeof payload === 'object')
? ('message' in payload ? String((payload as any).message) : ('text' in payload ? String((payload as any).text) : undefined))
: undefined;
return msg ?? `Mock message #${index + 1}`;
})(),
};
});
};
const toMapPoints = (packets: MeshCorePacketRecord[]): MeshCoreNodePoint[] => {
const toMapPoints = (packets: Packet[]): MeshCoreNodePoint[] => {
const byNode = new Map<string, MeshCoreNodePoint>();
packets.forEach((packet) => {
@@ -327,57 +277,47 @@ const toMapPoints = (packets: MeshCorePacketRecord[]): MeshCoreNodePoint[] => {
};
export const MeshCoreDataProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [packets, setPackets] = useState<MeshCorePacketRecord[]>([]);
const [packets, setPackets] = useState<Packet[]>([]);
const [streamReady, setStreamReady] = useState(false);
const stream = useMemo(() => new MeshCoreStream(false), []);
const meshCoreService = useMemo(() => new MeshCoreServiceImpl(API), []);
const meshCoreService = useMemo(() => new MeshCoreService(API), []);
// Populate stream key manager with known groups so live decryption works
useEffect(() => {
let mounted = true;
(async () => {
try {
const groups = await meshCoreService.fetchGroups();
if (!mounted) return;
for (const g of groups) {
try {
stream.keyManager.addGroup(g.name, g.secret.toBytes());
} catch (e) {
// ignore
}
}
} catch (e) {
// ignore
}
})();
return () => { mounted = false; };
}, [meshCoreService, stream]);
useEffect(() => {
let isMounted = true;
const fetchPackets = async () => {
try {
const fetchedPackets = await meshCoreService.fetchPackets();
const fetchedPackets = await meshCoreService.fetchPackets(undefined, undefined, undefined, stream.keyManager);
if (!isMounted) {
return;
}
const records: MeshCorePacketRecord[] = fetchedPackets.map((packet) => {
const raw = base64ToBytes(packet.raw);
let decodedPayload: Payload | undefined;
try {
const p = new Packet();
p.parse(raw);
decodedPayload = p.decode();
} catch {
decodedPayload = undefined;
}
const pathLength = raw[1] & 0x3f;
const payloadBytes = raw.slice(2 + pathLength);
return {
timestamp: new Date(packet.received_at),
hash: packet.hash,
nodeType: packet.payload_type === PayloadType.ADVERT ? NodeType.TYPE_UNKNOWN : 0,
payloadType: packet.payload_type,
routeType: packet.route_type,
version: packet.version,
path: raw.slice(2, 2 + pathLength),
raw,
decodedPayload,
payloadSummary: summarizePayload(packet.payload_type, decodedPayload, payloadBytes),
snr: packet.snr,
rssi: packet.rssi,
};
});
setPackets((prev) => {
const merged = dedupeByHashAndPath([...records, ...prev]);
const merged = dedupeByHashAndPath([...fetchedPackets, ...prev]);
return merged
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
.sort((a, b) => b.receivedAt.getTime() - a.receivedAt.getTime())
.slice(0, 500);
});
} catch (error) {
@@ -396,7 +336,7 @@ export const MeshCoreDataProvider: React.FC<{ children: React.ReactNode }> = ({
'meshcore/packet/#',
(message) => {
setPackets((prev) => {
const packet: MeshCorePacketRecord = {
const packet: Packet = {
timestamp: message.receivedAt,
hash: message.hash,
nodeType: 0, // Default; would be extracted from payload if needed
@@ -406,7 +346,6 @@ export const MeshCoreDataProvider: React.FC<{ children: React.ReactNode }> = ({
path: new Uint8Array(),
raw: message.raw,
decodedPayload: message.decodedPayload,
payloadSummary: '',
radioName: message.radioName,
snr: message.snr,
rssi: message.rssi,
@@ -423,12 +362,12 @@ export const MeshCoreDataProvider: React.FC<{ children: React.ReactNode }> = ({
const pathLength = message.raw[1] & 0x3f;
packet.path = message.raw.slice(2, 2 + pathLength);
// Summarize payload
const payloadBytes = message.raw.slice(2 + pathLength);
packet.payloadSummary = summarizePayload(packet.payloadType, message.decodedPayload, payloadBytes);
// If the stream provided a decrypted group message, attach it to the record
if (message.decryptedGroup) {
packet.decryptedGroup = message.decryptedGroup;
}
} catch (error) {
console.error('Failed to parse packet:', error);
packet.payloadSummary = `raw=${bytesToHex(packet.raw.slice(0, Math.min(6, packet.raw.length)))}`;
}
const merged = dedupeByHashAndPath([packet, ...prev]);