3 Commits
Author SHA1 Message Date
swaits 5f53251cb2 chore: release 1.1.0 2026-05-11 14:54:37 -06:00
swaits f9e23f45ef feat: new example which tests sync word setting 2026-05-11 14:53:44 -06:00
swaits 61eb2cfeba chore: depend on published donglora from PyPI
Drop the [tool.uv.sources] override that pointed donglora at the
sibling client-py checkout. uv.lock now resolves donglora from
pypi.org, so example consumers get the published package.
2026-04-25 20:53:06 -06:00
4 changed files with 132 additions and 27 deletions

No files matched your search

+19
View File
@@ -1,5 +1,24 @@
# Changelog
## 1.1.0 — 2026-05-11
### Added
- `sync_word_test.py`: two-dongle verification that
`LoRaConfig.sync_word` actually reaches the SX126x. TX rotates
through a set of sync words; RX pins one and should hear only the
matching cycles. RX drops `LOCAL_LOOPBACK` events so accidental
donglora-mux sharing fails loud instead of silently "matching"
every sync word.
## 1.0.1 — 2026-04-25
### Changed
- Depend on the published `donglora` 1.0.0 from PyPI rather than the
editable `../client-py` path. No script changes; release-time
reproducibility only.
## 1.0.0 — 2026-04-22
Initial public release of the example scripts, aligned to the 1.0
+1 -4
View File
@@ -1,6 +1,6 @@
[project]
name = "donglora-examples"
version = "1.0.0"
version = "1.1.0"
description = "DongLoRa example scripts"
license = "MIT"
requires-python = ">=3.10"
@@ -18,9 +18,6 @@ dev = ["ruff"]
[tool.uv]
package = false
[tool.uv.sources]
donglora = { path = "../client-py", editable = true }
[tool.ruff]
line-length = 100
target-version = "py310"
+104
View File
@@ -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.")
Generated
+8 -23
View File
@@ -170,11 +170,15 @@ wheels = [
[[package]]
name = "donglora"
version = "1.0.0"
source = { editable = "../client-py" }
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cobs" },
{ name = "pyserial" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/f1/04a6befb8dac4e0d4b5be15f9589ac7800e15e5cf415fbf268e320a43aa3/donglora-1.0.0.tar.gz", hash = "sha256:6203ff8239a9d8a8e572d677dcd7783b457c2dd6e3dd1ae1b43eac670c7a86a9", size = 93130, upload-time = "2026-04-22T15:55:22.923Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/53/982d9ef76efae11c56ccaa4f92096d9849addb78111399d4c02d46cd3515/donglora-1.0.0-py3-none-any.whl", hash = "sha256:b2cae03149b54a7676c0c8e4850bcbd99d36edd6a4606aeb1980d50a0f5d94fe", size = 30566, upload-time = "2026-04-22T15:55:21.742Z" },
]
[package.optional-dependencies]
meshcore = [
@@ -187,25 +191,6 @@ orac = [
{ name = "pynacl" },
]
[package.metadata]
requires-dist = [
{ name = "anthropic", marker = "extra == 'orac'" },
{ name = "cobs" },
{ name = "pycryptodome", marker = "extra == 'meshcore'" },
{ name = "pycryptodome", marker = "extra == 'orac'" },
{ name = "pynacl", marker = "extra == 'meshcore'" },
{ name = "pynacl", marker = "extra == 'orac'" },
{ name = "pyserial" },
]
provides-extras = ["meshcore", "orac"]
[package.metadata.requires-dev]
dev = [
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "ruff" },
]
[[package]]
name = "donglora-examples"
version = "1.0.0"
@@ -229,9 +214,9 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "donglora", editable = "../client-py" },
{ name = "donglora", extras = ["meshcore"], marker = "extra == 'meshcore'", editable = "../client-py" },
{ name = "donglora", extras = ["orac"], marker = "extra == 'orac'", editable = "../client-py" },
{ name = "donglora", specifier = ">=1.0.0,<2.0.0" },
{ name = "donglora", extras = ["meshcore"], marker = "extra == 'meshcore'", specifier = ">=1.0.0,<2.0.0" },
{ name = "donglora", extras = ["orac"], marker = "extra == 'orac'", specifier = ">=1.0.0,<2.0.0" },
]
provides-extras = ["meshcore", "orac"]