← Back to Blog
Aviation Apr 16, 2026

Aircraft Over Home

How the /aircraft page turns dump1090-fa data from a Raspberry Pi SDR receiver into a live local traffic map.

Aircraft Over Home

One of my favorite pages on this site is [aircraft](/aircraft). It is a small, focused view of the sky above my house: live aircraft positions, headings, altitudes, distance from home, recent connection events, and a quick compare link out to Flightradar24. Under the hood, though, it is doing something more interesting than rendering a map. It is taking raw ADS-B data collected by a Raspberry Pi and an SDR, pushing that data through a Rails Action Cable channel, and turning the result into a live browser experience.

The Radio Side

The radio work does not happen in the website at all. The SDR is attached to a Raspberry Pi running dump1090-fa, which is the component that actually listens for 1090 MHz ADS-B transmissions and decodes them. That decoder writes its rolling aircraft state into aircraft.json, and in my setup the stable integration point is /run/dump1090-fa/aircraft.json.

That separation matters. I did not want the web app talking to SDR hardware directly, and I did not want the publisher process re-implementing ADS-B decoding. dump1090-fa stays responsible for RF ingest and message decoding. Everything else downstream just consumes its JSON output.

The Pi Publisher

The bridge between the Pi and the site lives in a separate Ruby service called adsb_push. It is intentionally small. On startup it loads a few environment variables, auto-detects the local aircraft.json path if one is not configured explicitly, and constructs a websocket URL for the Rails API by appending an ingest_token to the Action Cable endpoint.

Raspberry Pi with SDR

From there, the service runs a tight one-second loop:

  1. Read the latest aircraft.json.
  2. Normalize the aircraft rows into a smaller payload.
  3. Drop stale aircraft that have not been seen recently.
  4. Deduplicate entries by hex code, keeping the freshest row.
  5. Publish one snapshot over Action Cable.

The normalized payload is compact and purpose-built for the site. Each aircraft can include fields like hex, callsign, latitude, longitude, altitude, groundspeed, track, vertical rate, squawk, emergency status, and seconds since last seen. The publisher also adds generated_at, interval_ms, and the home coordinates for the receiver. That makes each frame self-describing and easy for the frontend to consume.

Another detail I like is that adsb_push behaves like infrastructure instead of a script. It is built to reconnect on websocket failures, backs off when the cable connection is unavailable, and can run under systemd as a long-lived service on the Pi. That means the SDR decoder and the web publisher can fail and recover independently.

The Rails Realtime Layer

On the server side, the Faceted Thought API exposes an Action Cable endpoint and an AdsbChannel. The connection layer distinguishes between two roles:

  • A publisher connects with the private ingest_token.
  • A viewer connects with the public-side viewer_token.

When the Pi publisher sends a publish_snapshot action, the channel normalizes the incoming snapshot one more time and rebroadcasts it on a shared stream for connected viewers. That rebroadcast step is useful because it gives the API a single place to enforce the payload shape seen by browsers.

The public site also exposes /api/v1/site_config, which is how the /aircraft page learns the cable path, channel name, snapshot interval, and viewer token. That keeps the page configuration dynamic and avoids hard-coding the websocket auth token into the frontend source.

The Browser Side

In the public React app, the /aircraft page fetches site_config, opens an Action Cable subscription, and listens for snapshot messages. When a new snapshot arrives, the client normalizes the aircraft list, calculates each aircraft’s distance from home, and updates a short trail history so the map shows movement instead of just blinking points.

There are a few implementation details that make the page feel better than a raw feed dump:

  • Live trails are capped so the client does not grow memory forever.
  • A timeout drops the page out of live mode if snapshots stop arriving.
  • A demo mode can synthesize aircraft and trails when no live feed is available.
  • A debug panel shows connection state, payload counts, event timing, and the effective cable URL.
  • A “Closest traffic” list surfaces the aircraft nearest to home without making you parse the map first.

The map itself uses Mapbox, custom aircraft markers, a home marker labeled “Receiver,” and smoothed trail lines so the motion reads clearly. The end result is a page that feels more like a local radar console than a generic embedded map.

Why I Built It This Way

The architecture is simple on purpose:

  • dump1090-fa handles the radio and decoding.
  • adsb_push handles local file reads, cleanup, and websocket publishing.
  • Rails handles token-gated realtime fan-out.
  • React handles rendering, interaction, and debugging.

That separation keeps each piece understandable and replaceable. If I ever swap decoders, the publisher can still target aircraft.json. If I change the frontend, the Pi-side pipeline stays the same. And if I want to inspect what the browser is seeing, the payload moving through Action Cable is already normalized and compact.

The page is also useful beyond being a fun side project. It gives me a live end-to-end check that the SDR is healthy, the Pi service is still reading dump1090-fa, the cable connection is alive, and the browser is receiving fresh snapshots. When all of those layers line up, /aircraft becomes a real-time status board for the whole chain.

Compare Against The Big Feed

I recently added a compare link from the page out to Flightradar24. That is not there to replace my local receiver. It is there as a sanity check. If I see traffic on my page and similar traffic in the same area on Flightradar24, I know the local pipeline is behaving. If the public feed shows planes that my page does not, it usually means I should inspect the Pi, the SDR, or the live cable path rather than the map UI.

That is probably my favorite part of the whole project: the /aircraft page is personal infrastructure. It is not just “an aviation page.” It is my receiver, my Pi, my websocket pipeline, and my frontend all visible in one place.