Files
roro9stack/site/content/dev/milestones/m3.md
T
twislaandClaude Sonnet 5.5 3b4100dc6a
CI / build (pull_request) Successful in 8m38s
Site / build (pull_request) Successful in 9s
Site: the developer docs (phase 4), with the Debug Builds and the Debug Console first
/dev/ has Debug Builds and the Debug Console (builds and the token, the
console and its protocol, files and screenshots, driving the UI, crashes and
Safe Mode, the command reference), Build, test and release (including how an
update works), the architecture decisions and the milestone plans.

Generated from the repository by site/tools/gen_dev_docs.py: the ADRs, the
milestones, the README's sections, and the command reference, read from the
firmware's own `help` text. The pages are committed (Zola cannot read outside
its folder); the Site workflow checks they are current, and now also runs
when src/main.cpp changes. M0, M1 and CONTEXT.md are not published.
README: the gnss commands that the table lacked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-06 21:25:33 +02:00

11 KiB
Raw Blame History

+++ title = "Radio bring-up: the LoRa Scanner" description = "The LoRa radio on the Cap works, receive only: a Radio Service owns it and shares the SPI bus with the SD card safely, and a LoRa Scanner App shows what's on the air, either packets (Sniffer) or energy across the band (Sweep).…" weight = 40

[extra] docs = true source = "docs/milestones/M3.md" tag = "M3" +++ Status: done, tagged v0.6.0. Everything was checked on the device except one "Done when" item: Sweep was never tried against a known transmitter (see below). The listening hour heard nothing, so by Q104 a reference Meshtastic node is a requirement for M4.

Goal: the LoRa radio on the Cap works, receive only: a Radio Service owns it and shares the SPI bus with the SD card safely, and a LoRa Scanner App shows what's on the air, either packets (Sniffer) or energy across the band (Sweep). Nothing in M3 can transmit. The mesh comes on top of this in M4 (receive) and M5 (transmit).

Hardware: the Cap LoRa-1262 carries an SX1262 (868–923 MHz, +22 dBm) with an RP-SMA antenna. Pins, as in Meshtastic's board file for the Cardputer ADV: NSS 5, RST 3, DIO1 (IRQ) 4, BUSY 6, on the SPI bus shared with the microSD card (SCK 40, MISO 39, MOSI 14; card CS 12). Meshtastic uses DIO2 as the RF switch and DIO3 for a 1.8 V TCXO, marked optional. M5Stack's page adds an FM8625H antenna switch enabled by P0 of a PI4IOE5V6408 I/O expander on the internal I2C bus, address not given; Meshtastic doesn't mention it. Step 1 measures which is true.

No other LoRa device yet. meshmap.net (2026-10-05) lists two Meshtastic nodes within 10 km of the desk and six within 30 km, with positions blurred by a few km; none is known to be in range. M3 needs none: it only receives.

Measured

  • Internal I2C bus (8/9): 0x18 (ES8311 codec), 0x34 (TCA8418 keyboard), 0x43 (PI4IOE5V6408, ID register 0xA2), 0x69 (BMI270 IMU).
  • The SX1262 answers on NSS 5, RST 3, DIO1 4, BUSY 6. Its version string reads SX1261 V2D 2D02, which SX1262 chips report too. The 1.8 V TCXO works on the first try; the radio is ready 38 ms after begin.
  • The expander's P0 connects the antenna; it's required. At power-on P0 is an input (direction 0x00, high-impedance 0xFF), and the receiver reads a flat -111.9 dBm at 869.525 MHz, BW 250 kHz: the chip's own floor, deaf. With P0 driven high, the noise floor is -87 to -94 dBm: the antenna hearing the room. So the Radio Service drives P0 high at boot (Q91).
  • DIO2 doesn't change reception (within ±2 dB over three runs, P0 high). It likely selects TX versus RX in the FM8625H; it stays the RF switch, as in Meshtastic.
  • The noise floor at the desk is high (-87 to -94 dBm, varying run to run), about 25 dB above thermal noise for 250 kHz. Something nearby is loud, possibly the Cardputer itself or the PC; Sweep (step 5) should show where it sits.
  • Step 2: presets, frequencies and the channel hash are checked against Meshtastic's source (MeshRadio.h, RadioInterface.cpp): LongFast and the default key give hash 8, MediumFast 31, as Meshtastic shows. Captures were checked with TShark 4.2.5: every LoRaTap field reads back. Wireshark ignores the spec's quarter-dB packet RSSI below 0 dB SNR, so packet RSSI is plain dBm.
  • Step 3: the DIO1 interrupt works (a 100 ms receive timeout wakes the task after 105 ms). While listening: four 1.7 MB uploads and Gemini pages to the card, no radio or card errors (the card refuses a write about once in five uploads with the radio asleep too; put now catches it, and issue #21 follows the cause). The ring takes 9.8 KB while listening; the radio task's stack peaks at 2.0 KB.
  • No packets yet, and a loud desk. Twenty minutes on LongFast and on LoRaWAN's three uplink frequencies (SF7, SF9, SF12): no packet and no header, valid or not. The noise floor reads -83 to -94 dBm, against -112 dBm with the antenna switched off: 20 to 30 dB lost to something nearby, not the screen and not the GNSS receiver. Preamble detections are false alarms at this noise level (more on an empty frequency, 869.0 MHz, than on LongFast).
  • Step 4: a Capture made on the device reads back in TShark field for field (time, frequency, SF, RSSI, SNR, payload). A Capture keeps the radio listening with the App closed; stopping it puts the radio back to sleep.
  • Step 5: a pass across 863–870 MHz (71 steps, the strongest of three RSSI readings at each, measured at 125 kHz) takes about 607 ms. At the desk the band is flat at -100 to -102 dBm, about 15 dB above the chip's own floor at 125 kHz, with a steady carrier at 863.2 MHz (-89 dBm at its strongest) and fainter lines elsewhere: broadband noise from nearby electronics rather than a transmitter. Sweep's waterfall takes 2.6 KB while shown; 91.9 KB free during a Sweep. The radio task's stack peaks at 1.9 KB.
  • Outside, on battery (step 6). The noise is no lower than at the desk: a Sweep floor of -96 to -99 dBm (median -97) with Wi-Fi on, -99 to -100 with Wi-Fi off, so Wi-Fi accounts for 2 or 3 dB. The same narrow peaks come back on every pass, at 863.2, 863.6, 864.4, 864.8, 865.9, 866.3, 867.8, 869.0 and 869.4 MHz (-88 to -91 dBm), several of them 400 kHz apart; 869.4 MHz is the lower edge of LongFast's channel. A source that follows the device outside and onto its battery is the device: about 15 dB of the floor is the Cardputer's own (issue #20). At SF11 that puts the weakest decodable packet near -114 dBm on LongFast, against about -130 dBm for a quiet receiver.
  • The listening hour (step 6, Q104): 20:33 to 21:34 on 2026-10-05, outside, on battery, LongFast, with a Capture running. 0 packets, 0 headers, 0 radio errors; noise -85 to -87 dBm at 250 kHz throughout; 860 preamble detections, all false alarms. The Capture holds its 24-byte header and nothing else. No restart in 1 h 10 min.
  • Floors (Q86): with the radio listening, Wi-Fi and IRC connected over TLS, 52.6 KB free (lowest 22.7 KB during the TLS handshake, the dip accepted in G1). Without IRC, 93 KB.
  • Receive only: nothing in src or lib calls a transmit function.
  • Cost: RadioLib 7.8.1 and the probe add 23.6 KB of flash and 656 bytes of static RAM to the release firmware (1,679,843 bytes of 3,342,336). The whole milestone: 51.8 KB of flash (1,708,091 bytes), 365 tests (27 new).

Decisions (design round 2026-10-05)

# Decision
Q89 M3 is the radio only: Radio Service, Sniffer, Sweep. Notes and the File Browser (Q30) move out to issue #3 and a Notes issue, as a later side milestone.
Q90 RadioLib, pinned (ADR 0001). SX1262 on NSS 5, RST 3, DIO1 4, BUSY 6; DIO2 as RF switch; TCXO at 1.8 V tried first, falling back to the crystal (Meshtastic's TCXO_OPTIONAL).
Q91 Step 1 is lora probe: chip status and version, which oscillator setting worked, and an I2C scan of the internal bus for the PI4IOE5V6408. If present, its P0 is set high at boot (harmless) and DIO2 stays the switch. A wrong switch receives deaf, so compare noise floors.
Q92 A Radio Service owns the SX1262: driver, bus lock, IRQ task. The LoRa Scanner uses it in M3; the Mesh Service sits on top of it in M4.
Q93 Every radio transfer takes the shared bus lock (SPI.beginTransaction, as the card does). DIO1's interrupt only wakes the task; no SPI in the ISR. Done when a Gemini page streams to the card while the Sniffer receives, with no lost packets and no card errors (lora status counters).
Q94 Receive only: the Radio Service has no transmit function in M3. It doesn't exist, rather than being unused.
Q95 Sniffer defaults: EU868 LongFast, 869.525 MHz, BW 250 kHz, SF 11, CR 4/5, sync word 0x2B, preamble 16 (Q19). The other Meshtastic presets are offered, plus custom settings.
Q96 The Sniffer lists packets (time, RSSI, SNR, frequency error, length; hex dump on Enter) and decodes the Meshtastic header: the first 16 bytes are never encrypted (destination, sender, packet ID, hop limit and hop start, channel hash, next hop, relay node). Host-tested. Payload decryption is M4.
Q97 A Sniffer Capture is pcap with LoRaTap headers (link type 270), for Wireshark. Started by hand, Status Bar mark, its own Clean-up category, the 90% rule.
Q98 Sweep steps across the Region's band (863–870 MHz) in 100 kHz steps by default, reading instant RSSI: bars with peak hold, and a waterfall, Wi-Fi Tools style. Optionally CAD on the preset's frequency to tell LoRa traffic from noise.
Q99 Sweep takes the radio and pauses the Sniffer, visibly (Q18). From M4 it pauses the Mesh Service the same way.
Q100 The Sniffer runs while the App is open or a Capture is recording; otherwise the radio sleeps. From M4 the Mesh Service keeps it on.
Q101 Status Bar: a radio mark while receiving, flashing on each packet; muted during a Sweep.
Q102 Debug aids: lora status (settings, counters, last RSSI/SNR, noise floor), lora probe, lora rx on/off (packets on the consoles). Raw packets are never written to the card outside a Capture.
Q103 A ring of the last 32 packets in RAM (about 9 KB at full length); older ones are dropped unless capturing. IRQ task stack trimmed by measurement; RadioLib's flash and RAM measured in step 1 against the floors.
Q104 Done when (below) includes an hour of listening on LongFast by a window. Real packets heard become M4 test fixtures. If none are heard, M3 still closes, and a reference Meshtastic node (Q21) becomes a requirement for M4.

Done when

  • lora probe reports the SX1262, its oscillator setting and the RF switch arrangement, and the result is written here.
  • The Sniffer receives on LongFast with the App open or a Capture running, and the radio sleeps otherwise.
  • Sweep shows the noise floor across 863–870 MHz, and a known signal (a remote key fob, a 868 MHz sensor, anything) stands out. Half met: the floor and the device's own steady peaks show; no known transmitter was tried.
  • The shared-bus test passes (Q93): a Gemini page to the card while the Sniffer receives, no lost packets, no card errors.
  • A Capture opens in Wireshark with LoRaTap fields.
  • The Status Bar mark follows Q101.
  • One hour of LongFast listening by a window has been run and its result recorded here. Run outside, on battery.
  • Free heap stays above the floors (Q86) with the Sniffer, Wi-Fi, IRC on TLS and the UI running.
  • Nothing in the firmware can transmit.

Work breakdown

  1. Hardware check: RadioLib in the build, lora probe (Q91), flash and RAM cost measured.
  2. Meshtastic header and LoRaTap (host-tested): header parsing, presets and their radio settings, pcap/LoRaTap writing.
  3. Radio Service: receive on its own task behind the bus lock, the packet ring, lora status and lora rx, the shared-bus test.
  4. LoRa Scanner App, Sniffer: the packet list, details, preset choice, Captures, Status Bar mark.
  5. Sweep: the RSSI sweep, bars and waterfall, pausing the Sniffer.
  6. Listening hour and the measurements above, recorded here.