Building a Custom ESP32-S3 ADS-B Radar Display with Ultrafeeder

How to build a dedicated, real-time ADS-B flight-tracking display using an ESP32-S3 480x480 screen, LVGL, and a local Ultrafeeder/dump1090 stream.

Building a Custom ESP32-S3 ADS-B Radar Display with Ultrafeeder

If you run a local ADS-B receiver using Ultrafeeder, dump1090, or readsb, you are probably familiar with keeping a browser tab open to check tar1090. While browser maps are great, having a dedicated physical radar widget sitting under your monitor provides an entirely different level of satisfaction.

In this post, I am walking through my custom desktop ADS-B display built on an ESP32-S3 with a round-cornered 480x480 RGB touchscreen. It streams live data over Wi-Fi, renders real-time aircraft vectors, plots continuous elevation and distance metrics, and even caches Esri/Geoapify basemaps for terrain overlay.


The Hardware and Core Stack

The hardware centers around the ESP32-S3 box86 form factor, which packs enough RAM and processing power to handle low-latency rendering alongside stream parsing.

Component Specification
MCU ESP32-S3 DevKitC-1 N16R16 (16 MB Flash, 8 MB PSRAM)
Display 480x480 RGB Panel (ST7701 Driver)
Touch Controller TAMC GT911 Capacitive Touch (I2C)
Interface SPI (Panel Init) + Parallel RGB (Pixel Data Pipeline)
Data Source Local aircraft.json over HTTP (Ultrafeeder / dump1090 / readsb)

Box86 ESP32-S3 Arduino LVGL WIFI and Bluetooth development board 4.0 inch 480 * 480 smart display capacitive touch without bottom - available here: https://www.aliexpress.com/item/1005008214679682.html?spm=a2g0o.order_list.order_list_main.4.2d051802nIySUl

To maintain a fluid interface, the code relies on LVGL v8.3.11 with a double-buffered draw pipeline allocated directly in PSRAM.


How the Data Flow Works

Instead of pulling full JSON blobs into memory (which easily causes out-of-memory crashes on embedded devices), the ESP32 opens a direct HTTP socket connection to the Ultrafeeder feed every few seconds.

Using ArduinoJson v7, the data is stream-parsed directly off the TCP socket. The parser filters down strictly to the fields needed for computation (ICAO, callsign, latitude, longitude, altitude, velocity, squawk, heading, and vertical rate).

// Streaming fetch task running on FreeRTOS Core 1
void fetchTask(void *pvParameters) {
    for (;;) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        if (xSemaphoreTake(xFlightStatsMutex, pdMS_TO_TICKS(1000))) {
            fetchFlightData(); // Connects to local Ultrafeeder endpoint
            xSemaphoreGive(xFlightStatsMutex);
        }
    }
}

A dedicated FreeRTOS task handles all network operations on Core 1, completely decoupled from the main UI thread. This design guarantees that touch actions and radar animations remain smooth even during network latency spikes.


The UI auto-cycles through several views, or allows manual navigation via invisible touch zones in the bottom corners of the screen.

1. The Closest Aircraft Dashboard

The Closest Aircraft card dynamically tracks whichever plane is closest to the home receiver location.

Closest aircraft tracking display showing G-LHER telemetry
Figure 1: Telemetry detail showing closest aircraft tracking with look direction, squawk, altitude, and elevation metrics.

Beyond simple speed and altitude stats, the firmware calculates the look direction (bearing relative to true North) and the elevation above the horizon. This makes it effortless to glance at the screen and immediately point out the window toward an incoming target.

2. Live Radar and Basemap Compositing

The main event is the live radar sweep screen. Aircraft locations are transformed from GPS coordinates to relative polar coordinates (range and true bearing) centered on the receiver's location.

Radar display with G-EZTY Airbus A-320 detail card
Figure 3: Tapping an aircraft target reveals a detail card with live telemetry including altitude, speed, squawk, and flight status.

Basemap Fetching and Custom Recoloring

The background basemap is fetched over HTTPS from Esri ArcGIS (or Geoapify), decoded from JPEG, and cached directly to the onboard SPIFFS filesystem at /radarmap-v5.jpg.

When loaded into PSRAM, the dark map style undergoes custom client-side color manipulation:

  • Roads: Shifted to high-contrast cyan lines.
  • Water Bodies: Recolored to deep midnight blue.
  • Terrain Labels: Normalized to high-visibility white.

Dynamic Aircraft Glyphs and Heading Calculations

A crucial detail when building a mini radar display is making sure icons represent both aircraft category and vector orientation correctly. The display handles rendering differently depending on whether the target is a fixed-wing aircraft or a helicopter.

Hand-drawn X-rotor helicopter glyph with forward direction arrow
Figure 4: Custom X-rotor glyph used for rotorcraft tracking, with the forward direction vector shown in red.
Hand-drawn swept-wing aircraft glyph with forward direction arrow
Figure 5: Custom swept-wing glyph used for standard fixed-wing traffic, rotating to match the reported track heading.

If an aircraft sends ICAO category data matching category A7 or includes rotorcraft keywords in its model description, the renderer swaps the glyph to an X-rotor helicopter icon. Otherwise, it defaults to a swept-wing plane icon.

Icons rotate dynamically around their center point using the target's reported track heading.

Glitch Recovery: Distorted Framebuffers

During early development, high PSRAM memory bandwidth utilization caused occasional display signal corruption, resulting in pixel distortion across parallel RGB channels. This issue was resolved by tuning panel timing constants within Arduino_GFX, disabling unnecessary bus transfers, and utilizing dedicated PSRAM allocations for the LVGL display double-buffer.


Local Web UI and Management

The ESP32 runs a local web server (accessible via mDNS at http://espADSBMonitor.local/), allowing on-the-fly customization without recompiling firmware.

  • Data Source: Change aircraft.json endpoints instantly.
  • Basemap Control: Toggle background map rendering, swap basemap styles, and change range rings (for example, 20nmi vs 40nmi).
  • Display Settings: Adjust screen timeout, backlight levels, and radar sweep dwell speed.
  • OTA Updates: Upload compiled .bin updates directly via /serverIndex.

Getting Started with the Code

The complete source code, LVGL assets, and configuration files are open source.

Quick Setup

1. Clone the repo:

git clone https://github.com/your-username/esp32_ADSB_esp32s3_box86.git
cd esp32_ADSB_esp32s3_box86

2. Configure environment. Copy include/connectionDetails.example.h to include/connectionDetails.h and populate your Wi-Fi credentials and MQTT broker details:

#define WIFI_ACCESSPOINT "Your_SSID"
#define WIFI_ACCESSPOINT_PASSWORD "Your_Password"

3. Set ADS-B receiver origin. Open include/merlinFlightStats.h and input your feeder's local IP address alongside your receiver's exact latitude and longitude:

const char* host = "192.168.1.150"; // Ultrafeeder IP
const double myLat = 51.4000;
const double myLon = -1.3200;

4. Build and flash using PlatformIO:

pio run --target upload

Conclusion

Combining an ESP32-S3 with a local Ultrafeeder setup produces a responsive, self-contained aviation radar that sits comfortably on any desk. Whether monitoring local general aviation or catching high-altitude commercial traffic overhead, having custom hardware dedicated to the job brings local flight tracking to life.

github repo: https://github.com/merlinmb/esp32_ADSB_esp32s3_box86