Build a receiver

← Home  ·  Parts list  ·  Contact

A receiver listens for the Remote ID broadcasts drones are required to transmit and reports them to your account. It receives only — it transmits nothing, and needs no permission to run.

What you need

Raspberry Pi
A 4 or 5; anything running 64-bit Raspberry Pi OS.
Bluetooth adapter
Must support Bluetooth 5 extended advertising. A Bluetooth 4 dongle will pair happily and silently miss most drones.
Wi-Fi adapter
Must support monitor mode, and must be a second adapter — monitor mode takes it off your network.
A USB 2.0 port
Not optional. See below.

Full parts list, with the exact hardware we run →

Install

sudo apt update && sudo apt install -y python3-pip bluez aircrack-ng git
sudo git clone https://github.com/thetopnach/cielotrack-receiver.git /opt/cielotrack-receiver
cd /opt/cielotrack-receiver
sudo pip3 install -r requirements.txt --break-system-packages
sudo cp .env.example .env        # set BASE_LAT, BASE_LON and WIFI_INTERFACE

sudo ./provision.sh              # lists your Wi-Fi adapters
sudo ./provision.sh wlan1        # monitor mode, udev, NetworkManager, bluetoothd

sudo cp cielotrack-receiver.service /etc/systemd/system/
sudo systemctl enable --now cielotrack-receiver
Don't skip provision.sh. The receiver retunes the Wi-Fi channel; it does not create monitor mode. Without that step the Wi-Fi path cannot work at all, and the failure reads as broken software rather than an unprovisioned host.

Check it afterwards with cat /opt/cielotrack-receiver/status.json — written every minute, needing neither the server nor a claimed device, which are usually the things you are trying to diagnose. radios.problems is empty when both radios are as intended.

Full instructions are in the repository README.

Register it

On first start the receiver prints a six-digit claim code:

🔑 Central server: unclaimed. Claim code: 481-207 — enter this at https://cielotrack.com/receivers

Sign in, then enter that code on the Receivers page. Your receiver picks up its API key within a minute and starts reporting. The key is handed over exactly once and then erased server-side, so it never sits in a database waiting to be read.

Things that cost us time

USB 3.0 ports jam 2.4 GHz. A SuperSpeed port radiates broadband noise across the band both radios use. Ours raised the noise floor to −74 dBm and caught nothing for 46 hours while drones passed overhead at −90 dBm. Use USB 2.0 ports or a USB 2.0 extension cable for both radios.
Bluetooth 4 dongles miss most drones. Remote ID uses extended advertising. Check the log says BLE: extended scanning active rather than legacy.
Don't rotate Wi-Fi channels. Every beacon we have decoded arrived on channel 6, and a rotating scan is elsewhere most of the time.
Antenna placement beats everything. Moving a receiver across the house changed reception more than any software change we made.

Or send data from your own hardware

You don't have to run our code. Anything that can make HTTPS requests can report to CieloTrack.

1. Register the device

curl -X POST https://cielotrack.com/v1/devices/claim   -H 'Content-Type: application/json'   -d '{"device_id":"<uuid>","bootstrap_secret":"<random>"}'

2. Claim the returned code at /receivers, then collect the key

curl https://cielotrack.com/v1/devices/<uuid>/status   -H 'X-Bootstrap-Secret: <random>'

3. Send detections

curl -X POST https://cielotrack.com/v1/detections   -H 'Authorization: Bearer <api-key>'   -H 'Content-Type: application/json'   -d '{"detected_at":"2026-01-01T00:00:00Z","uas_id":"…","lat":32.9,"lon":-96.7}'

Batch up to 500 rows at /v1/detections/batch as {"detections":[…]}. Invalid rows come back with their index and the reason; valid rows in the same batch are still stored.

Detection fields

Only detected_at is required — send whatever else your hardware can decode, and omit or null the rest. Values outside these ranges are rejected rather than stored, so a bad reading never lands on the map.

FieldTypeAccepts
detected_at
When the aircraft was seen. The only required field.
timestamp required, ISO-8601
uas_id
Serial or registration broadcast by the aircraft.
string up to 128 characters
ua_type
Airframe class, e.g. Helicopter, Hybrid Lift, Fixed Wing.
string up to 64 characters
protocol
How it was heard: BLE or Wi-Fi.
string up to 64 characters
mac
Radio address the broadcast came from.
string up to 64 characters
lat
Aircraft latitude, degrees.
number -90 to 90
lon
Aircraft longitude, degrees.
number -180 to 180
altitude_m
Aircraft altitude, metres.
number -1,000 to 100,000
altitude_ref
What altitude_m is measured against: absolute (WGS84 or barometric) or agl. Prefer sending absolute here and putting height above takeoff or ground in height_m — an aircraft broadcasts both, and they answer different questions. agl remains accepted so existing senders keep working.
string up to 32 characters
height_m
Height above the takeoff point or the ground, metres. This is the figure comparable to the 400 ft limit. Send it alongside altitude_m rather than instead of it.
number -1,000 to 100,000
height_ref
What height_m is measured from: takeoff or ground.
string up to 32 characters
speed_mps
Ground speed, metres per second.
number -1,000 to 1,000
operator_lat
Controller latitude, degrees.
number -90 to 90
operator_lon
Controller longitude, degrees.
number -180 to 180
operator_altitude_m
Controller altitude, metres.
number -1,000 to 100,000
operator_location_type
How the operator position was derived: takeoff, live-gnss or fixed.
string up to 32 characters
identity_source
decoded when read from this broadcast; inferred-from-mac when recovered from an earlier sighting of the same radio; undecoded when the contact carried no identifying message and none could be recovered.
string up to 64 characters
message_count
How many Remote ID messages went into this record.
integer 0 to 10,000,000
rssi_dbm
Received signal strength, dBm (negative). Send null when the radio reports it as unavailable rather than sending the sentinel.
integer -200 to 20