Point a COSMOS grid B210 at 1090 MHz and decode the ADS-B messages that airliners broadcast — call sign, altitude, and live position — with a ten-line experiment description and one command.
Aircraft continuously broadcast ADS-B (Automatic Dependent Surveillance–Broadcast) on 1090 MHz: their ICAO address, flight number, barometric altitude, and GPS position, in Mode-S extended-squitter frames. Any SDR that tunes 1090 MHz with a few MHz of bandwidth can receive them, and open-source decoders turn the raw IQ into a live aircraft list.
This tutorial uses a USRP B210 on the COSMOS grid as the receiver and wiedehopf/readsb as the decoder. It is a passive, receive-only experiment: nothing transmits.
You can run it two ways, and both are shown below. The manual path is every command spelled out, so you can see exactly what happens to the node. The Ansible path is the supported one: the orchestration framework is already installed on every console, so you supply only the experiment description and it selects a node, readies it, builds the decoder, runs it, and collects the results.
After completing this tutorial you will be able to:
sdr.usrp: {model: b210}) instead of naming a node.| Difficulty | Beginner |
| Estimated time | 20 min |
| Domain / sandbox | grid |
| Topic group | SDR & GNU Radio |
| Transmits? | No — receive only |
| Last verified | 2026-08-28 on grid (node18-19), bundle Ansible flow; earlier 2026-07-28 via cosmos-run |
Background knowledge — basic SSH and Linux command line. Nothing about ADS-B or readsb is assumed.
Account & access
| Role | Qty | Notes |
|---|---|---|
| B210-equipped grid node | 1 | Window-row nodes (node18-x to node20-x) hear far more aircraft than nodes deep in the grid |
| Image | Loaded onto | Provides |
|---|---|---|
baseline-sdr.ndz |
the B210 node | Ubuntu 24.04, UHD 4.9, SoapyUHD, GNU Radio 3.10, USRP FPGA images pre-baked. A symlink to the current reference image — load the symlink, not its target, so this page keeps working when the image is rebuilt. |
| Component | Version | Source |
|---|---|---|
| UHD | 4.9 | preinstalled in the image |
| SoapyUHD | matches UHD 4.9 | preinstalled in the image |
| readsb | dev |
built from source during the run |
| cosmos-orchestration | ≥ 0.1.0 | preinstalled on the console |
A single node with a USRP B210 attached over USB 3.0. The control interface is used only to drive the node; the RF path is the B210's antenna receiving over the air. No inter-node connectivity is needed.
[ grid B210 node ] --USB3--> [ USRP B210 ] --ant--> 1090 MHz ADS-B (aircraft)
Log into the grid console. Everything runs from there.
ssh <username>@console.grid.cosmos-lab.org
Confirm the framework is present, and note which copy is in use:
cosmos-run --version
cosmos-run (cosmos_lab.orchestration 0.1.0)
loaded from: /usr/share/ansible/collections/ansible_collections/cosmos_lab/orchestration
Imaging and node selection are handled by the run itself — see Instructions.
Use this if you want to see exactly what happens on the node, or to debug a step by hand. Substitute your reserved node for node18-19.
# 1. Image and boot the node
omf tell -a offh -t node18-19
omf load -i baseline-sdr.ndz -t node18-19
omf tell -a on -t node18-19
omf stat -t node18-19
# 2. Confirm UHD sees the B210. type=b200 filters out networked X310s.
ssh root@node18-19 'uhd_find_devices --args "type=b200"'
--------------------------------------------------
-- UHD Device 0
--------------------------------------------------
type: b200
product: B210
serial: 3073463
# 3. Install the build dependencies and fetch the decoder.
# Note what is NOT installed: soapysdr-module-uhd from apt pulls an older
# UHD that shadows the image's own and breaks the pairing at run time.
ssh root@node18-19 'apt-get install -y git build-essential pkg-config \
libzstd-dev libncurses-dev zlib1g-dev libusb-1.0-0-dev'
ssh root@node18-19 'git clone --depth 1 https://github.com/wiedehopf/readsb /root/readsb'
# 4. Build with SoapySDR support
ssh root@node18-19 'cd /root/readsb && make SOAPYSDR=yes -j4'
# 5. Decode for 60 seconds
ssh root@node18-19 'UHD_IMAGES_DIR=/usr/share/uhd/4.9.0/images timeout 60 /root/readsb \
--device-type=soapysdr --soapy-device="driver=uhd,type=b200" \
--freq=1090000000 --gain=60 --write-json /run/readsb --write-json-every 1 --quiet'
readsb exits with code 124 when timeout stops it — that is expected, not an error.
Hearing nothing? Air traffic varies by hour. Raise
--gaintoward 65–70 and listen longer (120 s); window-row nodes hear far more than nodes deep in the grid.
# 6. Read the result
ssh root@node18-19 'cat /run/readsb/aircraft.json' | head -40
When you are done, release the node:
omf tell -a offh -t node18-19
The tutorials bundle carries this experiment ready to run:
cd ~ && wget -q https://www.cosmos-lab.org/files/cosmos-tutorials-wifi.zip
unzip -o cosmos-tutorials-wifi.zip && cd tutorials # bundle contains all tutorials
export OMF_NODES="node18-19" # your reserved B210 node
ansible-playbook sdr/adsb.yml # add -e adsb_gain=65 -e adsb_run_secs=120 for quiet hours
A preflight play first checks the node is reachable and booted a real image; the run then
installs dependencies, builds readsb, decodes for the configured time, prints the aircraft
table, and writes a JSON result contract to /tmp/adsb-$USER/. Parameters (frequency, gain,
duration) live in group_vars/all.yml — one place for every mode.
cosmos-run)The same experiment also ships pre-packaged on every console. Its description is the whole
experiment:
apiVersion: cosmos/v1
name: adsb-reception
description: Decode 1090 MHz ADS-B transponders with a USRP B210
resources:
rx:
requires:
sdr.usrp: {model: b210}
image: baseline-sdr.ndz
software:
packages: [git, build-essential, pkg-config, libzstd-dev, libncurses-dev, zlib1g-dev, libusb-1.0-0-dev]
packages_absent: [soapysdr-module-uhd]
steps:
- role: cosmos_lab.orchestration.readsb
target: rx
vars:
cosmos_readsb_duration: 60
collect: [/run/cosmos/adsb]
Note that it names no node. requires asks for a B210 and the framework resolves that against live inventory.
Check first whether it can run right now, without booking anything:
cosmos-run sdr-gnuradio/adsb-reception.yml --resolve-only
adsb-reception on grid: rx=node18-19.grid.cosmos-lab.org
Then run it:
cosmos-run sdr-gnuradio/adsb-reception.yml
The run powers the node on if needed, probes it, images it only if the probe finds the SDR stack insufficient, installs the build dependencies, builds and runs the decoder, fetches the results to ~/cosmos-artifacts/<run-id>/, and writes a run.json recording exactly what ran.
The role prints a one-line summary:
node18-19.grid.cosmos-lab.org: heard 4 aircraft, 3 with position, in 60s at 1090.0 MHz
The collected adsb.json holds the decoded table. Real traffic over central New Jersey:
hex=ac643b flight=N898NC alt=17225 lat=40.474704 lon=-74.28618 msgs=387 rssi=-7.1
hex=a79ed6 flight=EJA590 alt=35925 lat=40.509245 lon=-74.166267 msgs=190 rssi=-13.6
hex=a5ac3b flight=AAL1416 alt=21000 lat=40.516617 lon=-73.911865 msgs=137 rssi=-14.0
Success = the table is non-empty and at least one aircraft reports a position. RSSI in roughly the −15 to −30 dB range indicates healthy reception. Counts vary between runs — this is a passive receiver, so what you hear depends on what is overhead.
Nothing to save; the build lives on the node's ephemeral image and re-imaging returns it to a clean state. The framework leaves nothing running. To release the node:
omf tell -a offh -t <your node>
| Symptom | Likely cause | Fix |
|---|---|---|
No UHD Devices Found |
FPGA/firmware images missing | Run uhd_images_downloader once on the node |
| Decoder grabs a networked X310 | driver=uhd matched the first UHD device on the network |
Keep type=b200 in the Soapy device string |
make fails on zstd.h |
libzstd-dev not installed |
It is in the description's packages list; check the staging step ran |
| SoapySDR/UHD version conflict at run time | soapysdr-module-uhd installed from apt shadows the image's SoapyUHD in /usr/local/lib |
Do not install it. The description lists it under packages_absent, and the run refuses to continue if it is present |
heard 0 aircraft |
Weak signal, or a node deep in the grid | Use a window-row node, attach a ~1 GHz antenna, raise the gain toward 60–70 |
readsb exits 124 |
timeout stopped it at the end of the capture window |
Expected — not an error |
Orchestration: cosmos_lab.orchestration (cosmos-run); imaging: omf. Last verified: 2026-07-28 on grid (node18-19, baseline-sdr.ndz). Tags: sdr, uhd, soapysdr, adsb, ansible.