Files
pyMC_Repeater/tests/test_radio_config.py
T
itk80 541b25b47c config: add pymc_tcp / pymc_usb radio_type branches
Wires the TCPLoRaRadio and USBLoRaRadio drivers that landed in pyMC_core
on 2026-05-13 (PR pyMC-dev/pyMC_core#68) into get_radio_for_board() so
they can be selected from a repeater config file without any code change
in main.py / api_endpoints.

Both branches follow the existing pattern: read host/port (TCP) or
serial port (USB) plus auth/LBT options from their own config section,
share the LoRa parameters from the common `radio` section, fall back to
the firmware-default sync word 0x12, and surface ImportError as a clear
RuntimeError if the installed pymc_core is too old to ship the drivers.

config.yaml.example documents both sections and updates the radio_type
header comment with the full supported list. Five new tests in
tests/test_radio_config.py monkeypatch the radio classes and verify the
section/parameter wiring + missing-required-field errors.

No web UI / endpoint changes — the deployment this targets edits the
config file directly. A GUI wizard for these radio types can land
separately if there's appetite.
2026-05-13 17:30:09 +02:00

180 lines
4.8 KiB
Python

import pytest
from repeater.config import get_radio_for_board
class _DummyRadio:
_initialized = True
def begin(self):
return True
def test_get_radio_for_board_passes_en_pins(monkeypatch):
captured_kwargs = {}
class _DummySX1262Radio:
@classmethod
def get_instance(cls, **kwargs):
captured_kwargs.update(kwargs)
return _DummyRadio()
monkeypatch.setattr(
"pymc_core.hardware.sx1262_wrapper.SX1262Radio",
_DummySX1262Radio,
)
board_config = {
"radio_type": "sx1262",
"sx1262": {
"bus_id": 0,
"cs_id": 0,
"cs_pin": -1,
"reset_pin": 18,
"busy_pin": 5,
"irq_pin": 6,
"txen_pin": -1,
"rxen_pin": -1,
"en_pins": [26, 23],
},
"radio": {
"frequency": 915000000,
"tx_power": 22,
"spreading_factor": 9,
"bandwidth": 125000,
"coding_rate": 5,
"preamble_length": 17,
"sync_word": 0x3444,
},
}
get_radio_for_board(board_config)
assert captured_kwargs["en_pins"] == [26, 23]
assert "en_pin" not in captured_kwargs
# ─── pymc_tcp / pymc_usb branches ────────────────────────────────────
def _pymc_radio_cfg():
"""Common radio params for the pymc_* tests."""
return {
"frequency": 869618000,
"tx_power": 22,
"spreading_factor": 8,
"bandwidth": 62500,
"coding_rate": 8,
"preamble_length": 16,
"sync_word": 0x12,
}
def test_get_radio_for_board_pymc_tcp(monkeypatch):
pytest.importorskip("pymc_core.hardware.tcp_radio")
captured = {}
class _DummyTCPLoRaRadio(_DummyRadio):
def __init__(self, **kwargs):
captured.update(kwargs)
monkeypatch.setattr(
"pymc_core.hardware.tcp_radio.TCPLoRaRadio",
_DummyTCPLoRaRadio,
)
board_config = {
"radio_type": "pymc_tcp",
"pymc_tcp": {
"host": "pymc-3e2834.local",
"port": 5055,
"token": "shared-secret",
"connect_timeout": 7.5,
"lbt_enabled": False,
"lbt_max_attempts": 3,
},
"radio": _pymc_radio_cfg(),
}
get_radio_for_board(board_config)
assert captured["host"] == "pymc-3e2834.local"
assert captured["port"] == 5055
assert captured["token"] == "shared-secret"
assert captured["connect_timeout"] == 7.5
assert captured["frequency"] == 869618000
assert captured["sync_word"] == 0x12
assert captured["lbt_enabled"] is False
assert captured["lbt_max_attempts"] == 3
def test_get_radio_for_board_pymc_tcp_requires_host(monkeypatch):
pytest.importorskip("pymc_core.hardware.tcp_radio")
monkeypatch.setattr(
"pymc_core.hardware.tcp_radio.TCPLoRaRadio",
lambda **kwargs: _DummyRadio(),
)
board_config = {
"radio_type": "pymc_tcp",
"pymc_tcp": {"port": 5055},
"radio": _pymc_radio_cfg(),
}
with pytest.raises(ValueError, match="Missing 'host'"):
get_radio_for_board(board_config)
def test_get_radio_for_board_pymc_usb(monkeypatch):
pytest.importorskip("pymc_core.hardware.usb_radio")
captured = {}
class _DummyUSBLoRaRadio(_DummyRadio):
def __init__(self, **kwargs):
captured.update(kwargs)
monkeypatch.setattr(
"pymc_core.hardware.usb_radio.USBLoRaRadio",
_DummyUSBLoRaRadio,
)
board_config = {
"radio_type": "pymc_usb",
"pymc_usb": {
"port": "/dev/ttyACM0",
"baudrate": 921600,
},
"radio": _pymc_radio_cfg(),
}
get_radio_for_board(board_config)
assert captured["port"] == "/dev/ttyACM0"
assert captured["baudrate"] == 921600
assert captured["frequency"] == 869618000
assert captured["sync_word"] == 0x12
# LBT defaults preserved when omitted from pymc_usb section.
assert captured["lbt_enabled"] is True
assert captured["lbt_max_attempts"] == 5
def test_get_radio_for_board_pymc_usb_requires_port(monkeypatch):
pytest.importorskip("pymc_core.hardware.usb_radio")
monkeypatch.setattr(
"pymc_core.hardware.usb_radio.USBLoRaRadio",
lambda **kwargs: _DummyRadio(),
)
board_config = {
"radio_type": "pymc_usb",
# Section present (baudrate set) but `port` deliberately omitted to
# exercise the inner "Missing 'port'" guard rather than the outer
# "Missing 'pymc_usb' section" one.
"pymc_usb": {"baudrate": 921600},
"radio": _pymc_radio_cfg(),
}
with pytest.raises(ValueError, match="Missing 'port'"):
get_radio_for_board(board_config)