v2.0: Full localization (6 languages), first-run language screen, aircraft detail overhaul, aircraft-model API fallback, and multiple bugfixes

This commit is contained in:
Eiswolf-BG
2026-08-04 01:32:44 +02:00
parent 4e8bfcd30c
commit 665130e8bb
45 changed files with 2705 additions and 240 deletions

View File

@@ -10,9 +10,11 @@
#include "radar_math.h"
#include "aircraft_table.h"
#include "airline_lookup.h"
#include "airline_filter.h"
#include "sd_storage.h"
#include "wifi_manager.h"
#include "location_manager.h"
#include "location_presets.h"
#include "adsb_client.h"
#include "touch_input.h"
#include "calibration_screen.h"
@@ -25,12 +27,11 @@
#include "led_alert.h"
#include "flight_logbook.h"
#include "screenshot.h"
#include "i18n.h"
#include "first_run_language_screen.h"
TFT_eSPI tft = TFT_eSPI();
// Schlanker Header (Titel + Buttons) PLUS eine zweite, duenne Statuszeile
// (Uhrzeit + WLAN-Signalstaerke) darunter - der Radar-Kreis bekommt den Rest
// des Screens.
constexpr int16_t HEADER_TITLE_H = 30;
constexpr int16_t STATUS_LINE_H = 12;
constexpr int16_t CONTENT_TOP = HEADER_TITLE_H + STATUS_LINE_H;
@@ -45,6 +46,12 @@ bool forceRedraw = false;
bool wasEmergency = false;
bool bannerBlinkOn = false;
constexpr uint8_t BACKLIGHT_FULL = 255;
constexpr uint8_t BACKLIGHT_DIMMED = 12;
constexpr uint8_t BACKLIGHT_PWM_CHANNEL = 0;
uint32_t lastInteractionMs = 0;
bool screenDimmed = false;
struct Rect {
int16_t x, y, w, h;
bool contains(int16_t px, int16_t py) const {
@@ -117,10 +124,10 @@ void takeScreenshotWithFeedback() {
tft.setTextColor(TFT_WHITE, TFT_NAVY);
tft.setCursor(6, 10);
if (filename.length() > 0) {
tft.print("Saved: " + filename);
tft.print(String(I18n::t(StringId::SCREENSHOT_SAVED_PREFIX)) + filename);
} else {
tft.setTextColor(TFT_RED, TFT_NAVY);
tft.print("Screenshot failed");
tft.print(I18n::t(StringId::SCREENSHOT_FAILED));
}
delay(1200);
drawHeader();
@@ -137,13 +144,13 @@ void haltWithSdRequiredScreen() {
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextSize(2);
tft.drawString("For this app a", cx, cy - 40);
tft.drawString("SD card is", cx, cy - 10);
tft.drawString("required", cx, cy + 20);
tft.drawString(I18n::t(StringId::SD_REQUIRED_LINE1), cx, cy - 40);
tft.drawString(I18n::t(StringId::SD_REQUIRED_LINE2), cx, cy - 10);
tft.drawString(I18n::t(StringId::SD_REQUIRED_LINE3), cx, cy + 20);
tft.setTextSize(1);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.drawString("Insert a card and restart the device", cx, cy + 60);
tft.drawString(I18n::t(StringId::SD_REQUIRED_HINT), cx, cy + 60);
tft.setTextDatum(TL_DATUM);
while (true) {
@@ -176,8 +183,13 @@ void setup() {
tft.init();
tft.setRotation(0);
tft.invertDisplay(true);
tft.fillScreen(TFT_BLACK);
ledcSetup(BACKLIGHT_PWM_CHANNEL, 5000, 8);
ledcAttachPin(TFT_BL, BACKLIGHT_PWM_CHANNEL);
ledcWrite(BACKLIGHT_PWM_CHANNEL, BACKLIGHT_FULL);
TouchInput::begin();
LedAlert::begin();
@@ -188,13 +200,21 @@ void setup() {
}
SdStorage::seedDefaultDataFiles();
// "Allererster Start" erkennen: config.txt existiert noch nicht (wird
// erst beim ersten SettingsStore::save()-Aufruf angelegt). Muss VOR
// SettingsStore::load() geprueft werden, da load() die Datei nicht
// erzeugt, nur liest.
bool isFirstRun = !SD.exists(Config::SD_SETTINGS_FILE);
SettingsStore::load();
tft.invertDisplay(SettingsStore::displayInverted());
WifiMgr::init();
LocationPresets::init();
AirlineFilter::init();
SplashScreen::begin(tft);
SplashScreen::setStatusLine(tft, 0, "SD Card: OK", TFT_WHITE);
SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);
if (!TouchInput::loadCalibration()) {
CalibrationScreen::run(tft);
@@ -203,7 +223,7 @@ void setup() {
if (WifiMgr::networkCount() == 0) {
WifiSetupScreen::run(tft);
} else {
SplashScreen::setStatusLine(tft, 1, "Connecting WiFi...");
SplashScreen::setStatusLine(tft, 1, I18n::t(StringId::SPLASH_CONNECTING_WIFI));
WifiMgr::beginConnect();
uint32_t waitStart = millis();
@@ -213,15 +233,24 @@ void setup() {
}
if (WifiMgr::getState() == WifiMgr::State::Connected) {
SplashScreen::setStatusLine(tft, 1, String("WiFi OK: ") + WifiMgr::getIP());
SplashScreen::setStatusLine(tft, 1, String(I18n::t(StringId::SPLASH_WIFI_OK_PREFIX)) + WifiMgr::getIP());
} else {
SplashScreen::setStatusLine(tft, 1, "WiFi FAILED", TFT_RED);
SplashScreen::setStatusLine(tft, 1, I18n::t(StringId::SPLASH_WIFI_FAILED), TFT_RED);
}
}
// --- Sprachauswahl: NUR beim allerersten Start, direkt nach Touch-
// Kalibrierung und WLAN-Ersteinrichtung, bevor irgendwelche weiteren
// Texte in (falscher) Standardsprache gezeigt werden. ---
if (isFirstRun) {
FirstRunLanguageScreen::run(tft);
SplashScreen::begin(tft);
SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);
}
AdsbClient::primeTime();
SplashScreen::setStatusLine(tft, 2, "Getting location...");
SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_GETTING_LOCATION));
LocationManager::init();
uint32_t locStart = millis();
while (LocationManager::currentSource() == LocationManager::Source::None &&
@@ -229,7 +258,7 @@ void setup() {
LocationManager::requestIpLookupIfNeeded();
delay(200);
}
SplashScreen::setStatusLine(tft, 2, "Ready!");
SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_READY));
if (LocationManager::hasUtcOffset()) {
configTime(LocationManager::utcOffsetSeconds(), 0, "pool.ntp.org", "time.nist.gov");
@@ -246,11 +275,25 @@ void setup() {
drawHeader();
updateStatusLine();
RadarScreen::render(tft, CONTENT_TOP);
lastInteractionMs = millis();
}
void loop() {
TouchInput::Point tap;
if (TouchInput::wasTapped(tap)) {
bool tapped = TouchInput::wasTapped(tap);
if (tapped) {
lastInteractionMs = millis();
if (screenDimmed) {
ledcWrite(BACKLIGHT_PWM_CHANNEL, BACKLIGHT_FULL);
screenDimmed = false;
tapped = false;
}
}
if (tapped) {
if (menuBtn.contains(tap.x, tap.y)) {
MenuScreen::run(tft);
drawHeader();
@@ -291,4 +334,13 @@ void loop() {
lastStatusLineMs = nowMs;
updateStatusLine();
}
uint8_t timeoutMin = SettingsStore::screenTimeoutMinutes();
if (!screenDimmed && timeoutMin > 0) {
uint32_t timeoutMs = (uint32_t)timeoutMin * 60000UL;
if (nowMs - lastInteractionMs >= timeoutMs) {
ledcWrite(BACKLIGHT_PWM_CHANNEL, BACKLIGHT_DIMMED);
screenDimmed = true;
}
}
}