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).
|
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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user