v2.6.0: Show highest-altitude flight of the day on Statistics screen

Adds FlightLogbook::todayMaxAltitude(), which scans today's logbook
CSV for the entry with the greatest altitude at first sighting, and
displays it as "Highest flight today: <callsign> (<alt> ft)" on the
Statistics screen (Menu -> Flight Options -> Statistics).

Also documents the CYD-flightradar.bin rename step (for GitHub
release uploads) in the CLAUDE.md release workflow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Eiswolf-BG
2026-08-06 15:58:08 +02:00
parent 82b0877e8d
commit f966255329
12 changed files with 100 additions and 7 deletions

View File

@@ -169,6 +169,15 @@ folgende Schritte in dieser Reihenfolge:
Tag erstellt wurde). Tag erstellt wurde).
7. Kurze Zusammenfassung am Ende: was committet/getaggt/gepusht wurde, und 7. Kurze Zusammenfassung am Ende: was committet/getaggt/gepusht wurde, und
ob die README aktualisiert wurde (und falls ja, welche Abschnitte). ob die README aktualisiert wurde (und falls ja, welche Abschnitte).
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 ## Nach jedem erfolgreichen Build automatisch flashen
Sobald `pio run` (Build) erfolgreich ohne Fehler durchgelaufen ist, IMMER direkt Sobald `pio run` (Build) erfolgreich ohne Fehler durchgelaufen ist, IMMER direkt
@@ -176,3 +185,5 @@ 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.ä. fragen - außer der Nutzer sagt ausdrücklich "nur bauen, nicht flashen" o.ä.
Kurz danach bestätigen, dass der Upload ebenfalls erfolgreich war (inkl. Kurz danach bestätigen, dass der Upload ebenfalls erfolgreich war (inkl.
"[SUCCESS]"-Zeile am Ende). "[SUCCESS]"-Zeile am Ende).
Bitte diese Regel jetzt in die CLAUDE.md-Datei einpflegen.

View File

@@ -105,6 +105,7 @@ Shows at a glance:
- **Days with sightings** – how many days anything was logged at all - **Days with sightings** – how many days anything was logged at all
- **Average per day** – total count divided by number of days - **Average per day** – total count divided by number of days
- **Uptime** – how long the device has been running since the last restart - **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. The **"Reset logbook data"** button (tap twice to confirm) permanently deletes all logged data.

View File

@@ -191,6 +191,58 @@ void update() {
uint16_t todayCount() { return seenCount; } 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) { void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays) {
totalAircraft = 0; totalAircraft = 0;
totalDays = 0; totalDays = 0;

View File

@@ -8,6 +8,18 @@ namespace FlightLogbook {
uint16_t todayCount(); 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); void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays);
struct DayEntry { struct DayEntry {

View File

@@ -117,6 +117,7 @@ enum class StringId : uint8_t {
STATS_RESET_CONFIRM, STATS_RESET_CONFIRM,
STATS_RESET_DONE, STATS_RESET_DONE,
STATS_UPTIME_PREFIX, STATS_UPTIME_PREFIX,
STATS_TOP_ALTITUDE_PREFIX,
STATS_HISTORY_TITLE, STATS_HISTORY_TITLE,
STATS_HISTORY_EMPTY, STATS_HISTORY_EMPTY,
STATS_HISTORY_INFO_TITLE, STATS_HISTORY_INFO_TITLE,

View File

@@ -117,6 +117,7 @@ static const char* const I18N_DE[] = {
"Nochmal tippen zum Bestätigen!", "Nochmal tippen zum Bestätigen!",
"Flugbuch-Daten zurückgesetzt.", "Flugbuch-Daten zurückgesetzt.",
"Laufzeit: ", "Laufzeit: ",
"Höchster Flug heute: ",
"Verlauf (letzte 7 Tage)", "Verlauf (letzte 7 Tage)",
"Noch keine Flugbuch-Einträge.", "Noch keine Flugbuch-Einträge.",
"Wie der Verlauf funktioniert", "Wie der Verlauf funktioniert",

View File

@@ -118,6 +118,7 @@ static const char* const I18N_EN[] = {
"Tap again to confirm!", "Tap again to confirm!",
"Logbook data reset.", "Logbook data reset.",
"Uptime: ", "Uptime: ",
"Highest flight today: ",
"History (last 7 days)", "History (last 7 days)",
"No logbook entries yet.", "No logbook entries yet.",
"How the history chart works", "How the history chart works",

View File

@@ -117,6 +117,7 @@ static const char* const I18N_ES[] = {
"¡Toque de nuevo para confirmar!", "¡Toque de nuevo para confirmar!",
"Registro reiniciado.", "Registro reiniciado.",
"Tiempo activo: ", "Tiempo activo: ",
"Vuelo más alto hoy: ",
"Historial (últimos 7 días)", "Historial (últimos 7 días)",
"Aún no hay registros.", "Aún no hay registros.",
"Cómo funciona el historial", "Cómo funciona el historial",

View File

@@ -117,6 +117,7 @@ static const char* const I18N_FR[] = {
"Retouchez pour confirmer !", "Retouchez pour confirmer !",
"Journal réinitialisé.", "Journal réinitialisé.",
"Durée de fonctionnement : ", "Durée de fonctionnement : ",
"Vol le plus haut aujourd'hui : ",
"Historique (7 derniers jours)", "Historique (7 derniers jours)",
"Aucune entrée pour le moment.", "Aucune entrée pour le moment.",
"Comment fonctionne l'historique", "Comment fonctionne l'historique",

View File

@@ -117,6 +117,7 @@ static const char* const I18N_IT[] = {
"Tocca di nuovo per confermare!", "Tocca di nuovo per confermare!",
"Registro reimpostato.", "Registro reimpostato.",
"Tempo di attività: ", "Tempo di attività: ",
"Volo più alto oggi: ",
"Cronologia (ultimi 7 giorni)", "Cronologia (ultimi 7 giorni)",
"Ancora nessun dato.", "Ancora nessun dato.",
"Come funziona la cronologia", "Come funziona la cronologia",

View File

@@ -117,6 +117,7 @@ static const char* const I18N_TR[] = {
"Onaylamak için tekrar dokunun!", "Onaylamak için tekrar dokunun!",
"Kayıt defteri sıfırlandı.", "Kayıt defteri sıfırlandı.",
"Çalışma süresi: ", "Çalışma süresi: ",
"Bugünkü en yüksek uçuş: ",
"Geçmiş (son 7 gün)", "Geçmiş (son 7 gün)",
"Henüz kayıt yok.", "Henüz kayıt yok.",
"Geçmiş grafiği nasıl çalışır", "Geçmiş grafiği nasıl çalışır",

View File

@@ -36,11 +36,12 @@ namespace {
constexpr uint32_t CONFIRM_WINDOW_MS = 4000; constexpr uint32_t CONFIRM_WINDOW_MS = 4000;
constexpr int16_t ROW1_Y = 44; constexpr int16_t ROW1_Y = 40;
constexpr int16_t ROW2_Y = 84; constexpr int16_t ROW2_Y = 76;
constexpr int16_t ROW3_Y = 124; constexpr int16_t ROW3_Y = 112;
constexpr int16_t ROW4_Y = 164; constexpr int16_t ROW4_Y = 148;
constexpr int16_t UPTIME_Y = 202; 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) { void run(TFT_eSPI& tft) {
@@ -56,6 +57,7 @@ void run(TFT_eSPI& tft) {
uint32_t allTimeAircraft = 0; uint32_t allTimeAircraft = 0;
uint16_t allTimeDays = 0; uint16_t allTimeDays = 0;
FlightLogbook::computeAllTimeStats(allTimeAircraft, allTimeDays); 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 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}; 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.setCursor(10, UPTIME_Y);
tft.print(String(I18n::t(StringId::STATS_UPTIME_PREFIX)) + upBuf); 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) { if (confirmPending) {
drawButton(tft, resetBtn, I18n::t(StringId::STATS_RESET_CONFIRM), true); drawButton(tft, resetBtn, I18n::t(StringId::STATS_RESET_CONFIRM), true);
} else { } else {