Debugging packet delivery

The event log records a cause for every reception and every failure, so the question has an answer rather than a theory.

The workflow is a loop: a packet failed, find it on the timeline, read its cause, follow the cause to the panel that explains it, change the one variable it points at, and rerun. Because runs are deterministic, the rerun is an answer: same seed, same scenario, only your change differs.

Start in the Debug view#

It arranges the panels this question needs: the packet timeline, the waterfall, consoles and link budgets, with the Inspector for whichever node is selected.

The Debug view. The marks: a flood burst on the packet timeline, and the Inspector's SNR column where each event's strength is read

The causes, and what each means#

causewhat happenedwhat to change
below the demodulator floorthe signal arrived, too weak to decodeheight, power, antenna, or a lower spreading factor
corrupted by overlapanother transmission arrived at the same timetiming, offered load, relays; and re-run in waveform mode before trusting it: calculated mode has no capture effect and overstates collision loss
radio was elsewherethe node was transmitting or on another frequencyscheduling, duty cycle
no routenothing relayed itregions, flood_max, loop detection
dropped by regionthe node does not hold the scopethe region map, or the scope being sent on

A collision says where it landed. "Decoded its header, then N symbol(s) were destroyed" is a frame whose payload failed its CRC; "locked on, then N of its 8 header symbols were destroyed" is a header the receiver could not read. The receiving node's firmware is told the same thing its radio would report: a CRC error, with the damaged bytes, or a header error and nothing more. A frame the receiver could detect and never locked onto, because something stronger swamped it while its preamble was still to be found, raises nothing, as on the radio: on a native node the preamble flag stays set until the firmware clears it, and a receiver re-armed part-way through a frame loses that frame. Only native firmware builds that support it are told; emulated boards are not yet.

"It did not arrive" and "it arrived 3 dB under the floor" lead to completely different fixes, which is why the cause is recorded rather than a boolean.

Working backwards from a missing delivery#

Did anybody transmit it? Events, filtered to the origin above the radio firmware, scope, or a node not running no yes Did a neighbour hear it? receptions at one hop a link budget question the Link panel for that pair, both directions no yes Did the neighbour relay it? a reception with no transmission after it the firmware declined the region map, the hop ceiling, loop detection no yes Ask the node console.type: what it believes, not what was set
Each question is one panel’s worth of looking, and each “no” names the fix to reach for.
  1. Did anybody transmit it? The Events panel filtered to the origin. If there is no transmission, the problem is above the radio: firmware, scope, or a node that is not running.
  2. Did a neighbour hear it? Receptions at one hop. If not, it is a link budget question: check the Link panel for that pair, in both directions.
  3. Did the neighbour relay it? A reception without a following transmission means the firmware decided not to forward. The usual reasons are the region map, the hop ceiling, and loop detection; Testing a repeater walks setting one node right.
  4. Ask the node. console.type runs a line on its own CLI and returns what it said, which is the difference between what you configured and what it believes.

Reachability is asymmetric#

A can hear B while B cannot hear A: different antennas, heights, powers and noise figures on each end. The Link panel reports both directions, and a result that gives one number for "the link" is hiding half the answer.

Capture the packets#

Simulation, then Capture to a pcapng file - or Watch it live in Wireshark, which starts the stream, installs the dissector and opens Wireshark as one action. Stop capturing ends either.

{"id":1,"method":"capture.file","params":{"path":"/tmp/run.pcapng"}}
wb.call("capture.file", {"path": "/tmp/run.pcapng"})
_, err := wb.Call(ctx, "capture.file", map[string]any{"path": "/tmp/run.pcapng"})

The file is pcapng, which Wireshark reads with the MeshCore dissector in tools/dissector/; capture.wireshark is the live stream the menu entry uses.

Capture is started per session, not per run. A sweep rebuilds the engine between runs and the capture survives that, but a restarted workbench has no capture until one is started again.

Read the node's radio#

A node's window has a Radio tab: what the firmware left in the chip's registers, and what the chip counted. Two rows answer whether the firmware could have seen a packet at all.

The Radio tab of a MeshCore 1.16 repeater: IRQ mask 0x0272, 14 events suppressed, spurious detections 0

From a script, nodes.stats answers the same count as irq_suppressed, absent for a chip that does not keep it.

Common causes that produce no error#

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.