feat: implement duplicate packet recording for UI visibility in RepeaterHandler

- Added record_duplicate method to RepeaterHandler to log known duplicate packets without forwarding.
- Enhanced RepeaterDaemon to subscribe to raw packets for deduplication logging, ensuring all path variants are visible in the UI.
- Updated recent_packets management to group duplicates under their original packets for better tracking.
This commit is contained in:
agessaman
2026-03-23 17:02:29 -07:00
parent 369b420ae3
commit 744826199e
2 changed files with 75 additions and 0 deletions
+56
View File
@@ -458,6 +458,62 @@ class RepeaterHandler(BaseHandler):
if len(self.recent_packets) > self.max_recent_packets:
self.recent_packets.pop(0)
def record_duplicate(self, packet: Packet, rssi: int = 0, snr: float = 0.0) -> None:
"""Record a known-duplicate packet for UI/storage visibility without forwarding.
Called by the raw_packet_subscriber path so that path variants blocked
by the Dispatcher's payload-based dedup still appear in the UI.
"""
self.rx_count += 1
route_type = packet.header & PH_ROUTE_MASK
if route_type in (ROUTE_TYPE_FLOOD, ROUTE_TYPE_TRANSPORT_FLOOD):
self.recv_flood_count += 1
self.flood_dup_count += 1
elif route_type in (ROUTE_TYPE_DIRECT, ROUTE_TYPE_TRANSPORT_DIRECT):
self.recv_direct_count += 1
self.direct_dup_count += 1
header_info = PacketHeaderUtils.parse_header(packet.header)
payload_type = header_info["payload_type"]
route_type_parsed = header_info["route_type"]
original_path_hashes = packet.get_path_hashes_hex()
path_hash_size = packet.get_path_hash_size()
path_hash = self._path_hash_display(original_path_hashes)
src_hash, dst_hash = self._packet_record_src_dst(packet, payload_type)
packet_record = self._build_packet_record(
packet, payload_type, route_type_parsed, rssi, snr,
original_path_hashes, path_hash_size, path_hash,
src_hash, dst_hash,
transmitted=False,
drop_reason="Duplicate",
is_duplicate=True,
)
if self.storage:
try:
self.storage.record_packet(packet_record, skip_letsmesh_if_invalid=False)
except Exception as e:
logger.error(f"Failed to store duplicate record: {e}")
# Group under original in recent_packets
if len(self.recent_packets) > 0:
for idx in range(len(self.recent_packets) - 1, -1, -1):
prev_pkt = self.recent_packets[idx]
if prev_pkt.get("packet_hash") == packet_record["packet_hash"]:
if "duplicates" not in prev_pkt:
prev_pkt["duplicates"] = []
prev_pkt["duplicates"].append(packet_record)
break
else:
self.recent_packets.append(packet_record)
else:
self.recent_packets.append(packet_record)
if len(self.recent_packets) > self.max_recent_packets:
self.recent_packets.pop(0)
def cleanup_cache(self):
now = time.time()
+19
View File
@@ -306,6 +306,10 @@ class RepeaterDaemon:
n,
)
# Subscribe to parsed packets (pre-dedup) so duplicate path variants
# still appear in the web UI even though the Dispatcher blocks them.
self.dispatcher.add_raw_packet_subscriber(self._on_raw_packet_for_dedup_logging)
# When trace reaches final node, push PUSH_CODE_TRACE_DATA (0x89) to companion clients (firmware onTraceRecv)
self.trace_helper.on_trace_complete = self._on_trace_complete_for_companions
@@ -717,6 +721,21 @@ class RepeaterDaemon:
except Exception as e:
logger.debug("Push RX raw to companion: %s", e)
def _on_raw_packet_for_dedup_logging(self, pkt, data: bytes, analysis: dict) -> None:
"""Record duplicate packets for UI visibility.
Called by Dispatcher's raw_packet_subscriber (pre-dedup) so we see
all path variants. Only records packets the engine has already seen;
novel packets are left for the normal handler path.
"""
if not self.repeater_handler:
return
if not self.repeater_handler.is_duplicate(pkt):
return # First variant — will reach engine via normal handler path
rssi = getattr(pkt, "_rssi", 0) or 0
snr = getattr(pkt, "_snr", 0.0) or 0.0
self.repeater_handler.record_duplicate(pkt, rssi=rssi, snr=snr)
async def deliver_control_data(
self,
snr: float,