From d5e4bd754c34627f7da800cc8f5fdcd8f667fed2 Mon Sep 17 00:00:00 2001 From: agessaman Date: Wed, 30 Sep 2026 21:35:28 -0700 Subject: [PATCH] fix(config): name the radios[] entry with a stray sync word build_radio_stack pops _radio_id before building each radio, so the warning never named the entry on a real node. A _radio_label survives the pop, and the warning now points at radios[].radio, saying the value is inherited only when it is. config.yaml.example documents sync_word. --- config.yaml.example | 5 +++ repeater/config.py | 21 +++++++++--- tests/test_radio_config.py | 66 +++++++++++++++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/config.yaml.example b/config.yaml.example index 6a013aa..2481b5f 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -599,6 +599,11 @@ radio: # Preamble length in symbols preamble_length: 32 + # LoRa sync word, used by modem_tcp/modem_usb radios (the SX1262 driver + # ignores it). MeshCore uses 0x12; a modem on any other value hears nothing. + # radios[] entries inherit this value unless they set their own. + # sync_word: 0x12 + # Use implicit header mode implicit_header: false diff --git a/repeater/config.py b/repeater/config.py index e4f1857..63a5c25 100644 --- a/repeater/config.py +++ b/repeater/config.py @@ -517,19 +517,26 @@ def _checked_modem_sync_word(sync_word: int, board_config: dict, radio_type: str These are the drivers that actually push the value to the radio, and a modem on another sync word connects, answers pings and transmits, yet hears no MeshCore traffic -- a silent failure. A radios[] entry inherits the - top-level ``radio.sync_word``, which the SX1262 driver ignores, so a stray - top-level value is harmless until a modem entry picks it up. + top-level ``radio.sync_word``, which the SX1262 driver ignores (SX1262Radio + stores it but never calls setSyncWord), so a stray top-level value is + harmless until a modem entry picks it up. """ if sync_word != MESHCORE_SYNC_WORD: - radio_id = board_config.get("_radio_id") + radio_id = board_config.get("_radio_label") or board_config.get("_radio_id") + if radio_id: + where = f"radios[{radio_id}].radio" + if board_config.get("_sync_word_inherited"): + where += " (it inherits the top-level radio.sync_word)" + else: + where = "its radio section" logger.warning( "%s sync_word is 0x%X, not MeshCore's 0x%02X: the modem will not hear " - "MeshCore traffic. Set sync_word: 0x%02X in its radio section%s.", + "MeshCore traffic. Set sync_word: 0x%02X in %s.", f"Radio {radio_id!r} ({radio_type})" if radio_id else f"{radio_type} radio", sync_word, MESHCORE_SYNC_WORD, MESHCORE_SYNC_WORD, - " (radios[] entries inherit the top-level radio.sync_word)" if radio_id else "", + where, ) return sync_word @@ -920,6 +927,10 @@ def _merge_radio_entry(global_config: dict, entry: dict) -> dict: raise ValueError(f"radios[] entry {radio_id!r} missing radio_type") merged["_radio_id"] = str(radio_id) + # build_radio_stack pops _radio_id; this survives it, to name the radio in logs. + merged["_radio_label"] = str(radio_id) + own_radio = entry.get("radio") if isinstance(entry.get("radio"), dict) else {} + merged["_sync_word_inherited"] = "sync_word" not in own_radio # Hint for CH341 path: do not install process-global SPI/GPIO defaults. merged["_ch341_per_instance"] = True return merged diff --git a/tests/test_radio_config.py b/tests/test_radio_config.py index cc68451..001ee57 100644 --- a/tests/test_radio_config.py +++ b/tests/test_radio_config.py @@ -364,7 +364,7 @@ def test_a_radios_entry_inheriting_a_stray_top_level_sync_word_is_named(monkeypa assert captured["sync_word"] == 0x3444 assert "Radio 'narrow' (modem_tcp) sync_word is 0x3444" in caplog.text - assert "inherit the top-level radio.sync_word" in caplog.text + assert "inherits the top-level radio.sync_word" in caplog.text def test_a_radios_entry_overriding_the_stray_sync_word_is_not_warned_about(monkeypatch, caplog): @@ -398,3 +398,67 @@ def test_an_explicit_null_sync_word_means_the_default(monkeypatch, caplog, radio assert captured["sync_word"] == 0x12 assert "sync_word" not in caplog.text + + +def test_build_radio_stack_names_the_radios_entry_with_the_stray_sync_word(monkeypatch, caplog): + """The real path: build_radio_stack pops _radio_id before building the + radio, so the warning must still name the entry.""" + from repeater.config import build_radio_stack + + captured = _capture_modem(monkeypatch, "modem_tcp") + config = { + "radio_type": "sx1262", + "radio": {**_modem_radio_cfg(), "sync_word": 13380}, + "radios": [ + { + "id": "narrow", + "radio_type": "modem_tcp", + "radio": {"frequency": 910525000}, + "modem_tcp": {"host": "192.168.50.154"}, + } + ], + } + + with caplog.at_level("WARNING", logger="Config"): + build_radio_stack(config) + + assert captured["sync_word"] == 0x3444 + assert "Radio 'narrow' (modem_tcp) sync_word is 0x3444" in caplog.text + assert "inherits the top-level radio.sync_word" in caplog.text + + +def test_each_modem_entry_gets_its_own_sync_word_and_warning(monkeypatch, caplog): + from repeater.config import build_radio_stack + + pytest.importorskip("openhop_core.hardware.tcp_radio") + built = [] + + class _Dummy(_DummyRadio): + def __init__(self, **kwargs): + built.append(kwargs) + + monkeypatch.setattr("openhop_core.hardware.tcp_radio.TCPLoRaRadio", _Dummy) + config = { + "radio_type": "sx1262", + "radio": {**_modem_radio_cfg(), "sync_word": 13380}, + "radios": [ + {"id": "local", "radio_type": "modem_tcp", "modem_tcp": {"host": "10.0.0.1"}}, + { + "id": "narrow", + "radio_type": "modem_tcp", + "radio": {"sync_word": 0x34}, + "modem_tcp": {"host": "10.0.0.2"}, + }, + ], + "fabric": {"default_radio": "local"}, + } + + with caplog.at_level("WARNING", logger="Config"): + build_radio_stack(config) + + assert [b["sync_word"] for b in built] == [0x3444, 0x34] + lines = [r.getMessage() for r in caplog.records] + local = next(m for m in lines if "Radio 'local'" in m) + narrow = next(m for m in lines if "Radio 'narrow'" in m) + assert "radios[local].radio (it inherits" in local + assert "radios[narrow].radio." in narrow and "inherits" not in narrow