v1.3: Screenshot, double-tap zoom, statistics screen, flight trail

This commit is contained in:
Eiswolf-BG
2026-08-03 04:47:53 +02:00
parent b913ca2f44
commit 7b19726b95
11 changed files with 323 additions and 44 deletions

View File

@@ -32,6 +32,7 @@ namespace Config {
constexpr const char* SD_AIRLINES_CSV = "/Flightradar_cyd/airlines.csv"; constexpr const char* SD_AIRLINES_CSV = "/Flightradar_cyd/airlines.csv";
constexpr const char* SD_AIRCRAFT_TYPES_CSV = "/Flightradar_cyd/aircraft_types.csv"; constexpr const char* SD_AIRCRAFT_TYPES_CSV = "/Flightradar_cyd/aircraft_types.csv";
constexpr const char* SD_LOG_DIR = "/Flightradar_cyd/logs"; constexpr const char* SD_LOG_DIR = "/Flightradar_cyd/logs";
constexpr const char* SD_SCREENSHOT_DIR = "/Flightradar_cyd/screenshots";
constexpr const char* SD_SETTINGS_FILE = "/Flightradar_cyd/config.txt"; constexpr const char* SD_SETTINGS_FILE = "/Flightradar_cyd/config.txt";
constexpr const char* SD_WIFI_CREDENTIALS_FILE = "/Flightradar_cyd/wifi.txt"; constexpr const char* SD_WIFI_CREDENTIALS_FILE = "/Flightradar_cyd/wifi.txt";
constexpr const char* SD_CALIBRATION_FILE = "/Flightradar_cyd/calibration.txt"; constexpr const char* SD_CALIBRATION_FILE = "/Flightradar_cyd/calibration.txt";
@@ -58,10 +59,8 @@ namespace Config {
constexpr uint16_t COLOR_LOW_ALT_THRESHOLD_FT = 10000; constexpr uint16_t COLOR_LOW_ALT_THRESHOLD_FT = 10000;
constexpr uint16_t COLOR_MID_ALT_THRESHOLD_FT = 30000; constexpr uint16_t COLOR_MID_ALT_THRESHOLD_FT = 30000;
// Notfall-Squawk-Codes: 7500 (Entfuehrung), 7600 (Funkausfall), 7700 (allg. Notfall)
constexpr const char* EMERGENCY_SQUAWKS[] = {"7500", "7600", "7700"}; constexpr const char* EMERGENCY_SQUAWKS[] = {"7500", "7600", "7700"};
constexpr uint8_t EMERGENCY_SQUAWK_COUNT = 3; constexpr uint8_t EMERGENCY_SQUAWK_COUNT = 3;
// Maximal gespeicherte WLAN-Netzwerke (z.B. Zuhause, Auto-Hotspot, Arbeit)
constexpr uint8_t MAX_WIFI_NETWORKS = 3; constexpr uint8_t MAX_WIFI_NETWORKS = 3;
} }

View File

@@ -13,7 +13,7 @@ namespace {
constexpr uint16_t MAX_SEEN = 400; constexpr uint16_t MAX_SEEN = 400;
char seenHex[MAX_SEEN][7]; char seenHex[MAX_SEEN][7];
uint16_t seenCount = 0; uint16_t seenCount = 0;
char currentDateStr[11] = {0}; // "YYYY-MM-DD" char currentDateStr[11] = {0};
bool computeDateStr(char* out, size_t outSize) { bool computeDateStr(char* out, size_t outSize) {
time_t now = time(nullptr); time_t now = time(nullptr);
@@ -140,4 +140,33 @@ void update() {
} }
} }
uint16_t todayCount() { return seenCount; }
void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays) {
totalAircraft = 0;
totalDays = 0;
File dir = SD.open(Config::SD_LOG_DIR);
if (!dir || !dir.isDirectory()) return;
File entry = dir.openNextFile();
while (entry) {
if (!entry.isDirectory()) {
String name = String(entry.name());
if (name.endsWith(".csv")) {
totalDays++;
uint32_t lines = 0;
while (entry.available()) {
entry.readStringUntil('\n');
lines++;
}
if (lines > 0) totalAircraft += (lines - 1);
}
}
entry.close();
entry = dir.openNextFile();
}
dir.close();
}
} }

View File

@@ -19,4 +19,12 @@ namespace FlightLogbook {
// heutige Logbuch. Kuemmert sich auch um den Tageswechsel (neue Datei, // heutige Logbuch. Kuemmert sich auch um den Tageswechsel (neue Datei,
// neue "gesehen"-Liste). // neue "gesehen"-Liste).
void update(); void update();
// Anzahl der HEUTE bereits geloggten (unterschiedlichen) Flugzeuge.
uint16_t todayCount();
// Summiert alle taeglichen Logbuch-Dateien auf der SD-Karte:
// 'totalAircraft' = Summe aller Eintraege ueber alle Tage,
// 'totalDays' = Anzahl der Tage, an denen ueberhaupt geloggt wurde.
void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays);
} }

View File

@@ -2,6 +2,7 @@
#include "touch_input.h" #include "touch_input.h"
#include "calibration_screen.h" #include "calibration_screen.h"
#include "wifi_manage_screen.h" #include "wifi_manage_screen.h"
#include "stats_screen.h"
#include "settings_store.h" #include "settings_store.h"
#include "config.h" #include "config.h"
@@ -43,7 +44,8 @@ void run(TFT_eSPI& tft) {
Rect emergencyBtn = rowRect(3); Rect emergencyBtn = rowRect(3);
Rect proximityBtn = rowRect(4); Rect proximityBtn = rowRect(4);
Rect logbookBtn = rowRect(5); Rect logbookBtn = rowRect(5);
Rect backBtn = rowRect(6); Rect statsBtn = rowRect(6);
Rect backBtn = rowRect(7);
bool done = false; bool done = false;
while (!done) { while (!done) {
@@ -64,6 +66,7 @@ void run(TFT_eSPI& tft) {
drawButton(tft, emergencyBtn, "Emergency alert: " + onOff(SettingsStore::emergencyAlertEnabled())); drawButton(tft, emergencyBtn, "Emergency alert: " + onOff(SettingsStore::emergencyAlertEnabled()));
drawButton(tft, proximityBtn, "Proximity LED: " + onOff(SettingsStore::proximityAlertEnabled())); drawButton(tft, proximityBtn, "Proximity LED: " + onOff(SettingsStore::proximityAlertEnabled()));
drawButton(tft, logbookBtn, "Flight logbook: " + onOff(SettingsStore::flightLogbookEnabled())); drawButton(tft, logbookBtn, "Flight logbook: " + onOff(SettingsStore::flightLogbookEnabled()));
drawButton(tft, statsBtn, "Statistics");
drawButton(tft, backBtn, "Back"); drawButton(tft, backBtn, "Back");
@@ -87,6 +90,8 @@ void run(TFT_eSPI& tft) {
SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled()); SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled());
} else if (logbookBtn.contains(tap.x, tap.y)) { } else if (logbookBtn.contains(tap.x, tap.y)) {
SettingsStore::setFlightLogbookEnabled(!SettingsStore::flightLogbookEnabled()); SettingsStore::setFlightLogbookEnabled(!SettingsStore::flightLogbookEnabled());
} else if (statsBtn.contains(tap.x, tap.y)) {
StatsScreen::run(tft);
} else if (backBtn.contains(tap.x, tap.y)) { } else if (backBtn.contains(tap.x, tap.y)) {
done = true; done = true;
} }

View File

@@ -58,6 +58,12 @@ namespace {
char selectedHex[7] = {0}; char selectedHex[7] = {0};
uint32_t lastEmptyTapMs = 0;
int16_t lastEmptyTapX = -1000;
int16_t lastEmptyTapY = -1000;
constexpr uint32_t DOUBLE_TAP_MS = 400;
constexpr int16_t DOUBLE_TAP_RADIUS = 25;
bool ledBlinkOn = true; bool ledBlinkOn = true;
bool isEmergencySquawk(const char* squawk) { bool isEmergencySquawk(const char* squawk) {
@@ -78,6 +84,79 @@ namespace {
return TFT_RED; return TFT_RED;
} }
// Gedaempfte Version der Hoehenfarbe fuer die Flugbahn-Linie (Trail) -
// deutlich dunkler als der Punkt selbst, damit der Trail nicht mit dem
// aktuellen Punkt/Label verwechselt wird.
uint16_t dimColorForAltitude(int32_t altFt) {
if (altFt < Config::COLOR_LOW_ALT_THRESHOLD_FT) return TFT_DARKGREEN;
if (altFt < Config::COLOR_MID_ALT_THRESHOLD_FT) return TFT_OLIVE;
return TFT_MAROON;
}
// --- Flugbahn-Trail: merkt sich die letzten paar Bildschirm-Positionen
// pro Flugzeug (per Hex-Code), damit man die grobe Flugrichtung ueber die
// letzten Datenupdates hinweg sieht. Wird nur bei render() (Vollbild-
// Redraw) aktualisiert/gezeichnet, nicht bei jedem Sweep-Tick.
constexpr uint8_t TRAIL_LEN = 4;
constexpr uint32_t TRAIL_STALE_MS = Config::FETCH_INTERVAL_MS * 3;
struct TrailEntry {
char hex[7] = {0};
bool active = false;
int16_t xs[TRAIL_LEN] = {0};
int16_t ys[TRAIL_LEN] = {0};
uint8_t count = 0;
uint32_t lastUpdateMs = 0;
};
constexpr uint8_t MAX_TRAILS = Config::MAX_TRACKED_AIRCRAFT;
TrailEntry trails[MAX_TRAILS];
void pruneStaleTrails() {
uint32_t now = millis();
for (uint8_t i = 0; i < MAX_TRAILS; i++) {
if (trails[i].active && now - trails[i].lastUpdateMs > TRAIL_STALE_MS) {
trails[i] = TrailEntry{};
}
}
}
TrailEntry* findOrCreateTrail(const char* hex) {
int16_t freeIdx = -1;
for (uint8_t i = 0; i < MAX_TRAILS; i++) {
if (trails[i].active && strcmp(trails[i].hex, hex) == 0) return &trails[i];
if (!trails[i].active && freeIdx < 0) freeIdx = (int16_t)i;
}
if (freeIdx < 0) return nullptr;
trails[freeIdx] = TrailEntry{};
strncpy(trails[freeIdx].hex, hex, sizeof(trails[freeIdx].hex) - 1);
trails[freeIdx].active = true;
return &trails[freeIdx];
}
void pushTrailPoint(TrailEntry* t, int16_t x, int16_t y) {
if (!t) return;
if (t->count < TRAIL_LEN) {
t->xs[t->count] = x;
t->ys[t->count] = y;
t->count++;
} else {
for (uint8_t i = 1; i < TRAIL_LEN; i++) {
t->xs[i - 1] = t->xs[i];
t->ys[i - 1] = t->ys[i];
}
t->xs[TRAIL_LEN - 1] = x;
t->ys[TRAIL_LEN - 1] = y;
}
t->lastUpdateMs = millis();
}
void drawTrail(TFT_eSPI& gfx, const TrailEntry* t, uint16_t color) {
if (!t || t->count < 2) return;
for (uint8_t i = 1; i < t->count; i++) {
gfx.drawLine(t->xs[i - 1], t->ys[i - 1], t->xs[i], t->ys[i], color);
}
}
void printLineTruncated(TFT_eSPI& gfx, int16_t x, int16_t y, int16_t maxWidth, const String& text) { void printLineTruncated(TFT_eSPI& gfx, int16_t x, int16_t y, int16_t maxWidth, const String& text) {
String s = text; String s = text;
if (gfx.textWidth(s) > maxWidth) { if (gfx.textWidth(s) > maxWidth) {
@@ -99,9 +178,6 @@ namespace {
gfx.setTextDatum(TL_DATUM); gfx.setTextDatum(TL_DATUM);
} }
// Kleine Farb-Legende (Hoehenband -> Farbe), damit auf den ersten Blick
// klar ist, was Gruen/Gelb/Rot bei den Flugzeug-Punkten bedeuten. Zeigt
// Meter statt Fuss, wenn die Region (per Geolocation) metrisch ist.
void drawLegend(TFT_eSPI& gfx, int16_t y) { void drawLegend(TFT_eSPI& gfx, int16_t y) {
bool metric = LocationManager::useMetricUnits(); bool metric = LocationManager::useMetricUnits();
@@ -309,6 +385,8 @@ void render(TFT_eSPI& tft, int16_t top) {
for (uint8_t i = 0; i < MAX_HIT_POINTS; i++) hitPoints[i].valid = false; for (uint8_t i = 0; i < MAX_HIT_POINTS; i++) hitPoints[i].valid = false;
pruneStaleTrails();
for (uint8_t i = 0; i < count && i < MAX_HIT_POINTS; i++) { for (uint8_t i = 0; i < count && i < MAX_HIT_POINTS; i++) {
Aircraft& a = snapshot[i]; Aircraft& a = snapshot[i];
if (a.distanceKm > rangeKm * 1.05f) continue; if (a.distanceKm > rangeKm * 1.05f) continue;
@@ -320,6 +398,12 @@ void render(TFT_eSPI& tft, int16_t top) {
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0; bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk); bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
// Flugbahn-Trail: erst die bisherige Spur zeichnen, DANN den neuen
// Punkt anhaengen und den Punkt/Ring/Label darueber legen.
TrailEntry* trail = findOrCreateTrail(a.hex);
drawTrail(tft, trail, dimColorForAltitude(a.altBaroFt));
pushTrailPoint(trail, pt.x, pt.y);
if (isSelected) { if (isSelected) {
tft.drawCircle(pt.x, pt.y, 9, TFT_WHITE); tft.drawCircle(pt.x, pt.y, 9, TFT_WHITE);
selectionStillPresent = true; selectionStillPresent = true;
@@ -462,6 +546,21 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
} }
} }
uint32_t nowMs = millis();
bool isDoubleTap = (nowMs - lastEmptyTapMs <= DOUBLE_TAP_MS) &&
(abs((int)x - (int)lastEmptyTapX) <= DOUBLE_TAP_RADIUS) &&
(abs((int)y - (int)lastEmptyTapY) <= DOUBLE_TAP_RADIUS);
lastEmptyTapMs = nowMs;
lastEmptyTapX = x;
lastEmptyTapY = y;
if (isDoubleTap) {
uint8_t idx = SettingsStore::rangeIndex();
idx = (idx == 0) ? (Config::RANGE_STEP_COUNT - 1) : (idx - 1);
SettingsStore::setRangeIndex(idx);
lastEmptyTapMs = 0;
}
return true; return true;
} }

View File

@@ -3,30 +3,9 @@
#include <TFT_eSPI.h> #include <TFT_eSPI.h>
namespace RadarScreen { namespace RadarScreen {
// Zeichnet den kompletten Radar-Frame neu (Ringe, Flugzeuge als Punkte,
// Rufzeichen-Label, Sweep-Strahl, Auswahl-Infoleiste, Reichweiten-Button)
// im Bereich von y=top bis zum unteren Bildschirmrand. Wird nur bei
// tatsaechlichen Datenaenderungen oder nach Touch-Interaktion aufgerufen
// (siehe main.cpp), NICHT bei jedem Sweep-Tick - sonst wuerde es flackern.
void render(TFT_eSPI& tft, int16_t top); void render(TFT_eSPI& tft, int16_t top);
// Guenstiger, haeufiger Aufruf (z.B. alle 100ms): dreht NUR den
// Sweep-Strahl ein Stueck weiter und zeichnet ihn (plus dezente
// Nachzieh-Linien), OHNE den Bildschirm vorher zu loeschen. So entsteht
// eine fluessige Drehbewegung ohne Vollbild-Neuzeichnen (= kein Flackern).
// deltaMs = vergangene Zeit seit dem letzten tick()-Aufruf.
void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs); void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs);
// Verarbeitet einen Touch-Tap (Reichweite umschalten, Flugzeug auswaehlen,
// leere Flaeche antippen = Auswahl aufheben). Gibt true zurueck, wenn der
// Tap im Radar-Bereich (y >= top) verarbeitet wurde.
bool handleTap(int16_t x, int16_t y, int16_t top); bool handleTap(int16_t x, int16_t y, int16_t top);
// Haeufig aufrufen (unabhaengig davon, ob ein Detail-Fenster offen ist):
// prueft, ob ein Flugzeug innerhalb des Alarmradius ist ODER ein
// Notfall-Squawk sendet, steuert die LED entsprechend (Notfall hat
// Vorrang, blinkt rot statt gruen) und merkt sich den Blink-Zustand,
// damit tick() den betroffenen Punkt synchron mitblinken lassen kann.
void updateProximityAlert(uint32_t nowMs); void updateProximityAlert(uint32_t nowMs);
struct EmergencyInfo { struct EmergencyInfo {
@@ -35,7 +14,5 @@ namespace RadarScreen {
char squawk[5] = {0}; char squawk[5] = {0};
}; };
// Reine Abfrage (kein Seiteneffekt): gibt das erste Flugzeug mit einem
// Notfall-Squawk zurueck, falls vorhanden. Fuer das Banner im Header.
EmergencyInfo checkEmergency(); EmergencyInfo checkEmergency();
} }

78
src/screenshot.cpp Normal file
View File

@@ -0,0 +1,78 @@
#include "screenshot.h"
#include "config.h"
#include <SD.h>
#include <time.h>
namespace Screenshot {
namespace {
void putU32(uint8_t* buf, uint32_t v) {
buf[0] = v & 0xFF;
buf[1] = (v >> 8) & 0xFF;
buf[2] = (v >> 16) & 0xFF;
buf[3] = (v >> 24) & 0xFF;
}
}
String save(TFT_eSPI& tft) {
if (!SD.exists(Config::SD_SCREENSHOT_DIR)) {
SD.mkdir(Config::SD_SCREENSHOT_DIR);
}
time_t now = time(nullptr);
struct tm tmNow;
localtime_r(&now, &tmNow);
char filename[32];
snprintf(filename, sizeof(filename), "%04d%02d%02d_%02d%02d%02d.bmp",
tmNow.tm_year + 1900, tmNow.tm_mon + 1, tmNow.tm_mday,
tmNow.tm_hour, tmNow.tm_min, tmNow.tm_sec);
char fullPath[64];
snprintf(fullPath, sizeof(fullPath), "%s/%s", Config::SD_SCREENSHOT_DIR, filename);
File f = SD.open(fullPath, FILE_WRITE);
if (!f) return String();
int32_t w = tft.width();
int32_t h = tft.height();
uint32_t rowSize = ((uint32_t)w * 3 + 3) & ~3u;
uint32_t dataSize = rowSize * (uint32_t)h;
uint32_t fileSize = 54 + dataSize;
uint8_t header[54] = {0};
header[0] = 'B'; header[1] = 'M';
putU32(&header[2], fileSize);
putU32(&header[10], 54);
putU32(&header[14], 40);
putU32(&header[18], (uint32_t)w);
putU32(&header[22], (uint32_t)h);
header[26] = 1;
header[28] = 24;
putU32(&header[34], dataSize);
f.write(header, 54);
uint16_t rowPixels[Config::SCREEN_WIDTH];
uint8_t rowBuf[Config::SCREEN_WIDTH * 3 + 3];
for (int32_t y = h - 1; y >= 0; y--) {
tft.readRect(0, y, w, 1, rowPixels);
for (int32_t x = 0; x < w; x++) {
uint16_t px = rowPixels[x];
uint8_t r5 = (px >> 11) & 0x1F;
uint8_t g6 = (px >> 5) & 0x3F;
uint8_t b5 = px & 0x1F;
rowBuf[x * 3 + 0] = (b5 * 255) / 31;
rowBuf[x * 3 + 1] = (g6 * 255) / 63;
rowBuf[x * 3 + 2] = (r5 * 255) / 31;
}
for (uint32_t p = (uint32_t)w * 3; p < rowSize; p++) rowBuf[p] = 0;
f.write(rowBuf, rowSize);
}
f.close();
return String(filename);
}
}

13
src/screenshot.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <Arduino.h>
#include <TFT_eSPI.h>
// Liest den aktuell angezeigten Bildschirminhalt per SPI-Readback zurueck
// (TFT_MISO ist am CYD verkabelt) und speichert ihn als 24-Bit-BMP-Datei auf
// der SD-Karte. Funktioniert mit allem, was gerade angezeigt wird (Radar,
// Detail-Fenster, Menue, ...).
namespace Screenshot {
// Blockierend (dauert ca. 1-2 Sekunden). Gibt bei Erfolg den Dateinamen
// (ohne Pfad) zurueck, sonst einen leeren String.
String save(TFT_eSPI& tft);
}

View File

@@ -64,26 +64,14 @@ namespace {
} }
bool init() { bool init() {
Serial.printf("[SD] Pins: CLK=%d MISO=%d MOSI=%d CS=%d\n",
Config::SD_SPI_CLK_PIN, Config::SD_SPI_MISO_PIN,
Config::SD_SPI_MOSI_PIN, Config::SD_SPI_CS_PIN);
Serial.println("[SD] vor sdSpi.begin()");
sdSpi.begin(Config::SD_SPI_CLK_PIN, Config::SD_SPI_MISO_PIN, sdSpi.begin(Config::SD_SPI_CLK_PIN, Config::SD_SPI_MISO_PIN,
Config::SD_SPI_MOSI_PIN, Config::SD_SPI_CS_PIN); Config::SD_SPI_MOSI_PIN, Config::SD_SPI_CS_PIN);
Serial.println("[SD] sdSpi.begin() fertig");
Serial.println("[SD] vor SD.begin()");
mounted = SD.begin(Config::SD_SPI_CS_PIN, sdSpi, 4000000); mounted = SD.begin(Config::SD_SPI_CS_PIN, sdSpi, 4000000);
Serial.printf("[SD] SD.begin() fertig, mounted=%d\n", mounted);
if (!mounted) return false; if (!mounted) return false;
Serial.println("[SD] vor ensureDir(ROOT)");
ensureDir(Config::SD_ROOT_DIR); ensureDir(Config::SD_ROOT_DIR);
Serial.println("[SD] vor ensureDir(LOG)");
ensureDir(Config::SD_LOG_DIR); ensureDir(Config::SD_LOG_DIR);
Serial.println("[SD] init() komplett fertig"); ensureDir(Config::SD_SCREENSHOT_DIR);
return true; return true;
} }

73
src/stats_screen.cpp Normal file
View File

@@ -0,0 +1,73 @@
#include "stats_screen.h"
#include "flight_logbook.h"
#include "touch_input.h"
#include "config.h"
namespace StatsScreen {
namespace {
struct Rect {
int16_t x, y, w, h;
bool contains(int16_t px, int16_t py) const {
return px >= x && px < x + w && py >= y && py < y + h;
}
};
void drawButton(TFT_eSPI& tft, const Rect& r, const String& label) {
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, TFT_NAVY);
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_DARKGREY);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(TFT_WHITE, TFT_NAVY);
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
tft.setTextDatum(TL_DATUM);
}
void drawStatRow(TFT_eSPI& tft, int16_t y, const String& label, const String& value) {
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
tft.setCursor(10, y);
tft.print(label);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextSize(2);
tft.setCursor(10, y + 14);
tft.print(value);
tft.setTextSize(1);
}
}
void run(TFT_eSPI& tft) {
uint16_t today = FlightLogbook::todayCount();
uint32_t allTimeAircraft = 0;
uint16_t allTimeDays = 0;
FlightLogbook::computeAllTimeStats(allTimeAircraft, allTimeDays);
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setCursor(10, 10);
tft.println("Statistics");
drawStatRow(tft, 44, "Aircraft logged today", String(today));
drawStatRow(tft, 90, "Aircraft logged all-time", String(allTimeAircraft));
drawStatRow(tft, 136, "Days with sightings", String(allTimeDays));
if (allTimeDays > 0) {
float avgPerDay = (float)allTimeAircraft / (float)allTimeDays;
char buf[16];
snprintf(buf, sizeof(buf), "%.1f", avgPerDay);
drawStatRow(tft, 182, "Average per day", String(buf));
}
drawButton(tft, backBtn, "Back");
bool done = false;
while (!done) {
TouchInput::Point tap;
if (TouchInput::wasTapped(tap) && backBtn.contains(tap.x, tap.y)) {
done = true;
}
delay(20);
}
}
}

10
src/stats_screen.h Normal file
View File

@@ -0,0 +1,10 @@
#pragma once
#include <Arduino.h>
#include <TFT_eSPI.h>
namespace StatsScreen {
// Blockierend: zeigt einfache Statistiken aus dem Flight Logbook
// (heute gesehen, insgesamt gesehen, Anzahl Tage geloggt). Kehrt zurueck,
// sobald "Back" angetippt wird.
void run(TFT_eSPI& tft);
}