TL;DR
- The radio works, receive only. A Radio Service owns the SX1262 on its own task and shares the SPI bus with the SD card. Nothing in the firmware can transmit: the function doesn't exist.
- The antenna needs switching on. P0 of an I/O expander on the Cap connects it. Without that the receiver is deaf, reading a flat -112 dBm. Meshtastic's board file for this hardware doesn't mention it; M5Stack's page does.
- A LoRa Scanner App:
- The Sniffer lists packets and decodes the Meshtastic header, which is never encrypted.
- Captures are pcap files with LoRaTap headers, and Wireshark reads them field by field.
- Sweep draws the whole 863–870 MHz band as bars and a waterfall.
- Testing found two older bugs, both fixed:
- The Debug Console's file upload could silently write 3 KB of zeros to the card.
- The Gemini client left less memory than it promised on big pages.
- It heard nothing. Twenty minutes at the desk on Meshtastic's channel and three LoRaWAN frequencies, then an hour outside on battery: zero packets and zero headers. The noise floor is about 15 dB above what the chip hears on its own, and Sweep says most of that is the Cardputer itself.
- Tagged v0.6.0; the code is on my Gitea, and the plan with every measurement is docs/milestones/M3.md.
Why it only listens
The plan from the first post put the mesh last: receiving in M4, transmitting in M5, and before that, a second Meshtastic device to test against. M3 is the radio coming up: drivers, the shared bus, a scanner. It needs nothing to talk to, which is lucky, because I have nothing to talk to.
I checked anyway. meshmap.net publishes the nodes that report to the internet. I downloaded the whole list and filtered it on my own machine, so my position stayed home. Within 10 km of my desk there are two nodes; within 30 km, six, all seen that day. The map blurs positions by a few kilometres, and it only shows nodes that report online, so there are probably more. Whether any of them reaches a small antenna in a city is another question. Spoiler: no.
The cast
- The radioSemtech SX1262 on the Cap LoRa-1262
- 868 to 923 MHz, a 1.8 V temperature-compensated crystal, an RP-SMA antenna. It identifies itself as an SX1261, which every SX1262 does. Nobody knows why; everybody's code checks for it anyway.
- The expanderPI4IOE5V6408 at I2C 0x43
- A tiny I/O chip on the Cap with one job that matters: its pin P0 connects the antenna. At power-on it doesn't. The project that supports this exact hardware never mentions it.
- The neighbours6 Meshtastic nodes within 30 km
- According to meshmap.net, with positions blurred by a few kilometres on purpose. Two within 10 km, the nearest about 5 km east. None of them has been heard from yet.
- The noiseabout 15 dB of it, home-made
- Broadband, flat across the band, with a comb of steady spikes on top. It followed the Cardputer outside, onto the battery, and mostly stayed when Wi-Fi went off.
Designed by interrogation, round five
Sixteen questions, Q89 to Q104. The first one cut the milestone in half. The original M3 also had Notes and a file browser. Since then, the file browser had grown into a proper design of its own, in the project's issue tracker. So M3 became the radio only, and the other two moved out to issues #3 and #19 for later. The radio is the risky part, and nothing else in the milestone needed it.
The rest settled how it would work:
- a Radio Service that owns the chip;
- Meshtastic's LongFast settings by default (869.525 MHz, 250 kHz, spreading factor 11);
- the Meshtastic header decoded, but not the encrypted payload;
- captures in a format Wireshark reads;
- a Sweep that pauses the Sniffer while it runs.
One decision mattered more than the others. The Radio Service has no transmit function. It isn't unused: it doesn't exist. Nothing in M3 can put this device on the air by accident, because there's no code for it to do so with. Safety by absence, the only kind that survives a tired developer at 1 am.
The antenna that wasn't
Step one is always a probe: a console command that asks the hardware what it is before any real code trusts it. lora probe found the chip on the pins Meshtastic uses. The chip identified itself as SX1261 V2D 2D02, which is what every SX1262 says. Its 1.8 V crystal worked on the first try, and it was ready in 38 ms.
Then it measured how much noise it could hear, and that was suspiciously little: a flat -111.9 dBm. That's the sound of a receiver listening to itself.
The two sources disagreed:
- Meshtastic's board file says the radio drives its own antenna switch through its DIO2 pin, and that's all.
- M5Stack's page says the switch is enabled by pin P0 of an I/O expander on the Cap's I2C bus. It gives no address for the expander.
The probe scanned the bus and found the expander at 0x43, its pin P0 configured as an input, so not driving anything. With P0 set high, the noise jumped to -87 to -94 dBm: the antenna, finally hearing the room. DIO2 made no difference to reception either way; it most likely picks transmit or receive inside the switch.
So the Radio Service sets P0 high at boot. Without it, this radio would have listened politely to its own thoughts forever, and I'd have blamed the neighbours.
One task to own it
The radio is shared, slow to talk to, and interrupt-driven, all at once. So it gets one owner. The radio task does every SPI transfer to the chip, under the same bus lock the SD card uses. The radio's interrupt line (DIO1) only wakes that task; nothing happens in the interrupt itself. The main loop posts requests ("listen", "use this preset", "sweep") and reads the packets the task leaves in a ring of 32.
That ring takes 9.8 KB, and only while something is listening. When nobody is, the radio sleeps and the memory goes back. RadioLib and all of this cost 24 KB of flash.
The I/O expander is on the I2C bus that the keyboard also uses. Rather than share a bus between tasks, everything on it stays on the main loop. One owner per bus. I learned that rule the hard way in M2 and didn't fancy learning it twice.
The bus test that caught someone else
The risk of sharing a bus is that two users corrupt each other's traffic. So the test was a 1.7 MB file uploaded to the card over Wi-Fi while the radio listened, then read back and compared.
It differed. Bytes 188,416 to 191,487, exactly six 512-byte sectors, had come back as zeros.
Blaming the radio would have been easy. But the same test with the radio asleep failed too, in a different place. And the console's log had the answer: put: write failed at 191488, retry 1.
Here's what happened:
- The card occasionally refuses a write.
- The upload code's retry closed the file, which threw away the last 3 KB still sitting in the write buffer.
- It then truncated the file up to the length it thought it had written. Truncating a file to a larger size fills the gap with zeros.
- The SHA-256 check covered the bytes received over the network, not what landed on the card, so it passed.
The checksum was checking the postman, not the letterbox. This bug came from the OTA milestone and had been waiting for someone to look.
Now the upload reads the file back from the card and hashes it, and a retry that finds lost data gives up instead of papering over it. The card still refuses a write about once in five 1.7 MB uploads, with the radio listening or asleep. With the radio listening, four uploads in a row came back intact.
The same test also caught the Gemini client leaving 1.5 to 3 KB less memory than its 40 KB promise on large pages, radio or no radio. Its budget counted the page text and forgot the App's own tables. That's fixed too.
Nothing to hear
With the radio up and the bus proven, I listened for twenty minutes:
- On LongFast.
- On the three frequencies LoRaWAN sensors use (868.1, 868.3 and 868.5 MHz), at spreading factors 7, 9 and 12. Brussels is full of those sensors, and I hoped one would at least wave.
Zero packets. Zero headers, valid or broken.
The radio also flags "preamble detected" when something looks like the start of a packet. It flagged 25 in two minutes on LongFast, which looked hopeful. So I ran a control on 869.0 MHz, where nobody transmits LoRa: 97 detections in two minutes. False alarms, then. Noise that happens to look like a preamble.
Proving the ears work without a voice
Zero is a suspicious number. If the interrupt never reached the task, the result would look exactly like this.
The chip can prove its own interrupt without any transmitter: start a receive with a 100 ms timeout, route the timeout to DIO1, and see whether the task wakes. It woke after 105 ms. So the antenna, the radio, the interrupt and the task are all fine, and the silence is real.
To test everything after the radio, Debug Builds got lora inject. It puts a made-up packet into the ring as if it had been received, and nothing goes on the air. That made the App and the captures testable.
A capture made on the device opens in TShark with every field right: time, frequency, spreading factor, RSSI, SNR and payload. One detail took a second look. The LoRaTap spec says packet RSSI below 0 dB SNR is stored in quarter dB, a formula from older chips. Wireshark ignores that rule and reads plain dBm. I went with Wireshark, since that's who reads the files.
The Scanner
The Sniffer lists packets newest first:
- time, RSSI and SNR on every row;
- for Meshtastic packets, also who sent it to whom, by the last four hex digits Meshtastic uses as a default name, and how many hops it took.
Enter shows the details: the full node numbers, the packet ID, whether it wants an acknowledgement, the hops, the channel (hash 0x08 is LongFast with the public default key), the node that relayed it, and a hex dump. p picks one of the seven Meshtastic presets allowed in Europe. c starts a capture, which keeps recording after you leave the App.
Sweep, and the loudest thing in the room
Sweep steps across 863–870 MHz in 100 kHz steps and reads the signal strength at each, a full pass every 0.6 seconds. At the desk, the band came back flat at -100 to -102 dBm, with a steady carrier at 863.2 MHz. Flat noise across the band is the signature of digital electronics nearby, not of a transmitter. My desk has plenty of candidates, so I asked for the Cardputer to be taken outside, on battery.
Outside wasn't quieter. It was slightly louder, and the spikes were still there, in the same places, pass after pass. A noise source that follows the device outside and onto its battery is in the device.
Then Wi-Fi went off for a minute, read from the screen since my connection went with it: -99 to -100 dBm, against -97 with it on. Two or three decibels, no more.
So about 15 dB of the noise floor is the Cardputer's own: the processor, the display, the SD card, its power supply, all within a few centimetres of the antenna. One suspect is the radio itself: the spike at 864.0 MHz is exactly the 27th harmonic of its own 32 MHz crystal. A suspect, not a verdict.
What 15 dB costs: LoRa at spreading factor 11 decodes down to about 17 dB below the noise floor. On this Cardputer that's roughly -114 dBm, against about -130 for a quiet receiver. In a city that divides the range by something like three to five. A node across the street will get through; one 5 km away, through Brussels, probably won't.
The hour outside
From 20:33 to 21:34 the Cardputer sat outside on its battery, listening on LongFast with a capture running, while I checked on it every five minutes over Wi-Fi.
Zero packets. Zero headers. Zero radio errors. The noise stayed at -85 to -87 dBm for the whole hour, the preamble detector cried wolf 860 times, and the capture file ended at 24 bytes: a pcap header, and nothing to put after it. The firmware, at least, didn't blink: no restart, 93 KB free.
The design round had a rule for this (Q104): if the hour hears nothing, M3 closes anyway, and a Meshtastic node of my own becomes a requirement for the next milestone. So that's what happens. One item on the checklist stays half done, and I'd rather say so than tick it: Sweep was never pointed at a known transmitter, like a car key. It shows the floor and the Cardputer's own spikes; nobody has keyed a real signal in front of it yet.
Where it stands
-
Step 1: the probe, and the antenna that needed switching on. -
Step 2: the Meshtastic header, presets and captures, tested on the PC against Meshtastic's own source and TShark. -
Step 3: the Radio Service, and the bus test that found the upload bug. -
Step 4: the Scanner App, with captures that outlive it. -
Step 5: Sweep, and finding out who's making all that noise. -
Step 6: an hour of listening, outside. It heard nothing.v0.6.0. -
Next, M4: receiving the mesh. It needs a Meshtastic node of my own, sitting a metre away, where even this receiver can't miss it. The self-noise has an issue of its own (#20): switch things off one at a time, Sweep, and find out which part of the Cardputer is shouting.
By the numbers
| Design questions | 16 (Q89 to Q104) |
| Tests | 365, 27 of them new |
| Flash cost of the radio, Scanner and Sweep | about 52 KB (1.72 MB of 3.3 MB) |
| RAM while listening | 9.8 KB (the packet ring), nothing when idle |
| Radio task stack, peak | 2.0 KB |
| A Sweep pass, 863–870 MHz | 71 steps, 0.6 s |
| Noise floor at 125 kHz: chip alone / desk / outside | about -115 / -101 / -97 dBm |
| Zeros the upload bug wrote | 3,072 bytes, now none |
| Free memory with the radio listening and IRC connected | 52.6 KB, above the 40 KB floor |
| The listening hour | 61 minutes, 860 false alarms |
| Packets from another device | 0 |
The radio is up, it listens, it records, it draws the band in colour. All it needs now is someone to say something, louder than it talks to itself.