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.
This commit is contained in:
agessaman
2026-07-15 22:27:25 -07:00
parent f91a30afa1
commit 7c2e121d08
2 changed files with 35 additions and 35 deletions
+5 -34
View File
@@ -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."""
+30 -1
View File
@@ -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):