The SPI radio listens for the preamble the mesh actually sends
MeshCore transmits a 32-symbol preamble up to SF8 (16 above), but the node and the bridge configured the SX1262 for the library's fixed 12. The chip waits for the sync word only about as long as the preamble it expects, so it locked onto each packet, gave up, and locked on again further along the same preamble - decoding a packet only when it happened to catch the tail. On the uConsole that meant 0 of 5 detected packets decoded after a send; with 32 symbols, 4 of 4. The node now derives the preamble from the spreading factor as MeshCore does, and re-sends it to the listening chip when Device config changes the SF. preamble_length leaves the SPI wiring table (it was protocol, never wiring) and is retired; the bridge defaults to the same rule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9538bfedce
commit
dee4674942
8 files changed
+149
-10
No files matched your search
@@ -134,3 +134,51 @@ def test_an_identity_is_minted_once_and_then_kept(tmp_path: Path) -> None:
|
||||
again = radionode.load_identity_seed(tmp_path, lambda: next(minted))
|
||||
assert first == again == b"\x11" * 32
|
||||
assert (tmp_path / "identity.key").read_bytes() == first
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("sf", "symbols"), [(5, 32), (7, 32), (8, 32), (9, 16), (12, 16)])
|
||||
def test_the_preamble_follows_meshcore(sf: int, symbols: int) -> None:
|
||||
"""MeshCore sends 32 symbols up to SF8 and 16 above, and the receiver must expect it."""
|
||||
assert radionode.preamble_for_sf(sf) == symbols
|
||||
|
||||
|
||||
def test_the_radio_is_built_with_the_meshcore_preamble() -> None:
|
||||
"""The chip comes up expecting the preamble the mesh sends, never the library's 12."""
|
||||
prefs = type(
|
||||
"P",
|
||||
(),
|
||||
{
|
||||
"frequency_hz": 910_525_000,
|
||||
"bandwidth_hz": 62_500,
|
||||
"spreading_factor": 7,
|
||||
"coding_rate": 5,
|
||||
"tx_power_dbm": 22,
|
||||
},
|
||||
)()
|
||||
kwargs = radionode.radio_kwargs({"preamble_length", "spreading_factor"}, {}, prefs)
|
||||
assert kwargs == {"preamble_length": 32, "spreading_factor": 7}
|
||||
|
||||
|
||||
class _Chip:
|
||||
"""The calls ``apply_preamble`` makes on the driver, recorded in order."""
|
||||
|
||||
STANDBY_RC, HEADER_EXPLICIT, CRC_ON, IQ_STANDARD, RX_CONTINUOUS = 0, 0, 1, 0, 0xFFFFFF
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple] = []
|
||||
|
||||
def __getattr__(self, name: str): # noqa: ANN204
|
||||
return lambda *args: self.calls.append((name, *args))
|
||||
|
||||
|
||||
def test_a_retune_resends_the_preamble_to_the_listening_chip() -> None:
|
||||
"""Reception keeps its last packet parameters, so a new preamble must be sent to it."""
|
||||
chip = _Chip()
|
||||
radio = type("R", (), {"preamble_length": 32, "lora": chip})()
|
||||
radionode.apply_preamble(radio, 16)
|
||||
assert radio.preamble_length == 16
|
||||
assert [c[0] for c in chip.calls] == ["setStandby", "setPacketParamsLoRa", "request"]
|
||||
assert chip.calls[1][1] == 16
|
||||
chip.calls.clear()
|
||||
radionode.apply_preamble(radio, 16) # unchanged: the chip is left listening undisturbed
|
||||
assert chip.calls == []
|
||||
@@ -27,6 +27,7 @@ class _FakeRadio:
|
||||
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
self.kwargs = kwargs
|
||||
self.preamble_length = kwargs.get("preamble_length", 12)
|
||||
self.cleaned = False
|
||||
_FakeRadio.instances.append(self)
|
||||
|
||||
@@ -64,9 +65,23 @@ class _FakeRadio:
|
||||
return 5.0
|
||||
|
||||
|
||||
def _fake_init(self, bus_id=1, reset_pin=25, frequency=0, tx_power=0, **kwargs): # noqa: ANN001, ANN202
|
||||
def _fake_init( # noqa: ANN202
|
||||
self, # noqa: ANN001
|
||||
bus_id=1, # noqa: ANN001
|
||||
reset_pin=25, # noqa: ANN001
|
||||
frequency=0, # noqa: ANN001
|
||||
tx_power=0, # noqa: ANN001
|
||||
preamble_length=12, # noqa: ANN001 - the library's own default, which must never survive
|
||||
**kwargs, # noqa: ANN003
|
||||
):
|
||||
_FakeRadio.__init__(
|
||||
self, bus_id=bus_id, reset_pin=reset_pin, frequency=frequency, tx_power=tx_power, **kwargs
|
||||
self,
|
||||
bus_id=bus_id,
|
||||
reset_pin=reset_pin,
|
||||
frequency=frequency,
|
||||
tx_power=tx_power,
|
||||
preamble_length=preamble_length,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -145,8 +160,10 @@ async def test_channels_name_and_radio_survive_a_restart(
|
||||
assert channel is not None and channel.get("channel_name") == "Lakeside ops"
|
||||
assert bytes(channel.get("channel_secret") or b"")[:16] == secret
|
||||
assert info.get("name") == "Lakeside"
|
||||
# The chip is brought up on the saved radio, not the seed.
|
||||
# The chip is brought up on the saved radio, not the seed — and at SF11 with the
|
||||
# preamble MeshCore sends there.
|
||||
assert _FakeRadio.instances[-1].kwargs["frequency"] == 869_525_000
|
||||
assert _FakeRadio.instances[-1].preamble_length == 16
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -58,6 +58,12 @@ def test_a_wiring_mistake_is_refused_not_ignored(table: dict, complaint: str) ->
|
||||
SpiWiring.from_toml(table)
|
||||
|
||||
|
||||
def test_the_retired_preamble_key_is_refused_with_its_reason() -> None:
|
||||
"""A config still carrying the old fixed preamble is told why, not left deaf."""
|
||||
with pytest.raises(ValueError, match="preamble_length is no longer a wiring key"):
|
||||
SpiWiring.from_toml({"preamble_length": 12})
|
||||
|
||||
|
||||
def test_a_profile_with_an_spi_table_is_an_spi_profile(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
Reference in new issue
Block a user