Community project
Baue Mir Einen Live-flug-radarscanner Mit Dem M5
Generated with AIThis project turns an M5Dial into a live flight radar scanner that displays aircraft in your airspace. The device connects to WiFi, fetches real-time flight data from a public aviation API, and renders a radar view on the circular display showing aircraft position, distance, altitude, and speed relative to your location.
The guide provides a complete parts list, wiring diagram, and ready-to-flash firmware. Assembly takes minutes: connect the M5Dial via USB, configure your WiFi credentials and home coordinates in the code, and start scanning the skies. Use the dial's encoder to browse through detected aircraft and view their details on screen.
Wiring diagram
Interactive · read-onlyPan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Assembly
3 stepsM5Dial bereitlegen
Lege nur das M5Stack M5Dial bereit. Bildschirm, Drehknopf und kleiner Signaltongeber sind bereits im Gerät eingebaut; es werden keine zusätzlichen Kabel oder Bauteile benötigt.
- Tip: Ziehe vorerst keine Leitungen an die seitlichen Anschlüsse. Dieses Projekt nutzt ausschließlich die eingebaute Hardware.
- ⚠ Öffne das Gehäuse nicht und schließe keine Spannung an die seitlichen Anschlüsse an; falsche Anschlüsse können das Gerät beschädigen.
Über USB anschließen
Stecke ein USB-Datenkabel in den USB-C-Anschluss des M5Dial und in deinen Computer. Das Kabel versorgt das Gerät mit Strom und überträgt die Firmware.
- Tip: Das Display sollte nach dem Einstecken kurz aufleuchten. Ein reines Ladekabel ohne Datenleitungen kann die Firmware nicht übertragen.
- ⚠ Verwende eine normale USB-Stromquelle; schließe keine Batterie oder ein Netzteil an die GPIO-Anschlüsse an.
Radar bedienen
Nach dem Übertragen verbindet sich das M5Dial mit dem angegebenen WLAN und lädt Flugzeuge rund um Siegen. Drehe den großen Knopf, um einen gelben Punkt und seine Angaben auszuwählen. Drücke den Knopf einmal, um die Flugdaten sofort neu zu laden.
- Tip: Die Punkte werden außerdem automatisch etwa jede Minute aktualisiert. Der Kreis reicht ungefähr 50 km weit.
- Tip: Wenn keine Punkte erscheinen, kann in diesem Bereich gerade kein Flugzeug von der öffentlichen Datenquelle gemeldet sein.
- ⚠ Das Gerät braucht WLAN mit Internetzugang. Ein WLAN-Anmeldeportal, wie es in Hotels üblich ist, kann nicht automatisch bestätigt werden.
Firmware
ESP32#include <Arduino.h>
#include <M5Dial.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <math.h>
// Hoisted type definitions
struct Aircraft {
String callsign;
float lat;
float lon;
int altitudeFt;
int speedKt;
float distanceNm;
float bearingDeg;
};
// Forward declarations
float radiansf(float degrees);
float distanceNm(float lat1, float lon1, float lat2, float lon2);
float bearingDeg(float lat1, float lon1, float lat2, float lon2);
String callsignFrom(JsonObject item);
void drawRadar();
void sortByDistance();
void fetchFlights();
const char* WIFI_SSID = "KT-Gast";
const char* WIFI_PASSWORD = "KT-WW-22";
const float HOME_LAT = 50.87481f;
const float HOME_LON = 8.02431f;
const int RADIUS_NM = 27; // about 50 km
const uint32_t REFRESH_INTERVAL_MS = 60000;
const uint8_t MAX_AIRCRAFT = 12;
Aircraft aircraft[MAX_AIRCRAFT];
uint8_t aircraftCount = 0;
int selected = 0;
long lastEncoder = 0;
uint32_t lastRefresh = 0;
String statusText = "WLAN wird verbunden";
float radiansf(float degrees) { return degrees * PI / 180.0f; }
float distanceNm(float lat1, float lon1, float lat2, float lon2) {
float dLat = radiansf(lat2 - lat1);
float dLon = radiansf(lon2 - lon1);
float a = sinf(dLat / 2) * sinf(dLat / 2) +
cosf(radiansf(lat1)) * cosf(radiansf(lat2)) * sinf(dLon / 2) * sinf(dLon / 2);
return 3440.065f * 2.0f * atan2f(sqrtf(a), sqrtf(1.0f - a));
}
float bearingDeg(float lat1, float lon1, float lat2, float lon2) {
float dLon = radiansf(lon2 - lon1);
float y = sinf(dLon) * cosf(radiansf(lat2));
float x = cosf(radiansf(lat1)) * sinf(radiansf(lat2)) -
sinf(radiansf(lat1)) * cosf(radiansf(lat2)) * cosf(dLon);
float result = atan2f(y, x) * 180.0f / PI;
return result < 0 ? result + 360.0f : result;
}
String callsignFrom(JsonObject item) {
const char* flight = item["flight"] | "";
String result(flight);
result.trim();
if (result.length()) return result;
const char* hex = item["hex"] | "Unbekannt";
return String(hex);
}
void drawRadar() {
auto& display = M5Dial.Display;
display.fillScreen(TFT_BLACK);
const int cx = 120, cy = 117, radius = 82;
display.drawCircle(cx, cy, radius, TFT_DARKGREEN);
display.drawCircle(cx, cy, radius * 2 / 3, TFT_DARKGREEN);
display.drawCircle(cx, cy, radius / 3, TFT_DARKGREEN);
display.drawLine(cx, cy - radius, cx, cy + radius, TFT_DARKGREEN);
display.drawLine(cx - radius, cy, cx + radius, cy, TFT_DARKGREEN);
display.setTextDatum(top_center);
display.setTextColor(TFT_GREEN, TFT_BLACK);
display.setTextSize(1);
display.drawString("SIEGEN | " + String(RADIUS_NM) + " nm", cx, 4);
display.setTextDatum(middle_center);
display.drawString("N", cx, cy - radius - 8);
for (uint8_t i = 0; i < aircraftCount; i++) {
float scaled = aircraft[i].distanceNm / RADIUS_NM;
if (scaled > 1.0f) continue;
float angle = radiansf(aircraft[i].bearingDeg - 90.0f);
int x = cx + (int)(cosf(angle) * radius * scaled);
int y = cy + (int)(sinf(angle) * radius * scaled);
display.fillCircle(x, y, (i == selected) ? 5 : 3, (i == selected) ? TFT_YELLOW : TFT_CYAN);
}
display.fillRect(0, 205, 240, 35, TFT_BLACK);
display.setTextDatum(top_left);
if (aircraftCount == 0) {
display.setTextColor(TFT_ORANGE, TFT_BLACK);
display.drawString(statusText, 8, 211);
} else {
const Aircraft& a = aircraft[selected];
display.setTextColor(TFT_YELLOW, TFT_BLACK);
display.drawString(a.callsign, 8, 208);
display.setTextColor(TFT_WHITE, TFT_BLACK);
display.drawString(String((int)roundf(a.distanceNm)) + " nm " + String(a.altitudeFt) + " ft", 8, 221);
display.setTextDatum(top_right);
display.drawString(String(a.speedKt) + " kt", 232, 221);
}
}
void sortByDistance() {
for (uint8_t i = 0; i < aircraftCount; i++) {
for (uint8_t j = i + 1; j < aircraftCount; j++) {
if (aircraft[j].distanceNm < aircraft[i].distanceNm) {
Aircraft temp = aircraft[i]; aircraft[i] = aircraft[j]; aircraft[j] = temp;
}
}
}
}
void fetchFlights() {
if (WiFi.status() != WL_CONNECTED) {
statusText = "WLAN nicht verbunden";
drawRadar();
return;
}
statusText = "Flugdaten laden";
drawRadar();
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
String url = "https://api.adsb.lol/v2/point/" + String(HOME_LAT, 5) + "/" + String(HOME_LON, 5) + "/" + String(RADIUS_NM);
http.setTimeout(15000);
if (!http.begin(client, url)) { statusText = "Verbindung fehlgeschlagen"; drawRadar(); return; }
int response = http.GET();
if (response != HTTP_CODE_OK) { statusText = "Datenquelle nicht erreichbar"; http.end(); drawRadar(); return; }
JsonDocument doc;
DeserializationError error = deserializeJson(doc, http.getStream());
http.end();
if (error) { statusText = "Antwort unlesbar"; drawRadar(); return; }
aircraftCount = 0;
JsonArray list = doc["ac"].as<JsonArray>();
for (JsonObject item : list) {
if (aircraftCount >= MAX_AIRCRAFT) break;
if (item["lat"].isNull() || item["lon"].isNull()) continue;
Aircraft& a = aircraft[aircraftCount++];
a.callsign = callsignFrom(item);
a.lat = item["lat"].as<float>();
a.lon = item["lon"].as<float>();
a.altitudeFt = item["alt_baro"] | 0;
a.speedKt = item["gs"] | 0;
a.distanceNm = distanceNm(HOME_LAT, HOME_LON, a.lat, a.lon);
a.bearingDeg = bearingDeg(HOME_LAT, HOME_LON, a.lat, a.lon);
}
sortByDistance();
selected = 0;
M5Dial.Encoder.write(0);
lastEncoder = 0;
statusText = aircraftCount ? "" : "Keine Flugzeuge im Bereich";
lastRefresh = millis();
drawRadar();
}
void setup() {
auto cfg = M5.config();
M5Dial.begin(cfg, true, false);
M5Dial.Display.setRotation(0);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
uint32_t started = millis();
while (WiFi.status() != WL_CONNECTED && millis() - started < 15000) delay(250);
fetchFlights();
}
void loop() {
M5Dial.update();
long position = M5Dial.Encoder.read();
if (aircraftCount > 0 && position != lastEncoder) {
int next = selected + (position > lastEncoder ? 1 : -1);
if (next < 0) next = aircraftCount - 1;
if (next >= aircraftCount) next = 0;
selected = next;
lastEncoder = position;
M5Dial.Speaker.tone(3000, 15);
drawRadar();
}
if (M5Dial.BtnA.wasPressed()) fetchFlights();
if (millis() - lastRefresh >= REFRESH_INTERVAL_MS) fetchFlights();
}“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.