mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-03 15:32:35 +02:00
Add ProtocolRequestHelper for handling protocol requests and update related components
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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(
|
||||
'<HHhhIIIIIIIIIhIII',
|
||||
0, # batt_milli_volts (not available on Pi)
|
||||
0, # curr_tx_queue_len (TODO)
|
||||
noise_floor,
|
||||
last_rssi,
|
||||
n_packets_recv,
|
||||
n_packets_sent,
|
||||
total_air_time_secs,
|
||||
int(time.time()), # total_up_time_secs
|
||||
n_sent_flood,
|
||||
n_sent_direct,
|
||||
n_recv_flood,
|
||||
n_recv_direct,
|
||||
0, # err_events
|
||||
last_snr,
|
||||
n_direct_dups,
|
||||
n_flood_dups,
|
||||
total_rx_air_time_secs,
|
||||
)
|
||||
|
||||
logger.debug(f"GET_STATUS: noise={noise_floor}dBm, rssi={last_rssi}dBm, snr={last_snr/4}dB")
|
||||
|
||||
return stats
|
||||
@@ -231,15 +231,19 @@ class RepeaterCLI:
|
||||
|
||||
elif param == "radio":
|
||||
radio = self.config.get('radio', {})
|
||||
freq = radio.get('frequency', 915.0)
|
||||
bw = radio.get('bandwidth', 125.0)
|
||||
freq_hz = radio.get('frequency', 915000000)
|
||||
bw_hz = radio.get('bandwidth', 125000)
|
||||
sf = radio.get('spreading_factor', 7)
|
||||
cr = radio.get('coding_rate', 5)
|
||||
return f"> {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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
+19
-1
@@ -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}")
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user