From 2dce072124749d60206952b0c6cf4698e864032d Mon Sep 17 00:00:00 2001 From: Lloyd Date: Wed, 17 Dec 2025 16:20:16 +0000 Subject: [PATCH] Add ProtocolRequestHelper for handling protocol requests and update related components --- repeater/handler_helpers/__init__.py | 3 +- repeater/handler_helpers/path.py | 3 +- repeater/handler_helpers/protocol_request.py | 197 +++++++++++++++++++ repeater/handler_helpers/repeater_cli.py | 14 +- repeater/handler_helpers/text.py | 5 - repeater/main.py | 20 +- repeater/packet_router.py | 8 + 7 files changed, 237 insertions(+), 13 deletions(-) create mode 100644 repeater/handler_helpers/protocol_request.py diff --git a/repeater/handler_helpers/__init__.py b/repeater/handler_helpers/__init__.py index e0e0990..51da12b 100644 --- a/repeater/handler_helpers/__init__.py +++ b/repeater/handler_helpers/__init__.py @@ -6,5 +6,6 @@ from .advert import AdvertHelper from .login import LoginHelper from .text import TextHelper from .path import PathHelper +from .protocol_request import ProtocolRequestHelper -__all__ = ["TraceHelper", "DiscoveryHelper", "AdvertHelper", "LoginHelper", "TextHelper", "PathHelper"] +__all__ = ["TraceHelper", "DiscoveryHelper", "AdvertHelper", "LoginHelper", "TextHelper", "PathHelper", "ProtocolRequestHelper"] diff --git a/repeater/handler_helpers/path.py b/repeater/handler_helpers/path.py index f9534b3..fecacd1 100644 --- a/repeater/handler_helpers/path.py +++ b/repeater/handler_helpers/path.py @@ -52,7 +52,8 @@ class PathHelper: return False mac_and_data = packet.payload[2:] # Skip dest_hash and src_hash - decrypted = CryptoUtils.mac_then_decrypt(shared_secret, mac_and_data) + aes_key = shared_secret[:16] + decrypted = CryptoUtils.mac_then_decrypt(aes_key, shared_secret, mac_and_data) if not decrypted: logger.debug(f"Failed to decrypt PATH packet from 0x{src_hash:02X}") diff --git a/repeater/handler_helpers/protocol_request.py b/repeater/handler_helpers/protocol_request.py new file mode 100644 index 0000000..376117b --- /dev/null +++ b/repeater/handler_helpers/protocol_request.py @@ -0,0 +1,197 @@ +""" +Protocol request (REQ) handling helper for pyMC Repeater. + +Provides repeater-specific callbacks for status and telemetry requests. +""" + +import asyncio +import logging +import struct +import time + +from pymc_core.node.handlers.protocol_request import ( + ProtocolRequestHandler, + REQ_TYPE_GET_STATUS, + REQ_TYPE_GET_TELEMETRY_DATA, + REQ_TYPE_GET_ACCESS_LIST, + REQ_TYPE_GET_NEIGHBOURS, + SERVER_RESPONSE_DELAY_MS +) + +logger = logging.getLogger("ProtocolRequestHelper") + + +class ProtocolRequestHelper: + """Provides repeater-specific protocol request handlers.""" + + def __init__(self, identity_manager, packet_injector=None, acl_dict=None, radio=None, engine=None, neighbor_tracker=None): + + self.identity_manager = identity_manager + self.packet_injector = packet_injector + self.acl_dict = acl_dict or {} + self.radio = radio + self.engine = engine + self.neighbor_tracker = neighbor_tracker + + # Dictionary of core handlers keyed by dest_hash + self.handlers = {} + + def register_identity(self, name: str, identity, identity_type: str = "repeater"): + + hash_byte = identity.get_public_key()[0] + + # Get ACL for this identity + identity_acl = self.acl_dict.get(hash_byte) + if not identity_acl: + logger.warning(f"Cannot register identity '{name}': no ACL for hash 0x{hash_byte:02X}") + return + + # Create ACL contacts wrapper + acl_contacts = self._create_acl_contacts_wrapper(identity_acl) + + # Build request handlers dict + request_handlers = { + REQ_TYPE_GET_STATUS: self._handle_get_status, + } + + # Create core handler + handler = ProtocolRequestHandler( + local_identity=identity, + contacts=acl_contacts, + get_client_fn=lambda src_hash: self._get_client_from_acl(identity_acl, src_hash), + request_handlers=request_handlers, + log_fn=logger.info, + ) + + self.handlers[hash_byte] = { + "handler": handler, + "identity": identity, + "name": name, + "type": identity_type, + } + + logger.info(f"Registered protocol request handler for '{name}': hash=0x{hash_byte:02X}") + + def _create_acl_contacts_wrapper(self, acl): + """Create contacts wrapper from ACL.""" + class ACLContactsWrapper: + def __init__(self, identity_acl): + self._acl = identity_acl + + @property + def contacts(self): + return self._acl.get_all_clients() + + return ACLContactsWrapper(acl) + + def _get_client_from_acl(self, acl, src_hash: int): + """Get client from ACL by source hash.""" + for client_info in acl.get_all_clients(): + if client_info.id.get_public_key()[0] == src_hash: + return client_info + return None + + async def process_request_packet(self, packet): + + try: + if len(packet.payload) < 2: + return False + + dest_hash = packet.payload[0] + + handler_info = self.handlers.get(dest_hash) + if not handler_info: + return False + + # Let core handler build response + response_packet = await handler_info["handler"](packet) + + # Send response after delay + if response_packet and self.packet_injector: + await asyncio.sleep(SERVER_RESPONSE_DELAY_MS / 1000.0) + await self.packet_injector(response_packet, wait_for_ack=False) + + packet.mark_do_not_retransmit() + return True + + except Exception as e: + logger.error(f"Error processing protocol request: {e}", exc_info=True) + return False + + def _handle_get_status(self, client, timestamp: int, req_data: bytes): + + # C++ struct RepeaterStats (44 bytes total): + # uint16_t batt_milli_volts; + # uint16_t curr_tx_queue_len; + # int16_t noise_floor; + # int16_t last_rssi; + # uint32_t n_packets_recv; + # uint32_t n_packets_sent; + # uint32_t total_air_time_secs; + # uint32_t total_up_time_secs; + # uint32_t n_sent_flood; + # uint32_t n_sent_direct; + # uint32_t n_recv_flood; + # uint32_t n_recv_direct; + # uint32_t err_events; + # int16_t last_snr; + # uint32_t n_direct_dups; + # uint32_t n_flood_dups; + # uint32_t total_rx_air_time_secs; + + # Get stats from radio/engine + noise_floor = int(self.radio.get_noise_floor() * 1.0) if self.radio else -120 + last_rssi = int(self.radio.last_rssi) if self.radio and hasattr(self.radio, 'last_rssi') else -120 + last_snr = int((self.radio.last_snr * 4.0) if self.radio and hasattr(self.radio, 'last_snr') else 0) + + # Get packet counts + n_packets_recv = self.radio.packets_received if self.radio and hasattr(self.radio, 'packets_received') else 0 + n_packets_sent = self.radio.packets_sent if self.radio and hasattr(self.radio, 'packets_sent') else 0 + + # Get airtime stats + total_air_time_secs = 0 + total_rx_air_time_secs = 0 + if self.engine and hasattr(self.engine, 'airtime_manager'): + total_air_time_secs = int(self.engine.airtime_manager.total_tx_airtime_ms / 1000) + + # Get routing stats + n_sent_flood = 0 + n_sent_direct = 0 + n_recv_flood = 0 + n_recv_direct = 0 + n_direct_dups = 0 + n_flood_dups = 0 + + if self.engine: + n_sent_flood = getattr(self.engine, 'sent_flood_count', 0) + n_sent_direct = getattr(self.engine, 'sent_direct_count', 0) + n_recv_flood = getattr(self.engine, 'recv_flood_count', 0) + n_recv_direct = getattr(self.engine, 'recv_direct_count', 0) + n_direct_dups = getattr(self.engine, 'direct_dup_count', 0) + n_flood_dups = getattr(self.engine, 'flood_dup_count', 0) + + # Pack struct (little-endian) + stats = struct.pack( + ' {freq},{bw},{sf},{cr}" + # Convert Hz to MHz for freq, Hz to kHz for bandwidth (match C++ ftoa output) + freq_mhz = freq_hz / 1_000_000.0 + bw_khz = bw_hz / 1_000.0 + return f"> {freq_mhz},{bw_khz},{sf},{cr}" elif param == "freq": - freq = self.config.get('radio', {}).get('frequency', 915.0) - return f"> {freq}" + freq_hz = self.config.get('radio', {}).get('frequency', 915000000) + freq_mhz = freq_hz / 1_000_000.0 + return f"> {freq_mhz}" elif param == "tx": power = self.config.get('radio', {}).get('tx_power', 20) diff --git a/repeater/handler_helpers/text.py b/repeater/handler_helpers/text.py index bbd3fdf..864b623 100644 --- a/repeater/handler_helpers/text.py +++ b/repeater/handler_helpers/text.py @@ -338,11 +338,6 @@ class TextHelper: route_type=route_type ) - # Debug reply packet structure - if len(reply_packet.payload) >= 2: - reply_dest_hash = reply_packet.payload[0] - reply_src_hash = reply_packet.payload[1] - logger.debug(f"CLI reply: Packet created - dest=0x{reply_dest_hash:02X}, src=0x{reply_src_hash:02X}, route={reply_packet.get_route_type()}") # Add path for direct routing if available from PATH packets if client.out_path_len >= 0 and len(client.out_path) > 0: diff --git a/repeater/main.py b/repeater/main.py index 280a024..a33b352 100644 --- a/repeater/main.py +++ b/repeater/main.py @@ -6,7 +6,7 @@ import sys from repeater.config import get_radio_for_board, load_config from repeater.engine import RepeaterHandler from repeater.web.http_server import HTTPStatsServer, _log_buffer -from repeater.handler_helpers import TraceHelper, DiscoveryHelper, AdvertHelper, LoginHelper, TextHelper, PathHelper +from repeater.handler_helpers import TraceHelper, DiscoveryHelper, AdvertHelper, LoginHelper, TextHelper, PathHelper, ProtocolRequestHelper from repeater.packet_router import PacketRouter from repeater.identity_manager import IdentityManager @@ -31,6 +31,7 @@ class RepeaterDaemon: self.login_helper = None self.text_helper = None self.path_helper = None + self.protocol_request_helper = None self.acl = None self.router = None @@ -222,6 +223,23 @@ class RepeaterDaemon: log_fn=logger.info, ) logger.info("PATH packet processing helper initialized") + + # Initialize protocol request handler for status/telemetry requests + self.protocol_request_helper = ProtocolRequestHelper( + identity_manager=self.identity_manager, + packet_injector=self.router.inject_packet, + acl_dict=self.login_helper.get_acl_dict(), + radio=self.radio, + engine=self.repeater_handler, + neighbor_tracker=self.advert_helper, + ) + # Register repeater identity for protocol requests + self.protocol_request_helper.register_identity( + name="repeater", + identity=self.local_identity, + identity_type="repeater" + ) + logger.info("Protocol request handler initialized") except Exception as e: logger.error(f"Failed to initialize dispatcher: {e}") diff --git a/repeater/packet_router.py b/repeater/packet_router.py index 08c8381..385e3b4 100644 --- a/repeater/packet_router.py +++ b/repeater/packet_router.py @@ -7,6 +7,7 @@ from pymc_core.node.handlers.advert import AdvertHandler from pymc_core.node.handlers.login_server import LoginServerHandler from pymc_core.node.handlers.text import TextMessageHandler from pymc_core.node.handlers.path import PathHandler +from pymc_core.node.handlers.protocol_request import ProtocolRequestHandler logger = logging.getLogger("PacketRouter") @@ -115,6 +116,13 @@ class PacketRouter: await self.daemon.path_helper.process_path_packet(packet) # Note: process_path_packet returns False to allow forwarding + elif payload_type == ProtocolRequestHandler.payload_type(): + # Process protocol request packet (status, telemetry, neighbors, etc.) + if self.daemon.protocol_request_helper: + handled = await self.daemon.protocol_request_helper.process_request_packet(packet) + if handled: + processed_by_injection = True + # Only pass to repeater engine if not already processed by injection if self.daemon.repeater_handler and not processed_by_injection: metadata = {