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,10 +169,21 @@ 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).
## 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 Sobald `pio run` (Build) erfolgreich ohne Fehler durchgelaufen ist, IMMER direkt
im Anschluss auch flashen (`pio run --target upload`), ohne extra danach zu 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 {