A Raspberry Pi that watches the aircraft around your airport and shows the single most relevant flight on an RGB LED matrix — callsign, route (FROM › TO), a direction arrow telling you where to look, altitude, and time-to-airport — so when you hear a plane you glance at the panel and know what it is. It also serves a live map web UI (with an aviation-chart overlay), keeps flight history, and can play tower-comms audio as a plane arrives.
Everything is configured for your location at runtime — set your airport's ICAO code and the receiver's position, and the airport coordinates, runways, watch sector and map all follow. No code is specific to any one airport.
Runs as a few Docker containers on balenaOS, managed from a small web UI.
ANTENNAS (window/balcony) SDRs (USB) Raspberry Pi · balenaOS · docker-compose
┌────────────────────────┐ 1090 ┌────────────┐
│ 1090 MHz omni │─────►│ Airspy Mini│─┐
└────────────────────────┘ └────────────┘ │ ┌────────────────────────────────┐
┌────────────────────────┐ 118– ┌────────────┐ ├─►│ ultrafeeder ─► app ──► display │
│ airband dipole (opt.) │─137─►│ RTL-SDR │─┘ │ (decode) (enrich·filter· │
└────────────────────────┘ MHz └────────────┘ │ gpsd ──► pick the plane·UI) │
│ rtl_airband ─► USB audio │
└────────┼────────────┼──────────┘
HUB75+5V│ │USB
┌──────▼───────┐ ┌──▼────┐
│ 64×32 panel │ │🔊 spkr│
└──────────────┘ └───────┘
- Picks the one flight you'd notice — nearest/lowest in a configurable watch sector, with hysteresis so it doesn't flicker between planes.
- Route + enrichment — FROM › TO city pair, aircraft type, operator, registration, military flag, looked up from open route APIs and cached.
- Runway awareness — infers the active landing runway from ADS-B tracks and tells you whether an arrival passes your window or lands on the far side. Runway geometry is resolved at runtime for your airport (no hardcoded tables).
- Live map web UI — Leaflet map (vendored — works with no internet) with aircraft, trails, your watch sector, runways + extended centerlines, a Weather (METAR) layer showing airport wind + the head/cross-wind on each runway (which one the wind favours), a Flights (recent) layer with the last few hours' observed arrivals/departures, an ADS-B coverage layer (max reception range per bearing from your history, with presets + custom date ranges — compare antenna placements), and toggleable Airways / Navaids / Fixes overlays.
- Flight history — every observed flight saved to SQLite; browse, filter, and replay tracks, plus a Stats card (busiest hours, per-day counts, runway split, top operators/types).
- Tower-comms audio (optional) — a 2nd SDR runs
rtl_airbandover your airport's VHF frequencies and plays it to a USB speaker. - LED panel — a 64×32 HUB75 panel with several layouts, tunable live from the UI. Idle faces include a flip clock, a METAR weather face (wind/temp/QNH), or both alternating; quiet hours dim (or switch off) the panel on a nightly schedule.
| Container | What it does |
|---|---|
airspy |
Decodes ADS-B from the Airspy SDR and forwards Beast messages to ultrafeeder. |
ultrafeeder |
sdr-enthusiasts image — computes positions and publishes a live map (tar1090) + aircraft.json. |
app |
The brain + web UI (FastAPI). Polls aircraft.json, computes distance/bearing from your location, looks up routes, picks the featured flight, serves the UI/REST/WebSocket, and resolves your airport's coords + runways from OurAirports at startup. |
display |
Renders the featured flight on the LED panel (hzeller rpi-rgb-led-matrix, PIO backend on the Pi 5). |
gpsd |
(optional) Live receiver location from a USB GPS. |
airband, airband-speaker |
(optional) VHF tower audio via a 2nd SDR + USB sound card. |
Approximate USD; every part is sold worldwide. See docs/HARDWARE.md for a detailed buying guide with specific products, caveats, and example retailer links.
Core build
| Part | Role | Qty | ~USD |
|---|---|---|---|
| Raspberry Pi 5 (2 GB+) | The computer | 1 | 60–80 |
| Airspy Mini SDR | 1090 MHz ADS-B receiver (great overload handling at close range) | 1 | 100–130 |
| 1090 MHz omni antenna + coax | Receives aircraft | 1 | 40–70 |
| Adafruit 64×32 RGB LED matrix (P4) | The display | 1 | 40 |
| Adafruit RGB Matrix Bonnet | Drives the panel from the Pi | 1 | 20 |
| 5 V / 10 A PSU (e.g. Mean Well LRS‑50‑5) | Powers the panel | 1 | 13 |
| Official Pi 5 27 W USB‑C PSU | Powers the Pi (full USB budget for the SDRs) | 1 | 12 |
| microSD 32 GB+ (A2) | Boots balenaOS | 1 | 9 |
| Core subtotal | ~300 |
Optional add‑ons
| Part | Role | ~USD |
|---|---|---|
| VK‑162 USB GPS (u‑blox) | Auto‑locates the receiver (else set lat/lon by hand) | 15 |
| RTL‑SDR Blog V3 kit (dongle + dipole) | 2nd SDR for VHF tower comms | 35 |
| USB sound card + small powered speaker | Audio out (onboard audio is taken by the panel) | 25 |
A budget build (RTL‑SDR instead of Airspy, a basic dipole antenna, no airband/GPS extras) runs closer to ~$120. Want a wider display? Chain a 2nd 64×32 panel and set
MATRIX_CHAIN=2.
You need a free balenaCloud account (or any way to run
docker-compose on the Pi).
-
Create a fleet in balenaCloud (Raspberry Pi 5 / 64‑bit), add a device, and flash the downloaded balenaOS image to the microSD (balenaEtcher).
-
Set variables (Device/Fleet → Variables) — optional: everything here can also be set in the web UI. Any variable you set overrides the matching UI field (which then greys out with a 🔒). The common ones to get started:
Variable Example Meaning HOME_AIRPORTKSEAYour airport's ICAO — coords + runways are resolved from it. READSB_LAT/READSB_LON47.45/-122.31Receiver position (fallback if no GPS). READSB_ALT40mReceiver altitude. TZAmerica/Los_AngelesYour timezone. The full list — including how to pin the airport coordinates and route API — is in Environment variables below.
-
Deploy: clone this repo and
balena push <you>/<fleet>(or wire up the GitHub Action below). The containers build and start. -
Configure in the browser: open
http://<device-ip>and set your watch sector, display layout, traffic mode, and (optional) airband frequencies. Everything is live. -
Aviation overlay: the Airways/Navaids/Fixes layers build themselves on first boot from your configured airport (nothing to do) — details below.
- Panels are powered from the 5 V PSU directly (not through the Pi). Keep leads short/thick.
- On the Pi 5, the panel uses the hardware PIO backend; no GPIO solder mod is needed.
- Two SDRs draw real current — use the 27 W PSU (or a powered USB hub) to avoid brownouts.
- Full wiring, antenna placement, and connector notes are in docs/HARDWARE.md.
The Airways / Navaids / Fixes layers are generated automatically at runtime from your
configured airport: on startup the app fetches open navdata once, caches it to /config, and
serves the overlay — no API key, no committed per-airport data, nothing to configure. (To
pre-generate it offline instead — e.g. a device with no internet — run the same builder locally;
it writes app/static/navdata.json:)
python3 tools/build_navdata.py KSEA # by ICAO
python3 tools/build_navdata.py --lat 47.45 --lon -122.31Source data is the open X‑Plane navdata (~2012 cycle), so a few recent terminal waypoints may differ from current charts; the enroute airway network is stable.
The map's Flights layer lists the airport's arrivals/departures. By default it uses OpenSky (free, no key) — recent observed traffic from the last few hours. For a real scheduled timetable (airline, flight number, scheduled time, status), switch it to AeroDataBox:
- Sign up at RapidAPI and subscribe to AeroDataBox — the free Basic plan is plenty.
- Copy your RapidAPI key (the
X-RapidAPI-Keyvalue shown on the endpoint page). - Set it as the
FLIGHTS_API_KEYvariable (Device/Fleet → Variables). The container restarts and the layer switches to scheduled data (its header then reads "scheduled").
Usage is light: one airport call per refresh returns every arrival and departure at once (never one-per-aircraft) and the schedule is cached ~3 h — the route-correction below consults it around the clock, so the cadence is tuned to stay inside the Basic plan's monthly API-unit quota. If the quota is ever exhausted anyway, the app parks AeroDataBox and falls back to OpenSky (the box header then reads "observed (ADS-B), not a schedule") until the quota resets.
The schedule also corrects the featured flight's route: community callsign→route data (adsbdb) can be stale when airlines reuse callsigns, so when the featured flight matches a scheduled arrival/departure at your airport, the timetable's city pair wins over the community answer.
The map's Airspace layer draws the controlled/restricted airspace around your airport (CTR, TMA, danger/restricted areas) from OpenAIP. It needs a free key:
- Create a free account at OpenAIP (→ accounts.openaip.net).
- Generate an API key in your account settings.
- Set it as the
OPENAIP_API_KEYvariable (Device/Fleet → Variables).
Then toggle Airspace in the map's layers menu — controlled airspace is blue, restricted/ danger/prohibited is red; click any zone for its class + altitude limits. (Airspaces barely change, so it's cached for hours — negligible API use.)
.github/workflows/balena-deploy.yml pushes a release on
every push to main. In your repo settings add:
- Secret
BALENA_TOKEN— a balenaCloud API key. - Variable
BALENA_FLEET— your fleet slug, e.g.youruser/flight-tracker.
Every app setting can be configured in the web UI and is stored on the device. Setting the matching environment variable overrides the UI for that field — the UI then shows it read-only (greyed out, 🔒). Leave a variable unset to manage it from the browser. Most people set none of these beyond the getting-started ones and configure everything in the UI.
App configuration — override the corresponding UI field when set:
| Variable | UI field | Meaning |
|---|---|---|
HOME_AIRPORT |
Home airport | Airport ICAO; coords + runways resolve from OurAirports. |
READSB_LAT, READSB_LON |
Receiver lat / lon | Receiver position (live GPS overrides this when enabled). RECEIVER_LAT / RECEIVER_LON are also accepted. |
AIRPORT_LAT, AIRPORT_LON, AIRPORT_ELEV_FT |
Airport lat / lon | Pin the airport location/elevation instead of resolving it from the ICAO. |
ROUTE_API |
— | Route-enrichment source (adsbdb, the default). |
FLIGHTS_API_KEY |
— | RapidAPI key for AeroDataBox → the map's Flights layer shows scheduled arrivals/departures. Without it, that layer falls back to OpenSky's recent observed traffic (free, no key). |
OPENAIP_API_KEY |
— | OpenAIP API key → enables the Airspace map layer (CTR/TMA/restricted around your airport). Free. |
READSB_ALT |
— | Receiver altitude (used by the decoder). |
TZ |
— | Timezone, e.g. America/Los_Angeles. |
Deploy (GitHub Action — see below):
| Variable | Meaning |
|---|---|
BALENA_TOKEN (secret) |
balenaCloud API key. |
BALENA_FLEET |
Fleet slug, e.g. youruser/flight-tracker. |
Advanced / internal (sensible defaults; rarely changed): SHOW_PANEL_TUNING (set to reveal
the LED Panel tuning card — the PWM/flicker knobs, hidden by default), POLL_SECONDS,
AIRCRAFT_JSON_URL (point the app at a tar1090 feed for local dev), GPSD_HOST / GPSD_PORT,
CONFIG_PATH, NAVDATA_PATH, HISTORY_DB_PATH / HISTORY_POSITION_DAYS, plus the airband/watchdog
service knobs. See .env.example for the annotated full set.
The app service runs without any hardware (it just needs an aircraft.json feed):
cd app
pip install -r requirements.txt
HOME_AIRPORT=KSEA AIRCRAFT_JSON_URL=http://<a-tar1090-host>/data/aircraft.json \
uvicorn app.main:app --reload --port 8080
# open http://localhost:8080Copy .env.example to .env for the full set of variables.
| Path | What |
|---|---|
app/ |
FastAPI brain + web UI: poll → enrich → pick the featured flight → REST/WS. |
display/ |
LED-matrix renderer (subscribes to app). |
airband/ |
rtl_airband config template for the tower-comms SDR. |
tools/build_navdata.py |
Generates the map aviation overlay for an airport. |
docker-compose.yml |
The balena multi-container stack. |
.env.example |
Configuration template. |
docs/ |
SPEC · ARCHITECTURE · HARDWARE · PRIOR-ART. |
- OurAirports — airport + runway data (public domain).
- X‑Plane navdata (ptsmonteiro/x-plane-navdata, GPL‑3.0) — airways/navaids/fixes overlay (generated locally; not redistributed here).
- hzeller/rpi-rgb-led-matrix — LED panel driver.
- sdr-enthusiasts — the ultrafeeder, airspy_adsb, and rtl_airband container images.
- Route enrichment via adsbdb / adsb.lol.
- aviationweather.gov — airport METAR (public domain).
- OpenSky Network — recent arrivals/departures (observed, not scheduled).
⚠️ Airband legality is per‑country — listening to aviation radio is restricted in some places (e.g. Germany). Check your local rules before enabling the tower-comms feature.
MIT © flight-tracker contributors.