SignalSweep: pocket counter-surveillance scanner on a XIAO ESP32-C5
I built a little box that listens for surveillance and tracking gear and tells you what it heard by sound alone. It works face-down in a cupholder or buried in a jacket pocket. No screen, no phone required.
What it hears
- Drones: full ASTM F3411 Remote ID over BLE, Wi-Fi beacon and Wi-Fi NAN. You get ID, altitude, speed, heading and operator location.
- Trackers: Apple Find My, Google, Samsung, Tile. When someone floods the air with fake tags it notices and stops beeping.
- Surveillance hardware: Flock ALPR, Axon body cams, smart glasses. Signatures live in a list you edit from the app (OUI, company ID, name, UUID, SSID). No recompile.
- Attack gear (opt-in): BLE spam, deauth bursts, karma APs, Flipper hints, pwnagotchis.
How it talks
Each category has its own beep pattern:
- two long beeps = camera
- rising trill = drone
- fast ticking = tracker
Lock onto a device and it switches to hunt mode. The beeps speed up as you close in, so you walk it down by ear.
Hardware
XIAO ESP32-C5 (~$13), dual-band U.FL antenna, passive buzzer, WS2812 bar. microSD and a DS3231 are optional. The C5 sweeps 2.4 and 5 GHz together. It also runs on the S3, 2.4 GHz only.
The ESP32 bits you might care about
- BLE scanning plus dual-band Wi-Fi promiscuous at the same time on the C5
- Settings survive a pulled cable, because a car or a battery bank power-cycles it constantly
- Web flasher that works out whether it's talking to a C5 or an S3
- pcap export off the SD card over serial, straight into Wireshark
Things that bit me
- Continuous NimBLE scans leak heap.
setMaxResultsdefaults to 0xFF and results only clear when a scan completes, which a continuous scan never does. Every rotating random MAC leaked: 187 KB down to 78 KB of internal heap over 17 hours on the S3.esp_get_free_heap_size()hid it completely because it counts 8 MB of PSRAM.setMaxResults(0)fixed it. notify()silently drops when the mbuf pool is empty. NimBLE-Arduino 1.4 ignores the return code, so big replies over BLE just went missing with no errors on either side. Now every chunk goes throughble_gattc_notify_custom(), which waits and retries on ENOMEM.- The NimBLE host task stack is 4 KB. I handled commands in
onWrite, and a couple of new locals boot-looped the board the moment a phone connected. Over USB it worked fine, because those same commands run onloop()there. - One radio, two jobs. BLE scanning and Wi-Fi promiscuous mode time-slice the same radio. With a near-100% BLE scan window, Wi-Fi captured literally zero frames.
- The C5 promiscuous callback hands you failed frames (
rx_state != 0). Unless you drop them, you get random garbage and thousands of fake transmitters. - Pwnagotchi beacons are gzipped. The S3 ROM has
tinfl_decompressbuilt in. The C5 ROM doesn't, so I had to vendor an inflate for it.
What it won't do
It only receives. No jamming, no deauth, no injection, no spoofing. You can also kill its own BLE advertising, so it doesn't show up on someone else's scanner. The only winning move is not to transmit.
Getting it
Firmware is GPL-3.0. Flash it from the browser, then run the app in Chrome/Edge or as an Android APK. Or skip both, since the board beeps fine on its own.
Repo: https://github.com/B0z0dcl0wn/SignalSweep
Signatures are the weak spot. If it misses gear you know about, please send a PR. Happy to answer build questions.
(Full disclosure: I plan to sell assembled units and kits later. The build-your-own path is fully open now and stays that way.)