
I wanted to know what was flying over my head. Not on a phone, not in a browser tab – on a small round thing sitting on a shelf that I could glance at.
The result is a two-screen device. You swipe between them.
The first screen is the radar. Aircraft appear as winged arrows pointing the way they are actually going, over an outline of the surrounding countries and their cities. Tap one and a panel opens with the details. Long-press anywhere to change the range – 10, 25, 50 or 100 km.
The second screen is settings: brightness, WiFi status, location, and a button that opens the configuration portal.
That is the whole thing. It deliberately does not try to be a phone.

Aircraft are coloured by altitude
This turned out to be the single change that made the radar readable at a glance. A screen full of identical yellow triangles tells you there is traffic. A screen where the colour means something tells you what kind of traffic.
| Altitude | Colour | Typically |
|---|---|---|
| Below 2 km | Red | Approach and departure, helicopters, light aircraft |
| 2-6 km | Orange | Climb, descent, regional traffic |
| 6-10 km | Yellow | Lower cruise levels |
| 10 km and above | Blue | Long-haul cruise |
Red overhead means something is coming down to land near you. A screen of blue means you are under a corridor and everyone is just passing through at cruise.

One detail that matters more than it sounds: an aircraft that reports no altitude is drawn grey, not red. If you fold “unknown” into the lowest band, every target with a missing field turns an alarming red and the colour stops meaning anything. Unknown should look unknown.
The bands are also contiguous on purpose.
The map is the whole of Europe, but it draws almost nothing
The underlay covers EU-27 plus the UK, Switzerland and Norway: about 31,000 border vertices and 1,100 cities, taken from Natural Earth (public domain) and GeoNames (CC BY).

Two things had to be true for that to be practical on a microcontroller.
It has to fit. Coordinates are stored as uint16 fixed point rather than floats, which halves the cost. The quantisation step works out at about 122 m – far below the ~555 m tolerance the outlines were simplified to, so it costs nothing you can see. Total: about 170 kB of flash, roughly 5% of the APP partition. That is measured, not estimated – I compiled the data and read the section size.
It has to be fast. Drawing 31,000 points every frame would be hopeless. So the screen works out which geographic window can actually be on the display and hands that to the map module, which rejects whole countries by bounding box before it touches the framebuffer. In practice fewer than ~300 line segments survive per frame anywhere in Europe. The map is effectively free.
Cities are tiered by population so dense regions stay legible. At 100 km only cities above 300k are drawn; at 50 km, above 150k; below 25 km, everything down to 50k. Without that, the Ruhr or the Randstad buries the traffic under a wall of labels.
Three things I got wrong on the way
The interesting part of a project like this is rarely the part that works.
GeoNames thinks Žižkov is a city
Filtering GeoNames to “places above 50,000 people” gives you Prague – and also Stodůlky, Žižkov and Chodov. Those are Prague districts. The same happens everywhere: Hamburg-Mitte, Bucharest’s “Sector 3”, the City of Westminster. All would have rendered as phantom cities stacked on top of the real one.
The fix is geometric rather than editorial. Any place sitting within 12 km of a city at least three times larger is treated as a district of it and dropped. That removed 397 entries and left 1,100 genuine cities. It is not perfect – it also absorbs a few legitimate municipalities like L’Hospitalet next to Barcelona – but at these zoom levels their labels would have collided anyway.
The selected aircraft kept changing under my finger
This one is my favourite, because it looked like a display glitch and was actually a data-modelling mistake.
Tap an aircraft, leave the detail open, and after about five seconds it would silently show a different aircraft.

The reason: the selection was stored as an index into the list. The list is rebuilt from scratch on every fetch, and adsb.fi makes no promise about ordering. One aircraft leaves the area, everything after it shifts up, and index 7 is now somebody else entirely. The detail panel dutifully showed whoever had inherited the slot.
The fix is to key the selection on the ICAO hex address – the 24-bit identifier burned into the airframe, which does not change between fetches. The panel now either follows the aircraft you actually picked, or closes when it genuinely leaves the area.
Two things fell out of that for free. The detail panel now updates live while it is open – altitude and speed tick over as new data arrives, because every frame re-resolves the identity against the latest fetch. And closing the panel became deliberate: the old code only closed it when the stale index happened to run off the end of the array, which is to say it worked by luck.
An 84 km surprise
After wiring up the European map I tested it centred on Prague and got no border at all at 10, 25 or 50 km. Just city dots on black. It looked exactly like the culling had over-pruned.
It had not. Prague is 84 km from the nearest national border. At those ranges there is genuinely nothing to draw.
Worth knowing, because it will look like a fault to anyone inland. It is not – switch to 100 km and the outline appears.
Hardware
One board: Waveshare ESP32-S3-Touch-LCD-2.1.
- ESP32-S3R8 – 8 MB PSRAM, 16 MB flash
- Round IPS display, 480×480, ST7701 controller over an RGB interface
- CST820 capacitive touch
- TCA9554 I/O expander driving LCD reset, CS and power
The display and the microcontroller are on the same board. There is nothing to wire.
Two things that will bite you in the Arduino IDE
Both cost me time, so:
PSRAM must be set to OPI PSRAM. Tools → PSRAM → OPI PSRAM. Get this wrong and the display stays black with no other symptom. It is far and away the most common cause of “it doesn’t work”.
LovyanGFX will not drive this panel easily. The ST7701 runs over an RGB interface and its chip-select hangs off the I2C expander, which the library does not handle out of the box. I used Arduino_GFX on top of a direct esp_lcd RGB panel with a hand-written init sequence. Note that the library you want is “GFX Library for Arduino” by moononournation – not Adafruit GFX.
One more, less obvious: drawing straight into the panel makes the display flicker on every redraw. The whole frame is instead composed into an off-screen canvas in PSRAM and pushed out in a single flush().
Setup
Flash it, and the device brings up an open WiFi network called PlaneRadar-Setup and shows a QR code on screen. Scan it with a phone, pick your home network, save.
You do not have to type in a location – it is detected from your IP address. If you enter one manually, that always wins.
Where the data comes from
| Data | Source | Note |
|---|---|---|
| Aircraft | adsb.fi | Free, no API key, personal use |
| Location | ip-api.com | Automatic detection by IP |
| Borders | Natural Earth 1:10m | Public domain |
| Cities | GeoNames | CC BY 4.0, population 50,000+ |
You do not need your own ADS-B receiver. adsb.fi aggregates feeds from thousands of volunteers, and it is free without registration – but credit it if you use it, and note that the free API is intended for personal, non-commercial use. GeoNames is CC BY, which means attribution is a licence condition rather than a courtesy.
Standing on other people’s shoulders
This did not appear out of nowhere. It builds on two existing projects:
- MatixYo/ESP32-Plane-Radar – the original aircraft radar and the adsb.fi source
- Selbyl/ESP32-S30Touch-LCD-2.1_Plane-Radar – the port to the Waveshare 480×480
The code
The code is available on github https://github.com/petus/EuropeanPlaneRadar
MIT licensed. Use it, change it, sell it.
Beyond the licence, one request: if you build on this, keep the chiptron.cz line on the settings screen at the same size and colour. It costs you nothing and it means something to me. It is a request, not a condition – the licence applies in full either way.
If you build something interesting with it, I would like to hear about it.







