diff --git a/CLAUDE.md b/CLAUDE.md index 3d3a149..aaaf159 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -169,10 +169,21 @@ folgende Schritte in dieser Reihenfolge: Tag erstellt wurde). 7. Kurze Zusammenfassung am Ende: was committet/getaggt/gepusht wurde, und ob die README aktualisiert wurde (und falls ja, welche Abschnitte). - ## Nach jedem erfolgreichen Build automatisch flashen +8. ERST DANACH (nach Schritt 5, wenn bootloader/firmware/partitions bereits + ins Hauptverzeichnis kopiert sind): die `firmware.bin` im + `.pio/build/esp32dev/`-Ordner zusätzlich zu `CYD-flightradar.bin` + umbenennen (bzw. kopieren) - das ist die Datei für den GitHub-Release- + Upload, erspart das manuelle Umbenennen nach jedem Release. WICHTIG: + diese Umbenennung darf NICHT vor dem Auto-Flash (siehe unten) passieren, + sonst schlägt `pio run --target upload` fehl, weil es die Datei unter + dem Namen `firmware.bin` erwartet. + +## Nach jedem erfolgreichen Build automatisch flashen Sobald `pio run` (Build) erfolgreich ohne Fehler durchgelaufen ist, IMMER direkt im Anschluss auch flashen (`pio run --target upload`), ohne extra danach zu 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). \ No newline at end of file +"[SUCCESS]"-Zeile am Ende). + +Bitte diese Regel jetzt in die CLAUDE.md-Datei einpflegen. \ No newline at end of file diff --git a/README.md b/README.md index d3108a2..16b4856 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Shows at a glance: - **Days with sightings** – how many days anything was logged at all - **Average per day** – total count divided by number of days - **Uptime** – how long the device has been running since the last restart +- **Highest flight today** – callsign and altitude of whichever aircraft was logged at the greatest altitude today (the altitude at first sighting, not necessarily its current one), e.g. "DLH441 (38000 ft)" The **"Reset logbook data"** button (tap twice to confirm) permanently deletes all logged data. diff --git a/src/flight_logbook.cpp b/src/flight_logbook.cpp index 3078255..1cefbf6 100644 --- a/src/flight_logbook.cpp +++ b/src/flight_logbook.cpp @@ -191,6 +191,58 @@ void update() { uint16_t todayCount() { return seenCount; } +TopAltitude todayMaxAltitude() { + TopAltitude result; + + SdMutex::Guard guard; + if (currentDateStr[0] == 0) return result; // Uhrzeit noch nicht bekannt + + char filename[64]; + logFilename(filename, sizeof(filename)); + if (!SD.exists(filename)) return result; + + File f = SD.open(filename, FILE_READ); + if (!f) return result; + + bool firstLine = true; + while (f.available()) { + String line = f.readStringUntil('\n'); + if (firstLine) { firstLine = false; continue; } // CSV-Header ueberspringen + if (line.length() == 0) continue; + + // Spalten: timestamp,hex,callsign,reg,type,distance_km,altitude_ft + int commaIdx[6]; + int found = 0; + int searchFrom = 0; + for (int c = 0; c < 6; c++) { + int idx = line.indexOf(',', searchFrom); + if (idx < 0) break; + commaIdx[c] = idx; + searchFrom = idx + 1; + found++; + } + if (found < 6) continue; // unvollstaendige/kaputte Zeile ueberspringen + + String callsign = line.substring(commaIdx[1] + 1, commaIdx[2]); + String altStr = line.substring(commaIdx[5] + 1); + altStr.trim(); + if (altStr.length() == 0) continue; + + int32_t alt = altStr.toInt(); + if (alt > result.altitudeFt || !result.found) { + result.found = true; + result.altitudeFt = alt; + callsign.trim(); + strncpy(result.callsign, callsign.c_str(), sizeof(result.callsign) - 1); + result.callsign[sizeof(result.callsign) - 1] = 0; + } + yield(); + } + f.close(); + + return result; +} + void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays) { totalAircraft = 0; totalDays = 0; diff --git a/src/flight_logbook.h b/src/flight_logbook.h index 36437a0..45fa652 100644 --- a/src/flight_logbook.h +++ b/src/flight_logbook.h @@ -8,6 +8,18 @@ namespace FlightLogbook { uint16_t todayCount(); + struct TopAltitude { + bool found = false; + char callsign[9] = {0}; + int32_t altitudeFt = 0; + }; + + // Sucht in der heutigen Logbuch-Datei den Eintrag mit der hoechsten + // geloggten Flughoehe (jeweils die Hoehe BEIM ERSTEN Sichten, nicht der + // aktuelle Wert) und gibt dessen Rufzeichen + Hoehe zurueck. found=false, + // wenn heute noch nichts geloggt wurde oder die Datei fehlt. + TopAltitude todayMaxAltitude(); + void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays); struct DayEntry { diff --git a/src/i18n.h b/src/i18n.h index bdd37d3..82fdeaf 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -117,6 +117,7 @@ enum class StringId : uint8_t { STATS_RESET_CONFIRM, STATS_RESET_DONE, STATS_UPTIME_PREFIX, + STATS_TOP_ALTITUDE_PREFIX, STATS_HISTORY_TITLE, STATS_HISTORY_EMPTY, STATS_HISTORY_INFO_TITLE, diff --git a/src/i18n_de.h b/src/i18n_de.h index 2af0d1d..947341c 100644 --- a/src/i18n_de.h +++ b/src/i18n_de.h @@ -117,6 +117,7 @@ static const char* const I18N_DE[] = { "Nochmal tippen zum Bestätigen!", "Flugbuch-Daten zurückgesetzt.", "Laufzeit: ", + "Höchster Flug heute: ", "Verlauf (letzte 7 Tage)", "Noch keine Flugbuch-Einträge.", "Wie der Verlauf funktioniert", diff --git a/src/i18n_en.h b/src/i18n_en.h index 1f1892e..62b670a 100644 --- a/src/i18n_en.h +++ b/src/i18n_en.h @@ -118,6 +118,7 @@ static const char* const I18N_EN[] = { "Tap again to confirm!", "Logbook data reset.", "Uptime: ", + "Highest flight today: ", "History (last 7 days)", "No logbook entries yet.", "How the history chart works", diff --git a/src/i18n_es.h b/src/i18n_es.h index 6e9dc27..da3556b 100644 --- a/src/i18n_es.h +++ b/src/i18n_es.h @@ -117,6 +117,7 @@ static const char* const I18N_ES[] = { "¡Toque de nuevo para confirmar!", "Registro reiniciado.", "Tiempo activo: ", + "Vuelo más alto hoy: ", "Historial (últimos 7 días)", "Aún no hay registros.", "Cómo funciona el historial", diff --git a/src/i18n_fr.h b/src/i18n_fr.h index 3e7a271..49f6d11 100644 --- a/src/i18n_fr.h +++ b/src/i18n_fr.h @@ -117,6 +117,7 @@ static const char* const I18N_FR[] = { "Retouchez pour confirmer !", "Journal réinitialisé.", "Durée de fonctionnement : ", + "Vol le plus haut aujourd'hui : ", "Historique (7 derniers jours)", "Aucune entrée pour le moment.", "Comment fonctionne l'historique", diff --git a/src/i18n_it.h b/src/i18n_it.h index 3037da1..e7ad426 100644 --- a/src/i18n_it.h +++ b/src/i18n_it.h @@ -117,6 +117,7 @@ static const char* const I18N_IT[] = { "Tocca di nuovo per confermare!", "Registro reimpostato.", "Tempo di attività: ", + "Volo più alto oggi: ", "Cronologia (ultimi 7 giorni)", "Ancora nessun dato.", "Come funziona la cronologia", diff --git a/src/i18n_tr.h b/src/i18n_tr.h index e4cfc7d..7d18ba1 100644 --- a/src/i18n_tr.h +++ b/src/i18n_tr.h @@ -117,6 +117,7 @@ static const char* const I18N_TR[] = { "Onaylamak için tekrar dokunun!", "Kayıt defteri sıfırlandı.", "Çalışma süresi: ", + "Bugünkü en yüksek uçuş: ", "Geçmiş (son 7 gün)", "Henüz kayıt yok.", "Geçmiş grafiği nasıl çalışır", diff --git a/src/stats_screen.cpp b/src/stats_screen.cpp index 2e63a04..cfbd6e5 100644 --- a/src/stats_screen.cpp +++ b/src/stats_screen.cpp @@ -36,11 +36,12 @@ namespace { constexpr uint32_t CONFIRM_WINDOW_MS = 4000; - constexpr int16_t ROW1_Y = 44; - constexpr int16_t ROW2_Y = 84; - constexpr int16_t ROW3_Y = 124; - constexpr int16_t ROW4_Y = 164; - constexpr int16_t UPTIME_Y = 202; + constexpr int16_t ROW1_Y = 40; + constexpr int16_t ROW2_Y = 76; + constexpr int16_t ROW3_Y = 112; + constexpr int16_t ROW4_Y = 148; + constexpr int16_t UPTIME_Y = 184; + constexpr int16_t TOP_ALT_Y = 204; // "Hoechster Flug heute" - einzeilig, daher naeher an UPTIME als die anderen Rows } void run(TFT_eSPI& tft) { @@ -56,6 +57,7 @@ void run(TFT_eSPI& tft) { uint32_t allTimeAircraft = 0; uint16_t allTimeDays = 0; FlightLogbook::computeAllTimeStats(allTimeAircraft, allTimeDays); + FlightLogbook::TopAltitude topAlt = FlightLogbook::todayMaxAltitude(); Rect resetBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 100), (int16_t)(Config::SCREEN_WIDTH - 20), 40}; Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40}; @@ -94,6 +96,14 @@ void run(TFT_eSPI& tft) { tft.setCursor(10, UPTIME_Y); tft.print(String(I18n::t(StringId::STATS_UPTIME_PREFIX)) + upBuf); + if (topAlt.found) { + String csign = topAlt.callsign[0] ? String(topAlt.callsign) : "?"; + char altBuf[24]; + snprintf(altBuf, sizeof(altBuf), "%s (%ld ft)", csign.c_str(), (long)topAlt.altitudeFt); + tft.setCursor(10, TOP_ALT_Y); + tft.print(String(I18n::t(StringId::STATS_TOP_ALTITUDE_PREFIX)) + altBuf); + } + if (confirmPending) { drawButton(tft, resetBtn, I18n::t(StringId::STATS_RESET_CONFIRM), true); } else {