v1.3: Screenshot, double-tap zoom, statistics screen, flight trail
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
78
src/screenshot.cpp
Normal 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
13
src/screenshot.h
Normal 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);
|
||||||
|
}
|
||||||
@@ -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
73
src/stats_screen.cpp
Normal 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
10
src/stats_screen.h
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user