mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-04 16:03:14 +02:00
fix(companion): expose repeater radio state
This commit is contained in:
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
@@ -69,6 +70,8 @@ class RepeaterCompanionBridge(CompanionBridge):
|
||||
authenticate_callback: Optional[Callable[..., tuple[bool, int]]] = None,
|
||||
initial_contacts: Optional[Any] = None,
|
||||
*,
|
||||
radio_settings_getter: Optional[Callable[[], Mapping[str, Any]]] = None,
|
||||
max_tx_power_getter: Optional[Callable[[], Optional[int]]] = None,
|
||||
sqlite_handler=None,
|
||||
companion_hash: str = "",
|
||||
on_prefs_saved: Optional[Callable[[str], None]] = None,
|
||||
@@ -87,6 +90,8 @@ class RepeaterCompanionBridge(CompanionBridge):
|
||||
radio_config=radio_config,
|
||||
authenticate_callback=authenticate_callback,
|
||||
initial_contacts=initial_contacts,
|
||||
radio_settings_getter=radio_settings_getter,
|
||||
max_tx_power_getter=max_tx_power_getter,
|
||||
)
|
||||
|
||||
def _save_prefs(self) -> None:
|
||||
|
||||
@@ -559,6 +559,75 @@ class RepeaterDaemon:
|
||||
total_identities = len(self.identity_manager.list_identities())
|
||||
logger.info(f"Identity manager loaded {total_identities} total identities")
|
||||
|
||||
def _get_companion_radio_settings(self) -> dict:
|
||||
"""Return the current repeater radio settings for virtual companions.
|
||||
|
||||
The values are read-only to companion sessions. Prefer attributes of
|
||||
the active backend, then retain the configured value when a backend
|
||||
cannot expose that field.
|
||||
"""
|
||||
config = (
|
||||
self.repeater_handler.radio_config
|
||||
if self.repeater_handler
|
||||
else self.config.get("radio", {})
|
||||
)
|
||||
settings = dict(config) if isinstance(config, dict) else {}
|
||||
radio = self.radio
|
||||
if radio is None:
|
||||
return settings
|
||||
|
||||
for config_key, attr in (
|
||||
("frequency", "frequency"),
|
||||
("bandwidth", "bandwidth"),
|
||||
("spreading_factor", "spreading_factor"),
|
||||
("coding_rate", "coding_rate"),
|
||||
("tx_power", "tx_power"),
|
||||
):
|
||||
value = getattr(radio, attr, None)
|
||||
if value is not None:
|
||||
settings[config_key] = value
|
||||
return settings
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
async def _load_companion_identities(self) -> None:
|
||||
"""Load companion identities from config and create CompanionBridge + frame server for each."""
|
||||
from openhop_core import LocalIdentity
|
||||
@@ -674,6 +743,8 @@ class RepeaterDaemon:
|
||||
),
|
||||
node_name=node_name,
|
||||
radio_config=radio_config,
|
||||
radio_settings_getter=self._get_companion_radio_settings,
|
||||
max_tx_power_getter=self._get_companion_max_tx_power_dbm,
|
||||
sqlite_handler=sqlite_handler,
|
||||
companion_hash=companion_hash_str,
|
||||
on_prefs_saved=_make_sync_node_name_to_config(name),
|
||||
@@ -893,6 +964,8 @@ class RepeaterDaemon:
|
||||
),
|
||||
node_name=node_name,
|
||||
radio_config=radio_config,
|
||||
radio_settings_getter=self._get_companion_radio_settings,
|
||||
max_tx_power_getter=self._get_companion_max_tx_power_dbm,
|
||||
sqlite_handler=sqlite_handler,
|
||||
companion_hash=companion_hash_str,
|
||||
**bridge_kwargs,
|
||||
|
||||
@@ -52,3 +52,35 @@ def test_load_prefs_restores_default_scope_key_as_bytes(identity):
|
||||
assert scope is not None
|
||||
assert scope[0] == "region1"
|
||||
assert scope[1] == bytes(range(16))
|
||||
|
||||
|
||||
def test_bridge_accepts_host_radio_callbacks(identity):
|
||||
"""Repeater must forward host-radio callbacks required by CompanionBridge."""
|
||||
|
||||
async def inject(pkt, wait_for_ack=False):
|
||||
return True
|
||||
|
||||
bridge = RepeaterCompanionBridge(
|
||||
identity,
|
||||
inject,
|
||||
radio_settings_getter=lambda: {
|
||||
"frequency": 915_000_000,
|
||||
"bandwidth": 250_000,
|
||||
"spreading_factor": 10,
|
||||
"coding_rate": 5,
|
||||
"tx_power": 19,
|
||||
},
|
||||
max_tx_power_getter=lambda: 20,
|
||||
)
|
||||
|
||||
radio = bridge.get_radio_params()
|
||||
assert radio == {
|
||||
"frequency_hz": 915_000_000,
|
||||
"bandwidth_hz": 250_000,
|
||||
"spreading_factor": 10,
|
||||
"coding_rate": 5,
|
||||
"tx_power_dbm": 19,
|
||||
"rx_delay_base": 0,
|
||||
"airtime_factor": 0,
|
||||
}
|
||||
assert bridge.get_max_tx_power_dbm() == 20
|
||||
|
||||
@@ -21,6 +21,7 @@ from repeater.companion.utils import (
|
||||
trim_companion_contacts_to_fit,
|
||||
validate_companion_config_capacity,
|
||||
)
|
||||
from repeater.main import RepeaterDaemon
|
||||
|
||||
# openhop_core defaults (CompanionBridge / ContactStore)
|
||||
_DEFAULT_MAX_CONTACTS = 1000
|
||||
@@ -62,6 +63,46 @@ class TestParseCompanionBridgeKwargs:
|
||||
parse_companion_bridge_kwargs({"max_contacts": -1})
|
||||
|
||||
|
||||
class TestCompanionRadioCapabilities:
|
||||
def test_reads_active_radio_state_and_known_sx1262_limit(self):
|
||||
radio = SimpleNamespace(
|
||||
frequency=868_000_000,
|
||||
bandwidth=125_000,
|
||||
spreading_factor=7,
|
||||
coding_rate=8,
|
||||
tx_power=14,
|
||||
)
|
||||
daemon = RepeaterDaemon.__new__(RepeaterDaemon)
|
||||
daemon.config = {"radio_type": "sx1262", "radio": {"frequency": 915_000_000}}
|
||||
daemon.repeater_handler = SimpleNamespace(radio_config={"frequency": 915_000_000})
|
||||
daemon.radio = radio
|
||||
|
||||
assert RepeaterDaemon._get_companion_radio_settings(daemon) == {
|
||||
"frequency": 868_000_000,
|
||||
"bandwidth": 125_000,
|
||||
"spreading_factor": 7,
|
||||
"coding_rate": 8,
|
||||
"tx_power": 14,
|
||||
}
|
||||
assert RepeaterDaemon._get_companion_max_tx_power_dbm(daemon) == 22
|
||||
|
||||
def test_prefers_backend_declared_maximum(self):
|
||||
daemon = RepeaterDaemon.__new__(RepeaterDaemon)
|
||||
daemon.config = {"radio_type": "sx1262"}
|
||||
daemon.repeater_handler = SimpleNamespace(radio_config={})
|
||||
daemon.radio = SimpleNamespace(max_tx_power_dbm=19)
|
||||
|
||||
assert RepeaterDaemon._get_companion_max_tx_power_dbm(daemon) == 19
|
||||
|
||||
def test_uses_configured_limit_when_backend_cannot_declare_one(self):
|
||||
daemon = RepeaterDaemon.__new__(RepeaterDaemon)
|
||||
daemon.config = {"radio_type": "kiss"}
|
||||
daemon.repeater_handler = SimpleNamespace(radio_config={"max_tx_power_dbm": 15})
|
||||
daemon.radio = SimpleNamespace()
|
||||
|
||||
assert RepeaterDaemon._get_companion_max_tx_power_dbm(daemon) == 15
|
||||
|
||||
|
||||
class TestEffectiveMaxContacts:
|
||||
def test_default(self):
|
||||
assert effective_max_contacts({}) == _DEFAULT_MAX_CONTACTS
|
||||
|
||||
Reference in New Issue
Block a user