From 7c2e121d0838bd3b39937aeda2fdf03f6bd00a77 Mon Sep 17 00:00:00 2001 From: agessaman Date: Wed, 15 Jul 2026 22:27:25 -0700 Subject: [PATCH] refactor(daemon): resolve companion max TX power via the core resolver The daemon's max-TX-power lookup duplicated the core fallback chain and ended in a radio_type string match to know the SX1262's 22 dBm limit -- a driver constant that every new backend name would silently lose. The SX1262 driver class now declares max_tx_power_dbm itself (the CH341 variant instantiates the same class, only the transport differs), so the lookup collapses to the shared resolver over the radio object and the validated deployment settings. --- repeater/main.py | 39 ++++---------------------------- tests/test_companion_settings.py | 31 ++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/repeater/main.py b/repeater/main.py index c5b15e7..4739095 100644 --- a/repeater/main.py +++ b/repeater/main.py @@ -37,6 +37,7 @@ from repeater.sensors import SensorManager from repeater.utils_packet import create_scoped_advert_packet from repeater.web.http_server import HTTPStatsServer, _log_buffer +from openhop_core.companion.radio_capabilities import resolve_max_tx_power_dbm from openhop_core.protocol.constants import PAYLOAD_TYPE_RAW_CUSTOM logger = logging.getLogger("RepeaterDaemon") @@ -676,42 +677,12 @@ class RepeaterDaemon: def _get_companion_max_tx_power_dbm(self): """Return the active backend's TX limit when it declares one. - SX1262 backends have an enforced 22 dBm driver limit. Other backends - can expose a ``get_max_tx_power_dbm`` method, a - ``max_tx_power_dbm`` attribute, or a validated deployment setting. + The backend can expose a ``get_max_tx_power_dbm`` method, a + ``max_tx_power_dbm`` attribute (SX1262 backends declare their 22 dBm + driver limit this way), or a validated deployment setting. Returning ``None`` lets Core use its generic protocol fallback. """ - radio = self.radio - getter = getattr(radio, "get_max_tx_power_dbm", None) - if callable(getter): - try: - value = getter() - if value is not None: - return int(value) - except (TypeError, ValueError): - logger.warning("Radio reported an invalid maximum TX power") - except Exception as e: - logger.warning("Could not get radio maximum TX power: %s", e) - - value = getattr(radio, "max_tx_power_dbm", None) - if value is not None: - try: - return int(value) - except (TypeError, ValueError): - logger.warning("Radio reported an invalid maximum TX power: %r", value) - - settings = self._get_companion_radio_settings() - value = settings.get("max_tx_power_dbm", settings.get("max_tx_power")) - if value is not None: - try: - return int(value) - except (TypeError, ValueError): - logger.warning("Configured maximum TX power is invalid: %r", value) - - radio_type = str(self.config.get("radio_type", "")).lower().strip() - if radio_type in {"sx1262", "sx1262_ch341"}: - return 22 - return None + return resolve_max_tx_power_dbm(self.radio, self._get_companion_radio_settings()) async def _load_companion_identities(self) -> None: """Load companion identities from config and create CompanionBridge + frame server for each.""" diff --git a/tests/test_companion_settings.py b/tests/test_companion_settings.py index d6d5dbf..b2fc2d3 100644 --- a/tests/test_companion_settings.py +++ b/tests/test_companion_settings.py @@ -4,10 +4,13 @@ from __future__ import annotations import logging from types import SimpleNamespace -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock import pytest +from openhop_core.companion import CompanionBridge +from openhop_core.protocol import LocalIdentity + from repeater.companion.utils import ( COMPANION_SETTINGS_ALLOWLIST, CompanionContactCapacityError, @@ -65,12 +68,16 @@ class TestParseCompanionBridgeKwargs: class TestCompanionRadioCapabilities: def test_reads_active_radio_state_and_known_sx1262_limit(self): + # The SX1262 driver declares its 22 dBm limit as a backend attribute + # (SX1262Radio.max_tx_power_dbm); the daemon no longer string-matches + # radio_type to recover it. radio = SimpleNamespace( frequency=868_000_000, bandwidth=125_000, spreading_factor=7, coding_rate=8, tx_power=14, + max_tx_power_dbm=22, ) daemon = RepeaterDaemon.__new__(RepeaterDaemon) daemon.config = {"radio_type": "sx1262", "radio": {"frequency": 915_000_000}} @@ -102,6 +109,28 @@ class TestCompanionRadioCapabilities: assert RepeaterDaemon._get_companion_max_tx_power_dbm(daemon) == 15 + def test_backend_class_attribute_reaches_self_info_max_tx_power(self): + # A driver declares its limit as a class attribute (as SX1262Radio + # does); no radio_type string match is involved. + class _FakeRadio: + max_tx_power_dbm = 20 + + daemon = RepeaterDaemon.__new__(RepeaterDaemon) + daemon.config = {"radio_type": "sx1262_ch341"} + daemon.repeater_handler = SimpleNamespace(radio_config={}) + daemon.radio = _FakeRadio() + + assert RepeaterDaemon._get_companion_max_tx_power_dbm(daemon) == 20 + + # The daemon getter is what a bridge is wired with at load time; the + # value must surface through the companion SELF_INFO max-tx-power path. + bridge = CompanionBridge( + LocalIdentity(), + AsyncMock(return_value=True), + max_tx_power_getter=daemon._get_companion_max_tx_power_dbm, + ) + assert bridge.get_max_tx_power_dbm() == 20 + class TestEffectiveMaxContacts: def test_default(self):