ReplayRadioTransport
A RadioTransport that replays a pre-captured stream of FromRadio frames entirely on-device — no network and no paired radio. It is the deterministic, self-contained traffic source behind the "Demo Mode (Replay)" connection entry, used to drive realistic ~200-node mesh traffic into the app for Macrobenchmark / Baseline Profile journeys and populated-UI (node list / map / message) tests.
What it exercises
Frames are handed verbatim to RadioTransportCallback.handleFromRadio — the same ingestion entry a live BLE/TCP/Serial radio uses — so everything downstream (protobuf decode, the want_config state machine, node-DB and packet handling, the UI) runs exactly as in production. The only things it skips are the physical link and the stream framing layer: it deals in already-deframed FromRadio payloads.
Asset format (*.fromradio)
A flat, three-section container. Every integer is a big-endian (network-order) unsigned 32-bit value:
┌─ Stage-1: config section ────────────────────────────────────────────┐
│ u32 C number of config frames │
│ C × ( u32 len, len bytes ) each blob = one serialized FromRadio │
├─ Stage-2: node section ──────────────────────────────────────────────┤
│ u32 N number of node_info frames │
│ N × ( u32 len, len bytes ) each blob = one serialized FromRadio │
├─ packet section ─────────────────────────────────────────────────────┤
│ ( u32 len, len bytes ) * repeated until end-of-buffer │
└──────────────────────────────────────────────────────────────────────┘Each len-prefixed blob is a single FromRadio message as produced by Wire's encode(). By section:
config —
my_info,metadata,config.*,moduleConfig.*, andchannelframes: everything the app needs to come up, but nonode_info(the app ignores NodeInfo during Stage 1).node — the connected node first, then the captured node DB (one
node_infoper frame).packet — the captured
MeshPackettraffic, in capture order; the section has no count and runs to EOF.
config_complete_id is deliberately omitted from the file — this transport injects it per phase (below) with the app's own nonce, exactly as real firmware echoes the request.
The file is produced by the burningmesh-replay tool (replay_server.py --export PATH, which also sanitizes capture PII). This class is the authoritative reader of the format; keep the exporter and ReplayRadioTransportTest's asset() encoder in sync with the layout above.
Handshake & runtime behaviour
Driven by the app's two-phase want_config handshake (HandshakeConstants):
Stage 1 (HandshakeConstants.CONFIG_NONCE) → emit the config section, then
config_complete_id.Stage 2 (HandshakeConstants.NODE_INFO_NONCE) → emit the node section, then
config_complete_id, then start streaming the packet section once (a re-issued Stage-2 request does not restart it).
The packet stream is paced at packetDelayMs and does not loop; it stops when scope or this transport is closed. Outbound ToRadio traffic other than want_config (heartbeats, app packets) is ignored — the replay is strictly read-only. close is terminal: it rejects later handshakes and cancels transport-owned replay work.
Robustness
The asset is parsed up front and every length is validated against the bytes actually remaining, so a truncated or corrupt file fails fast with IllegalArgumentException rather than a cryptic buffer underflow or an out-of-memory allocation. handleSendToRadio likewise tolerates any inbound bytes (undecodable ToRadio is ignored, not thrown). The bundled asset is trusted, but this same reader is a convenient injection point for malformed-input / fuzz testing of the ingestion path — see ReplayFuzz in the test sources.