diff --git a/web/src/components/PacketList.tsx b/web/src/components/PacketList.tsx index a05a8fa..4327930 100644 --- a/web/src/components/PacketList.tsx +++ b/web/src/components/PacketList.tsx @@ -16,9 +16,6 @@ export const PacketList: React.FC = () => { const dispatch = useAppDispatch(); const [currentPage, setCurrentPage] = useState(1); - // A unique ID is attached to each rendered packet element - const [packetKeys, setPacketKeys] = useState>({}); - // Generate a reproducible hash code for a string const hashString = useCallback((str: string): string => { let hash = 0; @@ -31,42 +28,21 @@ export const PacketList: React.FC = () => { return Math.abs(hash).toString(36); }, []); - // Create a consistent, unique fingerprint for a packet - const createPacketFingerprint = useCallback((packet: Packet): string => { - // Combine multiple fields to maximize uniqueness - const parts = [ - packet.data.from?.toString() ?? 'unknown', - packet.data.id?.toString() ?? 'noid', - packet.data.portNum?.toString() ?? 'noport', - packet.info.channel ?? 'nochannel', - packet.info.userId ?? 'nouser', - // Include a hash of raw JSON data for extra uniqueness - hashString(JSON.stringify(packet)), - ]; - return parts.join('_'); + // Create a packet key using data.id and from address + // This should match the key generation logic in the reducer + const createPacketKey = useCallback((packet: Packet): string => { + if (packet.data.id !== undefined && packet.data.from !== undefined) { + // Use Meshtastic node ID format (! followed by lowercase hex) and packet ID + const nodeId = `!${packet.data.from.toString(16).toLowerCase()}`; + return `${nodeId}_${packet.data.id}`; + } else { + // Fallback to hash-based key if no ID or from (should be rare) + return `hash_${hashString(JSON.stringify(packet))}`; + } }, [hashString]); - // Update the packet keys whenever the packets change - useEffect(() => { - // Store new packet keys - const newKeys: Record = {...packetKeys}; - - // Assign keys to any packets that don't already have them - packets.forEach((packet) => { - const fingerprint = createPacketFingerprint(packet); - - // Only create new keys for packets we haven't seen before - if (!newKeys[fingerprint]) { - const randomPart = Math.random().toString(36).substring(2, 10); - newKeys[fingerprint] = `${fingerprint}_${randomPart}`; - } - }); - - // Only update state if we actually added new keys - if (Object.keys(newKeys).length !== Object.keys(packetKeys).length) { - setPacketKeys(newKeys); - } - }, [packets, createPacketFingerprint]); + // We don't need to track packet keys in state anymore since we use data.id + // and it's deterministic - removing this effect to prevent the infinite loop issue if (loading) { return ( @@ -103,8 +79,6 @@ export const PacketList: React.FC = () => { if (window.confirm("Are you sure you want to clear all packets?")) { dispatch(clearPackets()); setCurrentPage(1); - // Clear the packet keys too - setPacketKeys({}); } }; @@ -116,11 +90,9 @@ export const PacketList: React.FC = () => { } }; - // Get the key for a packet + // Get the key for a packet - directly use createPacketKey const getPacketKey = (packet: Packet, index: number): string => { - const fingerprint = createPacketFingerprint(packet); - // Fallback to index-based key if no fingerprint key exists - return packetKeys[fingerprint] || `packet_${index}_${Date.now()}`; + return createPacketKey(packet) || `fallback_${index}`; }; return ( diff --git a/web/src/components/packets/GenericPacket.tsx b/web/src/components/packets/GenericPacket.tsx index cd4c0ab..44187b6 100644 --- a/web/src/components/packets/GenericPacket.tsx +++ b/web/src/components/packets/GenericPacket.tsx @@ -46,7 +46,7 @@ export const GenericPacket: React.FC = ({ packet }) => { - From: {data.from || "Unknown"} + From: {data.from ? `!${data.from.toString(16).toLowerCase()}` : "Unknown"} diff --git a/web/src/components/packets/NodeInfoPacket.tsx b/web/src/components/packets/NodeInfoPacket.tsx index 9ff45c7..785769e 100644 --- a/web/src/components/packets/NodeInfoPacket.tsx +++ b/web/src/components/packets/NodeInfoPacket.tsx @@ -22,7 +22,7 @@ export const NodeInfoPacket: React.FC = ({ packet }) => { - From: {data.from || "Unknown"} + From: {data.from ? `!${data.from.toString(16).toLowerCase()}` : "Unknown"} diff --git a/web/src/components/packets/PositionPacket.tsx b/web/src/components/packets/PositionPacket.tsx index db95276..c2c9159 100644 --- a/web/src/components/packets/PositionPacket.tsx +++ b/web/src/components/packets/PositionPacket.tsx @@ -26,7 +26,7 @@ export const PositionPacket: React.FC = ({ packet }) => { - From: {data.from || "Unknown"} + From: {data.from ? `!${data.from.toString(16).toLowerCase()}` : "Unknown"} diff --git a/web/src/components/packets/TelemetryPacket.tsx b/web/src/components/packets/TelemetryPacket.tsx index b9662cb..aab31bb 100644 --- a/web/src/components/packets/TelemetryPacket.tsx +++ b/web/src/components/packets/TelemetryPacket.tsx @@ -49,7 +49,7 @@ export const TelemetryPacket: React.FC = ({ packet }) => { - From: {data.from || "Unknown"} + From: {data.from ? `!${data.from.toString(16).toLowerCase()}` : "Unknown"} diff --git a/web/src/components/packets/TextMessagePacket.tsx b/web/src/components/packets/TextMessagePacket.tsx index 818d8fe..2acd8a5 100644 --- a/web/src/components/packets/TextMessagePacket.tsx +++ b/web/src/components/packets/TextMessagePacket.tsx @@ -17,7 +17,7 @@ export const TextMessagePacket: React.FC = ({ packet }) - From: {data.from || "Unknown"} + From: {data.from ? `!${data.from.toString(16).toLowerCase()}` : "Unknown"} diff --git a/web/src/store/slices/packetSlice.ts b/web/src/store/slices/packetSlice.ts index b859978..f15465f 100644 --- a/web/src/store/slices/packetSlice.ts +++ b/web/src/store/slices/packetSlice.ts @@ -10,6 +10,7 @@ interface PacketState { error: string | null; streamPaused: boolean; bufferedPackets: Packet[]; // Holds packets received while paused + seenPackets: Record; // Tracks already seen packet IDs to prevent duplicates } const initialState: PacketState = { @@ -18,6 +19,7 @@ const initialState: PacketState = { error: null, streamPaused: false, bufferedPackets: [], + seenPackets: {}, // Empty object to track seen packets }; const packetSlice = createSlice({ @@ -29,8 +31,26 @@ const packetSlice = createSlice({ state.error = null; }, fetchPacketsSuccess(state, action: PayloadAction) { + // Track unique packets and update the seen packets object + const uniquePackets: Packet[] = []; + + action.payload.forEach(packet => { + if (packet.data.from !== undefined && packet.data.id !== undefined) { + const nodeId = `!${packet.data.from.toString(16).toLowerCase()}`; + const packetKey = `${nodeId}_${packet.data.id}`; + + if (!state.seenPackets[packetKey]) { + state.seenPackets[packetKey] = true; + uniquePackets.push(packet); + } + } else { + // If we don't have from or id (rare), just add the packet + uniquePackets.push(packet); + } + }); + // Limit initial load to MAX_PACKETS - state.packets = action.payload.slice(-MAX_PACKETS); + state.packets = uniquePackets.slice(-MAX_PACKETS); state.loading = false; }, fetchPacketsFailure(state, action: PayloadAction) { @@ -38,9 +58,29 @@ const packetSlice = createSlice({ state.loading = false; }, addPacket(state, action: PayloadAction) { + const packet = action.payload; + + // Skip packets without valid from/id + if (packet.data.from === undefined || packet.data.id === undefined) { + return; + } + + // Create a Meshtastic node ID format + const nodeId = `!${packet.data.from.toString(16).toLowerCase()}`; + const packetKey = `${nodeId}_${packet.data.id}`; + + // Check if we've already seen this packet + if (state.seenPackets[packetKey]) { + // Packet is a duplicate, ignore it + return; + } + + // Mark this packet as seen + state.seenPackets[packetKey] = true; + if (state.streamPaused) { // When paused, add to buffer instead of main list - state.bufferedPackets.unshift(action.payload); + state.bufferedPackets.unshift(packet); // Ensure buffer doesn't grow too large if (state.bufferedPackets.length > MAX_PACKETS) { @@ -48,7 +88,7 @@ const packetSlice = createSlice({ } } else { // Normal flow - add to main list - state.packets.unshift(action.payload); + state.packets.unshift(packet); // Remove oldest packets if we exceed the limit if (state.packets.length > MAX_PACKETS) { @@ -59,6 +99,7 @@ const packetSlice = createSlice({ clearPackets(state) { state.packets = []; state.bufferedPackets = []; + state.seenPackets = {}; // Reset the seen packets object }, toggleStreamPause(state) { state.streamPaused = !state.streamPaused;