diff --git a/sync_word_test.py b/sync_word_test.py new file mode 100644 index 0000000..e77438b --- /dev/null +++ b/sync_word_test.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Verify the dongle applies ``LoRaConfig.sync_word`` to the radio. + +Two roles. Run on TWO SEPARATE PHYSICAL DONGLES (not the same dongle +shared via donglora-mux — a single chip can only hold one sync word at +a time, and the mux delivers local TX back to other sessions as +LOCAL_LOOPBACK events that bypass the chip's sync filter). Pass an +explicit port path to each invocation so neither side discovers the +mux socket: + + # Dongle A — pin to one sync word, listen: + python examples/sync_word_test.py --role rx /dev/ttyACM0 [--sync 0x5464] + + # Dongle B — rotate through several sync words, transmitting on each: + python examples/sync_word_test.py --role tx /dev/ttyACM1 + +Expected behavior with the firmware fix: + RX hears ONLY the cycles whose sync word matches its own + (one packet per rotation pass through SYNC_WORDS), and packets + have realistic RSSI / SNR (not 0.0 / 0.0). + +Without the fix: + RX hears every transmission regardless of which sync word the TX + side configured — the firmware was silently ignoring the field + and leaving the SX126x on the boot-time default 0x1424. + +The default RX sync word (0x5464) is deliberately off the MeshCore +(0x1424) and LoRaWAN-public (0x3444) values so the test isn't polluted +by ambient mesh traffic. The RX role drops `LOCAL_LOOPBACK` events +defensively so accidental mux sharing fails loud (zero packets) rather +than printing misleading "all sync words received" output. +""" + +import argparse +import dataclasses +import time + +import donglora as dl +from _common import MESHCORE_US + +# SX126x register-encoded sync words. Each is (hi << 8) | lo where +# [hi, lo] = convert_sync_word(byte) for the listed underlying byte: +# byte 0x12 → 0x1424 (LoRa "private" — MeshCore default) +# byte 0x34 → 0x3444 (LoRaWAN "public") +# byte 0x56 → 0x5464 +# byte 0x78 → 0x7484 +# byte 0xAB → 0xA4B4 +# byte 0xCD → 0xC4D4 +SYNC_WORDS = [0x1424, 0x3444, 0x5464, 0x7484, 0xA4B4, 0xC4D4] +TX_PAUSE_S = 1.5 + +parser = argparse.ArgumentParser(description="Sync-word verification test") +parser.add_argument("--role", choices=["tx", "rx"], required=True) +parser.add_argument( + "--sync", + type=lambda x: int(x, 0), + default=0x5464, + help="(rx) sync word to pin to, e.g. 0x5464 (default; avoids MeshCore noise)", +) +parser.add_argument("port", nargs="?", help="Serial port (auto-detect if omitted)") +args = parser.parse_args() + +if args.role == "tx": + with dl.connect(args.port, config=MESHCORE_US) as d: + print( + f"TX: rotating {len(SYNC_WORDS)} sync words, " + f"{TX_PAUSE_S}s between (Ctrl+C to stop)\n" + ) + try: + cycle = 0 + while True: + for sw in SYNC_WORDS: + cfg = dataclasses.replace(MESHCORE_US, sync_word=sw) + d.set_config(cfg) + msg = f"cycle={cycle} sync=0x{sw:04X}" + td = d.tx(msg.encode()) + print( + f" TX sync=0x{sw:04X} {msg!r} ({td.airtime_us} µs)" + ) + time.sleep(TX_PAUSE_S) + cycle += 1 + except KeyboardInterrupt: + print("\nDone.") +else: + sw = args.sync + cfg = dataclasses.replace(MESHCORE_US, sync_word=sw) + with dl.connect(args.port, config=cfg) as d: + print(f"RX: pinned to sync=0x{sw:04X} (Ctrl+C to stop)") + print("Only matching-sync-word transmissions should arrive.\n") + try: + for pkt in d.rx(): + if pkt.origin != dl.RxOrigin.OTA: + # Drop mux-injected local-loopback events. They + # arrive without going through the chip's sync + # filter and would falsely "match" every sync + # word the TX side rotates through. + continue + text = pkt.data.decode("utf-8", errors="replace") + print( + f" RX: {text!r} RSSI:{pkt.rssi_dbm:.1f}dBm " + f"SNR:{pkt.snr_db:.1f}dB" + ) + except KeyboardInterrupt: + print("\nDone.")