Harshwardhan's notes Print. Code. Lift.

← All notes

AboveUs: the flight above our living room, on e-ink

raspberry pie-inkhardwareaviation

My girlfriend and I are the kind of people who hear a heavy departure and look up before we finish the sentence. Full AvGeeks, plane spotters, pick your label. When we bought an apartment, the deciding feature was not the kitchen. It was the view: a high rise overlooking Schiphol, one of the busiest hubs in Europe, with approach traffic sliding past the windows all day.

Our routine it simple. A plane crosses the window, one of us grabs a phone, opens Flightradar24, zooms, taps, waits. Twenty seconds later: KLM to Rome, fine, back to dinner. We did this an embarrassing number of times a day. At some point it clicked that the answer to “what is that one” should simply be on the display, around the clock, without anyone touching a phone.

Commercial products for this exist. Flightwall is the well known one and it looks great, but the price tag for what is essentially a small computer with a screen did not impress me. Meanwhile my homelab inventory had a spare Raspberry Pi Zero 2 W and a Pimoroni Inky Impression 7.3”, a seven colour e-ink panel left over from an old project. So the budget was zero and the project got a name: AboveUs.

The card AboveUs renders: flight KL 1613, KLM Royal Dutch Airlines, route AMS to FCO with a plane riding the connecting line, aircraft type, registration and a row of live stats Actual AboveUs display

AboveUs in action

Why e-ink and not an old tablet

A living room display has constraints a desk gadget does not. It has to survive daylight without glare, it must not glow at night, and it should look like it belongs on a display rather than in a server rack. E-ink wins on all three. It reads like a print, it is invisible in a dark room, and it holds its image with the power off.

The usual complaint about e-ink is refresh speed, and the Impression is genuinely slow: a full colour cycle takes 30 to 40 seconds. For this use case that is irrelevant. The closest aircraft overhead changes every few minutes, not sixty times a second. Slow refresh is not a defect here, it is the reason the whole thing can run on a Pi Zero and draw about as much power as a phone charger left in the socket.

Where the data comes from

Every airliner constantly broadcasts its position, altitude, speed and identity on 1090 MHz. This is ADS-B, and thousands of volunteers around the world receive it with cheap SDR dongles and feed the results to community aggregators. Three of those aggregators expose free JSON APIs with no key and no signup:

  • adsb.lol, the primary source
  • airplanes.live and adsb.fi as automatic failover

One GET request returns every aircraft within a radius of your coordinates:

GET https://api.adsb.lol/v2/point/{lat}/{lon}/{radius_nm}

The response carries the hex transponder code, callsign, barometric altitude, ground speed, track, squawk and position. AboveUs polls this every 45 seconds, filters out anything on the ground or outside a sane altitude band, and picks the closest airborne aircraft within 15 nautical miles.

The raw feed is not enough for a nice display though. The callsign comes back as KLM1613, which no passenger would recognise, and there is no route in it. A second free service, adsbdb.com, fills the gaps: callsign in, airline name, origin and destination with city names, and the IATA style flight number out. A separate endpoint maps the hex code to registration and aircraft type. Both lookups are cached in memory for 24 hours, and unknown callsigns are negative cached too, because business jets and military traffic will 404 forever and there is no reason to keep asking.

The architecture

      aircraft overhead
             |
             |  ADS-B broadcasts on 1090 MHz
             v
   volunteer ground stations
             |
             v
   community aggregators
   adsb.lol -> airplanes.live -> adsb.fi     (tried in order)
             |
             |  GET /v2/point/{lat}/{lon}/{radius}
             v
   +------------------------------------------------+
   |  AboveUs on a Pi Zero 2 W                      |
   |                                                |
   |  poll every 45 s --> pick closest aircraft     |
   |         |                                      |
   |         v                                      |
   |  enrich via adsbdb.com                         |
   |  (route, airline, registration, type)          |
   |  24 h cache, 404s negative cached              |
   |         |                                      |
   |         v                                      |
   |  airline logo from CDN, cached on disk         |
   |         |                                      |
   |         v                                      |
   |  Pillow renders an 800x480 card                |
   |         |                                      |
   |         |  only when the flight changed        |
   |         v                                      |
   |  Inky Impression 7.3" e-ink panel              |
   +------------------------------------------------+

No API keys anywhere in that picture. That was a hard requirement, partly on principle and partly because keyed flight APIs charge per call and this thing makes about two thousand calls a day.

Deciding when to refresh

This turned out to be the actual engineering in the project. Polling is cheap, refreshing is not, so the two are decoupled. Every flight gets an identity:

def signature(self):
    return (self.hex, self.callsign, self.origin_iata, self.dest_iata)

The panel refreshes only when the signature of the closest aircraft changes, never more often than every 150 seconds, and at least once every 10 minutes as a heartbeat so a stale card cannot sit there for an hour. After three empty polls the display drops to an idle screen that says ALL QUIET OVERHEAD with the last aircraft seen, which under the Schiphol approaches is a rare sight. Three failed fetches in a row and it says so honestly instead of showing yesterday’s flight. There is also an optional quiet hours window, because an e-ink refresh in a silent living room at 2 AM has a faint crackle that you will absolutely notice.

Rendering and the small stuff

The card is drawn with Pillow directly, no browser, no HTML, nothing that would make a Pi Zero sweat. The layout is designed at 800x480 and scales itself with a single factor to smaller panels, with a separate compact layout below 320 pixels wide for the tiny Inky pHAT. Everything is pure white, near black and one accent colour, chosen so the image quantises cleanly whether the panel has seven colours or three. The little aircraft silhouette riding the route line is a hand plotted polygon, rendered at four times the size and downsampled so e-ink does not turn it into a staircase.

Airline logos were a late addition. adsbdb already returns the two letter IATA code, and a couple of long lived community CDNs serve logos by that code. Each one is downloaded once, stored on the SD card and reused forever. A confirmed 404 is remembered for a week, a network hiccup only for an hour, and if no logo can be had the card shows a monogram tile with the airline code instead, so the layout never breaks. On three colour panels the logo is thresholded to pure black and white, with a sanity check that falls back to the monogram when a logo does not survive the conversion.

If any aircraft squawks 7500, 7600 or 7700, the airline line is replaced by an emergency banner. In months of watching I hope it never renders outside of my test suite.

One gotcha worth writing down: on Raspberry Pi OS Bookworm the kernel claims the SPI chip select pin that the Inky library needs, and the fix is a single line, dtoverlay=spi0-0cs in /boot/firmware/config.txt. It cost me more time than the refresh logic.

Where it landed

AboveUs now runs as a systemd service that restarts itself, survives power cuts, and has been quietly doing its job on the display ever since. Total spend: nothing, everything came out of the drawer. Total dependencies: three Python packages and the goodwill of the ADS-B volunteer community, whose aggregators deserve a donation or, better, your own receiver feeding them.

The acceptance test was never technical but the day my girlfriend stops reaching for her phone and starts glancing at the display instead.