import React, { useEffect, useMemo, useState } from 'react'; 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'; 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 type { MeshCoreMessage } from '../../services/MeshCoreStream'; import API from '../../services/API'; import MeshCoreService from '../../services/MeshCoreService'; import MeshCoreStream from '../../services/MeshCoreStream'; import { MeshCoreDataContext, type MeshCoreDataContextValue, type MeshCoreGroupChatRecord, type MeshCoreNodePoint, } from './MeshCoreContext'; export { MeshCoreDataContext, useMeshCoreData, type MeshCoreDataContextValue, type MeshCoreGroupChatRecord, type MeshCoreNodePoint, } from './MeshCoreContext'; export const payloadNameByValue: Record = { [PayloadType.REQUEST]: 'Request', [PayloadType.RESPONSE]: 'Response', [PayloadType.TEXT]: 'Private Message', [PayloadType.ACK]: 'Acknowledgement', [PayloadType.ADVERT]: 'Advertisement', [PayloadType.GROUP_TEXT]: 'Group Message', [PayloadType.GROUP_DATA]: 'Group Data', [PayloadType.ANON_REQ]: 'Anonymous Request', [PayloadType.PATH]: 'Path', [PayloadType.TRACE]: 'Trace', [PayloadType.MULTIPART]: 'Multipart', [PayloadType.CONTROL]: 'Control', [PayloadType.RAW_CUSTOM]: 'Custom', }; export const nodeTypeNameByValue: Record = { [NodeType.TYPE_UNKNOWN]: 'Unknown', [NodeType.TYPE_CHAT_NODE]: 'Chat Node', [NodeType.TYPE_REPEATER]: 'Repeater', [NodeType.TYPE_ROOM_SERVER]: 'Room Server', [NodeType.TYPE_SENSOR]: 'Sensor', }; export const routeTypeNameByValue: Record = { [RouteType.TRANSPORT_FLOOD]: 'Transport Flood', [RouteType.FLOOD]: 'Flood', [RouteType.DIRECT]: 'Direct', [RouteType.TRANSPORT_DIRECT]: 'Transport Direct', }; export const RouteTypeIcon: React.FC<{ routeType: number }> = ({ routeType }) => { switch (routeType) { case RouteType.TRANSPORT_FLOOD: return ; case RouteType.FLOOD: return ; case RouteType.DIRECT: return ; case RouteType.TRANSPORT_DIRECT: return ; default: return ; } } export const payloadValueByName = Object.fromEntries( Object.entries(PayloadType).map(([name, value]) => [name, value]) ) as Record; export const PayloadTypeIcon: React.FC<{ payloadType: number }> = ({ payloadType }) => { switch (payloadType) { case PayloadType.REQUEST: return ; case PayloadType.RESPONSE: return ; case PayloadType.TEXT: return ; case PayloadType.ACK: return ; case PayloadType.ADVERT: return ; case PayloadType.GROUP_TEXT: return ; case PayloadType.GROUP_DATA: return ; case PayloadType.ANON_REQ: return ; case PayloadType.PATH: return ; case PayloadType.TRACE: return ; case PayloadType.MULTIPART: return ; case PayloadType.CONTROL: return ; case PayloadType.RAW_CUSTOM: return ; default: return ; } }; export const nodeTypeValueByName = Object.fromEntries( Object.entries(NodeType).map(([name, value]) => [name, value]) ) as Record; export const routeTypeValueByName = Object.fromEntries( Object.entries(RouteType).map(([name, value]) => [name, value]) ) as Record; // Human-readable display names (for UI) export const nodeTypeDisplayByValue: Record = { [NodeType.TYPE_UNKNOWN]: 'Unknown', [NodeType.TYPE_CHAT_NODE]: 'Chat Node', [NodeType.TYPE_REPEATER]: 'Repeater', [NodeType.TYPE_ROOM_SERVER]: 'Room Server', [NodeType.TYPE_SENSOR]: 'Sensor', }; export const payloadDisplayByValue: Record = Object.fromEntries( Object.entries(PayloadType).map(([name, value]) => { const cleanName = name.replace(/^PAYLOAD_TYPE_/, '').replace(/_/g, ' '); return [value, cleanName.charAt(0) + cleanName.slice(1).toLowerCase()]; }) ) as Record; export const routeDisplayByValue: Record = Object.fromEntries( Object.entries(RouteType).map(([name, value]) => { const cleanName = name.replace(/^ROUTE_TYPE_/, '').replace(/_/g, ' '); return [value, cleanName.charAt(0) + cleanName.slice(1).toLowerCase()]; }) ) as Record; // URL-friendly names (lowercase, no TYPE_ prefix) export const nodeTypeUrlByValue: Record = { [NodeType.TYPE_UNKNOWN]: 'unknown', [NodeType.TYPE_CHAT_NODE]: 'chat_node', [NodeType.TYPE_REPEATER]: 'repeater', [NodeType.TYPE_ROOM_SERVER]: 'room_server', [NodeType.TYPE_SENSOR]: 'sensor', }; export const nodeTypeValueByUrl: Record = { 'unknown': NodeType.TYPE_UNKNOWN, 'chat_node': NodeType.TYPE_CHAT_NODE, 'repeater': NodeType.TYPE_REPEATER, 'room_server': NodeType.TYPE_ROOM_SERVER, 'sensor': NodeType.TYPE_SENSOR, }; export const payloadUrlByValue: Record = Object.fromEntries( Object.entries(PayloadType).map(([name, value]) => { const urlName = name.replace(/^PAYLOAD_TYPE_/, '').toLowerCase(); return [value, urlName]; }) ) as Record; export const payloadValueByUrl: Record = Object.fromEntries( Object.entries(PayloadType).map(([name, value]) => { const urlName = name.replace(/^PAYLOAD_TYPE_/, '').toLowerCase(); return [urlName, value]; }) ) as Record; export const routeUrlByValue: Record = Object.fromEntries( Object.entries(RouteType).map(([name, value]) => { const urlName = name.replace(/^ROUTE_TYPE_/, '').toLowerCase(); return [value, urlName]; }) ) as Record; export const routeValueByUrl: Record = Object.fromEntries( Object.entries(RouteType).map(([name, value]) => { const urlName = name.replace(/^ROUTE_TYPE_/, '').toLowerCase(); return [urlName, value]; }) ) as Record; const DISCARD_DUPLICATE_PATH_PACKETS = true; const dedupeByHashAndPath = (packets: Packet[]): Packet[] => { if (!DISCARD_DUPLICATE_PATH_PACKETS) { return packets; } const sortedByReceiveTime = [...packets].sort((a, b) => a.receivedAt.getTime() - b.receivedAt.getTime()); const seen = new Set(); const deduped: Packet[] = []; sortedByReceiveTime.forEach((packet) => { const hash = packet.hash(); if (seen.has(hash)) { return; } seen.add(hash); deduped.push(packet); }); return deduped; }; // `payloadSummary` and its summarization logic were removed — summaries are rendered // directly in the packet rows now. Keep path/key helpers above. 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; 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.length > 0 ? packet.path[0].toString(16).padStart(2, '0') : 'unknown')) as string; return { hash: packet.hash, timestamp: packet.timestamp, channel, sender, 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: Packet[]): MeshCoreNodePoint[] => { const byNode = new Map(); packets.forEach((packet) => { if (!packet.path || packet.path.length === 0) { return; } const nodeId = packet.path[0].toString(16).padStart(2, '0'); const existing = byNode.get(nodeId); if (existing) { existing.packetCount += 1; return; } const byte = packet.path[0]; byNode.set(nodeId, { nodeId, nodeType: packet.nodeType, packetCount: 1, latitude: 52.0 + ((byte % 20) - 10) * 0.02, longitude: 5.0 + (((byte >> 1) % 20) - 10) * 0.03, }); }); return Array.from(byNode.values()); }; export const MeshCoreDataProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [packets, setPackets] = useState([]); const [streamReady, setStreamReady] = useState(false); const stream = useMemo(() => new MeshCoreStream(false), []); 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(undefined, undefined, undefined, stream.keyManager); if (!isMounted) { return; } setPackets((prev) => { const merged = dedupeByHashAndPath([...fetchedPackets, ...prev]); return merged .sort((a, b) => b.receivedAt.getTime() - a.receivedAt.getTime()) .slice(0, 500); }); } catch (error) { console.error('Failed to fetch MeshCore packets:', error); } }; fetchPackets(); stream.connect(); const unsubscribeState = stream.subscribeToState((state) => { setStreamReady(state.isConnected); }); const unsubscribePackets = stream.subscribe( 'meshcore/packet/#', (message) => { setPackets((prev) => { const packet: Packet = { timestamp: message.receivedAt, hash: message.hash, nodeType: 0, // Default; would be extracted from payload if needed payloadType: 0, routeType: 0, version: 1, path: new Uint8Array(), raw: message.raw, decodedPayload: message.decodedPayload, radioName: message.radioName, snr: message.snr, rssi: message.rssi, }; // Extract details from raw packet try { const p = new Packet(); p.parse(message.raw); packet.version = (message.raw[0] >> 6) & 0x03; packet.payloadType = (message.raw[0] >> 2) & 0x0f; packet.routeType = message.raw[0] & 0x03; const pathLength = message.raw[1] & 0x3f; packet.path = message.raw.slice(2, 2 + pathLength); // 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); } const merged = dedupeByHashAndPath([packet, ...prev]); return merged .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) .slice(0, 500); }); } ); return () => { isMounted = false; unsubscribeState(); unsubscribePackets(); stream.disconnect(); }; }, [stream, meshCoreService]); const groupChats = useMemo(() => toGroupChats(packets), [packets]); const mapPoints = useMemo(() => toMapPoints(packets), [packets]); const value = useMemo(() => { return { packets, groupChats, mapPoints, streamReady, }; }, [packets, groupChats, mapPoints, streamReady]); return {children}; };