Why v1 didn't work
The old tests used a mock radio: one shared queue where packets arrive instantly, nothing is lost and time doesn't exist. Real LoRa is the opposite. A frame takes tens of milliseconds to seconds to send, a node is deaf while it transmits, and two overlapping frames wreck each other.
The worst bug: the sender polled for its ACK in 20 ms windows, and an ACK is on the air for about 45 ms. It could never see one. Every acknowledged send failed on hardware and every test passed.
The simulator
Discrete-event, implementing the library's own radio interface. Each node runs in its own thread on a virtual clock, so a ten-minute scenario takes milliseconds and repeats exactly. It models time on air from the modem settings, half-duplex radios, collisions, random loss and who can hear whom. It also models two kinds of driver, because one that only listens inside receive() behaves very differently from one that listens all the time.
The old code failed 26 of the 37 new scenarios. That was the to-do list.
What changed
- Wait for an ACK in one long receive window sized from the real time on air.
- Exponential backoff scaled to airtime, so two nodes that collide once don't collide forever.
- Duplicate suppression, so a lost ACK doesn't deliver a message twice.
- A receive queue, so two nodes sending to each other at once both get through.
- Discovery with deterministic tie-breaking when two nodes pick the same name or ID.
- Real encryption. v1 said AES and was actually XOR with a 32-bit hash. v2 is AES-128-CBC with HMAC-SHA256 and PBKDF2, no dependencies, checked against standard vectors and against Python's cryptography package.
On real radios
Two SX1262 boards, about two hours on the bench. The link was bad, around −115 dBm, most likely because the boards had no antennas fitted. Which is fine: a marginal link is what a link layer is for.
| Check | Result |
|---|---|
| One hour at SF7, acknowledged ping every 5 s each way | 1,204 of 1,209 delivered (99.6%) |
| Messages delivered twice, any phase | 0 |
| Full-size 255 byte frames, content checked | 7 of 7 intact |
| A board reset mid-run | back with a new ID in 2 s |
| SF10 and SF12 | 184 of 184 and 40 of 41 |
| Encrypted link, 20 minutes | 386 of 386 at normal pace |
| Time on air, 9 byte frame | 46 ms measured, 45.3 ms modelled |
Hardware also found two things the simulator couldn't: the vendor radio library hangs forever without a per-chip licence in flash, and its transmit timeout is hardcoded at 3 s, too short for long frames at the slowest settings.
Not shown
Range with antennas, more than two boards, and runs longer than an hour. The README says the same.