A 15-agent system that surveys nearby radio spectrum with a HackRF One, catalogs the signals it finds, and attempts to decode them. Fourteen agents run in Docker. One agent, the Radio Controller, runs natively on the macOS host.
Docker Desktop on macOS runs containers inside a Linux VM and cannot pass a USB device through to a container. The official Docker FAQ confirms this; the only option is USB-over-IP, which is too slow and fiddly for the tens of MB/s of raw IQ a HackRF produces. So the single component that opens the USB device runs natively on the host, and it hands work to the containers over Redis plus a shared folder. This is also the only sane design given the hardware: a HackRF is a single half-duplex tuner, so the radio has to be time-shared in software rather than split across 15 independent listeners.
Native host (owns the USB):
- Radio Controller (
native/sdr-bridge) sweeps and captures IQ.
Containers:
2. Spectrum Sweeper turns raw sweep rows into occupancy.
3. IQ Broker / Channelizer forwards captures as channels.
4. Energy Detector clusters occupancy into signal candidates.
5. Modulation Classifier guesses AM/FM/FSK/OOK from the IQ.
6. Protocol Identifier maps band + bandwidth + modulation to a protocol.
7-12. Decoders (broadcast, aircraft, AIS, pager, ISM, satellite), one image run
six times with a different PROFILE.
13. Orchestrator schedules the single tuner across candidates and the band plan.
14. Catalog persists everything to sqlite and serves a JSON API.
15. Query / Report renders human-readable reports.
hackrf_sweep --> sweep.power --> Sweeper --> occupancy --> Energy Detector
--> candidate --> Orchestrator --> capture.request --> [Bridge captures IQ]
--> iq.ready --> Channelizer --> Modulation Classifier --> Protocol
Identifier --> Decoders --> decode.result --> Catalog --> Query
IQ never travels the bus. The bridge writes .cs8 files to shared/ and
publishes only a small pointer event; agents read the files directly.
- Apple Silicon Mac with Docker Desktop.
- Homebrew.
- A HackRF One (optional at first: there is a simulation mode).
cp .env.example .env
make up # build + start the 14 containers
make bridge-sim # native bridge, synthetic sweep and IQ
Then open the web dashboard:
make ui # or just open http://localhost:8090
The dashboard shows a live spectrum trace with catalogued signals flagged as frequency markers, live stats, and a signal table that refreshes every 2 s.
Prefer the terminal:
make report # everything catalogued so far
make new # signals first seen in the last hour
make unknown # signals with no protocol yet
Or hit the APIs directly:
curl -s http://localhost:8090/report # text report (/ now serves the dashboard)
curl -s http://localhost:8090/decoded # signals that produced a decode
curl -s http://localhost:8090/api/signals # dashboard's signal feed (JSON)
curl -s http://localhost:8090/api/spectrum # latest sweep power vs frequency (JSON)
curl -s http://localhost:8080/signals # raw JSON straight from the catalog
make brew # brew install hackrf rtl_433 soapysdr
hackrf_info # confirm the board is detected
make up
make bridge # native, real capture
Point the survey where you want with SWEEP_LOW_MHZ / SWEEP_HIGH_MHZ in
.env. The ISM decoder (rtl_433) is fully wired and will decode hundreds of
433 / 868 / 915 MHz devices from the captured files with no extra work. That is
the fastest path to seeing real decodes.
Real and working end to end: the bus, the shared-IQ handoff, sweep to occupancy
to candidate to capture to catalog to report, the feature-based modulation
classifier, the orchestrator's single-tuner scheduling, and the ISM decoder via
rtl_433.
Scaffold with a clear seam: the broadcast, aircraft, AIS, pager, and satellite
decoders record the detection and name the external tool that finishes the job
(for example dump1090 for ADS-B, rtl_ais for AIS, multimon-ng for POCSAG,
redsea for RDS). Each needs a short FM-demod-or-resample chain before its tool;
that chain is a localized change inside agents/decoder/main.py.
- New decoder: add a branch in
agents/decoder/main.pyand aPROFILEservice indocker-compose.yml. - New band: add a row to
BAND_PLANinagents/common/schema.py. - LLM-driven queries: replace the fixed endpoints in
agents/query/main.pywith an/askroute; the catalog's JSON API is all it needs. - Smarter scheduling: the orchestrator's priority policy is one small function, ready to swap for a model.
A HackRF is better at wideband survey than at weak narrowband signals, because of its 8-bit ADC. A common upgrade is to add a cheap RTL-SDR for narrowband decode (ADS-B, AIS) while the HackRF keeps surveying. Add a second capture backend in the bridge and tag captures with the source; nothing else changes.
Receiving and cataloging open transmissions (broadcast, ADS-B, AIS, ISM telemetry, amateur, weather satellites) is broadly fine in most places. Many jurisdictions, including the US under the Communications Act and ECPA, restrict intentionally intercepting or decrypting protected communications such as cellular or encrypted traffic. The decoder set here stays on the open-signal side of that line. This is general information, not legal advice, and the specifics vary by country, so check your local rules.