Validated on the air

How MeshBench's LoRa coding chain was checked against a real SX1262 - not against a paper, not against another simulator, but against what an actual chip put on the air. This is the experiment behind the golden vectors in internal/rf/lora/testdata/, run on 18 August 2026, and the method is repeatable whenever the chain changes.

The problem it solves: the LoRa PHY's bit-level details - the sync word, the parity equations, the whitening sequence, the CRC - are not published by Semtech. Everything anyone implements comes from reverse-engineering literature, and the implementations in the wild disagree with each other on several of them. A simulator that is merely *self-consistent* decodes its own transmissions perfectly and proves nothing. The only referee is silicon.

The rig#

Two pieces of ordinary MeshCore hardware, neither modified:

A repeater that normally lives next to the mast was put in monitor-only mode for the runs, so the test frames would not be relayed back over themselves.

KISS modem SX1262, MeshCore firmware known payload, known radio USB serial, KISS framing 869.618 MHz, ~10 m RTL-SDR on a mast rtl_tcp over the network 1 MS/s raw 8-bit IQ offset-tuned −150 kHz TCP tools/goldencap channelize to one sample per chip MeshBench's own receiver front end demodulate every symbol diff against internal/rf/lora's encoding The command path and the capture path never touch: what arrives is judged only by the air.

The method#

One run is one commanded transmission:

  1. goldencap programs the modem's radio - the UK/EU narrow preset, 869.618 MHz, 62.5 kHz, SF8, CR 4/8 - and confirms the settings took by reading them back.
  2. It opens the rtl_tcp stream, tuned 150 kHz below the channel so the dongle's DC spike stays out of the signal, and starts buffering IQ.
  3. It transmits a known payload and waits for the modem's TxDone.
  4. It finds the burst in the capture, mixes it to baseband, decimates to one sample per chip - the rate every piece of MeshBench's DSP speaks - and runs the simulator's own receiver over it: preamble search, SFD lock, CFO correction, per-symbol FFT.
  5. Every demodulated symbol is compared against what internal/rf/lora says that payload should encode to. Any difference is a place where the simulator and the chip disagree about LoRa itself.

The receiver being MeshBench's own is the point. The experiment does not ask "can something decode this" - it asks whether *MeshBench's* transmit chain and *MeshBench's* receive chain, pointed at real silicon, agree with it bit for bit.

What the air showed#

The first capture's raw structure, read window by window off the dechirped spectrum, confirmed the frame's shape exactly as modelled - and corrected one value:

32 preamble upchirps constant bin, exactly MeshCore's length sync word shifts +8, +16 2.25 downchirps the SFD, as modelled data symbols header block at SF−2, then payload blocks nibble × 8, whatever the SF - not the SF-scaled value the literature suggested

Then the symbol diff did something better than pass or fail: it *localised* the disagreement. Within every eight-symbol interleaver block, the first four symbols matched the simulator exactly and the last four did not. In LoRa's diagonal interleaver, the first four columns carry the data bits of every codeword and the last four carry the parity bits - so the diff itself said: whitening, Gray coding, the interleaver and the header layout are all exactly right, and only the parity equations are wrong.

one interleaver block, eight symbols: match match match match differ differ differ differ columns 0–3: the data bits of every codeword whitening, Gray, interleaver, header: confirmed exact columns 4–7: the parity bits the Hamming equations differed from the literature Seventy captured codewords then over-determine the real equations, and linear algebra solves them.

With the known data nibbles on one side and the captured parity bits on the other, each parity bit is an unknown XOR of data bits - sixteen possible masks, checked against seventy observations. The chip's answer:

p0 = d0 ^ d1 ^ d2        p1 = d1 ^ d2 ^ d3
p2 = d0 ^ d1 ^ d3        p3 = d0 ^ d2 ^ d3

Four three-input XORs - not the textbook Hamming-plus-overall-parity a first implementation reaches for. The same technique, applied to the frame's own CRC field against the known payload, settled the last convention: the payload CRC is CCITT from a zero seed over all but the final two bytes, with those two bytes then XORed straight into the result.

After those three corrections - sync word, parity matrix, CRC - a real SX1262 frame decodes end to end through MeshBench's receive chain: sync lock, frequency correction, demodulation, error correction, dewhitening, header, CRC, and the exact payload out the far side.

What the capture tooling defends against#

A live capture can mislead in four specific ways, and the tooling defends against each:

What stands afterwards#

Two clean captured frames, at different payload lengths and therefore different block layouts, are checked into internal/rf/lora/testdata/ as golden vectors. The test suite holds the encoder to them symbol by symbol - at each symbol's own coded rate, since reduced-rate symbols only carry their top bits - and requires each captured frame to decode to its payload with a valid CRC. The chain cannot drift from silicon without a test saying so.

To repeat the experiment after a chain change, or to add vectors at other spreading factors:

go run ./tools/goldencap -probe
go run ./tools/goldencap -run -payload "a known payload" \
    -out capture.iq -golden internal/rf/lora/testdata/golden-sfX-crY.json

-probe says who is on the serial port and how its radio is set; -run does a full transmit-capture-diff; -analyze re-runs the analysis on a saved capture without transmitting anything.

MeshBench documentation. Built from the running application, not from mock-ups. Screenshots are window-only captures; see CLAUDE.md for the rule that keeps them current. Edit this page.