-
Notifications
You must be signed in to change notification settings - Fork 0
Toolbox
The Geeky LoRa Toolbox (#3) is a small menu of on-badge LoRa diagnostic tools, reached from the Settings → Toolbox tile. Two tools ship in v2.7.0 — the Packet Log and the Coverage Test; tools not built yet render dimmed with a "soon" tag.
W/S move between tools, Enter opens the focused one, ESC returns to
Settings. The launcher lives in render_toolbox.c; each tool is its own view.
A live, terminal-style sniffer of every radio frame the badge sees, in both directions, newest-first. Use it to confirm you are hearing a network, to read what a node actually transmitted, and to capture frames for off-badge analysis.
Each row is one captured frame, with five columns:
| Column | Meaning |
|---|---|
| TS | Capture time in seconds since boot |
| DIR |
RX (green) for a received frame, TX (amber) for one we put on air |
| TYPE | Decoded payload type — ADVERT, CHAN, TXT, REQ, RESP, PATH, TRACE, … (or ? if the header did not parse) |
| RSSI | Received signal strength in dBm (RX only; -- on TX) |
| DETAIL | A one-line summary; its content depends on the mode (below) |
The header shows the current mode (HEX / DISSECT) and the total frames
captured since boot; a red PAUSED appears when the window is frozen. The
selected row is highlighted with an amber stripe.
Two detail modes toggle with H:
- HEX — the leading on-air bytes as a hex dump.
-
DISSECT — a per-field breakdown decoded by the pure
mc_proto/diag_decode: route, hop count, length, and per type the ADVERT pubkey/role/name or the DM destination/source hash. Public-channel (GRP_TXT) senders are anonymous by protocol design, so they show no identity.
Press Enter on a row for the full-screen detail view: every decoded field
plus the complete 32-byte public key and the raw bytes, in a monospace face so
the hex lines up. ESC closes it.
- Frames are captured into a small mutex-protected ring in PSRAM
(
mc_common/diag,DIAG_LOG_SIZE= 64 frames, up toDIAG_RAW_MAX= 176 bytes each), tapped straight off the transport inmc_radio— the RX task and theradio_tx_messagetail. Capture happens before the RX dedup, so flood retransmits stay visible to the sniffer. - The ring keeps filling regardless of the UI.
P(pause) only freezes the displayed window so you can read and scroll a stable view; capture continues underneath.Cclears the ring. - Each visible frame is dissected once at snapshot time, so the render loop never re-decodes a row it already drew. The dissect runs on the captured prefix (176 B), so a longer frame shows a display-only truncation; the header fields stay complete.
E dumps the whole ring to a plaintext CSV
/sd/meshcore/log/pkt_<unix>.csv (the filename timestamp comes from the C6
RTC), newest frame first, and raises a toast with the path and frame count. The
columns are:
ts_ms,dir,type,route,rssi_dbm,snr_db,len,raw_hex
rssi_dbm / snr_db are blank on TX rows and on RX frames with no measurement;
raw_hex is the captured leading bytes as lower-case hex. Row formatting is the
pure, host-tested diag_csv_row() (mc_proto/diag_decode); the UI side
(render_toolbox_log.c) writes the file, a no-op when no card is mounted. The
rows carry on-air frame bytes only — no decrypted message content. Pull a file
off with badgelink fs download /sd/meshcore/log/pkt_<unix>.csv.
Export lives on
EbecauseSis the scroll-down key in this view. Full key row:WSselect ·Enterdetail ·Hhex/dissect ·Ppause ·Eexport ·Cclear ·ESCback.
Field-test repeater reachability from different positions: pick a discovered repeater, auto-ping it 3x, classify the result (green = all OK / orange = partial / red = fail), and log every attempt (GPS-stamped) to one file on the SD card per session. This mirrors the MeshMapper wardriving workflow, but on-badge and offline. A later sub-phase adds a clean map view that drops a colour-coded marker per tested position.
Sub-phase 2a (list + auto-ping + SD log) shipped in v2.7.0 and is
hardware-verified against real repeaters (own repeater 3/3 reachable with correct
uplink/downlink SNR + RTT; out-of-range repeaters correctly report unreachable).
Sub-phase 2b (the coverage map) is still design only. Implemented in 2a:
mc_domain/coverage.{c,h} (result model + TRACE-tag matcher + SD CSV + repeater
collector), the ping controller coverage_ping_start + send_trace in mc_rx
with a rx_handle_trace hook, and VIEW_TOOLBOX_COVERAGE
(render_toolbox_coverage.c + input).
One card per discovered repeater (role == REPEATER), showing the name, the
3-byte pubkey prefix, and a right-aligned acks/attempts counter coloured by
result. W/S move the cursor, Enter pings the selected repeater 3x, R
starts a new session, ESC returns to the launcher. The header shows
testing… while a ping run is in flight.
The ping is an upstream MeshCore TRACE (PAYLOAD_TYPE_TRACE = 0x09), not a
DM. A repeater only ACKs a DM (TXT_MSG) from a logged-in admin client
(examples/simple_repeater/MyMesh.cpp), so a plain DM+ACK never turns green
against a real repeater (see issue #25). TRACE is handled by every role in the
base Mesh, so it is the correct probe.
-
send_trace(target_pub, tag)(mc_rx) builds a TRACE: payloadtag[4] | auth[4] | flags[1] | repeater_hash[...], DIRECT-routed, on-wirepath_length = 0(the hop hash rides in the payload, per upstreamMesh::sendDirect). The per-hop SNR path accumulates as it travels. - The repeater stamps its RX SNR into the path field and rebroadcasts; the
returning frame carries our random
tag, sorx_handle_tracerecognises it and callscoverage_note_tag(tag, uplink_snr, downlink_snr). -
uplink_snr= the SNR the repeater measured receiving from us (path[0]);downlink_snr= our SNR of the returning frame. Both go in the CSV.
TRACE is exempted from the RX dedup (radio.c): its payload is constant while
only the SNR path changes, so the payload-fingerprint dedup would otherwise drop
the return. The wire payload layout is the pure, host-tested mc_proto/trace.{c,h}
(test_trace); the TRACE envelope reuses the normal meshcore_serialize.
Three sequential bugs each silently killed the round-trip. They are non-obvious and cost real debugging, so they are recorded here. The fixed behaviour is the only thing that round-trips against an upstream repeater:
-
A TRACE must stay direct-routed.
apply_region_scope(radio.c) used to rewrite every scoped TX toTRANSPORT_FLOOD, including the TRACE. Upstream only processes a TRACE on a direct route (Mesh::onRecvPacketgates onisRouteDirect()) and explicitly refuses to flood one (Mesh::sendFlood: "TRACE type not supported"), so a flooded TRACE is dropped by every repeater. -
A TRACE is sent plain
DIRECT, unscoped — no transport codes. Upstream's companion sends it viaCMD_SEND_TRACE_PATH → Mesh::sendDirectwith no transport codes; repeaters region-gate flood packets only (simple_repeater::allowPacketForwardchecks the region solely underisRouteFlood()), so a direct trace is forwarded regardless of scope.apply_region_scopetherefore skips TRACE entirely. -
The path-control byte's size must be 1, not the hop-hash size. A TRACE's
wire path field is the per-hop SNR accumulator — one byte per hop — so its
path-control size bits must be 0 (control byte
0x00). The hop-hash size (2 bytes for NL) rides in the payloadflags(path_sz), not here. Encodingbytes_per_hop = hash_sizeproduced control byte0x40; a repeater then readpath_len = 0x40 = 64, computedoffset = 64 << path_szpast the hop list, and hit the "trace reached end of path" branch (onTraceRecv) instead of appending its SNR and retransmitting → no echo.send_tracesetsbytes_per_hop = 1.
Net: the probe is ROUTE_TYPE_DIRECT, no transport codes, path-control 0x00,
hop hash = the repeater's 2-byte pubkey prefix in the payload. See issue #25.
- On select, a background FreeRTOS task loops 3×: pick a random tag →
send_trace→ arm the tag → wait ~8 s for the return → record reachable/SNR →vTaskDelay(10 s). The UI never blocks. - Classify: green = 3/3, orange = 1–2/3, red = 0/3.
- Append one CSV row per attempt to a single session file
/sd/meshcore/coverage/cov_<unix>.csv, columns:ts_unix, repeater, pubkey, lat_e6, lon_e6, attempt, reachable, rtt_ms, uplink_snr_db, downlink_snr_db. GPS comes fromgps_live_lat_e6/lon_e6guarded bygps_live_valid(captured when the run starts, so all 3 attempts share the session-start fix; blank when there is no fix). One file per area test = all positions land in the same log.
Kept separate from the existing VIEW_MAP (which draws node/repeater pins),
so the coverage map stays clean:
- Dedicated
VIEW_COVERAGE_MAPwith its own centre/zoom state (don't mutatemap_center_*/map_zoom), zoom locked to 15–16. Reuse the tile cache + loader for the base raster, but render only coverage markers. - Markers: small in-RAM array of
{lat_e6, lon_e6, result}(also persisted in the CSV so a session can be replayed), drawn as a colour-coded dot. - "Save test area as one PNG": render the bounding box of all markers offscreen
and encode to
/sd/meshcore/coverage/<session>.png. Note: the vendored lodepng is decoder-only today (-DLODEPNG_NO_COMPILE_ENCODER), so this needs the encoder re-enabled (costs flash) or an uncompressed BMP.
- Noise-floor scan (#3c) — needs C6 radio firmware (RSSI sweep).
- Precise RTT: the logged
rtt_msis the measured TRACE round-trip (send → matched return), not a calibrated link metric.
Refs #3.