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:
11
CLAUDE.md
11
CLAUDE.md
@@ -169,6 +169,15 @@ 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).
|
||||
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
|
||||
@@ -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.ä.
|
||||
Kurz danach bestätigen, dass der Upload ebenfalls erfolgreich war (inkl.
|
||||
"[SUCCESS]"-Zeile am Ende).
|
||||
|
||||
Bitte diese Regel jetzt in die CLAUDE.md-Datei einpflegen.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user