A virtual Semtech SX1262, with a C ABI. This is the chip real MeshCore firmware talks to when it runs under MeshBench, and the only thing between an unmodified radio driver and a simulated sky.
This is not a model of MeshCore's radio driver. The driver is real and runs
unmodified: CustomSX1262 over RadioLib, clocking bytes at a chip select. What
is ours is the part that answers those bytes. A simulator that answered them
approximately would be deciding listen-before-talk for the firmware, which is
answering a different question from the one anybody asked.
Worth being exact about, because the interesting bugs live on the boundary.
| Layer | What runs |
|---|---|
| MeshCore application and mesh logic | real, unmodified |
MeshCore radio driver, CustomSX1262 and RadioLibWrapper |
real, unmodified |
| RadioLib | real, vendored by the host, unmodified |
| The SX1262 chip | this repository |
| Arduino, board, filesystem, RTC, sensors, RNG | the host's, per host |
| The air: path loss, noise, collisions, capture effect | the simulator's |
The line matters in both directions. This part never decides whether a packet
gets through: it is told "a carrier is present" and handed frames. And it never
guesses at airtime: vsx_est_airtime_ms is Semtech's own formula, because
MeshCore's CSMA is built on the figure the firmware computes and a second
formula beside it would drift.
It used to be a file inside MeshBench/meshcore-native, beside the host firmware
build, with a small server process wrapping it so emulators could reach it down a
socket. That put a shared model inside a repository named after one of its
consumers, and left every other consumer talking to it through a pipe.
It has four, and none is more entitled to it than the others:
| Host | How it links | Its licence |
|---|---|---|
MeshBench/meshcore-native, the native firmware build |
the C++ class, in process | MIT |
MeshBench/qemu, the sx1262 device |
this ABI, extern "C" |
GPLv2 |
MeshBench/meshbench, the simulator |
this ABI, via cgo | GPL-3.0-or-later |
| Renode's SX1262 peripheral | this ABI, via DllImport |
MIT |
One model, four hosts, no server. That table is also the whole of the licence argument below.
include/virtual_sx1262.h is the entire surface, and two shapes in it were
learned from the socket it replaces.
Whole transactions, not single bytes. vsx_spi_transaction takes one
chip-select framed exchange. A call per byte across a P/Invoke boundary costs
more than the socket it was meant to remove, and the chip has to know where a
command ends regardless: the SX1262 wire protocol carries no length, so chip
select is the only frame there is.
The chip raises DIO1. Nobody asks it. The socket was request-response, so the
model had no way to call back and its hosts sampled the line on a one
millisecond timer instead. That is a millisecond of latency on every received
packet, for a pin a real part asserts the instant it has something to say.
vsx_set_dio1_callback pushes the edge. vsx_dio1_asserted is still there for a
host that would rather ask.
The C++ class is not part of the ABI. Hosts hold an opaque vsx_chip *, so the
model can be rearranged without breaking a DllImport written by hand.
test/test_model.cpp is not coverage for its own sake. Every case is a bug that
previously took a four minute emulated boot to see, because the model had no
bench of its own:
- A carrier that has ended is not still present. The model raised
PreambleDetected,HeaderValidandSyncWordValidatRxDone, which asserts that a signal is arriving at the instant it stopped. MeshCore'sCustomSX1262::isReceivingreadsHEADER_VALIDas "the channel is busy", so a repeater holding a packet to forward never saw a clear channel and relayed nothing at all. Detection flags are raised as the signal arrives and cleared when it goes. - A packet handed to a deaf chip does not turn up later. A frame delivered while the chip was not listening waited in the inbox until it started listening, however long that took, so a packet could arrive seconds after the air that carried it went quiet.
- A brief re-arm gap still receives. The bound on that wait is a short grace
rather than an immediate drop, because the driver's own
startReceiveis standby, configure,SetRx: the chip is briefly not listening inside a perfectly healthy receiver, and a frame landing in that gap is receivable. - Unless the host asks for strict receive. Silicon gives no grace: SetStandby
aborts a reception, and a receiver back in receive locks on only if enough
preamble is left to detect.
vsx_set_strict_receivemodels that, so a driver that re-arms mid-frame or scans for activity while a frame arrives loses the frame here as it would on the air. It is for a host whose clock is the engine's own, such as the native build; the emulated hosts drift against the engine, and their frames' start times are not known closely enough to judge. Under strict receive the detection flags also stay latched when the carrier ends, as the part's do, until the firmware clears them; a frame the host says was never locked onto withdraws the HeaderValid the model raised for it and leaves PreambleDetected for the firmware's own timeout to deal with. - The chip only lies when asked to. Latching a detection flag past the signal is what real SX1262s sometimes do, and what MeshCore 1.17's recovery exists to survive, so it is a deliberate variant and never the default.
test/test_c_abi.c is compiled by the C compiler on purpose. Two hosts are
not C++, and a header that only builds under a C++ compiler passes every other
test here and then fails in the one place it matters.
./build.sh everything, then run both test binaries
./build.sh static libvirtualsx1262.a, for C and C++ hosts
./build.sh shared libvirtualsx1262.{so,dylib,dll}, for hosts loading at runtime
./build.sh test build and run the tests
./build.sh sanitize the tests under AddressSanitizer and UBSan
STRICT=1 turns warnings into errors, which is what CI uses. No dependencies
beyond a C++17 compiler, which is not an accident: see the licence.
Real errata, and a real preamble detector's behaviour under interference. It is our best understanding of an SX1262, which is a different thing from an SX1262, and every result that rests on it is a best case.
MIT, and deliberately rather than by inheritance.
Two of the four hosts above are copyleft, and they are not the same copyleft: QEMU is GPLv2 as upstream ships it, MeshBench is GPL-3.0-or-later. Those two cannot both link one copyleft library, because GPLv2-only and GPL-3.0 are mutually incompatible. A permissive licence is the only thing that lets the same chip answer a native node, a QEMU device and a Renode peripheral, and one chip answering all of them is the entire reason those backends are comparable.
Nothing here constrains the choice from below: the model links the C++ standard
library and nothing else, so no third-party terms are inherited. It was already
MIT in meshcore-native, so this is where it stays rather than where it moved.
See NOTICE.md.