Moving My ADS-B Receiver Outdoors: Ultrafeeder on a Raspberry Pi, Done Right
- 7 minutes read - 1362 wordsI run an ADS-B receiver at home — it decodes the transponder broadcasts from aircraft overhead, plots them on a live map, and feeds the data to FlightAware. It started life the lazy way: an RTL-SDR dongle plugged into a server indoors, with a run of coax to an antenna. That works, but it’s the wrong topology if you want range, and I wanted to move the whole thing up high and outdoors.
Two things came out of that project worth writing down. First, a diagnostic detour that taught me the map wasn’t lying to me even though it looked like it was. Second — the real content — how to relocate an SDR receiver to the top of a mast without breaking MLAT, which is the part most “just put a Pi outside” guides skip.
The detour: “the altitude is wrong on landed helicopters” #
Before the relocation, I got suspicious of my own data. Helicopters sitting on the ground at the local airport were showing altitudes of ~6,800 ft on the map. That looks broken. Is my antenna-height setting wrong?
Two-part answer: yes, there’s an antenna-height setting; no, it has nothing to do with this.
In my stack the decoder isn’t PiAware — it’s ultrafeeder (readsb). PiAware is just a relay forwarding Beast-format data to FlightAware. The antenna height is configured (READSB_ALT=${FEEDER_ALT_FT}ft), but that value is used only for MLAT timing/geometry and my site’s range rings. It does not touch the altitude shown for aircraft. Aircraft altitude is transmitted by each plane’s own transponder; my receiver displays it verbatim. No amount of height tuning changes those numbers.
So why did a landed helicopter read 6,800 ft?
- It’s MSL, not AGL. ADS-B altitude is height above sea level, not above ground. I’m in the Denver metro. Centennial Airport (KAPA) sits at a field elevation of 5,885 ft. A helicopter on the ramp there should read ~5,900 ft, not zero.
- Barometric altitude uses standard pressure (29.92 inHg), not the day’s local setting. Near the ground on a typical Denver day that adds several hundred to ~1,000 ft of offset. Hence ~6,800.
Nothing was misconfigured. To sanity-check a “landed” reading, compare the barometric altitude to the field elevation of the airport it’s at (KAPA 5,885, KBKF 5,662, KDEN 5,434) — or trust the Geometric (GPS-derived) column, which is closer to true MSL. There is simply no setting that converts the display to AGL. Mile-high living makes your ADS-B numbers look weird until you remember the “mile.”
The real project: get the receiver up high, without long lossy cable #
The instinct — co-locate the SDR with the antenna and send data over the network instead of fighting RF loss over 20 ft of coax/USB — is exactly right. The mistake most people make is putting the wrong cable up the mast. There are three possible runs and they have wildly different tolerances:
- Coax (antenna → SDR): RF at 1090 MHz. Lossy. Every foot hurts. Keep this as short as physically possible.
- USB (SDR → Pi): passive USB is ~5 m spec; 10 ft outdoors is noise- and voltage-drop-prone.
- Ethernet/PoE or Wi-Fi (Pi → server): digital. Runs 100 m with zero signal loss.
So the correct design isn’t “Pi low, SDR high on a long USB cable.” It’s put the Pi and SDR together in one weatherproof box up near the antenna, with a short coax pigtail to the antenna and a short USB inside the enclosure. The only things leaving the box are network and power:
Antenna (mast top)
│ short coax / SMA pigtail (inches to ~2 ft)
RTL-SDR + 1090 LNA/filter
│ short USB (inside the same enclosure)
Raspberry Pi ──── PoE Ethernet (or Wi-Fi) ──── server (.21)
(all in ONE weatherproof box up high)
That eliminates the long USB run entirely and keeps the lossy coax minimal — which is what actually improves decode rate and range.
Minimum specs for ultrafeeder on a Pi #
Ultrafeeder bundles readsb + tar1090 + mlat-client + graphs1090 in Docker. The decoder is light; MLAT and the web stack add a bit.
| Tier | Board | Verdict |
|---|---|---|
| Won’t work | Pi Zero W, Pi 1/2 | single-core / too slow; readsb + Docker chokes |
| Bare minimum | Pi Zero 2 W (1 GB) | works, but MLAT in busy airspace can peg it; disable graphs1090 |
| Recommended | Pi 3B+ / Pi 4 (2 GB) | comfortable headroom for readsb + MLAT + web UI |
| Ideal | Pi 4 (2 GB+) | plenty of margin; run everything |
readsb idles around 50–150 MB RAM and a fraction of a core; the dongle decode is the main CPU draw. In busy airspace and running MLAT, I’d go Pi 3B+ minimum. 1 GB RAM is fine.
Two things that bite outdoors:
- SD card wear. ultrafeeder maps
globe_historyandgraphs1090(collectd) to disk — constant writes. Use a good A1/A2 card, or better, boot from a small USB SSD. (Logs are already on tmpfs in the default config, which helps.) - Power. Pi + RTL-SDR pulls ~1–1.5 A peak. Use a real 5V/3A supply with thick conductors; brownouts cause silent dropouts and SD corruption.
The dongle matters more than you’d think (especially for MLAT) #
I started with the classic $15 generic stick — RTL2832U + R820T, the one with the suction-cup telescopic whip. Good news: it decodes 1090 MHz ADS-B fine; the R820T reaches ~1.7 GHz and the RTL2832U is the standard ADS-B chip. It’s perfect for proving the pipeline today. But for a permanent outdoor, mast-mounted, MLAT install it has three real weak spots:
| Weakness | Why it matters for an outdoor MLAT feeder |
|---|---|
| No 1090 MHz filter | Wideband front end desenses on strong FM/cell/pager signals in a metro → fewer decodes |
| No TCXO (frequency drift) | Generic R820T sticks drift 30–50+ ppm, and the drift changes with temperature. Outdoors, an enclosure that bakes by day and cools at night makes the frequency wander — and that breaks MLAT sync, which needs a stable clock. This is the big one. |
| Useless bundled antenna | The telescopic whip is tuned for DVB-T (VHF/UHF TV), far too long for 1090 MHz. A proper 1090 antenna is a ~2.7″ (69 mm) quarter-wave. |
So the plan: prove the whole chain with the cheap stick indoors (readsb → tar1090 → PiAware relay → FlightAware), set READSB_GAIN to a fixed value rather than auto (generic dongles often decode better with fixed gain), and then — before it goes up the mast — spend ~$30–50 on the two weak links: a TCXO dongle (RTL-SDR Blog v3 with its 1 ppm TCXO and bias-tee, or a FlightAware Pro Stick Plus with a built-in 1090 filter) plus a real 1090 antenna. The TCXO isn’t polish; it’s what keeps MLAT locked as the temperature swings. And the antenna is the single biggest factor in range.
Config changes when you relocate #
Right now ultrafeeder runs on the server (.21) and PiAware relays from BEASTHOST=192.168.1.21. Moving ultrafeeder to the Pi:
- Move the ultrafeeder compose to the Pi (it already does
/dev/bus/usbpassthrough withREADSB_DEVICE_TYPE=rtlsdr, so it claims the dongle directly). - Either run PiAware on the Pi too, or keep the PiAware relay on
.21and pointBEASTHOSTat the Pi’s new IP (ultrafeeder exposes Beast on30005). - Update
FEEDER_ALT_FT/ lat / lon in the Pi’s.envto the new antenna location — it matters for MLAT geometry, per the altitude detour above.
If you go Wi-Fi instead of PoE, pin the Pi with a DHCP reservation. But for a 24/7 feeder I’d choose PoE every time — one weatherproof run solves power and data, and it won’t drop out.
Takeaways #
- ADS-B altitude is MSL barometric, not AGL. At a mile-high airport a landed aircraft reads ~5,900+ ft and that’s correct. The antenna-height setting only feeds MLAT, not the displayed altitude.
- Move data, not RF. Put the Pi and SDR in one box up high; run network/PoE down, not long USB or coax.
- For outdoor MLAT, a TCXO dongle is mandatory, because temperature-driven frequency drift breaks MLAT sync.
- Prove the pipeline on the cheap stick first, then upgrade the antenna and dongle before the mast install.