Community project

Live Flight Radar Scanner

evanogorman

Published July 15, 2026

ESP320 components2 assembly steps
Remix this project

This project turns an M5Dial into a live flight radar scanner that displays aircraft within a 100 km radius of the user's location. The device automatically detects its geographic position on first boot, then periodically fetches real-time aircraft data from public aviation APIs and renders a radar-style visualization on the dial's circular display.

The guide includes a complete parts list, wiring diagram, and step-by-step assembly instructions. Firmware is provided with Wi-Fi auto-configuration, JSON parsing for flight data, and a refresh cycle that updates the radar every 60 seconds. Readers will learn how to build a networked IoT device that combines geolocation, HTTP requests, and real-time data visualization on a compact embedded platform.

Wiring diagram

Interactive · read-only

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Assembly

2 steps
  1. Power the M5Dial

    Connect the M5Dial to a stable USB-C power source. No external sensors, GPS receiver, or wiring are required.

    • Tip: Use a data-capable USB-C cable when deploying firmware from Schematik.
    • The IP-based location is approximate—normally city or region level, not GPS precision.
  2. Join Wi-Fi on first boot

    After deploying, connect a phone or computer to the temporary Wi-Fi network named M5Dial-FlightRadar. Its setup page opens automatically; choose the normal Wi-Fi network that has internet access. The M5Dial saves these credentials for later boots.

    • Tip: Keep the M5Dial within reliable Wi-Fi coverage; live aircraft data requires internet access.
    • Do not use this for navigation, safety, or air-traffic-control decisions. OpenSky public access can be rate-limited or temporarily unavailable.

Firmware

ESP32
firmware.cppDeploy to device
#include <Arduino.h>
#include <M5Dial.h>
#include <WiFiManager.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <math.h>


// Hoisted type definitions
struct Aircraft {
  String callsign;
  float lat;
  float lon;
  float altitudeM;
  float velocityMs;
};


// Forward declarations
bool getJson(const String &url, DynamicJsonDocument &doc);
bool locateFromIp();
float longitudeDegreesForRadius(float latitude);
bool fetchAircraft();
void drawRadar();
void refreshRadar();

constexpr uint32_t REFRESH_MS = 60000;
constexpr float SCAN_RADIUS_KM = 100.0f;
constexpr int MAX_AIRCRAFT = 45;



Aircraft aircraft[MAX_AIRCRAFT];
int aircraftCount = 0;
float homeLat = 0.0f;
float homeLon = 0.0f;
bool haveLocation = false;
String locationLabel = "Locating...";
uint32_t lastRefresh = 0;
String statusLine = "Starting Wi-Fi...";

bool getJson(const String &url, DynamicJsonDocument &doc) {
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;
  http.setTimeout(12000);
  if (!http.begin(client, url)) return false;
  http.addHeader("User-Agent", "M5Dial-FlightRadar/1.0");
  int code = http.GET();
  if (code != HTTP_CODE_OK) {
    statusLine = "HTTP " + String(code);
    http.end();
    return false;
  }
  DeserializationError error = deserializeJson(doc, http.getStream());
  http.end();
  return !error;
}

bool locateFromIp() {
  DynamicJsonDocument doc(4096);
  if (!getJson("https://ipapi.co/json/", doc)) return false;
  if (doc["latitude"].isNull() || doc["longitude"].isNull()) return false;
  homeLat = doc["latitude"].as<float>();
  homeLon = doc["longitude"].as<float>();
  String city = doc["city"] | "Unknown city";
  String country = doc["country_name"] | "";
  locationLabel = city + (country.length() ? ", " + country : "");
  haveLocation = true;
  return true;
}

float longitudeDegreesForRadius(float latitude) {
  float c = cosf(latitude * DEG_TO_RAD);
  return SCAN_RADIUS_KM / (111.32f * max(0.2f, c));
}

bool fetchAircraft() {
  if (!haveLocation) return false;
  const float latDelta = SCAN_RADIUS_KM / 110.57f;
  const float lonDelta = longitudeDegreesForRadius(homeLat);
  String url = "https://opensky-network.org/api/states/all?lamin=" + String(homeLat - latDelta, 4) +
               "&lomin=" + String(homeLon - lonDelta, 4) +
               "&lamax=" + String(homeLat + latDelta, 4) +
               "&lomax=" + String(homeLon + lonDelta, 4);
  DynamicJsonDocument doc(50000);
  if (!getJson(url, doc)) return false;
  JsonArray states = doc["states"].as<JsonArray>();
  if (states.isNull()) return false;

  aircraftCount = 0;
  for (JsonVariant rowVariant : states) {
    JsonArray row = rowVariant.as<JsonArray>();
    if (row.size() < 10 || row[5].isNull() || row[6].isNull()) continue;
    if (aircraftCount >= MAX_AIRCRAFT) break;
    Aircraft &a = aircraft[aircraftCount++];
    a.callsign = row[1].isNull() ? String("UNKNOWN") : String(row[1].as<const char *>());
    a.callsign.trim();
    if (!a.callsign.length()) a.callsign = "UNKNOWN";
    a.lon = row[5].as<float>();
    a.lat = row[6].as<float>();
    a.altitudeM = row[7].isNull() ? 0.0f : row[7].as<float>();
    a.velocityMs = row[9].isNull() ? 0.0f : row[9].as<float>();
  }
  return true;
}

void drawRadar() {
  auto &d = M5Dial.Display;
  const int cx = 120;
  const int cy = 126;
  const int radius = 92;
  d.fillScreen(TFT_BLACK);
  d.setTextColor(TFT_CYAN, TFT_BLACK);
  d.setTextSize(1);
  d.setCursor(12, 8);
  d.print("FLIGHT RADAR");
  d.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
  d.setCursor(12, 20);
  d.print(locationLabel.substring(0, 28));
  d.setTextColor(TFT_DARKGREEN, TFT_BLACK);
  d.drawCircle(cx, cy, radius, TFT_DARKGREEN);
  d.drawCircle(cx, cy, radius * 2 / 3, TFT_DARKGREEN);
  d.drawCircle(cx, cy, radius / 3, TFT_DARKGREEN);
  d.drawFastHLine(cx - radius, cy, radius * 2, TFT_DARKGREEN);
  d.drawFastVLine(cx, cy - radius, radius * 2, TFT_DARKGREEN);
  d.setTextColor(TFT_GREEN, TFT_BLACK);
  d.setCursor(cx - 3, cy - radius - 11); d.print("N");
  d.fillCircle(cx, cy, 4, TFT_WHITE);

  for (int i = 0; i < aircraftCount; ++i) {
    float northKm = (aircraft[i].lat - homeLat) * 110.57f;
    float eastKm = (aircraft[i].lon - homeLon) * 111.32f * cosf(homeLat * DEG_TO_RAD);
    int x = cx + (int)(eastKm * radius / SCAN_RADIUS_KM);
    int y = cy - (int)(northKm * radius / SCAN_RADIUS_KM);
    if ((x - cx) * (x - cx) + (y - cy) * (y - cy) > radius * radius) continue;
    d.fillTriangle(x, y - 4, x - 3, y + 3, x + 3, y + 3, TFT_YELLOW);
  }
  d.setTextColor(TFT_WHITE, TFT_BLACK);
  d.setTextSize(1);
  d.setCursor(12, 224);
  d.printf("%d aircraft  %s", aircraftCount, statusLine.c_str());
}

void refreshRadar() {
  statusLine = "Updating";
  drawRadar();
  if (!haveLocation && !locateFromIp()) {
    statusLine = "Location failed";
    drawRadar();
    return;
  }
  if (fetchAircraft()) {
    statusLine = "Live";
  } else {
    statusLine = "Data unavailable";
  }
  drawRadar();
}

void setup() {
  auto cfg = M5.config();
  M5Dial.begin(cfg, true, false);
  M5Dial.Display.setRotation(0);
  M5Dial.Display.fillScreen(TFT_BLACK);
  M5Dial.Display.setTextColor(TFT_CYAN, TFT_BLACK);
  M5Dial.Display.setTextSize(2);
  M5Dial.Display.setCursor(28, 100);
  M5Dial.Display.print("Flight Radar");

  WiFiManager wifiManager;
  wifiManager.setConfigPortalTimeout(180);
  if (!wifiManager.autoConnect("M5Dial-FlightRadar")) {
    statusLine = "Wi-Fi setup needed";
    drawRadar();
    return;
  }
  refreshRadar();
  lastRefresh = millis();
}

void loop() {
  M5Dial.update();
  if (WiFi.status() != WL_CONNECTED) {
    statusLine = "Wi-Fi disconnected";
    drawRadar();
    delay(1000);
    return;
  }
  if (millis() - lastRefresh >= REFRESH_MS) {
    refreshRadar();
    lastRefresh = millis();
  }
  delay(20);
}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik