Multi-radio: radios[].sx1262 replaces the top-level section instead of overlaying it, so a TCXO board comes up with no clock (noise floor -125 dBm, no RX) #162

Open
opened 2026-10-27 09:35:54 +01:00 by adam · 1 comment
Owner

Summary

In multi-radio mode, a radios[] entry's sx1262: block replaces the top-level sx1262: section
wholesale instead of overlaying it. Any hardware key the entry omits silently takes the code default,
even when the top-level section (which the setup wizard fills in) has the right value. For a board
that needs use_dio3_tcxo: true — RAK6421 + RAK13302, and every other preset with a TCXO — the
result is an SX1262 with no clock: it initialises, reports radio_status: ok, shows a noise floor of
−125…−127 dBm, and never decodes a packet. The same config works in single-radio mode.

The shipped config.yaml.example multi-radio block reproduces this: its sx1262: entries list only
the pins, no use_dio3_tcxo, use_dio2_rf, dio3_tcxo_voltage or en_pins.

Where

repeater/config.py, _merge_radio_entry (13eb8b2, line 760):

merged = dict(global_config)
for key in ("radio_type", "radio", "sx1262", "ch341", "kiss", "modem_tcp", "modem_usb"):
    if key in entry:
        merged[key] = entry[key]        # whole section swapped, not merged

and get_radio_for_board, line 564:

"use_dio3_tcxo": spi_config.get("use_dio3_tcxo", False),
"use_dio2_rf": spi_config.get("use_dio2_rf", False),

So with radios[0].sx1262 present but lacking those keys, the daemon builds SX1262Radio(use_dio3_tcxo=False, use_dio2_rf=False) regardless of what the top-level sx1262: says. The docstring on _merge_radio_entry describes the intent as "shared top-level settings are inherited unless the entry supplies its own", which is what happens for whole sections, not for keys within them.

Reproduce

RAK6421 + RAK13302 (slot 1), openHop Repeater 1.1.4 / core 1.1.3, Docker.

  1. Single-radio config from the wizard with the rak6421-13300x-slot1 preset. Top-level sx1262: has use_dio3_tcxo: true, use_dio2_rf: true. RX works, noise floor ≈ −100 dBm.
  2. Convert to multi-radio following config.yaml.example: add radios: with the same pins under - id: local / sx1262:, plus two modem_tcp radios.
  3. Restart. /api/stats → config.radios[0].sx1262 shows use_dio3_tcxo: false, use_dio2_rf: false; noise_floor_dbm: -127.5; radio_status: ok; over 24 h zero rows tagged rx_radio_id: local while the modem_tcp radios receive normally. No warning in the log.
  4. Add use_dio3_tcxo: true, dio3_tcxo_voltage: 1.8, use_dio2_rf: true to the radios[0].sx1262 block, restart → noise floor −99.6 dBm, local decodes at the same rate as the other two radios within a minute.

Suggested fix

Overlay per-radio hardware sections on the top-level ones instead of replacing them, so an entry only needs to state what differs:

for key in ("radio", "sx1262", "ch341", "kiss", "modem_tcp", "modem_usb"):
    if key in entry:
        base = global_config.get(key)
        if isinstance(base, dict) and isinstance(entry[key], dict):
            merged[key] = {**base, **entry[key]}
        else:
            merged[key] = entry[key]

Alongside that, or instead if the replace semantics are intended:

  • config.yaml.example's multi-radio block should carry the full hardware key set (use_dio3_tcxo, dio3_tcxo_voltage, use_dio2_rf, en_pin/en_pins, is_waveshare), or a comment saying the entry must be complete because nothing is inherited from the top-level section.
  • validate_config could warn when a radios[] sx1262 entry omits a key the top-level section sets to a non-default value — that's the exact case that bites here.

A silent noise floor of −125 dBm with radio_status: ok is also worth a log warning on its own; it's a reliable sign the TCXO isn't running.

## Summary In multi-radio mode, a `radios[]` entry's `sx1262:` block **replaces** the top-level `sx1262:` section wholesale instead of overlaying it. Any hardware key the entry omits silently takes the code default, even when the top-level section (which the setup wizard fills in) has the right value. For a board that needs `use_dio3_tcxo: true` — RAK6421 + RAK13302, and every other preset with a TCXO — the result is an SX1262 with no clock: it initialises, reports `radio_status: ok`, shows a noise floor of −125…−127 dBm, and never decodes a packet. The same config works in single-radio mode. The shipped `config.yaml.example` multi-radio block reproduces this: its `sx1262:` entries list only the pins, no `use_dio3_tcxo`, `use_dio2_rf`, `dio3_tcxo_voltage` or `en_pins`. ## Where `repeater/config.py`, `_merge_radio_entry` (`13eb8b2`, line 760): ```python merged = dict(global_config) for key in ("radio_type", "radio", "sx1262", "ch341", "kiss", "modem_tcp", "modem_usb"): if key in entry: merged[key] = entry[key] # whole section swapped, not merged ``` and `get_radio_for_board`, line 564: ```python "use_dio3_tcxo": spi_config.get("use_dio3_tcxo", False), "use_dio2_rf": spi_config.get("use_dio2_rf", False), ``` So with `radios[0].sx1262` present but lacking those keys, the daemon builds `SX1262Radio(use_dio3_tcxo=False, use_dio2_rf=False)` regardless of what the top-level `sx1262:` says. The docstring on `_merge_radio_entry` describes the intent as "shared top-level settings are inherited unless the entry supplies its own", which is what happens for whole sections, not for keys within them. ## Reproduce RAK6421 + RAK13302 (slot 1), openHop Repeater 1.1.4 / core 1.1.3, Docker. 1. Single-radio config from the wizard with the `rak6421-13300x-slot1` preset. Top-level `sx1262:` has `use_dio3_tcxo: true`, `use_dio2_rf: true`. RX works, noise floor ≈ −100 dBm. 2. Convert to multi-radio following `config.yaml.example`: add `radios:` with the same pins under `- id: local` / `sx1262:`, plus two `modem_tcp` radios. 3. Restart. `/api/stats` → `config.radios[0].sx1262` shows `use_dio3_tcxo: false, use_dio2_rf: false`; `noise_floor_dbm: -127.5`; `radio_status: ok`; over 24 h zero rows tagged `rx_radio_id: local` while the modem_tcp radios receive normally. No warning in the log. 4. Add `use_dio3_tcxo: true`, `dio3_tcxo_voltage: 1.8`, `use_dio2_rf: true` to the `radios[0].sx1262` block, restart → noise floor −99.6 dBm, `local` decodes at the same rate as the other two radios within a minute. ## Suggested fix Overlay per-radio hardware sections on the top-level ones instead of replacing them, so an entry only needs to state what differs: ```python for key in ("radio", "sx1262", "ch341", "kiss", "modem_tcp", "modem_usb"): if key in entry: base = global_config.get(key) if isinstance(base, dict) and isinstance(entry[key], dict): merged[key] = {**base, **entry[key]} else: merged[key] = entry[key] ``` Alongside that, or instead if the replace semantics are intended: - `config.yaml.example`'s multi-radio block should carry the full hardware key set (`use_dio3_tcxo`, `dio3_tcxo_voltage`, `use_dio2_rf`, `en_pin`/`en_pins`, `is_waveshare`), or a comment saying the entry must be complete because nothing is inherited from the top-level section. - `validate_config` could warn when a `radios[]` sx1262 entry omits a key the top-level section sets to a non-default value — that's the exact case that bites here. A silent noise floor of −125 dBm with `radio_status: ok` is also worth a log warning on its own; it's a reliable sign the TCXO isn't running. <!-- github-sync-id:5509759766 -->
Author
Owner

I believe this is fixed on dev. Can you verify that it is working the way you intended?

I believe this is fixed on dev. Can you verify that it is working the way you intended? <!-- github-comment-id:5954903621 -->
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adam/pyMC_Repeater#162