diff --git a/CLAUDE.md b/CLAUDE.md index fa31001..c4ae3ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,6 +190,11 @@ in dieser Reihenfolge: Alex nachfragen statt zu raten. Erst NACH dem Eintragen der korrekten Nummer: einmal sauber `pio run` bauen, DANACH erst der Rest des bekannten Workflows (README, Tag, index.html, Bin-Dateien, Commit/Push). + (Hier bewusst NUR bauen, NICHT flashen - Ausnahme von der sonst + geltenden Auto-Flash-Regel, siehe Abschnitt "Nach jedem erfolgreichen + Build automatisch flashen" weiter unten. Das Testgerät soll auf der + bisherigen Version bleiben, damit das neue Release per OTA getestet + werden kann.) 1. Prüfen, ob seit dem letzten Commit neue/geänderte Features hinzugekommen sind, die für Endnutzer sichtbar sind (neue Menüpunkte, geändertes Verhalten, neue Screens) - falls ja, **README.md entsprechend ergänzen** @@ -245,4 +250,13 @@ fragen - außer der Nutzer sagt ausdrücklich "nur bauen, nicht flashen" o.ä. Kurz danach bestätigen, dass der Upload ebenfalls erfolgreich war (inkl. "[SUCCESS]"-Zeile am Ende). +AUSNAHME: Innerhalb der Push & Release-Routine (siehe Abschnitt +"Standard-Workflow: Push & Release") NICHT automatisch flashen, selbst +nach erfolgreichem Build - dort wird bewusst nur gebaut. Grund: Alex +möchte das Testgerät auf der bisherigen Version belassen, um das neue +Release anschließend über die OTA-Update-Funktion zu testen, statt es +direkt per Kabel zu flashen. Für alle anderen Anlässe (normales +Entwickeln/Testen, einzelne Fixes) gilt die automatische Flash-Regel +unverändert weiter. + Bitte diese Regel jetzt in die CLAUDE.md-Datei einpflegen. \ No newline at end of file diff --git a/src/aircraft_watchlist.cpp b/src/aircraft_watchlist.cpp index bdf4a30..74a23f6 100644 --- a/src/aircraft_watchlist.cpp +++ b/src/aircraft_watchlist.cpp @@ -3,6 +3,8 @@ #include "sd_mutex.h" #include "sd_storage.h" #include +#include +#include #include #include @@ -14,6 +16,16 @@ namespace { char watched[MAX_WATCHED][9] = {{0}}; uint8_t watchedCount = 0; + // Schuetzt watched[]/watchedCount - urspruenglich nur von Core 1 (Menue- + // Screens, Radar) verwendet, seit der WebUI-Listenverwaltung (siehe + // web_export_server.cpp) aber auch von Core 0 (NetTask) aus erreichbar. + // Gleiches Muster wie AircraftDetails::mutex. + SemaphoreHandle_t mutex = nullptr; + + void ensureMutex() { + if (mutex == nullptr) mutex = xSemaphoreCreateMutex(); + } + // Ueberspringt fuehrende Leerzeichen, uebernimmt bis zu 8 Zeichen und // bricht bei einem Leerzeichen ab (ADS-B-Rufzeichen haben oft Padding), // alles in Grossbuchstaben. @@ -61,55 +73,83 @@ namespace { } void init() { + ensureMutex(); loadFromSd(); } -uint8_t count() { return watchedCount; } +uint8_t count() { + ensureMutex(); + xSemaphoreTake(mutex, portMAX_DELAY); + uint8_t c = watchedCount; + xSemaphoreGive(mutex); + return c; +} String callsignAt(uint8_t index) { - if (index >= watchedCount) return String(); - return String(watched[index]); + ensureMutex(); + xSemaphoreTake(mutex, portMAX_DELAY); + String out = (index >= watchedCount) ? String() : String(watched[index]); + xSemaphoreGive(mutex); + return out; } bool addWatched(const char* callsign) { - if (watchedCount >= MAX_WATCHED) return false; if (!callsign || !callsign[0]) return false; char normalized[9] = {0}; normalize(callsign, normalized); if (!normalized[0]) return false; - for (uint8_t j = 0; j < watchedCount; j++) { - if (strcmp(watched[j], normalized) == 0) return true; + ensureMutex(); + xSemaphoreTake(mutex, portMAX_DELAY); + bool ok = true; + bool alreadyPresent = false; + if (watchedCount >= MAX_WATCHED) { + ok = false; + } else { + for (uint8_t j = 0; j < watchedCount; j++) { + if (strcmp(watched[j], normalized) == 0) { alreadyPresent = true; break; } + } + if (!alreadyPresent) { + strncpy(watched[watchedCount], normalized, 8); + watched[watchedCount][8] = 0; + watchedCount++; + } } + xSemaphoreGive(mutex); - strncpy(watched[watchedCount], normalized, 8); - watched[watchedCount][8] = 0; - watchedCount++; - saveToSd(); - return true; + if (ok && !alreadyPresent) saveToSd(); + return ok; } void removeWatched(uint8_t index) { - if (index >= watchedCount) return; - for (uint8_t i = index; i < watchedCount - 1; i++) { - strncpy(watched[i], watched[i + 1], 9); + ensureMutex(); + xSemaphoreTake(mutex, portMAX_DELAY); + bool changed = index < watchedCount; + if (changed) { + for (uint8_t i = index; i < watchedCount - 1; i++) { + strncpy(watched[i], watched[i + 1], 9); + } + watchedCount--; + watched[watchedCount][0] = 0; } - watchedCount--; - watched[watchedCount][0] = 0; - saveToSd(); + xSemaphoreGive(mutex); + if (changed) saveToSd(); } bool isWatched(const char* callsign) { - if (watchedCount == 0) return false; + ensureMutex(); char normalized[9]; normalize(callsign, normalized); if (!normalized[0]) return false; + xSemaphoreTake(mutex, portMAX_DELAY); + bool found = false; for (uint8_t i = 0; i < watchedCount; i++) { - if (strcmp(watched[i], normalized) == 0) return true; + if (strcmp(watched[i], normalized) == 0) { found = true; break; } } - return false; + xSemaphoreGive(mutex); + return found; } } diff --git a/src/airline_filter.cpp b/src/airline_filter.cpp index 6030031..a7eb087 100644 --- a/src/airline_filter.cpp +++ b/src/airline_filter.cpp @@ -3,6 +3,8 @@ #include "sd_mutex.h" #include "sd_storage.h" #include +#include +#include #include #include @@ -14,6 +16,16 @@ namespace { char hidden[MAX_HIDDEN][4] = {{0}}; uint8_t hiddenCount = 0; + // Schuetzt hidden[]/hiddenCount - urspruenglich nur von Core 1 (Menue- + // Screens) verwendet, seit der WebUI-Listenverwaltung (siehe + // web_export_server.cpp) aber auch von Core 0 (NetTask) aus erreichbar. + // Gleiches Muster wie AircraftDetails::mutex. + SemaphoreHandle_t mutex = nullptr; + + void ensureMutex() { + if (mutex == nullptr) mutex = xSemaphoreCreateMutex(); + } + void extractPrefix(const char* callsign, char* out) { int i = 0; for (; i < 3 && callsign[i] && isalpha((unsigned char)callsign[i]); i++) { @@ -56,18 +68,27 @@ namespace { } void init() { + ensureMutex(); loadFromSd(); } -uint8_t count() { return hiddenCount; } +uint8_t count() { + ensureMutex(); + xSemaphoreTake(mutex, portMAX_DELAY); + uint8_t c = hiddenCount; + xSemaphoreGive(mutex); + return c; +} String icaoAt(uint8_t index) { - if (index >= hiddenCount) return String(); - return String(hidden[index]); + ensureMutex(); + xSemaphoreTake(mutex, portMAX_DELAY); + String out = (index >= hiddenCount) ? String() : String(hidden[index]); + xSemaphoreGive(mutex); + return out; } bool addHidden(const char* icaoPrefix) { - if (hiddenCount >= MAX_HIDDEN) return false; if (!icaoPrefix || !icaoPrefix[0]) return false; char normalized[4] = {0}; @@ -78,37 +99,56 @@ bool addHidden(const char* icaoPrefix) { normalized[i] = 0; if (i == 0) return false; - for (uint8_t j = 0; j < hiddenCount; j++) { - if (strcmp(hidden[j], normalized) == 0) return true; + ensureMutex(); + xSemaphoreTake(mutex, portMAX_DELAY); + bool ok = true; + bool alreadyPresent = false; + if (hiddenCount >= MAX_HIDDEN) { + ok = false; + } else { + for (uint8_t j = 0; j < hiddenCount; j++) { + if (strcmp(hidden[j], normalized) == 0) { alreadyPresent = true; break; } + } + if (!alreadyPresent) { + strncpy(hidden[hiddenCount], normalized, 3); + hidden[hiddenCount][3] = 0; + hiddenCount++; + } } + xSemaphoreGive(mutex); - strncpy(hidden[hiddenCount], normalized, 3); - hidden[hiddenCount][3] = 0; - hiddenCount++; - saveToSd(); - return true; + if (ok && !alreadyPresent) saveToSd(); + return ok; } void removeHidden(uint8_t index) { - if (index >= hiddenCount) return; - for (uint8_t i = index; i < hiddenCount - 1; i++) { - strncpy(hidden[i], hidden[i + 1], 4); + ensureMutex(); + xSemaphoreTake(mutex, portMAX_DELAY); + bool changed = index < hiddenCount; + if (changed) { + for (uint8_t i = index; i < hiddenCount - 1; i++) { + strncpy(hidden[i], hidden[i + 1], 4); + } + hiddenCount--; + hidden[hiddenCount][0] = 0; } - hiddenCount--; - hidden[hiddenCount][0] = 0; - saveToSd(); + xSemaphoreGive(mutex); + if (changed) saveToSd(); } bool isHidden(const char* callsign) { - if (hiddenCount == 0) return false; + ensureMutex(); char prefix[4]; extractPrefix(callsign, prefix); if (!prefix[0]) return false; + xSemaphoreTake(mutex, portMAX_DELAY); + bool found = false; for (uint8_t i = 0; i < hiddenCount; i++) { - if (strcmp(hidden[i], prefix) == 0) return true; + if (strcmp(hidden[i], prefix) == 0) { found = true; break; } } - return false; + xSemaphoreGive(mutex); + return found; } -} \ No newline at end of file +} diff --git a/src/i18n_de.h b/src/i18n_de.h index ebb1317..8ebae9c 100644 --- a/src/i18n_de.h +++ b/src/i18n_de.h @@ -198,7 +198,7 @@ static const char* const I18N_DE[] = { "Logbuch / WebUI", "Logbuch / WebUI", - "Über diese Webseite kannst du das Flugbuch direkt im Browser ansehen, herunterladen und einzeln löschen. Im selben WLAN einfach im Browser aufrufen:", + "Diese Webseite zeigt oben ein Live-Radar mit allen aktuell sichtbaren Flugzeugen und darunter das Flugbuch zum Ansehen, Herunterladen und einzelnen Löschen direkt im Browser. Im selben WLAN einfach im Browser aufrufen:", "Keine WLAN-Verbindung - die Seite ist nur erreichbar, solange eine Netzwerkverbindung besteht.", "Wetter", diff --git a/src/i18n_en.h b/src/i18n_en.h index 7969a19..281aaeb 100644 --- a/src/i18n_en.h +++ b/src/i18n_en.h @@ -199,7 +199,7 @@ static const char* const I18N_EN[] = { "Logbook / WebUI", "Logbook / WebUI", - "This web page lets you view, download and individually delete the flight logbook right from a browser. While connected to the same WiFi network, open a browser and go to:", + "This web page shows a live radar with all currently visible aircraft at the top, and below it lets you view, download and individually delete the flight logbook right from a browser. While connected to the same WiFi network, open a browser and go to:", "No WiFi connection - the page is only reachable while connected to a network.", "Weather", diff --git a/src/i18n_es.h b/src/i18n_es.h index 4ff0ec8..16f5b83 100644 --- a/src/i18n_es.h +++ b/src/i18n_es.h @@ -198,7 +198,7 @@ static const char* const I18N_ES[] = { "Registro / WebUI", "Registro / WebUI", - "Esta página web permite ver, descargar y eliminar individualmente el registro de vuelo directamente desde un navegador. Conectado a la misma red WiFi, simplemente abre un navegador y ve a:", + "Esta página web muestra arriba un radar en vivo con todos los aviones actualmente visibles y, debajo, permite ver, descargar y eliminar individualmente el registro de vuelo directamente desde un navegador. Conectado a la misma red WiFi, simplemente abre un navegador y ve a:", "Sin conexión WiFi - la página solo es accesible mientras haya una conexión de red.", "Clima", diff --git a/src/i18n_fr.h b/src/i18n_fr.h index 50c2d07..75db2f5 100644 --- a/src/i18n_fr.h +++ b/src/i18n_fr.h @@ -198,7 +198,7 @@ static const char* const I18N_FR[] = { "Journal / WebUI", "Journal / WebUI", - "Cette page web permet de consulter, télécharger et supprimer individuellement le journal de vol directement depuis un navigateur. Connecté au même réseau WiFi, ouvrez simplement un navigateur et allez à :", + "Cette page web affiche en haut un radar en direct avec tous les avions actuellement visibles, et en dessous permet de consulter, télécharger et supprimer individuellement le journal de vol directement depuis un navigateur. Connecté au même réseau WiFi, ouvrez simplement un navigateur et allez à :", "Pas de connexion WiFi - la page n'est accessible que lorsqu'une connexion réseau est établie.", "Météo", diff --git a/src/i18n_it.h b/src/i18n_it.h index 823cb14..4d0f1d8 100644 --- a/src/i18n_it.h +++ b/src/i18n_it.h @@ -198,7 +198,7 @@ static const char* const I18N_IT[] = { "Registro / WebUI", "Registro / WebUI", - "Questa pagina web permette di visualizzare, scaricare ed eliminare singolarmente il registro di volo direttamente dal browser. Connesso alla stessa rete WiFi, apri un browser e vai a:", + "Questa pagina web mostra in alto un radar dal vivo con tutti gli aerei attualmente visibili e, sotto, permette di visualizzare, scaricare ed eliminare singolarmente il registro di volo direttamente dal browser. Connesso alla stessa rete WiFi, apri un browser e vai a:", "Nessuna connessione WiFi - la pagina è raggiungibile solo mentre è attiva una connessione di rete.", "Meteo", diff --git a/src/i18n_tr.h b/src/i18n_tr.h index 7a75396..1eb4fde 100644 --- a/src/i18n_tr.h +++ b/src/i18n_tr.h @@ -198,7 +198,7 @@ static const char* const I18N_TR[] = { "Kayıt Defteri / WebUI", "Kayıt Defteri / WebUI", - "Bu web sayfası, uçuş kayıt defterini doğrudan tarayıcıdan görüntülemenizi, indirmenizi ve tek tek silmenizi sağlar. Aynı WiFi ağındayken tarayıcıyı açıp şu adrese gidin:", + "Bu web sayfasının üst kısmında o an görünen tüm uçakları gösteren canlı bir radar, altında ise uçuş kayıt defterini doğrudan tarayıcıdan görüntüleme, indirme ve tek tek silme imkanı bulunur. Aynı WiFi ağındayken tarayıcıyı açıp şu adrese gidin:", "WiFi bağlantısı yok - sayfaya yalnızca bir ağ bağlantısı varken erişilebilir.", "Hava Durumu", diff --git a/src/web_export_server.cpp b/src/web_export_server.cpp index f88566e..d57616a 100644 --- a/src/web_export_server.cpp +++ b/src/web_export_server.cpp @@ -2,7 +2,12 @@ #include "config.h" #include "flight_logbook.h" #include "sd_mutex.h" +#include "airline_filter.h" +#include "aircraft_watchlist.h" +#include "aircraft_table.h" +#include "settings_store.h" #include +#include #include #include @@ -23,6 +28,33 @@ namespace { return true; } + // Absichtliche Duplikate der gleichnamigen (lokalen/statischen) Funktionen + // aus radar_screen.cpp - dort nicht exportiert, und nach der im Projekt + // etablierten Konvention "jeder Screen/jedes Modul dupliziert seine + // eigenen kleinen Helfer statt eines gemeinsamen Moduls" (siehe z.B. + // timeout_screen.cpp) bewusst hier erneut definiert statt radar_screen.cpp + // umzubauen. Bei Aenderungen an der Logik in radar_screen.cpp bitte diese + // Kopie hier synchron halten, damit das WebUI-Radar dieselben Ringe/ + // Markierungen zeigt wie das Geraete-Display. + bool isEmergencySquawkWeb(const char* squawk) { + if (!squawk[0]) return false; + for (uint8_t i = 0; i < Config::EMERGENCY_SQUAWK_COUNT; i++) { + if (strcmp(squawk, Config::EMERGENCY_SQUAWKS[i]) == 0) return true; + } + return false; + } + + // "Auffaellig" beschraenkt sich aktuell auf die Heavy-Kategorie (ADS-B + // Emitter-Kategorie "A5") - der Teil ueber Militaer-/Regierungs- + // Rufzeichen-Praefixe (Config::NOTABLE_CALLSIGN_PREFIXES) ist NICHT + // umgesetzt, weil diese Konstante nirgends im Projekt existiert (auch + // nicht in radar_screen.cpp) - das zugehoerige Feature wurde bisher nie + // tatsaechlich spezifiziert/implementiert. Sobald es das gibt, hier den + // Praefix-Abgleich ergaenzen, analog zu isEmergencySquawkWeb() oben. + bool isHeavyCategoryWeb(const char* category) { + return category[0] == 'A' && category[1] == '5'; + } + String htmlHeader(const String& title) { String html; html += ""; @@ -38,16 +70,76 @@ namespace { html += "button{background:#0a0f0d;color:#ff3b3b;border:1px solid #ff3b3b;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}"; html += "button:hover{background:#ff3b3b;color:#0a0f0d;}"; html += ".dl{color:#39ff14;text-decoration:none;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;margin-right:6px;display:inline-block;}"; + // Gruen statt Rot fuer "Hinzufuegen"-Buttons - die roten button{}-Regeln + // oben bleiben fuer alle destruktiven "Entfernen/Loeschen"-Buttons + // unveraendert, .addbtn ist ausschliesslich fuer die neuen Listen- + // Formulare (siehe handleLists()) gedacht. + html += ".addbtn{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}"; + html += ".addbtn:hover{background:#39ff14;color:#0a0f0d;}"; + html += "input[type=text]{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:5px 8px;font-family:inherit;}"; + html += "nav{margin-bottom:10px;}nav a{color:#39ff14;margin-right:16px;}"; + html += "#radarCanvas{width:100%;max-width:400px;height:auto;background:#05100a;border:1px solid #1f3a2b;border-radius:8px;display:block;}"; + html += "#radarStatus{font-size:12px;color:#7a9a86;margin-top:4px;}"; html += ""; html += "

" + title + "

"; return html; } + // Live-Radar-Ansicht fuer die Startseite: ein , das per JavaScript + // alle paar Sekunden /radar.json abruft und die Flugzeuge polar (Peilung/ + // Distanz, genau wie auf dem Geraete-Display) zeichnet. Bewusst per + // fetch()-Polling statt WebSocket/SSE gehalten - deutlich weniger Code + // und Speicherbedarf auf dem ESP32, und fuer eine gelegentlich vom Handy + // aus aufgerufene Seite voellig ausreichend. + void appendRadarSection(String& html) { + html += "

Live Radar

"; + html += ""; + html += "

Loading...

"; + html += ""; + } + void handleRoot() { FlightLogbook::DayEntry days[MAX_DAYS_QUERIED]; uint8_t dayCount = FlightLogbook::listDays(days, MAX_DAYS_QUERIED); - String html = htmlHeader("Eiswolfs Flightradar - Logbook Export"); + String html = htmlHeader("Eiswolfs Flightradar"); + + appendRadarSection(html); + + html += ""; html += "

Flight Logbook

"; if (dayCount == 0) { @@ -145,6 +237,159 @@ namespace { server.send(303); } + // Verwaltung von Airline-Filter und Beobachtungsliste per Browser - + // beide Backend-Module (AirlineFilter/AircraftWatchlist) sind seit + // dieser Erweiterung mutex-geschuetzt (siehe dort), da sie jetzt sowohl + // von Core 1 (Radar-/Menue-Screens) als auch von hier aus - Core 0, + // WebExportServer laeuft innerhalb von NetTask - aufgerufen werden. + // Praktisch vor allem fuer laengere Eingaben (Rufzeichen, ICAO-Codes), + // die sich per Handy-Tastatur deutlich bequemer eintippen lassen als + // ueber die kleine Bildschirmtastatur des Geraets. + void handleLists() { + String html = htmlHeader("Eiswolfs Flightradar - Lists"); + + html += ""; + + html += "

Hidden Airlines

"; + html += "

Aircraft from these airlines (matched by callsign prefix, e.g. \"DLH\") are hidden from the radar.

"; + uint8_t airlineCount = AirlineFilter::count(); + if (airlineCount == 0) { + html += "

No hidden airlines yet.

"; + } else { + html += ""; + for (uint8_t i = 0; i < airlineCount; i++) { + html += ""; + } + html += "
ICAO prefix
" + AirlineFilter::icaoAt(i) + ""; + html += "
"; + html += ""; + html += "
"; + } + html += "
"; + html += " "; + html += "
"; + + html += "

Watched Aircraft

"; + html += "

The blue LED blinks when any of these exact callsigns appears on the radar (up to 5).

"; + uint8_t watchCount = AircraftWatchlist::count(); + if (watchCount == 0) { + html += "

No watched aircraft yet.

"; + } else { + html += ""; + for (uint8_t i = 0; i < watchCount; i++) { + html += ""; + } + html += "
Callsign
" + AircraftWatchlist::callsignAt(i) + ""; + html += "
"; + html += ""; + html += "
"; + } + html += "
"; + html += " "; + html += "
"; + + html += ""; + server.send(200, "text/html", html); + } + + void handleAirlineAdd() { + if (server.hasArg("icao")) { + String icao = server.arg("icao"); + icao.trim(); + if (icao.length() > 0 && icao.length() <= 3) { + AirlineFilter::addHidden(icao.c_str()); + } + } + server.sendHeader("Location", "/lists"); + server.send(303); + } + + void handleAirlineDelete() { + if (server.hasArg("index")) { + int idx = server.arg("index").toInt(); + if (idx >= 0 && idx < 255) { + AirlineFilter::removeHidden((uint8_t)idx); + } + } + server.sendHeader("Location", "/lists"); + server.send(303); + } + + void handleWatchlistAdd() { + if (server.hasArg("callsign")) { + String callsign = server.arg("callsign"); + callsign.trim(); + if (callsign.length() > 0 && callsign.length() <= 8) { + AircraftWatchlist::addWatched(callsign.c_str()); + } + } + server.sendHeader("Location", "/lists"); + server.send(303); + } + + void handleWatchlistDelete() { + if (server.hasArg("index")) { + int idx = server.arg("index").toInt(); + if (idx >= 0 && idx < 255) { + AircraftWatchlist::removeWatched((uint8_t)idx); + } + } + server.sendHeader("Location", "/lists"); + server.send(303); + } + + // Datenquelle fuer das Live-Radar auf der Startseite (siehe + // appendRadarSection()). Wendet dieselben Filter/Prioritaeten an wie das + // Geraete-Display (render() in radar_screen.cpp): Reichweite, "Boden- + // fahrzeuge ausblenden", Airline-Filter, sowie Notfall/Beobachtungsliste/ + // "auffaellig" als sich gegenseitig ausschliessende Ring-Markierungen in + // genau dieser Prioritaet. + void handleRadarJson() { + float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()]; + bool hideGround = SettingsStore::hideGroundVehicles(); + bool emergencyOn = SettingsStore::emergencyAlertEnabled(); + bool watchOn = SettingsStore::watchlistAlertEnabled(); + + JsonDocument doc; + doc["range_km"] = rangeKm; + JsonArray arr = doc["aircraft"].to(); + + AircraftTable::lock(); + Aircraft* table = AircraftTable::raw(); + for (uint8_t i = 0; i < AircraftTable::capacity(); i++) { + Aircraft& a = table[i]; + if (!a.valid) continue; + if (a.distanceKm > rangeKm * 1.05f) continue; + + bool isGroundVehicle = a.category[0] == 'C'; + if (hideGround && isGroundVehicle) continue; + if (AirlineFilter::isHidden(a.callsign)) continue; + + bool isRotorcraft = a.category[0] == 'A' && a.category[1] == '7'; + bool isEmergency = emergencyOn && isEmergencySquawkWeb(a.squawk); + bool isWatched = watchOn && AircraftWatchlist::isWatched(a.callsign); + bool isNotable = isHeavyCategoryWeb(a.category); + + JsonObject o = arr.add(); + o["hex"] = a.hex; + o["callsign"] = a.callsign[0] ? a.callsign : a.hex; + o["dist_km"] = a.distanceKm; + o["bearing_deg"] = a.bearingDeg; + o["alt_ft"] = a.altBaroFt; + o["track_deg"] = a.headingDeg; + o["ground_vehicle"] = isGroundVehicle; + o["rotorcraft"] = isRotorcraft; + o["emergency"] = isEmergency; + o["watched"] = isWatched; + o["notable"] = isNotable; + } + AircraftTable::unlock(); + + String out; + serializeJson(doc, out); + server.send(200, "application/json", out); + } + void handleNotFound() { server.send(404, "text/plain", "Not found"); } @@ -152,9 +397,15 @@ namespace { void begin() { server.on("/", handleRoot); + server.on("/radar.json", handleRadarJson); server.on("/export.csv", handleExportCsv); server.on("/csv", HTTP_GET, handleCsvDownload); server.on("/logbook/delete", HTTP_POST, handleLogbookDelete); + server.on("/lists", handleLists); + server.on("/lists/airlines/add", HTTP_POST, handleAirlineAdd); + server.on("/lists/airlines/delete", HTTP_POST, handleAirlineDelete); + server.on("/lists/watchlist/add", HTTP_POST, handleWatchlistAdd); + server.on("/lists/watchlist/delete", HTTP_POST, handleWatchlistDelete); server.onNotFound(handleNotFound); server.begin(); }