diff --git a/repeater/handler_helpers/advert.py b/repeater/handler_helpers/advert.py index e69de29..3a6fc91 100644 --- a/repeater/handler_helpers/advert.py +++ b/repeater/handler_helpers/advert.py @@ -0,0 +1,131 @@ +""" +Advertisement packet handling helper for pyMC Repeater. + +This module processes advertisement packets for neighbor tracking and discovery. +""" + +import asyncio +import logging +import time + +from pymc_core.node.handlers.advert import AdvertHandler + +logger = logging.getLogger("AdvertHelper") + + +class AdvertHelper: + """Helper class for processing advertisement packets in the repeater.""" + + def __init__(self, local_identity, storage, log_fn=None): + """ + Initialize the advert helper. + + Args: + local_identity: The LocalIdentity instance for this repeater + storage: StorageCollector instance for persisting advert data + log_fn: Optional logging function for AdvertHandler + """ + self.local_identity = local_identity + self.storage = storage + + # Create AdvertHandler internally as a parsing utility + self.advert_handler = AdvertHandler(log_fn=log_fn or logger.info) + + # Cache for tracking known neighbors (avoid repeated database queries) + self._known_neighbors = set() + + async def process_advert_packet(self, packet, rssi: int, snr: float) -> None: + """ + Process an incoming advertisement packet. + + This method uses AdvertHandler to parse the packet, then stores + the neighbor information for tracking and discovery. + + Args: + packet: The advertisement packet to process + rssi: Received signal strength indicator + snr: Signal-to-noise ratio + """ + try: + # Set signal metrics on packet for handler to use + packet._snr = snr + packet._rssi = rssi + + # Use AdvertHandler to parse the packet - it now returns parsed data + advert_data = await self.advert_handler(packet) + + if not advert_data or not advert_data.get("valid"): + logger.warning("Invalid advert packet received, dropping.") + packet.mark_do_not_retransmit() + packet.drop_reason = "Invalid advert packet" + return + + # Extract data from parsed advert + pubkey = advert_data["public_key"] + node_name = advert_data["name"] + contact_type = advert_data["contact_type"] + + # Skip our own adverts + if self.local_identity: + local_pubkey = self.local_identity.get_public_key().hex() + if pubkey == local_pubkey: + logger.debug("Ignoring own advert in neighbor tracking") + return + + # Get route type from packet header + from pymc_core.protocol.constants import PH_ROUTE_MASK + + route_type = packet.header & PH_ROUTE_MASK + + # Check if this is a new neighbor (run DB read in thread to avoid blocking event loop) + current_time = time.time() + if pubkey not in self._known_neighbors: + # Only check database if not in cache + if self.storage: + current_neighbors = await asyncio.to_thread( + self.storage.get_neighbors + ) + else: + current_neighbors = {} + is_new_neighbor = pubkey not in current_neighbors + + if is_new_neighbor: + self._known_neighbors.add(pubkey) + logger.info(f"Discovered new neighbor: {node_name} ({pubkey[:16]}...)") + else: + is_new_neighbor = False + + # Determine zero-hop: direct routes are always zero-hop, + # flood routes are zero-hop if path_len <= 1 (received directly) + path_len = len(packet.path) if packet.path else 0 + zero_hop = path_len == 0 + + # Build advert record + advert_record = { + "timestamp": current_time, + "pubkey": pubkey, + "node_name": node_name, + "is_repeater": "REPEATER" in contact_type.upper(), + "route_type": route_type, + "contact_type": contact_type, + "latitude": advert_data["latitude"], + "longitude": advert_data["longitude"], + "rssi": rssi, + "snr": snr, + "is_new_neighbor": is_new_neighbor, + "zero_hop": zero_hop, + } + + # Store to database (run in thread so event loop stays responsive; + # blocking here can cause companion TCP clients to disconnect) + if self.storage: + try: + await asyncio.to_thread( + self.storage.record_advert, + advert_record, + ) + except Exception as e: + logger.error(f"Failed to store advert record: {e}") + + except Exception as e: + logger.error(f"Error processing advert packet: {e}", exc_info=True)