An RF-accurate MeshCore network simulator: real firmware, modelled air.
MeshBench runs real MeshCore firmware. MeshBench models the air. Every node is MeshCore's own code, compiled and running as its own process; what MeshBench supplies is the radio spectrum, the terrain, the distances, the noise, and a shared clock. When a packet is not relayed, it is because the real firmware decided not to relay it.
Reception is judged one of two ways, and the choice is stamped into every result:
- Calculated RF (the default): a link budget against the demodulator's floor, with overlapping transmissions summed into the noise. Fast enough to sweep a national network: a 300-node flood burst prices in about 46 ms.
- Waveform RF: the actual chirps are synthesised as IQ samples, overlaps sum coherently, and a real receive chain (preamble lock, frequency correction, per-symbol FFT, Gray, deinterleave, Hamming FEC, dewhitening, CRC) recovers the frame or does not. Capture effect and partial collisions emerge from the physics, not from a rule, at 20 to 50 times the cost. The chain is held to real silicon by golden vectors: frames captured off a real SX1262 decode end to end through it.
The channel itself decides nothing in either mode: it produces signal, and the receiver finds out. RF simulation compares the two models; the RF chain walks the physics they share.
It exists because a link-budget rule cannot answer the questions that matter: a marginal link, two nodes transmitting at once, a hill in the way. Network operators use it to plan sites, firmware developers to A/B their branches on hundreds of nodes, and app developers to test against a mesh that speaks the real companion protocol.
Every result is a best case. The model is kinder than the air: no
multipath, no oscillator error, no body loss. If it says a link will not
work, believe it; if it says a link works marginally, go and measure.
docs/shortcomings.md is the maintained account of
what is not modelled, and it is stated in the interface on every result.
- Two reception models: calculated link budgets for scale, waveform synthesis with a real demodulator for collisions, capture and everything arithmetic cannot see. Switchable live; stamped into every saved run.
- Real firmware on every node: routing, flood suppression, duty-cycle
policing and CSMA timing are MeshCore's own, native for hundreds of
deterministic nodes or emulated from the published
.uf2/.binimages under QEMU and Renode. - Real terrain: elevation tiles, ITU-R P.526 diffraction, optional buildings; a terrain cut-through with the Fresnel zone explains every missed link, in both directions, because reachability is asymmetric.
- Coverage and planning: link budgets rasterised over terrain, combined across a fleet, and searched for where the next node should go.
- Import real networks live from CoreScope or Beacon, with transport regions inferred from a week of real traffic.
- Firmware A/B on one seed: half the repeaters on one build, half on another, same traffic, and diff. Deterministic: same seed, same scenario, same answer.
- An endpoint for your app:
meshbench serveexposes a real companion over TCP, a serial pty, or Bluetooth. Your client cannot tell it from a radio on a desk. - Scriptable end to end: a control socket and three clients (Go,
Python, Node) in
pkg/; the cookbook shows the same seven programs in each. - Evidence out: Wireshark live or as pcapng, IQ export and a served rtl_tcp stream an unmodified SDR client can tune, waterfall and dechirped symbol views, JUnit from the test runner.
Download the build for your platform from the latest release; nothing else has to be installed first.
# Linux
chmod +x meshbench-*.AppImage && ./meshbench-*.AppImagemacOS (.dmg) and Windows (.msi, or a .zip with no installer) builds are
in the same release;
per-platform detail, including the signing caveats, is in
docs/install.md.
To build from source instead (needs Go 1.25 and a C toolchain with GL and X11 headers):
git clone https://github.com/MeshBench/meshbench
cd meshbench
go build ./cmd/meshbenchOpen the workbench on a real network and press Play:
meshbench workbenchRun your MeshCore branch on every repeater in the mesh:
meshbench dev -from ~/src/MeshCoreGive your application a mesh and an endpoint that speaks the real companion protocol:
meshbench serveRun a fixture on real firmware and check its assertions, for CI:
meshbench test -fixture fixtures/fixture-fife-strict.json -junit results.xmlOr drive a session from code, in Go, Python or Node:
from meshbench import Workbench
with Workbench.headless(fixture="fife-strict", seed=7) as wb:
wb.sim.start()
wb.firmware.wait_started()
wb.sim.run(timedelta(minutes=5))
print(wb.assertions.check())The documentation site takes it
from here: your first simulation,
debugging packet delivery,
running experiments,
and the full CLI, control-socket and client references. This repository's
docs/ keeps the engineering notebook behind it.
MeshBench is 0.x deliberately, and there is no 1.0 scheduled.
docs/compatibility.md says what that means for
anything depending on it: what may break, what is refused at connect rather
than guessed at, and what would have to be true before a 1.0 was worth cutting.
The short version for a script is that a client and the workbench it drives
must be the same release, and a mismatched pair is refused before any verb
runs.
Which published board images have actually been run under emulation, and how far each one got. Every row is a measurement, not a claim: the firmware is the released image from MeshCore's own releases, and a blank cell means nobody has watched that board do that thing.
| Board | MCU | Emulator | build | boot | radio | tx | rx | flood | fem | power |
|---|---|---|---|---|---|---|---|---|---|---|
Generic_E22_sx1262 |
ESP32 | QEMU | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Heltec_t114 |
nRF52840 | Renode | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✓ |
Heltec_t096 |
nRF52840 | Renode | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ? | ✓ |
RAK_4631 |
nRF52840 | Renode | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✓ |
Xiao_nrf52 |
nRF52840 | Renode | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✓ |
Heltec_mesh_solar |
nRF52840 | Renode | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✓ |
Xiao_S3_WIO |
ESP32-S3 | QEMU | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ? |
Heltec_v3 |
ESP32-S3 | QEMU | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✓ |
LilyGo_TDeck |
ESP32-S3 | QEMU | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✓ |
Ebyte_EoRa-S3 |
ESP32-S3 | QEMU | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✓ |
Station_G2 |
ESP32-S3 | - | ||||||||
Heltec_v2 |
ESP32 | - |
✓ passed ✗ failed – not applicable ? not measurable yet blank not attempted
Measured one board at a time on an idle machine, all ten rows on 4 September
2026 against virtual-sx1262 v1.3.0 loaded inside the emulator, and every one
reproduced what it had shown the day before through the radio server that
arrangement replaced. The two blanks have never been attempted.
LilyGo_TDeck forwards 2 of 2, the same as every other board here, as of
v9.2.2-meshbench-sx1262-12. Two things had to be fixed and neither was the
board: the emulator discarded a peripheral's input above pin 39, and that board
is the only one whose DIO1 sits above it on GPIO 45; and the flood row then
counted an attempt the board could not have heard, its own transmitter having
been keyed as the packet arrived.
What each board's row means in detail is in
docs/emulated-published-firmware.md.
The columns, briefly: build is a published image whose digest checks out; boot means the emulator attached and the node did not spend the run restarting; radio/tx mean it put its own unprompted advert on the air; rx that it heard another node; flood that it forwarded somebody else's packet, judged at the board itself; fem that a front-end module was switched in; power that it still answered after being left idle.
What each ✗ turned out to be, and why it is not the board's fault, is
recorded in docs/emulated-published-firmware.md:
the ESP32-S3 pair's history runs through a flash quad-enable bit, an SPI
controller numbering difference, and a strapping pin read low.
Read CONTRIBUTING.md first: the house rules are
mechanical and mostly enforced by CI, so knowing them beforehand is quicker
than finding out from a failed run. Bug reports, board reports and "this
number looks wrong" each have their own issue template, because they need
different information.
GPL-3.0-or-later: see LICENSE, and
docs/licence.md for the reasoning. Anyone who receives
a MeshBench binary can get its source, study the RF model, and check the
numbers against the code that produced them; for a simulator whose output
is used to argue about real deployments, that is the property that matters
most. Attribution for everything MeshBench links, bundles, downloads or
draws is generated from the build graph: see
THIRD_PARTY_NOTICES.md.
