Skip to content
Brad Johnson, home
All words · 3 min read

Mapping the Planes Over My House

Tags:#ads-b#homelab

Updated

Update, November 6: Raken has been offline since November 3, when I reinstalled matrim as a host for stedding sandboxes. This is how it worked while it ran.

Every airliner and plenty of small planes broadcast their position, altitude, speed and callsign about twice a second on 1090 MHz. It’s called ADS-B, it isn’t encrypted, and a cheap RTL-SDR USB dongle can hear it. Mine has an antenna in a window and is plugged into matrim, a NixOS box on my homelab. On the morning of October 14 it was tracking 30 aircraft around Boston.

dump1090-fa decodes the signal and ships with a map page called SkyAware. Raken is my replacement for it.

Raken showing 30 aircraft around eastern Massachusetts on a dark map with 25 and 50 nautical mile range rings, colored by altitude, with trails from the last hour and a table of aircraft sorted by distance.

That’s live traffic from that morning. The one change for this post is that the home marker and range rings sit on Billerica’s town center instead of my actual receiver.

No backend

dump1090-fa already writes everything Raken needs to /run/dump1090-fa as JSON:

  • receiver.json: the receiver’s location, how often aircraft.json updates and how many history files there are.
  • aircraft.json: every aircraft heard recently, rewritten once a second.
  • history_0.json through history_119.json: snapshots going back about an hour.

So Raken has no server of its own. It’s static files, built by TanStack Start with the one route prerendered as a loading shell, and nginx on matrim serves them next to dump1090-fa’s JSON on the same origin. On load, the page reads the history files to build trails, then polls aircraft.json at whatever rate receiver.json says. The receiver’s location comes from receiver.json too, so no coordinates live in the repo.

Trails take a little care. The same position report shows up in consecutive snapshots until a new one gets decoded, so duplicates get dropped. A position more than 60 seconds old is a leftover, not a current report. And if an aircraft goes quiet for more than five minutes, the trail breaks there instead of drawing a straight line across the gap where it left coverage and came back.

ADS-B data is messy, too. In that screenshot, A1F8ED is a Piper Archer reporting 114,600 feet, more than eight times its service ceiling. Raken shows what the receiver decoded and doesn’t pretend to know better.

One flight

AAL802 selected: a Boeing 737-800 from ORD to BOS with its ETA, altitude 7,650 ft descending at 1,152 ft/min, ground speed, track, distance, squawk, signal and message counts, with its trail highlighted on the map.

Registration and aircraft type come from the database that ships with dump1090-fa. It was derived from Virtual Radar Server’s database and isn’t updated anymore, and the detail panel says so. Airport codes become names from OurAirports, which is public domain. That table covers about 11,800 airports and is about 230 kB gzipped, so it’s split into its own chunk and only loads the first time a route is shown.

Color

Altitude is the main thing you read off the map, so it gets the color. The scale runs teal near the ground through blue and violet to pink at cruise, defined in OKLCH with lightness rising a little with altitude, so the order still reads for someone who can’t tell the hues apart. Red, amber and green stay out of it on purpose, because those mean emergency and status elsewhere on the page. An aircraft squawking 7700 gets an Emergency label in the table.

Deploying with Nix

matrim is NixOS, so Raken is a Nix package. package.nix builds the static output with pnpm, and the homelab repo’s matrim config fetches Raken at a pinned commit and serves it with nginx. The one chore is the pnpmDeps hash. When the lockfile changes, I set it to lib.fakeHash, build once, and copy the real hash out of the error message. In exchange, the box only ever runs a build I can reproduce.