Add packet recording for injection-only types in RepeaterHandler

- Introduced `record_packet_only` method in `RepeaterHandler` to log packets for UI/storage without forwarding or duplicate checks, specifically for injection-only types like ANON_REQ and ACK.
- Updated `PacketRouter` to call `_record_for_ui` method, ensuring that relevant metadata is recorded for packets processed by various handlers.
- Enhanced handling of packet metadata, including RSSI and SNR values, to improve the visibility of packet information in the web UI.
This commit is contained in:
agessaman
2026-03-08 17:23:20 -07:00
parent 062dabc46e
commit 4490c9bb8c
2 changed files with 109 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ from pymc_core.protocol import Packet
from pymc_core.protocol.constants import (
MAX_PATH_SIZE,
PAYLOAD_TYPE_ADVERT,
PAYLOAD_TYPE_ANON_REQ,
PH_ROUTE_MASK,
PH_TYPE_MASK,
PH_TYPE_SHIFT,
@@ -409,6 +410,87 @@ class RepeaterHandler(BaseHandler):
if len(self.recent_packets) > self.max_recent_packets:
self.recent_packets.pop(0)
def record_packet_only(self, packet: Packet, metadata: dict) -> None:
"""Record a packet for UI/storage without running forwarding or duplicate logic.
Used by the packet router for injection-only types (ANON_REQ, ACK, PATH, etc.)
so they still appear in the web UI.
"""
if not self.storage:
return
rssi = metadata.get("rssi", 0)
snr = metadata.get("snr", 0.0)
if not hasattr(packet, "header") or packet.header is None:
logger.debug("record_packet_only: packet missing header, skipping")
return
header_info = PacketHeaderUtils.parse_header(packet.header)
payload_type = header_info["payload_type"]
route_type = header_info["route_type"]
pkt_hash = packet.calculate_packet_hash().hex().upper()
original_path_hashes = packet.get_path_hashes_hex()
path_hash_size = packet.get_path_hash_size()
display_hashes = original_path_hashes
path_hash = None
if display_hashes:
display = display_hashes[:8]
if len(display_hashes) > 8:
display = list(display) + ["..."]
path_hash = "[" + ", ".join(display) + "]"
src_hash = None
dst_hash = None
if payload_type in [0x00, 0x01, 0x02, 0x08]:
if hasattr(packet, "payload") and packet.payload and len(packet.payload) >= 2:
dst_hash = f"{packet.payload[0]:02X}"
src_hash = f"{packet.payload[1]:02X}"
elif payload_type == PAYLOAD_TYPE_ADVERT:
if hasattr(packet, "payload") and packet.payload and len(packet.payload) >= 1:
src_hash = f"{packet.payload[0]:02X}"
elif payload_type == PAYLOAD_TYPE_ANON_REQ:
if hasattr(packet, "payload") and packet.payload and len(packet.payload) >= 1:
dst_hash = f"{packet.payload[0]:02X}"
packet_record = {
"timestamp": time.time(),
"header": f"0x{packet.header:02X}",
"payload": (
packet.payload.hex() if hasattr(packet, "payload") and packet.payload else None
),
"payload_length": (
len(packet.payload) if hasattr(packet, "payload") and packet.payload else 0
),
"type": payload_type,
"route": route_type,
"length": len(packet.payload or b""),
"rssi": rssi,
"snr": snr,
"score": self.calculate_packet_score(
snr, len(packet.payload or b""), self.radio_config["spreading_factor"]
),
"tx_delay_ms": 0.0,
"transmitted": False,
"is_duplicate": False,
"packet_hash": pkt_hash[:16],
"drop_reason": None,
"path_hash": path_hash,
"src_hash": src_hash,
"dst_hash": dst_hash,
"original_path": original_path_hashes or None,
"forwarded_path": None,
"path_hash_size": path_hash_size,
"raw_packet": packet.write_to().hex() if hasattr(packet, "write_to") else None,
"lbt_attempts": 0,
"lbt_backoff_delays_ms": None,
"lbt_channel_busy": False,
}
try:
self.storage.record_packet(packet_record, skip_letsmesh_if_invalid=False)
except Exception as e:
logger.error(f"Failed to store packet record (record_packet_only): {e}")
return
self.recent_packets.append(packet_record)
if len(self.recent_packets) > self.max_recent_packets:
self.recent_packets.pop(0)
self.rx_count += 1
def cleanup_cache(self):
now = time.time()

View File

@@ -69,6 +69,15 @@ class PacketRouter:
self._companion_delivered[key] = now + _COMPANION_DEDUPE_TTL_SEC
return True
def _record_for_ui(self, packet, metadata: dict) -> None:
"""Record an injection-only packet for the web UI (storage + recent_packets)."""
handler = getattr(self.daemon, "repeater_handler", None)
if handler and getattr(handler, "storage", None):
try:
handler.record_packet_only(packet, metadata)
except Exception as e:
logger.debug("Record for UI failed: %s", e)
async def enqueue(self, packet):
"""Add packet to router queue."""
await self.queue.put(packet)
@@ -124,6 +133,11 @@ class PacketRouter:
payload_type = packet.get_payload_type()
processed_by_injection = False
metadata = {
"rssi": getattr(packet, "rssi", 0),
"snr": getattr(packet, "snr", 0.0),
"timestamp": getattr(packet, "timestamp", 0),
}
# Route to specific handlers for parsing only
if payload_type == TraceHandler.payload_type():
@@ -132,6 +146,7 @@ class PacketRouter:
await self.daemon.trace_helper.process_trace_packet(packet)
# Skip engine processing for trace packets - they're handled by trace helper
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif payload_type == ControlHandler.payload_type():
# Process control/discovery packet
@@ -180,6 +195,8 @@ class PacketRouter:
else:
# Login request for remote repeater (we already TXed it via inject); don't treat as RX.
processed_by_injection = True
if processed_by_injection:
self._record_for_ui(packet, metadata)
elif payload_type == AckHandler.payload_type():
# ACK has no dest in payload (4-byte CRC only); deliver to all bridges so sender sees send_confirmed
@@ -190,6 +207,7 @@ class PacketRouter:
except Exception as e:
logger.debug(f"Companion bridge ACK error: {e}")
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif payload_type == TextMessageHandler.payload_type():
dest_hash = packet.payload[0] if packet.payload else None
@@ -197,6 +215,7 @@ class PacketRouter:
if dest_hash is not None and dest_hash in companion_bridges:
await companion_bridges[dest_hash].process_received_packet(packet)
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif self.daemon.text_helper:
handled = await self.daemon.text_helper.process_text_packet(packet)
if handled:
@@ -209,6 +228,7 @@ class PacketRouter:
if self._should_deliver_path_to_companions(packet):
await companion_bridges[dest_hash].process_received_packet(packet)
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif companion_bridges and self._should_deliver_path_to_companions(packet):
# Dest not in bridges: path-return with ephemeral dest (e.g. multi-hop login).
# Deliver to all bridges; each will try to decrypt and ignore if not relevant.
@@ -223,6 +243,7 @@ class PacketRouter:
len(companion_bridges),
)
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif self.daemon.path_helper:
await self.daemon.path_helper.process_path_packet(packet)
@@ -244,6 +265,7 @@ class PacketRouter:
except Exception as e:
logger.debug(f"Companion bridge RESPONSE error: {e}")
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif dest_hash == local_hash and companion_bridges:
# Response addressed to this repeater (e.g. path-based reply to first hop)
for bridge in companion_bridges.values():
@@ -257,6 +279,7 @@ class PacketRouter:
len(companion_bridges),
)
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif companion_bridges:
# Dest not in bridges and not local: likely ANON_REQ response (dest = ephemeral
# sender hash). Deliver to all bridges; each will try to decrypt and ignore if
@@ -272,6 +295,7 @@ class PacketRouter:
len(companion_bridges),
)
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif payload_type == ProtocolResponseHandler.payload_type():
# PAYLOAD_TYPE_PATH (0x08): protocol responses (telemetry, binary, etc.).
@@ -285,6 +309,7 @@ class PacketRouter:
logger.debug(f"Companion bridge RESPONSE error: {e}")
if companion_bridges:
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif payload_type == ProtocolRequestHandler.payload_type():
dest_hash = packet.payload[0] if packet.payload else None
@@ -292,10 +317,12 @@ class PacketRouter:
if dest_hash is not None and dest_hash in companion_bridges:
await companion_bridges[dest_hash].process_received_packet(packet)
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif self.daemon.protocol_request_helper:
handled = await self.daemon.protocol_request_helper.process_request_packet(packet)
if handled:
processed_by_injection = True
self._record_for_ui(packet, metadata)
elif payload_type == GroupTextHandler.payload_type():
# GRP_TXT: pass to all companions (they filter by channel); still forward