Wenn ich aus dem Küchenfenster schaue, sehe ich alle Flugzeuge die aus dem Osten kommen und in Langenhagen (EDDV, HAJ) landen wollen. Habe schon einige Projekte in diesem Zusammenhang gemacht, aber die sind noch alle per Hand, und nicht mit KI entstanden.
Nun ist aus der Bucht ein ESP32 mit TFT und Touch angekommen, der noch eine Verwendung braucht.
Ok, hier schon mal vorab das Ergebnis, es werden alle Flugzeuge die aus dem osten kommen und landen wollen auf dem ESP32 ausgegeben, sogar in verschiedenen Farben, je nach Höhe:

Gestartet bin ich mit einem Bereich, von 20 km um Langenhagen, um das C++ Programm mit der Arduino IDE zu laufen zu bringen:

Als nächsten Schritt, habe ich mir die koordinaten für ein 2 km Rechteck von der Landebahn in Richtung Osten generiert:

Dann hat mich interessiert, in welchen Winkel die Flugzeuge landen, wenn ich sie sehe, sind sie so ca. 300 m hoch. Die KI sagt:
Spezifikation des Anflugvektors (Glide Path):
Bei einem Endanflug aus östlicher Richtung – was einer Ausrichtung auf die Pisten 27L oder 27R des Flughafens Hannover-Langenhagen (EDDV) entspricht -ist der vertikale Anflugwinkel für kommerzielle Passagierjets auf den Standardwert von 3° kalibriert. Das fand ich interessant. Dann habe ich mir noch eine Tabelle berechnen lassen, hier das Mathematisches Modell des Anflugprofils.
Zur Berechnung der vertikalen Position (Höhe h) des Luftfahrzeugs in Relation zur horizontalen Distanz (d) zum Aufsetzpunkt (Touchdown Zone) wird eine einfache trigonometrische Funktion appliziert. Da der Gleitpfad (Glide Slope) einen konstanten Winkel von 3,0° aufweist, lässt sich die Höhe über Grund (Above Ground Level, AGL) in Metern für jede diskrete Stützstelle berechnen.
Die zugehörige Formel lautet:

Ah, dann mal bis 30 km, bis nach Lehrte die Werte berechnen, und dies Bild mit KI generieren:

Dann beide Bilder zusammen morphen, cool:

Genau das wollte ich wissen.
Nun noch mit KI ein C++ Programm erstellen und in den ESP32 flashen. Hier die erste Version, mit 577 Zeilen. Da ist noch eine Einstellun nötig, die ich noch nie verwendet haben. Und zwar das Paritions Schema: „Huge APP …“ für die anderen ist das Programm zu groß geworden. Hier die Einstellung in der IDE:

Und nun der versprochene Code. Es muss da noch bei Touch, die Details angezeigt werden und einige andere Anpassungen. Aber wie immer, sind meine KI Token abgelaufen bzw. verbraucht 😉
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 |
/** * FlugMonitor.ino * ═══════════════════════════════════════════════════════════════════════════ * Zeigt Flugzeuge im ~20-km-Radius um Langenhagen, Niedersachsen an, * die sich unterhalb von 2000 m Höhe befinden. * * Datenquelle : OpenSky Network REST API (authenticated) * https://opensky-network.org/apidoc * Display : 240×320 TFT via TFT_eSPI, LVGL 9.x * Logische Auflösung nach Rotation 270°: 320×240 * * Benötigte Arduino-Bibliotheken: * - LVGL >= 9.x (lv_conf.h: LV_FONT_MONTSERRAT_12/14 = 1) * - TFT_eSPI * - ArduinoJson >= 6.x * * Dateien: * - FlugMonitor.ino (diese Datei) * - config.h (WiFi- und OpenSky-Zugangsdaten) * * Authentifizierung : OAuth2 Client Credentials (Bearer Token) * @author Thomas Wenzlaff */ #include <lvgl.h> #include <TFT_eSPI.h> #include <WiFi.h> #include <WiFiClientSecure.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include <map> #include <vector> #include "config.h" // ──────────────────────────────────────────────────────────────────────────── // Display // ──────────────────────────────────────────────────────────────────────────── #define SCREEN_WIDTH 240 #define SCREEN_HEIGHT 320 #define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 20 * (LV_COLOR_DEPTH / 8)) static uint32_t draw_buf[DRAW_BUF_SIZE / 4]; #define LOGICAL_W 320 #define LOGICAL_H 240 #define TOP_ZONE_H 46 // Header + Trennlinie + Statuszeile // ──────────────────────────────────────────────────────────────────────────── // Datenstrukturen // ──────────────────────────────────────────────────────────────────────────── struct FlightInfo { String icao24; String callsign; String country; float altitude; float speedKmh; float heading; String departure; String arrival; }; // ──────────────────────────────────────────────────────────────────────────── // OAuth2-Token-Cache // ──────────────────────────────────────────────────────────────────────────── static String g_accessToken = ""; static time_t g_tokenExpiry = 0; // ──────────────────────────────────────────────────────────────────────────── // Flugdaten & Polling-Steuerung // ──────────────────────────────────────────────────────────────────────────── static std::map<String, FlightInfo> g_cache; static std::vector<FlightInfo> g_activeFlights; static int g_lastStatesCode = 0; static unsigned long g_lastStatesUpdate = 0; const unsigned long INTERVAL_STATES = 15000UL; // 15 Sekunden const unsigned long PENALTY_BACKOFF = 60000UL; // 60s Pause bei HTTP 429 static bool g_apiPenaltyActive = false; static bool g_firstRun = true; static unsigned long g_detailsPenaltyUntil = 0; // ──────────────────────────────────────────────────────────────────────────── // LVGL-Objekte // ──────────────────────────────────────────────────────────────────────────── static lv_obj_t* g_headerLabel = nullptr; static lv_obj_t* g_statusLabel = nullptr; static lv_obj_t* g_flightList = nullptr; static lv_style_t g_styleHeader; static lv_style_t g_styleStatus; static lv_style_t g_styleItem; static lv_style_t g_styleItemDim; // ════════════════════════════════════════════════════════════════════════════ // Blockierendes Delay mit laufendem LVGL // ════════════════════════════════════════════════════════════════════════════ static void delayLvgl(unsigned long ms) { unsigned long start = millis(); while (millis() - start < ms) { lv_timer_handler(); delay(5); } } // ════════════════════════════════════════════════════════════════════════════ // Statuszeile // ════════════════════════════════════════════════════════════════════════════ static void setStatus(const char* msg) { if (g_statusLabel != nullptr) { lv_label_set_text(g_statusLabel, msg); lv_timer_handler(); } Serial.printf("[Status] %s\n", msg); } // ════════════════════════════════════════════════════════════════════════════ // WiFi // ════════════════════════════════════════════════════════════════════════════ static void connectWiFi() { if (WiFi.status() == WL_CONNECTED) return; setStatus("WiFi verbinden ..."); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) { delay(500); lv_timer_handler(); Serial.print('.'); } Serial.println(); if (WiFi.status() == WL_CONNECTED) { Serial.printf("[WiFi] IP: %s\n", WiFi.localIP().toString().c_str()); setStatus("WiFi verbunden."); } else { setStatus("WiFi FEHLER!"); } } // ════════════════════════════════════════════════════════════════════════════ // OAuth2: Bearer-Token holen // ════════════════════════════════════════════════════════════════════════════ static bool refreshOAuthToken() { Serial.println("[OAuth2] Token wird erneuert ..."); setStatus("OAuth2-Token holen ..."); WiFiClientSecure client; client.setInsecure(); HTTPClient http; if (!http.begin(client, OPENSKY_TOKEN_URL)) return false; http.addHeader("Content-Type", "application/x-www-form-urlencoded"); http.setTimeout(12000); String body = "grant_type=client_credentials"; body += "&client_id="; body += String(OPENSKY_CLIENT_ID); body += "&client_secret="; body += String(OPENSKY_CLIENT_SECRET); int code = http.POST(body); lv_timer_handler(); if (code != HTTP_CODE_OK) { char msg[40]; snprintf(msg, sizeof(msg), "Token-Fehler: HTTP %d", code); setStatus(msg); http.end(); return false; } String json = http.getString(); http.end(); DynamicJsonDocument doc(2048); if (deserializeJson(doc, json)) { setStatus("Token: JSON-Fehler"); return false; } const char* token = doc["access_token"]; int expiresIn = doc["expires_in"] | 300; if (!token || strlen(token) == 0) { setStatus("Token: leer"); return false; } g_accessToken = String(token); time_t now; time(&now); g_tokenExpiry = now + expiresIn - TOKEN_EXPIRY_BUFFER_S; Serial.printf("[OAuth2] Token OK. Gültig für %d s.\n", expiresIn); setStatus("OAuth2-Token OK."); return true; } static bool ensureValidToken() { time_t now; time(&now); if (g_accessToken.isEmpty() || now >= g_tokenExpiry) return refreshOAuthToken(); return true; } // ════════════════════════════════════════════════════════════════════════════ // HTTP-GET mit Bearer-Token // ════════════════════════════════════════════════════════════════════════════ static int httpGet(const String& url, String& body) { body = ""; if (WiFi.status() != WL_CONNECTED) return -1; if (!ensureValidToken()) return -3; WiFiClientSecure client; client.setInsecure(); HTTPClient http; if (!http.begin(client, url)) return -2; http.addHeader("Authorization", "Bearer " + g_accessToken); http.addHeader("Accept", "application/json"); http.setTimeout(12000); int code = http.GET(); lv_timer_handler(); if (code == HTTP_CODE_OK) { body = http.getString(); } else { if (code != 404) { Serial.printf("[HTTP] Statuscode %d\n", code); } if (code == 401) { g_accessToken = ""; g_tokenExpiry = 0; } } http.end(); return code; } // ════════════════════════════════════════════════════════════════════════════ // OpenSky: Zustandsvektoren (Bounding Box) // ════════════════════════════════════════════════════════════════════════════ static std::vector<FlightInfo> fetchStates() { std::vector<FlightInfo> result; String url = String("https://opensky-network.org/api/states/all") + "?lamin=" + String(BBOX_LAT_MIN, 2) + "&lamax=" + String(BBOX_LAT_MAX, 2) + "&lomin=" + String(BBOX_LON_MIN, 2) + "&lomax=" + String(BBOX_LON_MAX, 2); Serial.println("[States] Anfrage: " + url); String json; g_lastStatesCode = httpGet(url, json); if (g_lastStatesCode == 429) { setStatus("Rate-Limit (429)"); return result; } if (g_lastStatesCode == 401) { setStatus("Auth-Fehler (401)"); return result; } if (g_lastStatesCode != HTTP_CODE_OK || json.isEmpty()) { char msg[40]; snprintf(msg, sizeof(msg), "HTTP Fehler: %d", g_lastStatesCode); setStatus(msg); return result; } DynamicJsonDocument doc(32768); if (deserializeJson(doc, json)) { setStatus("JSON-Fehler"); return result; } JsonArray states = doc["states"].as<JsonArray>(); if (states.isNull()) { setStatus("Keine Daten."); return result; } for (JsonArray s : states) { if (s.size() < 14) continue; if (s[8].as<bool>()) continue; // am Boden float alt = -1.0f; if (!s[7].isNull()) alt = s[7].as<float>(); else if (!s[13].isNull()) alt = s[13].as<float>(); if (alt < 0.0f || alt > MAX_ALTITUDE_M) continue; FlightInfo fi; fi.icao24 = s[0].as<String>(); fi.icao24.trim(); fi.callsign = s[1].isNull() ? "N/A" : s[1].as<String>(); fi.callsign.trim(); fi.country = s[2].isNull() ? "---" : s[2].as<String>(); fi.altitude = alt; fi.speedKmh = s[9].isNull() ? 0.0f : s[9].as<float>() * 3.6f; fi.heading = s[10].isNull() ? 0.0f : s[10].as<float>(); fi.departure = "---"; fi.arrival = "---"; result.push_back(fi); } Serial.printf("[States] %d Flugzeug(e) unter %.0f m.\n", result.size(), MAX_ALTITUDE_M); return result; } // ════════════════════════════════════════════════════════════════════════════ // OpenSky: Flugdetails für einzelnes Flugzeug // ════════════════════════════════════════════════════════════════════════════ static int enrichWithFlightDetails(FlightInfo& fi) { time_t now; time(&now); if (now < 100000L) return -1; String url = String("https://opensky-network.org/api/flights/aircraft") + "?icao24=" + fi.icao24 + "&begin=" + String((long)now - 43200L) + "&end=" + String((long)now); String json; int code = httpGet(url, json); // Bei Fehler direkt den Code zurückgeben if (code != HTTP_CODE_OK || json.isEmpty() || json == "[]") return code; DynamicJsonDocument doc(4096); if (deserializeJson(doc, json) || !doc.is<JsonArray>()) return code; JsonArray arr = doc.as<JsonArray>(); if (arr.size() == 0) return code; JsonObject flight = arr[arr.size() - 1].as<JsonObject>(); if (!flight["estDepartureAirport"].isNull()) fi.departure = flight["estDepartureAirport"].as<String>(); if (!flight["estArrivalAirport"].isNull()) fi.arrival = flight["estArrivalAirport"].as<String>(); Serial.printf("[Flights] %s: %s -> %s\n", fi.icao24.c_str(), fi.departure.c_str(), fi.arrival.c_str()); return code; // Alles OK (200) } // ════════════════════════════════════════════════════════════════════════════ // LVGL: Anzeige aktualisieren // ════════════════════════════════════════════════════════════════════════════ static void updateDisplay() { // ── Header ──────────────────────────────────────────────────────────────── char header[52]; struct tm ti; if (getLocalTime(&ti, 100)) { snprintf(header, sizeof(header), "* Langenhagen %02d:%02d:%02d", ti.tm_hour, ti.tm_min, ti.tm_sec); } else { snprintf(header, sizeof(header), "* Langenhagen"); } lv_label_set_text(g_headerLabel, header); // ── Statuszeile ─────────────────────────────────────────────────────────── char status[52]; snprintf(status, sizeof(status), "%d Flugzeug(e) unter %.0f m", (int)g_activeFlights.size(), MAX_ALTITUDE_M); lv_label_set_text(g_statusLabel, status); // ── Flug-Container ──────────────────────────────────────────────────────── lv_obj_clean(g_flightList); if (g_activeFlights.empty()) { lv_obj_t* lbl = lv_label_create(g_flightList); lv_obj_add_style(lbl, &g_styleStatus, 0); lv_label_set_text(lbl, "Keine Flugzeuge\nim Radius erkannt."); lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP); lv_obj_set_width(lbl, LOGICAL_W - 16); } else { for (const FlightInfo& fi : g_activeFlights) { lv_color_t cardColor; if (fi.altitude < 500.0f) cardColor = lv_color_hex(0x3d1f1f); else if (fi.altitude < 1000.0f) cardColor = lv_color_hex(0x3d2e18); else cardColor = lv_color_hex(0x152535); lv_obj_t* card = lv_obj_create(g_flightList); lv_obj_set_width(card, LOGICAL_W - 18); lv_obj_set_height(card, LV_SIZE_CONTENT); lv_obj_set_style_bg_color(card, cardColor, 0); lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); lv_obj_set_style_border_color(card, lv_color_hex(0x30363d), 0); lv_obj_set_style_border_width(card, 1, 0); lv_obj_set_style_radius(card, 4, 0); lv_obj_set_style_pad_all(card, 4, 0); lv_obj_set_style_pad_row(card, 2, 0); lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); char line1[64]; snprintf(line1, sizeof(line1), "[%s] %.0fm %.0fkm/h %.0f\xc2\xb0", fi.callsign.c_str(), fi.altitude, fi.speedKmh, fi.heading); lv_obj_t* lbl1 = lv_label_create(card); lv_obj_add_style(lbl1, &g_styleItem, 0); lv_label_set_text(lbl1, line1); lv_label_set_long_mode(lbl1, LV_LABEL_LONG_WRAP); lv_obj_set_width(lbl1, LOGICAL_W - 30); char line2[64]; snprintf(line2, sizeof(line2), "%s | %s -> %s", fi.country.c_str(), fi.departure.c_str(), fi.arrival.c_str()); lv_obj_t* lbl2 = lv_label_create(card); lv_obj_add_style(lbl2, &g_styleItemDim, 0); lv_label_set_text(lbl2, line2); lv_label_set_long_mode(lbl2, LV_LABEL_LONG_WRAP); lv_obj_set_width(lbl2, LOGICAL_W - 30); } } lv_timer_handler(); lv_refr_now(NULL); } // ════════════════════════════════════════════════════════════════════════════ // Haupt-Update // ════════════════════════════════════════════════════════════════════════════ static void fetchAndUpdateStates() { setStatus("Lade Flugdaten ..."); std::vector<FlightInfo> fresh = fetchStates(); delayLvgl(1000); // Rate-Limit erkannt? Penalty aktivieren und abbrechen if (g_lastStatesCode == 429) { g_apiPenaltyActive = true; g_lastStatesUpdate = millis(); // Timer resetten, damit Penalty greift return; } g_apiPenaltyActive = false; // Wieder freigeben if (g_lastStatesCode == HTTP_CODE_OK) { std::map<String, FlightInfo> newCache; bool detailsFetchedThisCycle = false; // Drosselung: max 1 Detail-Request pro Zyklus for (FlightInfo& fi : fresh) { auto it = g_cache.find(fi.icao24); if (it != g_cache.end()) { fi.departure = it->second.departure; fi.arrival = it->second.arrival; } else { // Prüfen, ob wir überhaupt Detail-Abfragen machen dürfen (Circuit Breaker) TODO if (!detailsFetchedThisCycle && millis() > g_detailsPenaltyUntil) { setStatus(("Route prüfen: " + fi.icao24).c_str()); int detailCode = enrichWithFlightDetails(fi); detailsFetchedThisCycle = true; if (detailCode == 429) { Serial.println("[Flights] Rate-Limit 429 erreicht. Sperre Routen-Abfrage für 5 Minuten."); g_detailsPenaltyUntil = millis() + 300000UL; // 5 Minuten Backoff } delayLvgl(1000); } } newCache[fi.icao24] = fi; } g_cache = std::move(newCache); g_activeFlights = std::move(fresh); } else { Serial.printf("[Update] States HTTP %d – zeige letzte bekannte Daten.\n", g_lastStatesCode); } updateDisplay(); } // ════════════════════════════════════════════════════════════════════════════ // LVGL: UI aufbauen // ════════════════════════════════════════════════════════════════════════════ static void initUI() { lv_obj_t* scr = lv_screen_active(); lv_obj_set_style_bg_color(scr, lv_color_hex(0x0d1117), 0); lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); lv_style_init(&g_styleHeader); lv_style_set_text_font(&g_styleHeader, &lv_font_montserrat_14); lv_style_set_text_color(&g_styleHeader, lv_color_hex(0x58a6ff)); lv_style_init(&g_styleStatus); lv_style_set_text_font(&g_styleStatus, &lv_font_montserrat_12); lv_style_set_text_color(&g_styleStatus, lv_color_hex(0x8b949e)); lv_style_init(&g_styleItem); lv_style_set_text_font(&g_styleItem, &lv_font_montserrat_12); lv_style_set_text_color(&g_styleItem, lv_color_hex(0xe6edf3)); lv_style_init(&g_styleItemDim); lv_style_set_text_font(&g_styleItemDim, &lv_font_montserrat_12); lv_style_set_text_color(&g_styleItemDim, lv_color_hex(0x8b949e)); // Header g_headerLabel = lv_label_create(scr); lv_obj_add_style(g_headerLabel, &g_styleHeader, 0); lv_label_set_text(g_headerLabel, "* Langenhagen"); lv_obj_align(g_headerLabel, LV_ALIGN_TOP_LEFT, 5, 4); // Obere Trennlinie lv_obj_t* sepTop = lv_obj_create(scr); lv_obj_set_size(sepTop, LOGICAL_W - 2, 1); lv_obj_align(sepTop, LV_ALIGN_TOP_MID, 0, 22); lv_obj_set_style_bg_color(sepTop, lv_color_hex(0x30363d), 0); lv_obj_set_style_border_width(sepTop, 0, 0); lv_obj_set_style_pad_all(sepTop, 0, 0); // Statuszeile g_statusLabel = lv_label_create(scr); lv_obj_add_style(g_statusLabel, &g_styleStatus, 0); lv_label_set_text(g_statusLabel, "Starte ..."); lv_obj_align(g_statusLabel, LV_ALIGN_TOP_LEFT, 5, 26); // Scrollbarer Flug-Container (Nimmt nun den restlichen Platz ein) g_flightList = lv_obj_create(scr); lv_obj_set_size(g_flightList, LOGICAL_W, LOGICAL_H - TOP_ZONE_H); lv_obj_align(g_flightList, LV_ALIGN_TOP_MID, 0, TOP_ZONE_H); lv_obj_set_style_bg_color(g_flightList, lv_color_hex(0x0d1117), 0); lv_obj_set_style_bg_opa(g_flightList, LV_OPA_COVER, 0); lv_obj_set_style_border_color(g_flightList, lv_color_hex(0x30363d), 0); lv_obj_set_style_border_width(g_flightList, 1, 0); lv_obj_set_style_pad_all(g_flightList, 4, 0); lv_obj_set_style_pad_row(g_flightList, 5, 0); lv_obj_set_scroll_dir(g_flightList, LV_DIR_VER); lv_obj_set_scrollbar_mode(g_flightList, LV_SCROLLBAR_MODE_AUTO); lv_obj_set_flex_flow(g_flightList, LV_FLEX_FLOW_COLUMN); } // ════════════════════════════════════════════════════════════════════════════ // setup() // ════════════════════════════════════════════════════════════════════════════ void setup() { Serial.begin(115200); Serial.println("\n[Setup] FlugMonitor startet ..."); lv_init(); lv_display_t* disp = lv_tft_espi_create( SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf)); lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270); initUI(); lv_timer_handler(); lv_refr_now(NULL); connectWiFi(); setStatus("NTP synchronisieren ..."); configTime(NTP_GMT_OFFSET_SEC, NTP_DAYLIGHT_OFFSET, NTP_SERVER); struct tm ti; if (getLocalTime(&ti, 8000)) { Serial.printf("[NTP] OK: %02d:%02d:%02d\n", ti.tm_hour, ti.tm_min, ti.tm_sec); setStatus("NTP OK."); } else { setStatus("NTP fehlgeschlagen."); } if (!refreshOAuthToken()) { setStatus("Auth fehlgeschlagen!"); delayLvgl(3000); } setStatus("Warte vor erster Abfrage ..."); delayLvgl(5000); fetchAndUpdateStates(); g_lastStatesUpdate = millis(); g_firstRun = false; Serial.println("[Setup] Bereit."); } // ════════════════════════════════════════════════════════════════════════════ // loop() // ════════════════════════════════════════════════════════════════════════════ void loop() { lv_timer_handler(); unsigned long now = millis(); if (WiFi.status() != WL_CONNECTED) connectWiFi(); // Ermitteln des aktuellen Intervalls (inkl. Backoff-Strafe bei 429) unsigned long currentStatesInterval = g_apiPenaltyActive ? PENALTY_BACKOFF : INTERVAL_STATES; // Flugzeuge aktualisieren if (now - g_lastStatesUpdate >= currentStatesInterval) { g_lastStatesUpdate = now; fetchAndUpdateStates(); } if (g_firstRun) g_firstRun = false; delay(5); } |
Dann brauchen wir noch eine config.h für die WIFI und OpenSky Token im gleichen Verzeichnis:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#pragma once // ════════════════════════════════════════════════════════════════════════════ // config.h – FlugMonitor Konfiguration // ════════════════════════════════════════════════════════════════════════════ // ── WiFi ───────────────────────────────────────────────────────────────────── #define WIFI_SSID "WIFI-SSID" #define WIFI_PASSWORD "PASSWORT" // ── OpenSky Network Zugangsdaten ───────────────────────────────────────────── // Registrierung: https://opensky-network.org/index.php?option=com_users&view=registration #define OPENSKY_CLIENT_ID "client id" #define OPENSKY_CLIENT_SECRET "Secret" // ── OAuth2 Token-Endpunkt ───────────────────────────────────────────────────── #define OPENSKY_TOKEN_URL \ "https://auth.opensky-network.org/auth/realms/opensky-network" \ "/protocol/openid-connect/token" // ── Bounding Box Langenhagen, Niedersachsen (~20 km Radius) ────────────────── // Mittelpunkt: 52.4398° N, 9.7337° O // Breite: ±0.18° (≈ 20 km) // Länge: ±0.30° (≈ 20 km bei 52° Breite) // #define BBOX_LAT_MIN 52.26 // #define BBOX_LAT_MAX 52.62 // #define BBOX_LON_MIN 9.43 // #define BBOX_LON_MAX 10.04 // Aktualisierte Bounding Box (Erweiterter Anflugsektor Osten) // Definitionsbereich: // Südliche Begrenzung (Latitude Min): 52.40°N // Nördliche Begrenzung (Latitude Max): 52.48°N // Westliche Begrenzung (Longitude Min): 9.71°E // Östliche Begrenzung (Longitude Max): 10.60°E #define BBOX_LAT_MIN 52.40 #define BBOX_LAT_MAX 52.48 #define BBOX_LON_MIN 9.71 #define BBOX_LON_MAX 10.60 // ── Filterkriterium ─────────────────────────────────────────────────────────── #define MAX_ALTITUDE_M 2000.0f // Meter (barometrisch) // ── Aktualisierungsintervall ────────────────────────────────────────────────── // OpenSky erlaubt authentifizierten Nutzern min. 5 s zwischen Anfragen. #define REFRESH_INTERVAL_MS 120000UL // ── Puffer vor Token-Ablauf (Sekunden) ─────────────────────────────────────── #define TOKEN_EXPIRY_BUFFER_S 60 // ── Wartezeit vor erstem Request nach Boot ──────────────────────────────────── // Verhindert 429 direkt nach dem Einschalten #define INITIAL_DELAY_MS 15000UL // 15 Sekunden // ── NTP-Zeitserver ──────────────────────────────────────────────────────────── #define NTP_SERVER "pool.ntp.org" #define NTP_GMT_OFFSET_SEC 3600 // UTC+1 (MEZ) #define NTP_DAYLIGHT_OFFSET 3600 // +1h Sommerzeit (MESZ) |
Code wird dann später nach Gitlab hochgeladen.
Auch wenn der Papst vor Gefahren von Künstlicher Intelligenz (KI) in seiner ersten Enzyklika „Magnifica Humanitas“ (Großartige Menschheit) warnt, kann man sagen, das dieses Projekt ohne KI für mich in herkömmlicher Weise einige Tage gebraucht hätte. So konnte alles in ein paar Stunden „just for fun“ erstellt werden.
Software-Programmierer werden in Zukunft anders arbeiten, den in diesem Bereich ist die KI seit Jahren nicht mehr wegzudenken. In diesem Sinne, noch einen schönen Tag und sonnige Grüße aus EDDV bei 26,5 Grad Celsius.
Haftungsausschluss (Disclaimer)
Die in diesem Dokument/generierten Inhalten bereitgestellten Daten, Simulationen und Informationen dienen ausschließlich zu Bildungs- und VerständZwecken. Sie wurden bewusst erzeugt, um theoretische Konzepte zu veranschaulichen und das Verständnis von Flugdynamik, Steuerungsprinzipien oder verwandten technischen Zusammenhängen zu fördern.
Die Daten sind ausdrücklich nicht für die Durchführung echter Flugmanöver, nicht für den operationellen Einsatz in der realen Luftfahrt und nicht für jegliche Art von praktischer Navigation oder Flugsteuerung geeignet.
Die Verwendung dieser Informationen für reale Flugoperationen erfolgt ausschließlich auf eigene Gefahr. Der Autor übernimmt keinerlei Haftung für Schäden, Verluste, Verletzungen oder sonstige Folgen, die direkt oder indirekt aus der Anwendung, Fehlinterpretation oder dem Missbrauch dieser Daten entstehen – sei es durch Personen, Organisationen oder Dritte.
Jegliche Ähnlichkeit mit tatsächlichen Flugmanövern oder operationalen Verfahren ist rein zufällig und nicht beabsichtigt. Für den echten Flugsport und die Luftfahrt sind ausschließlich offizielle, zertifizierte Quellen und qualifiziertes Fachpersonal heranzuziehen.
