diff --git a/src/adsb_client.cpp b/src/adsb_client.cpp index 21e77a9..6b1d25a 100644 --- a/src/adsb_client.cpp +++ b/src/adsb_client.cpp @@ -1,9 +1,11 @@ +#include #include "adsb_client.h" #include #include #include #include + namespace AdsbClient { namespace { diff --git a/src/flight_logbook.cpp b/src/flight_logbook.cpp index 61e025a..d9a4cfb 100644 --- a/src/flight_logbook.cpp +++ b/src/flight_logbook.cpp @@ -65,6 +65,7 @@ namespace { : line.substring(firstComma + 1, secondComma); hex.trim(); if (hex.length() > 0) markSeen(hex.c_str()); + yield(); } f.close(); } @@ -79,19 +80,7 @@ namespace { } } - void writeLogLine(const Aircraft& a) { - char filename[64]; - logFilename(filename, sizeof(filename)); - - bool needsHeader = !SD.exists(filename); - - File f = SD.open(filename, FILE_APPEND); - if (!f) return; - - if (needsHeader) { - f.println("timestamp,hex,callsign,reg,type,distance_km,altitude_ft"); - } - + void writeLogLine(File& f, const Aircraft& a) { time_t now = time(nullptr); struct tm tmNow; localtime_r(&now, &tmNow); @@ -108,7 +97,6 @@ namespace { a.typeCode[0] ? a.typeCode : "", a.distanceKm, (int)a.altBaroFt); - f.close(); } } @@ -132,12 +120,32 @@ void update() { } AircraftTable::unlock(); + bool anyNew = false; + for (uint8_t i = 0; i < count; i++) { + if (snapshot[i].hex[0] && !alreadySeen(snapshot[i].hex)) { anyNew = true; break; } + } + if (!anyNew) return; + + char filename[64]; + logFilename(filename, sizeof(filename)); + bool needsHeader = !SD.exists(filename); + yield(); + + File f = SD.open(filename, FILE_APPEND); + if (!f) return; + if (needsHeader) { + f.println("timestamp,hex,callsign,reg,type,distance_km,altitude_ft"); + } + for (uint8_t i = 0; i < count; i++) { if (!snapshot[i].hex[0]) continue; if (alreadySeen(snapshot[i].hex)) continue; markSeen(snapshot[i].hex); - writeLogLine(snapshot[i]); + writeLogLine(f, snapshot[i]); + yield(); } + + f.close(); } uint16_t todayCount() { return seenCount; } @@ -159,6 +167,7 @@ void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays) { while (entry.available()) { entry.readStringUntil('\n'); lines++; + if (lines % 50 == 0) yield(); } if (lines > 0) totalAircraft += (lines - 1); } @@ -169,4 +178,39 @@ void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays) { dir.close(); } +uint8_t listDays(DayEntry* out, uint8_t maxEntries) { + uint8_t filled = 0; + + File dir = SD.open(Config::SD_LOG_DIR); + if (!dir || !dir.isDirectory()) return 0; + + File entry = dir.openNextFile(); + while (entry && filled < maxEntries) { + if (!entry.isDirectory()) { + String name = String(entry.name()); + if (name.endsWith(".csv")) { + String dateOnly = name.substring(0, name.length() - 4); + int slashIdx = dateOnly.lastIndexOf('/'); + if (slashIdx >= 0) dateOnly = dateOnly.substring(slashIdx + 1); + + uint32_t lines = 0; + while (entry.available()) { + entry.readStringUntil('\n'); + lines++; + if (lines % 50 == 0) yield(); + } + + strncpy(out[filled].date, dateOnly.c_str(), sizeof(out[filled].date) - 1); + out[filled].count = (lines > 0) ? (lines - 1) : 0; + filled++; + } + } + entry.close(); + entry = dir.openNextFile(); + } + dir.close(); + + return filled; +} + } \ No newline at end of file diff --git a/src/flight_logbook.h b/src/flight_logbook.h index c734346..d2e36a3 100644 --- a/src/flight_logbook.h +++ b/src/flight_logbook.h @@ -9,22 +9,21 @@ // beim Start aus der heutigen CSV-Datei). namespace FlightLogbook { - // Einmalig beim Boot aufrufen, NACHDEM die lokale Uhrzeit (UTC-Offset) - // bekannt ist - liest die heutige CSV-Datei (falls vorhanden), um bereits - // geloggte Flugzeuge nicht doppelt einzutragen. void init(); - - // Periodisch aufrufen (z.B. nach jeder erfolgreichen ADS-B-Abfrage): - // prueft auf neue, bisher ungesehene Flugzeuge und schreibt sie ins - // heutige Logbuch. Kuemmert sich auch um den Tageswechsel (neue Datei, - // neue "gesehen"-Liste). void update(); - // Anzahl der HEUTE bereits geloggten (unterschiedlichen) Flugzeuge. uint16_t todayCount(); - // Summiert alle taeglichen Logbuch-Dateien auf der SD-Karte: - // 'totalAircraft' = Summe aller Eintraege ueber alle Tage, - // 'totalDays' = Anzahl der Tage, an denen ueberhaupt geloggt wurde. void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays); + + struct DayEntry { + char date[11] = {0}; // "YYYY-MM-DD" + uint32_t count = 0; + }; + + // Fuellt 'out' mit bis zu 'maxEntries' Tagen (Dateiname + Anzahl + // Eintraege), sortiert wie sie auf der SD-Karte liegen (i.d.R. + // chronologisch). Gibt die tatsaechliche Anzahl gefuellter Eintraege + // zurueck. + uint8_t listDays(DayEntry* out, uint8_t maxEntries); } \ No newline at end of file diff --git a/src/logbook_files_screen.cpp b/src/logbook_files_screen.cpp new file mode 100644 index 0000000..7b33c83 --- /dev/null +++ b/src/logbook_files_screen.cpp @@ -0,0 +1,78 @@ +#include "logbook_files_screen.h" +#include "flight_logbook.h" +#include "touch_input.h" +#include "config.h" + +namespace LogbookFilesScreen { + +namespace { + struct Rect { + int16_t x, y, w, h; + bool contains(int16_t px, int16_t py) const { + return px >= x && px < x + w && py >= y && py < y + h; + } + }; + + void drawButton(TFT_eSPI& tft, const Rect& r, const String& label) { + tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, TFT_NAVY); + tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_DARKGREY); + tft.setTextDatum(MC_DATUM); + tft.setTextColor(TFT_WHITE, TFT_NAVY); + tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2); + tft.setTextDatum(TL_DATUM); + } + + constexpr uint8_t MAX_DAYS_QUERIED = 31; + constexpr uint8_t VISIBLE_ROWS = 10; +} + +void run(TFT_eSPI& tft) { + FlightLogbook::DayEntry days[MAX_DAYS_QUERIED]; + uint8_t count = FlightLogbook::listDays(days, MAX_DAYS_QUERIED); + + Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40}; + + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setCursor(10, 8); + tft.println("Logbook files"); + + if (count == 0) { + tft.setTextColor(TFT_DARKGREY, TFT_BLACK); + tft.setCursor(10, 40); + tft.println("No logbook entries yet."); + } else { + uint8_t startIdx = (count > VISIBLE_ROWS) ? (count - VISIBLE_ROWS) : 0; + int16_t y = 30; + + if (count > VISIBLE_ROWS) { + tft.setTextColor(TFT_DARKGREY, TFT_BLACK); + tft.setCursor(10, y); + tft.printf("Showing last %d of %d days", VISIBLE_ROWS, count); + y += 16; + } + + for (uint8_t i = startIdx; i < count; i++) { + tft.setTextColor(TFT_GREEN, TFT_BLACK); + tft.setCursor(10, y); + tft.print(days[i].date); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setCursor(110, y); + tft.printf("%lu aircraft", (unsigned long)days[i].count); + y += 20; + } + } + + drawButton(tft, backBtn, "Back"); + + bool done = false; + while (!done) { + TouchInput::Point tap; + if (TouchInput::wasTapped(tap) && backBtn.contains(tap.x, tap.y)) { + done = true; + } + delay(20); + } +} + +} \ No newline at end of file diff --git a/src/logbook_files_screen.h b/src/logbook_files_screen.h new file mode 100644 index 0000000..54d9a56 --- /dev/null +++ b/src/logbook_files_screen.h @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +namespace LogbookFilesScreen { + // Blockierend: listet die Logbuch-Dateien (ein Tag pro Zeile, mit Anzahl + // Eintraegen) auf. Kehrt zurueck, sobald "Back" angetippt wird. + void run(TFT_eSPI& tft); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index b4e6684..662b884 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include "config.h" #include "aircraft.h" @@ -22,15 +24,23 @@ #include "splash_screen.h" #include "led_alert.h" #include "flight_logbook.h" +#include "screenshot.h" TFT_eSPI tft = TFT_eSPI(); -constexpr int16_t CONTENT_TOP = 30; // schlanker Header, Radar bekommt den Rest des Screens -constexpr uint32_t POLL_INTERVAL_MS = 300; // wie oft wir NACHSCHAUEN, ob sich Daten geaendert haben -constexpr uint32_t SWEEP_TICK_MS = 80; // wie oft der Sweep-Strahl ein Stueck weiterdreht +// Schlanker Header (Titel + Buttons) PLUS eine zweite, duenne Statuszeile +// (Uhrzeit + WLAN-Signalstaerke) darunter - der Radar-Kreis bekommt den Rest +// des Screens. +constexpr int16_t HEADER_TITLE_H = 30; +constexpr int16_t STATUS_LINE_H = 12; +constexpr int16_t CONTENT_TOP = HEADER_TITLE_H + STATUS_LINE_H; +constexpr uint32_t POLL_INTERVAL_MS = 300; +constexpr uint32_t SWEEP_TICK_MS = 80; +constexpr uint32_t STATUS_LINE_UPDATE_MS = 1000; uint32_t lastPollMs = 0; uint32_t lastSweepMs = 0; -uint32_t lastRenderedVersion = 0xFFFFFFFF; // erzwingt einen ersten Render-Aufruf +uint32_t lastStatusLineMs = 0; +uint32_t lastRenderedVersion = 0xFFFFFFFF; bool forceRedraw = false; bool wasEmergency = false; bool bannerBlinkOn = false; @@ -43,6 +53,7 @@ struct Rect { }; Rect menuBtn = {Config::SCREEN_WIDTH - 60, 3, 54, 22}; +Rect camBtn = {(int16_t)(menuBtn.x - 46), 3, 42, 22}; void drawMenuButton() { tft.fillRoundRect(menuBtn.x, menuBtn.y, menuBtn.w, menuBtn.h, 4, TFT_NAVY); @@ -53,48 +64,96 @@ void drawMenuButton() { tft.setTextDatum(TL_DATUM); } +void drawCamButton() { + tft.fillRoundRect(camBtn.x, camBtn.y, camBtn.w, camBtn.h, 4, TFT_NAVY); + tft.drawRoundRect(camBtn.x, camBtn.y, camBtn.w, camBtn.h, 4, TFT_DARKGREY); + tft.setTextDatum(MC_DATUM); + tft.setTextColor(TFT_WHITE, TFT_NAVY); + tft.drawString("Cam", camBtn.x + camBtn.w / 2, camBtn.y + camBtn.h / 2); + tft.setTextDatum(TL_DATUM); +} + void drawHeader() { - // Bis CONTENT_TOP loeschen, damit kein Bildrest vom Menue zwischen Header - // und Radar-Bildschirm haengen bleibt. Schlanker Header, damit der Radar- - // Kreis darunter maximal viel Platz bekommt. tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(1); tft.setCursor(6, 10); tft.println("Eiswolfs Flightradar"); + drawCamButton(); drawMenuButton(); } -// Zeigt eine grosse, unmissverstaendliche Meldung und haelt das Geraet an - -// KEIN weiterer Bildschirm erscheint, solange keine SD-Karte steckt. Die App -// braucht die Karte fuer Einstellungen, WLAN-Zugangsdaten und Nachschlage- -// tabellen, daher macht ein Weiterlaufen ohne sie keinen Sinn. +void updateStatusLine() { + if (wasEmergency) return; + + tft.fillRect(0, HEADER_TITLE_H, Config::SCREEN_WIDTH, STATUS_LINE_H, TFT_BLACK); + tft.setTextColor(TFT_DARKGREY, TFT_BLACK); + + time_t now = time(nullptr); + if (now > 8 * 3600 * 2) { + struct tm tmNow; + localtime_r(&now, &tmNow); + char timeBuf[6]; + snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tmNow.tm_hour, tmNow.tm_min); + tft.setCursor(6, HEADER_TITLE_H + 2); + tft.print(timeBuf); + } + + if (WiFi.status() == WL_CONNECTED) { + char rssiBuf[14]; + snprintf(rssiBuf, sizeof(rssiBuf), "WiFi %ddBm", WiFi.RSSI()); + tft.setTextDatum(TR_DATUM); + tft.drawString(rssiBuf, Config::SCREEN_WIDTH - 4, HEADER_TITLE_H + 2); + tft.setTextDatum(TL_DATUM); + } +} + +void takeScreenshotWithFeedback() { + String filename = Screenshot::save(tft); + + tft.fillRect(0, 0, Config::SCREEN_WIDTH, CONTENT_TOP, TFT_NAVY); + tft.setTextColor(TFT_WHITE, TFT_NAVY); + tft.setCursor(6, 10); + if (filename.length() > 0) { + tft.print("Saved: " + filename); + } else { + tft.setTextColor(TFT_RED, TFT_NAVY); + tft.print("Screenshot failed"); + } + delay(1200); + drawHeader(); + updateStatusLine(); +} + void haltWithSdRequiredScreen() { + // Ohne SD-Karte kann SettingsStore::load() nie laufen, also ist auch die + // Invertierungs-Einstellung dieses Geraets unbekannt. Dieses konkrete + // CYD-Board braucht invertDisplay(true), um Farben korrekt darzustellen + // (ohne das erscheint Schwarz als Weiss und Gruen als Rosa/Magenta) - + // hier fest setzen, damit Schwarz/Gruen GARANTIERT korrekt aussieht. + tft.invertDisplay(true); + tft.fillScreen(TFT_BLACK); tft.setTextDatum(MC_DATUM); int16_t cx = Config::SCREEN_WIDTH / 2; int16_t cy = Config::SCREEN_HEIGHT / 2; - tft.setTextColor(TFT_RED, TFT_BLACK); + tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setTextSize(2); tft.drawString("For this app a", cx, cy - 40); tft.drawString("SD card is", cx, cy - 10); tft.drawString("required", cx, cy + 20); tft.setTextSize(1); - tft.setTextColor(TFT_DARKGREY, TFT_BLACK); + tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.drawString("Insert a card and restart the device", cx, cy + 60); tft.setTextDatum(TL_DATUM); while (true) { - delay(1000); // haengen bleiben - absichtlich kein weiterer Screen + delay(1000); } } -// Zeigt/versteckt ein blinkendes Notfall-Banner im Header-Bereich, wenn ein -// Flugzeug einen Notfall-Squawk (7500/7600/7700) sendet. Ersetzt kurzzeitig -// den normalen Titel; sobald der Notfall vorbei ist, wird drawHeader() genau -// einmal wieder aufgerufen, um den Titel sauber wiederherzustellen. void updateEmergencyBanner(uint32_t nowMs) { RadarScreen::EmergencyInfo emergency = RadarScreen::checkEmergency(); @@ -110,6 +169,7 @@ void updateEmergencyBanner(uint32_t nowMs) { } else if (wasEmergency) { wasEmergency = false; drawHeader(); + updateStatusLine(); } } @@ -119,42 +179,30 @@ void setup() { tft.init(); tft.setRotation(0); - // Sofort loeschen, bevor irgendetwas anderes passiert (SD-Init etc. - // braucht einen Moment) - sonst zeigt das Display kurz zufaelligen - // Bildspeicher-Muell an, bevor der erste echte Screen gezeichnet wird. tft.fillScreen(TFT_BLACK); TouchInput::begin(); LedAlert::begin(); - // --- SD-Karte: PFLICHT. Ohne Karte kein weiterer Screen. --- bool sdOk = SdStorage::init(); if (!sdOk) { haltWithSdRequiredScreen(); - return; // unerreichbar (haltWithSdRequiredScreen() haengt fuer immer), nur zur Klarheit + return; } SdStorage::seedDefaultDataFiles(); - // Einstellungen (u.a. Display-Invertierung) VOR dem Splash laden und - // anwenden, damit der Splash selbst schon in der richtigen Ausrichtung - // gezeichnet wird (kein Farbwechsel mitten in der Anzeige). SettingsStore::load(); tft.invertDisplay(SettingsStore::displayInverted()); - WifiMgr::init(); // laedt die gespeicherten Netzwerke (bis zu 3) von der SD-Karte + WifiMgr::init(); - // --- Splash-Screen: schwarzer Hintergrund, gruenes Flugzeug, mind. 5s --- SplashScreen::begin(tft); SplashScreen::setStatusLine(tft, 0, "SD Card: OK", TFT_WHITE); - // --- Touch-Kalibrierung (nur beim allerersten Start, oder wenn Datei fehlt) --- if (!TouchInput::loadCalibration()) { CalibrationScreen::run(tft); } - // --- WLAN: Ersteinrichtung (falls noch kein Netzwerk gespeichert ist) - // oder automatisches Verbinden mit dem ersten gerade sichtbaren - // gespeicherten Netzwerk (z.B. Zuhause ODER Auto-Hotspot). --- if (WifiMgr::networkCount() == 0) { WifiSetupScreen::run(tft); } else { @@ -176,7 +224,6 @@ void setup() { AdsbClient::primeTime(); - // --- Standort per IP-Geolocation (einmalig blockierend beim Start) --- SplashScreen::setStatusLine(tft, 2, "Getting location..."); LocationManager::init(); uint32_t locStart = millis(); @@ -187,25 +234,20 @@ void setup() { } SplashScreen::setStatusLine(tft, 2, "Ready!"); - // Sobald die IP-Geolocation einen UTC-Offset geliefert hat, die - // Zeitzone entsprechend setzen - Zeitstempel (Flugbuch, Log-Dateinamen) - // zeigen dann die ECHTE Ortszeit statt UTC, automatisch weltweit richtig. if (LocationManager::hasUtcOffset()) { configTime(LocationManager::utcOffsetSeconds(), 0, "pool.ntp.org", "time.nist.gov"); } AircraftTable::init(); AirlineLookup::init(); - FlightLogbook::init(); // rekonstruiert die "heute schon geloggt"-Liste aus der SD-Karte + FlightLogbook::init(); - // --- Ab hier uebernimmt der Netzwerk-Task (Core 0) laufend WLAN-Status, - // Standort-Updates und ADS-B-Abfragen im Hintergrund. --- NetTask::begin(); - // Splash bleibt mindestens 5 Sekunden sichtbar, egal wie schnell der Rest war. SplashScreen::waitRemaining(); drawHeader(); + updateStatusLine(); RadarScreen::render(tft, CONTENT_TOP); } @@ -215,19 +257,17 @@ void loop() { if (menuBtn.contains(tap.x, tap.y)) { MenuScreen::run(tft); drawHeader(); - forceRedraw = true; // sofort neu zeichnen + updateStatusLine(); + forceRedraw = true; + } else if (camBtn.contains(tap.x, tap.y)) { + takeScreenshotWithFeedback(); } else if (tap.y >= CONTENT_TOP) { if (RadarScreen::handleTap(tap.x, tap.y, CONTENT_TOP)) { - forceRedraw = true; // sofort neu zeichnen nach Interaktion + forceRedraw = true; } } } - // Nur alle POLL_INTERVAL_MS kurz nachschauen (billige Abfrage eines - // Zaehlers), statt staendig teuer neu zu zeichnen. Ein echtes Neuzeichnen - // (render()) passiert nur, wenn sich die Flugzeugdaten TATSAECHLICH - // geaendert haben (neue Abfrage im Hintergrund fertig) oder der Nutzer - // etwas angetippt hat - das vermeidet unnoetiges Flackern. if (forceRedraw || millis() - lastPollMs >= POLL_INTERVAL_MS) { lastPollMs = millis(); uint32_t currentVersion = AircraftTable::version(); @@ -235,22 +275,23 @@ void loop() { lastRenderedVersion = currentVersion; forceRedraw = false; RadarScreen::render(tft, CONTENT_TOP); - lastSweepMs = millis(); // Sweep-Delta nicht ueber den Vollbild-Redraw hinweg aufaddieren + lastSweepMs = millis(); } } - // Sweep-Strahl dreht sich unabhaengig von den Flugzeugdaten weiter - - // billige Linien-Zeichnung ohne Vollbild-Clear, daher kein Flackern. uint32_t nowMs = millis(); - // Naeherungs-/Notfall-Alarm (LED) laeuft IMMER, auch wenn gerade das - // Detail-Fenster offen ist - unabhaengig vom Sweep/Radar-Redraw. RadarScreen::updateProximityAlert(nowMs); if (nowMs - lastSweepMs >= SWEEP_TICK_MS) { uint32_t deltaMs = nowMs - lastSweepMs; lastSweepMs = nowMs; RadarScreen::tick(tft, CONTENT_TOP, deltaMs); - updateEmergencyBanner(nowMs); // gleiche Taktung wie der Sweep-Tick + updateEmergencyBanner(nowMs); + } + + if (nowMs - lastStatusLineMs >= STATUS_LINE_UPDATE_MS) { + lastStatusLineMs = nowMs; + updateStatusLine(); } } \ No newline at end of file diff --git a/src/menu_screen.cpp b/src/menu_screen.cpp index a3739c7..8f9b480 100644 --- a/src/menu_screen.cpp +++ b/src/menu_screen.cpp @@ -3,6 +3,7 @@ #include "calibration_screen.h" #include "wifi_manage_screen.h" #include "stats_screen.h" +#include "logbook_files_screen.h" #include "settings_store.h" #include "config.h" @@ -16,9 +17,9 @@ namespace { } }; - constexpr int16_t ROW_H = 30; - constexpr int16_t ROW_GAP = 4; - constexpr int16_t ROW_START_Y = 26; + constexpr int16_t ROW_H = 26; + constexpr int16_t ROW_GAP = 3; + constexpr int16_t ROW_START_Y = 24; Rect rowRect(uint8_t index) { return {10, (int16_t)(ROW_START_Y + index * (ROW_H + ROW_GAP)), @@ -45,13 +46,14 @@ void run(TFT_eSPI& tft) { Rect proximityBtn = rowRect(4); Rect logbookBtn = rowRect(5); Rect statsBtn = rowRect(6); - Rect backBtn = rowRect(7); + Rect logFilesBtn = rowRect(7); + Rect backBtn = rowRect(8); bool done = false; while (!done) { tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); - tft.setCursor(10, 8); + tft.setCursor(10, 6); tft.println("Settings"); drawButton(tft, calibBtn, "Calibrate touch"); @@ -67,6 +69,7 @@ void run(TFT_eSPI& tft) { drawButton(tft, proximityBtn, "Proximity LED: " + onOff(SettingsStore::proximityAlertEnabled())); drawButton(tft, logbookBtn, "Flight logbook: " + onOff(SettingsStore::flightLogbookEnabled())); drawButton(tft, statsBtn, "Statistics"); + drawButton(tft, logFilesBtn, "Logbook files"); drawButton(tft, backBtn, "Back"); @@ -92,6 +95,8 @@ void run(TFT_eSPI& tft) { SettingsStore::setFlightLogbookEnabled(!SettingsStore::flightLogbookEnabled()); } else if (statsBtn.contains(tap.x, tap.y)) { StatsScreen::run(tft); + } else if (logFilesBtn.contains(tap.x, tap.y)) { + LogbookFilesScreen::run(tft); } else if (backBtn.contains(tap.x, tap.y)) { done = true; } diff --git a/src/radar_screen.cpp b/src/radar_screen.cpp index 424d142..f17ae8a 100644 --- a/src/radar_screen.cpp +++ b/src/radar_screen.cpp @@ -84,22 +84,13 @@ namespace { return TFT_RED; } - // Zeichnet ein kleines Dreieck, das in Flugrichtung (headingDeg) zeigt - - // wie das klassische "Ziel"-Symbol auf echten ATC-Radarschirmen. Ersetzt - // den vorherigen Kreis + separaten Kursstrich in einem Symbol. void drawAircraftMarker(TFT_eSPI& gfx, int16_t x, int16_t y, float headingDeg, uint16_t color) { - double rad = headingDeg * PI / 180.0; - double backRad1 = (headingDeg + 150.0) * PI / 180.0; - double backRad2 = (headingDeg - 150.0) * PI / 180.0; + gfx.fillCircle(x, y, 5, color); - int16_t tipX = x + (int16_t)(7 * sin(rad)); - int16_t tipY = y - (int16_t)(7 * cos(rad)); - int16_t backLX = x + (int16_t)(5 * sin(backRad1)); - int16_t backLY = y - (int16_t)(5 * cos(backRad1)); - int16_t backRX = x + (int16_t)(5 * sin(backRad2)); - int16_t backRY = y - (int16_t)(5 * cos(backRad2)); - - gfx.fillTriangle(tipX, tipY, backLX, backLY, backRX, backRY, color); + double rad = headingDeg * PI / 180.0; + int16_t dx = (int16_t)(sin(rad) * 10); + int16_t dy = (int16_t)(-cos(rad) * 10); + gfx.drawLine(x, y, x + dx, y + dy, color); } uint16_t dimColorForAltitude(int32_t altFt) { @@ -237,6 +228,9 @@ namespace { gfx.setTextColor(TFT_DARKGREY, TFT_BLACK); gfx.setTextDatum(MC_DATUM); gfx.drawString("N", L.cx, L.cy - L.radius - 8); + gfx.drawString("S", L.cx, L.cy + L.radius - 10); + gfx.drawString("E", L.cx + L.radius - 10, L.cy); + gfx.drawString("W", L.cx - L.radius + 10, L.cy); char ringLabel[8]; snprintf(ringLabel, sizeof(ringLabel), "%.0f", rangeKm / 3); gfx.drawString(ringLabel, L.cx, L.cy - L.radius / 3); diff --git a/src/wifi_manager.cpp b/src/wifi_manager.cpp index 7f3526d..31db9a1 100644 --- a/src/wifi_manager.cpp +++ b/src/wifi_manager.cpp @@ -25,6 +25,14 @@ namespace { uint32_t lastReconnectAttemptMs = 0; constexpr uint32_t RECONNECT_RETRY_MS = 10000; + // Der Reconnect-Scan MUSS asynchron laufen: update() wird dauerhaft in + // der NetTask-Schleife (Core 0) aufgerufen, und ein blockierender Scan + // (WiFi.scanNetworks(false)) haelt dort mehrere Sekunden am Stueck fest - + // das hat frueher den Watchdog ausgeloest (Bootloop). Stattdessen wird + // der Scan hier gestartet und ueber mehrere update()-Aufrufe hinweg + // nicht-blockierend abgefragt. + bool reconnectScanPending = false; + SemaphoreHandle_t mutex = nullptr; void setState(State s) { @@ -66,6 +74,44 @@ namespace { } f.close(); } + + // Nicht-blockierender Ersatz fuer beginConnect() zur Verwendung INNERHALB + // von update() (NetTask). Startet beim ersten Aufruf einen asynchronen + // Scan; bei weiteren Aufrufen wird nur kurz nachgeschaut, ob er fertig + // ist (WiFi.scanComplete() ist eine billige, sofort zurueckkehrende + // Abfrage) - blockiert also nie. + void tryReconnectAsync() { + if (!reconnectScanPending) { + WiFi.scanNetworks(/*async=*/true); + reconnectScanPending = true; + return; + } + + int n = WiFi.scanComplete(); + if (n == WIFI_SCAN_RUNNING) return; + + reconnectScanPending = false; + + if (n <= 0) { + WiFi.scanDelete(); + return; + } + + int8_t chosen = -1; + for (uint8_t i = 0; i < networkCountVal && chosen < 0; i++) { + for (int j = 0; j < n; j++) { + if (WiFi.SSID(j) == networks[i].ssid) { + chosen = (int8_t)i; + break; + } + } + } + WiFi.scanDelete(); + + if (chosen < 0) return; + + connectTo(networks[chosen].ssid, networks[chosen].pass); + } } void init() { @@ -114,6 +160,12 @@ void beginConnect() { return; } + // Blockierender Scan (ca. 2-3s) - passiert nur einmal beim Booten, VOR + // dem Start von NetTask, daher unkritisch (kein Watchdog-Risiko). Wir + // verbinden uns mit dem ERSTEN gespeicherten Netzwerk, das gerade + // sichtbar ist (Prioritaet = Speicherreihenfolge), statt blind das erste + // gespeicherte zu versuchen - so klappt es automatisch mit Zuhause-WLAN + // ODER dem Auto-Hotspot, je nachdem was gerade in Reichweite ist. int visibleCount = WiFi.scanNetworks(/*async=*/false); int8_t chosen = -1; @@ -157,10 +209,16 @@ void update() { return; } + // In Idle/Failed (auch nach einem fehlgeschlagenen Erstverbindungsversuch + // beim Booten) in regelmaessigen Abstaenden automatisch erneut versuchen, + // alle gespeicherten Netzwerke durchzuscannen - NICHT-blockierend (siehe + // tryReconnectAsync()), damit NetTask den Watchdog nicht verpasst. if ((s == State::Idle || s == State::Failed) && networkCountVal > 0) { - if (millis() - lastReconnectAttemptMs >= RECONNECT_RETRY_MS) { + if (reconnectScanPending) { + tryReconnectAsync(); + } else if (millis() - lastReconnectAttemptMs >= RECONNECT_RETRY_MS) { lastReconnectAttemptMs = millis(); - beginConnect(); + tryReconnectAsync(); } } }