Community project
Live Flight Radar Scanner
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-onlyPan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Assembly
2 stepsPower 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.
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#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.