Add idle screensaver feature; fix OTA release asset filename

New "Screensaver" toggle in Menu > System (off by default). When the
inactivity timeout fires with it enabled, the display shows a dimmed
starfield with a large centered clock instead of turning the
backlight fully off; a tap wakes it and redraws normally. Off keeps
the existing behavior unchanged.

Also fixes OtaUpdate::checkForUpdate() to look for the release asset
"CYD-flightradar.bin" (the actual filename used by the release
workflow) instead of the placeholder "firmware.bin" it was checking
for before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Eiswolf-BG
2026-08-12 23:09:39 +02:00
parent d7454d9784
commit 7b31a92606
13 changed files with 138 additions and 24 deletions

View File

@@ -316,6 +316,11 @@ enum class StringId : uint8_t {
// noetig. // noetig.
WEBUI_QR_HINT, WEBUI_QR_HINT,
// Ruhebildschirm bei Inaktivitaets-Timeout (Menue > System >
// "Ruhebildschirm", siehe SettingsStore::screensaverEnabled() und
// main.cpp).
MENU_SCREENSAVER,
// OTA-Firmware-Update ueber WLAN (Menue > System > "Nach Update // OTA-Firmware-Update ueber WLAN (Menue > System > "Nach Update
// suchen", siehe menu_screen.cpp::runOtaUpdateScreen() und // suchen", siehe menu_screen.cpp::runOtaUpdateScreen() und
// ota_update.h/.cpp). // ota_update.h/.cpp).

View File

@@ -240,6 +240,7 @@ static const char* const I18N_DE[] = {
"Tippen zum Überspringen", "Tippen zum Überspringen",
"Route: ", "Route: ",
"Mit dem Handy scannen:", "Mit dem Handy scannen:",
"Ruhebildschirm: ",
"Nach Update suchen", "Nach Update suchen",
"Suche nach Updates...", "Suche nach Updates...",
"Prüfung fehlgeschlagen. WLAN prüfen.", "Prüfung fehlgeschlagen. WLAN prüfen.",

View File

@@ -241,6 +241,7 @@ static const char* const I18N_EN[] = {
"Tap to skip", "Tap to skip",
"Route: ", "Route: ",
"Scan with your phone:", "Scan with your phone:",
"Screensaver: ",
"Check for update", "Check for update",
"Checking for updates...", "Checking for updates...",
"Check failed. Check your WiFi connection.", "Check failed. Check your WiFi connection.",

View File

@@ -240,6 +240,7 @@ static const char* const I18N_ES[] = {
"Toca para saltar", "Toca para saltar",
"Ruta: ", "Ruta: ",
"Escanea con el móvil:", "Escanea con el móvil:",
"Salvapantallas: ",
"Buscar actualización", "Buscar actualización",
"Buscando actualizaciones...", "Buscando actualizaciones...",
"Error al comprobar. Revisa tu conexión WiFi.", "Error al comprobar. Revisa tu conexión WiFi.",

View File

@@ -240,6 +240,7 @@ static const char* const I18N_FR[] = {
"Toucher pour passer", "Toucher pour passer",
"Trajet : ", "Trajet : ",
"Scanner avec le téléphone :", "Scanner avec le téléphone :",
"Écran de veille : ",
"Rechercher une mise à jour", "Rechercher une mise à jour",
"Recherche de mises à jour...", "Recherche de mises à jour...",
"Échec de la vérification. Vérifiez votre connexion WiFi.", "Échec de la vérification. Vérifiez votre connexion WiFi.",

View File

@@ -240,6 +240,7 @@ static const char* const I18N_IT[] = {
"Tocca per saltare", "Tocca per saltare",
"Rotta: ", "Rotta: ",
"Scansiona con il telefono:", "Scansiona con il telefono:",
"Salvaschermo: ",
"Cerca aggiornamenti", "Cerca aggiornamenti",
"Ricerca aggiornamenti...", "Ricerca aggiornamenti...",
"Verifica fallita. Controlla la connessione WiFi.", "Verifica fallita. Controlla la connessione WiFi.",

View File

@@ -240,6 +240,7 @@ static const char* const I18N_TR[] = {
"Atlamak için dokun", "Atlamak için dokun",
"Rota: ", "Rota: ",
"Telefonla tarayın:", "Telefonla tarayın:",
"Ekran koruyucu: ",
"Güncelleme ara", "Güncelleme ara",
"Güncellemeler aranıyor...", "Güncellemeler aranıyor...",
"Kontrol başarısız. WiFi bağlantınızı kontrol edin.", "Kontrol başarısız. WiFi bağlantınızı kontrol edin.",

View File

@@ -60,6 +60,15 @@ uint32_t lastInteractionMs = 0;
bool screenDimmed = false; bool screenDimmed = false;
bool nightDimActive = false; bool nightDimActive = false;
// Ruhebildschirm (Menue > System > Ruhebildschirm, siehe SettingsStore::
// screensaverEnabled()) - true, waehrend statt des komplett dunklen
// Backlights ein gedimmter Sternenhimmel mit Uhrzeit angezeigt wird.
// Getrennt von screenDimmed, da NICHT jeder Timeout automatisch ein
// Ruhebildschirm ist (Default bleibt: Backlight komplett aus) - siehe
// loop() weiter unten.
bool screensaverShowing = false;
uint32_t lastScreensaverClockMs = 0;
// Wandelt die Nutzer-Helligkeit (Menue > System > Helligkeit, 10-100%) in // Wandelt die Nutzer-Helligkeit (Menue > System > Helligkeit, 10-100%) in
// einen PWM-Wert 0-255 um - ersetzt das bisher fest verdrahtete // einen PWM-Wert 0-255 um - ersetzt das bisher fest verdrahtete
// BACKLIGHT_FULL als "normale" Helligkeit ueberall unten. // BACKLIGHT_FULL als "normale" Helligkeit ueberall unten.
@@ -282,6 +291,41 @@ void drawWifiIcon(int16_t rightX, int16_t rowTop, int16_t rowH, int8_t rssi) {
} }
} }
// Grosse, zentrierte Uhrzeit fuer den Ruhebildschirm (siehe
// screensaverShowing oben) - eigene, groessere Variante der kleinen
// Kopfzeilen-Uhr aus updateStatusLine(), da diese bewusst kompakt gehalten
// ist. Loescht bei jedem Aufruf nur den eigenen schmalen Streifen in der
// Bildschirmmitte (nicht den ganzen Bildschirm), damit die Sternenanimation
// darum herum ungestoert weiterlaeuft. Zeichnet nichts, solange die Uhrzeit
// noch nicht per NTP synchronisiert ist (gleiche Pruefung wie
// updateStatusLine()/isNightDimHours()).
void drawScreensaverClock() {
time_t now = time(nullptr);
if (now <= 8 * 3600 * 2) return;
struct tm tmNow;
localtime_r(&now, &tmNow);
char timeBuf[9];
if (LocationManager::useMetricUnits()) {
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tmNow.tm_hour, tmNow.tm_min);
} else {
int hour12 = tmNow.tm_hour % 12;
if (hour12 == 0) hour12 = 12;
snprintf(timeBuf, sizeof(timeBuf), "%d:%02d%s", hour12, tmNow.tm_min,
tmNow.tm_hour < 12 ? "AM" : "PM");
}
constexpr int16_t CLOCK_BAND_H = 40;
int16_t cy = Config::SCREEN_HEIGHT / 2;
tft.fillRect(0, (int16_t)(cy - CLOCK_BAND_H / 2), Config::SCREEN_WIDTH, CLOCK_BAND_H, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(TFT_DARKGREEN, TFT_BLACK);
tft.setTextSize(3);
tft.drawString(timeBuf, Config::SCREEN_WIDTH / 2, cy);
tft.setTextSize(1);
tft.setTextDatum(TL_DATUM);
}
constexpr int16_t WIFI_ICON_W = 22; constexpr int16_t WIFI_ICON_W = 22;
constexpr int16_t WIFI_ICON_H = 14; constexpr int16_t WIFI_ICON_H = 14;
@@ -604,6 +648,18 @@ void loop() {
nightDimActive = shouldNightDim; nightDimActive = shouldNightDim;
screenDimmed = false; screenDimmed = false;
tapped = false; tapped = false;
if (screensaverShowing) {
// Der Ruhebildschirm hat den kompletten Bildschirminhalt
// ueberschrieben (Sternenhimmel + Uhrzeit) - anders als beim
// normalen Timeout (nur Backlight aus, Inhalt blieb im
// TFT-Speicher unveraendert stehen) muss hier explizit neu
// gezeichnet werden.
screensaverShowing = false;
drawHeader();
updateStatusLine();
forceRedraw = true;
}
} }
} }
@@ -657,6 +713,18 @@ void loop() {
if (!screenDimmed && timeoutMin > 0) { if (!screenDimmed && timeoutMin > 0) {
uint32_t timeoutMs = (uint32_t)timeoutMin * 60000UL; uint32_t timeoutMs = (uint32_t)timeoutMin * 60000UL;
if (nowMs - lastInteractionMs >= timeoutMs) { if (nowMs - lastInteractionMs >= timeoutMs) {
if (SettingsStore::screensaverEnabled()) {
// Ruhebildschirm statt komplett dunkel (Menue > System >
// Ruhebildschirm, AUS per Default) - gedimmter
// Sternenhimmel mit grosser Uhrzeit statt des Backlights
// ganz aus, fuer alle, die trotz Inaktivitaets-Timeout noch
// etwas auf dem Display sehen wollen.
screensaverShowing = true;
ledcWrite(BACKLIGHT_PWM_CHANNEL, nightDimBacklightPwm());
tft.fillScreen(TFT_BLACK);
MenuStars::reset();
lastScreensaverClockMs = 0; // sofortiges erstes Zeichnen erzwingen
} else {
// Bewusst ganz aus (0) statt nur gedimmt - fuer den // Bewusst ganz aus (0) statt nur gedimmt - fuer den
// Bildschirm-Timeout (Menue > System > Timeout). Dimmen // Bildschirm-Timeout (Menue > System > Timeout). Dimmen
// uebernimmt bereits der separate Nachtmodus // uebernimmt bereits der separate Nachtmodus
@@ -667,7 +735,16 @@ void loop() {
// oben wieder auf die eingestellte Helligkeit (bzw. Nachtmodus- // oben wieder auf die eingestellte Helligkeit (bzw. Nachtmodus-
// Helligkeit, falls gerade Nachtstunden sind). // Helligkeit, falls gerade Nachtstunden sind).
ledcWrite(BACKLIGHT_PWM_CHANNEL, 0); ledcWrite(BACKLIGHT_PWM_CHANNEL, 0);
}
screenDimmed = true; screenDimmed = true;
} }
} }
if (screenDimmed && screensaverShowing) {
MenuStars::update(tft);
if (nowMs - lastScreensaverClockMs >= 1000) {
lastScreensaverClockMs = nowMs;
drawScreensaverClock();
}
}
} }

View File

@@ -87,7 +87,7 @@ namespace {
// Ordner-Button ersetzt wurden: -2 (Backup/Restore raus) +1 (neuer // Ordner-Button ersetzt wurden: -2 (Backup/Restore raus) +1 (neuer
// Ordner-Button) = 9. Danach +1 fuer den neuen "Nach Update // Ordner-Button) = 9. Danach +1 fuer den neuen "Nach Update
// suchen"-Punkt (OTA-Update, siehe ota_update.h) = 10. // suchen"-Punkt (OTA-Update, siehe ota_update.h) = 10.
constexpr uint8_t SYSTEM_ROW_COUNT = 10; constexpr uint8_t SYSTEM_ROW_COUNT = 11;
constexpr int16_t SYSTEM_ROW_GAP = 4; constexpr int16_t SYSTEM_ROW_GAP = 4;
constexpr int16_t SYSTEM_START_Y = 18; constexpr int16_t SYSTEM_START_Y = 18;
constexpr int16_t SYSTEM_END_Y = Config::SCREEN_HEIGHT - 10; constexpr int16_t SYSTEM_END_Y = Config::SCREEN_HEIGHT - 10;
@@ -466,17 +466,18 @@ void run(TFT_eSPI& tft) {
Rect brightnessBtn = systemRowRect(2); Rect brightnessBtn = systemRowRect(2);
Rect timeoutBtn = systemRowRect(3); Rect timeoutBtn = systemRowRect(3);
Rect nightDimBtn = systemRowRect(4); Rect nightDimBtn = systemRowRect(4);
Rect webuiBtn = systemRowRect(5); Rect screensaverBtn = systemRowRect(5);
Rect webuiBtn = systemRowRect(6);
// "Sicherung & Reset" (Backup/Restore/Werksreset, siehe // "Sicherung & Reset" (Backup/Restore/Werksreset, siehe
// Page::BackupReset unten) und "Nach Update suchen" stehen // Page::BackupReset unten) und "Nach Update suchen" stehen
// bewusst direkt ueber "Info", das seinerseits als letzter // bewusst direkt ueber "Info", das seinerseits als letzter
// Punkt direkt ueber dem Zurueck-Button steht - so bleiben // Punkt direkt ueber dem Zurueck-Button steht - so bleiben
// beide Positionen stabil, egal wie viele weitere Punkte davor // beide Positionen stabil, egal wie viele weitere Punkte davor
// noch dazukommen. // noch dazukommen.
Rect backupResetBtn = systemRowRect(6); Rect backupResetBtn = systemRowRect(7);
Rect checkUpdateBtn = systemRowRect(7); Rect checkUpdateBtn = systemRowRect(8);
Rect aboutBtn = systemRowRect(8); Rect aboutBtn = systemRowRect(9);
Rect backBtn = systemRowRect(9); Rect backBtn = systemRowRect(10);
drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE)); drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE));
@@ -488,6 +489,7 @@ void run(TFT_eSPI& tft) {
drawButton(tft, brightnessBtn, brightnessLabel(SettingsStore::brightnessPercent())); drawButton(tft, brightnessBtn, brightnessLabel(SettingsStore::brightnessPercent()));
drawButton(tft, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes())); drawButton(tft, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes()));
drawButton(tft, nightDimBtn, I18n::t(StringId::MENU_NIGHT_DIMMING) + onOff(SettingsStore::nightDimmingEnabled())); drawButton(tft, nightDimBtn, I18n::t(StringId::MENU_NIGHT_DIMMING) + onOff(SettingsStore::nightDimmingEnabled()));
drawButton(tft, screensaverBtn, I18n::t(StringId::MENU_SCREENSAVER) + onOff(SettingsStore::screensaverEnabled()));
drawButton(tft, webuiBtn, I18n::t(StringId::MENU_LOGBOOK_WEBUI)); drawButton(tft, webuiBtn, I18n::t(StringId::MENU_LOGBOOK_WEBUI));
drawButton(tft, backupResetBtn, I18n::t(StringId::MENU_BACKUP_RESET)); drawButton(tft, backupResetBtn, I18n::t(StringId::MENU_BACKUP_RESET));
drawButton(tft, checkUpdateBtn, I18n::t(StringId::MENU_CHECK_UPDATE)); drawButton(tft, checkUpdateBtn, I18n::t(StringId::MENU_CHECK_UPDATE));
@@ -515,6 +517,8 @@ void run(TFT_eSPI& tft) {
SettingsStore::setScreenTimeoutMinutes(next); SettingsStore::setScreenTimeoutMinutes(next);
} else if (nightDimBtn.contains(tap.x, tap.y)) { } else if (nightDimBtn.contains(tap.x, tap.y)) {
SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled()); SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled());
} else if (screensaverBtn.contains(tap.x, tap.y)) {
SettingsStore::setScreensaverEnabled(!SettingsStore::screensaverEnabled());
} else if (backupResetBtn.contains(tap.x, tap.y)) { } else if (backupResetBtn.contains(tap.x, tap.y)) {
page = Page::BackupReset; page = Page::BackupReset;
} else if (checkUpdateBtn.contains(tap.x, tap.y)) { } else if (checkUpdateBtn.contains(tap.x, tap.y)) {

View File

@@ -68,17 +68,21 @@ CheckInfo checkForUpdate() {
strncpy(info.latestVersion, (tag[0] == 'v' || tag[0] == 'V') ? tag + 1 : tag, strncpy(info.latestVersion, (tag[0] == 'v' || tag[0] == 'V') ? tag + 1 : tag,
sizeof(info.latestVersion) - 1); sizeof(info.latestVersion) - 1);
// Der Release-Workflow (siehe CLAUDE.md im Repo) laedt die gebaute
// Firmware unter dem Namen "CYD-flightradar.bin" als Release-Asset
// hoch (umbenannt aus dem rohen "firmware.bin"-Build-Artefakt) - das
// ist auch der Dateiname, unter dem der Web-Flasher sie erwartet.
JsonArray assets = doc["assets"]; JsonArray assets = doc["assets"];
for (JsonObject asset : assets) { for (JsonObject asset : assets) {
const char* name = asset["name"] | ""; const char* name = asset["name"] | "";
if (strcmp(name, "firmware.bin") == 0) { if (strcmp(name, "CYD-flightradar.bin") == 0) {
const char* url = asset["browser_download_url"] | ""; const char* url = asset["browser_download_url"] | "";
strncpy(info.downloadUrl, url, sizeof(info.downloadUrl) - 1); strncpy(info.downloadUrl, url, sizeof(info.downloadUrl) - 1);
break; break;
} }
} }
if (!info.downloadUrl[0]) return info; // Release ohne firmware.bin-Anhang if (!info.downloadUrl[0]) return info; // Release ohne CYD-flightradar.bin-Anhang
int cmp = compareVersions(info.latestVersion, Config::APP_VERSION); int cmp = compareVersions(info.latestVersion, Config::APP_VERSION);
info.result = (cmp > 0) ? CheckResult::UpdateAvailable : CheckResult::UpToDate; info.result = (cmp > 0) ? CheckResult::UpdateAvailable : CheckResult::UpToDate;

View File

@@ -18,19 +18,20 @@ namespace OtaUpdate {
// sowohl bei UpToDate als auch bei UpdateAvailable gesetzt, damit // sowohl bei UpToDate als auch bei UpdateAvailable gesetzt, damit
// der aufrufende Screen sie in beiden Faellen anzeigen kann. // der aufrufende Screen sie in beiden Faellen anzeigen kann.
char latestVersion[16] = {0}; char latestVersion[16] = {0};
// Direkter Download-Link zur firmware.bin des Releases - nur // Direkter Download-Link zur CYD-flightradar.bin des Releases -
// gesetzt, wenn result == UpdateAvailable. // nur gesetzt, wenn result == UpdateAvailable.
char downloadUrl[192] = {0}; char downloadUrl[192] = {0};
}; };
// Fragt die GitHub-Releases-API nach dem neuesten Release ab, vergleicht // Fragt die GitHub-Releases-API nach dem neuesten Release ab, vergleicht
// dessen Versionsnummer (Tag-Name, "v"-Praefix wird ignoriert) gegen // dessen Versionsnummer (Tag-Name, "v"-Praefix wird ignoriert) gegen
// Config::APP_VERSION und sucht im Release den Anhang "firmware.bin". // Config::APP_VERSION und sucht im Release den Anhang
// CheckResult::Error bei jedem Fehler unterwegs (kein WLAN, Zeitueber- // "CYD-flightradar.bin". CheckResult::Error bei jedem Fehler unterwegs
// schreitung, unerwartetes JSON, kein firmware.bin im Release). // (kein WLAN, Zeitueberschreitung, unerwartetes JSON, kein
// CYD-flightradar.bin im Release).
CheckInfo checkForUpdate(); CheckInfo checkForUpdate();
// Laedt die firmware.bin von 'url' herunter und flasht sie (ueber die // Laedt die CYD-flightradar.bin von 'url' herunter und flasht sie (ueber die
// Standard-Arduino-HTTPUpdate-Bibliothek). Ruft bei Erfolg BEWUSST // Standard-Arduino-HTTPUpdate-Bibliothek). Ruft bei Erfolg BEWUSST
// NICHT selbst ESP.restart() auf - der Aufrufer zeigt zuerst eine // NICHT selbst ESP.restart() auf - der Aufrufer zeigt zuerst eine
// kurze Erfolgsmeldung an und startet danach selbst neu. onProgress // kurze Erfolgsmeldung an und startet danach selbst neu. onProgress

View File

@@ -22,6 +22,7 @@ namespace {
bool ledHeartbeatOn = true; bool ledHeartbeatOn = true;
uint8_t screenTimeoutMin = 0; uint8_t screenTimeoutMin = 0;
bool nightDimmingOn = true; bool nightDimmingOn = true;
bool screensaverOn = false;
bool hideGroundVehiclesOn = true; bool hideGroundVehiclesOn = true;
uint8_t languageIdx = 0; uint8_t languageIdx = 0;
uint8_t unitsModeVal = 0; uint8_t unitsModeVal = 0;
@@ -59,6 +60,8 @@ namespace {
if (v >= 0 && v <= 10) screenTimeoutMin = (uint8_t)v; if (v >= 0 && v <= 10) screenTimeoutMin = (uint8_t)v;
} else if (key == "night_dimming") { } else if (key == "night_dimming") {
nightDimmingOn = (value.toInt() != 0); nightDimmingOn = (value.toInt() != 0);
} else if (key == "screensaver") {
screensaverOn = (value.toInt() != 0);
} else if (key == "hide_ground_vehicles") { } else if (key == "hide_ground_vehicles") {
hideGroundVehiclesOn = (value.toInt() != 0); hideGroundVehiclesOn = (value.toInt() != 0);
} else if (key == "language") { } else if (key == "language") {
@@ -122,6 +125,7 @@ void save() {
f.printf("led_heartbeat=%d\n", ledHeartbeatOn ? 1 : 0); f.printf("led_heartbeat=%d\n", ledHeartbeatOn ? 1 : 0);
f.printf("screen_timeout_min=%d\n", screenTimeoutMin); f.printf("screen_timeout_min=%d\n", screenTimeoutMin);
f.printf("night_dimming=%d\n", nightDimmingOn ? 1 : 0); f.printf("night_dimming=%d\n", nightDimmingOn ? 1 : 0);
f.printf("screensaver=%d\n", screensaverOn ? 1 : 0);
f.printf("hide_ground_vehicles=%d\n", hideGroundVehiclesOn ? 1 : 0); f.printf("hide_ground_vehicles=%d\n", hideGroundVehiclesOn ? 1 : 0);
f.printf("language=%d\n", languageIdx); f.printf("language=%d\n", languageIdx);
f.printf("units_mode=%d\n", unitsModeVal); f.printf("units_mode=%d\n", unitsModeVal);
@@ -219,6 +223,13 @@ void setNightDimmingEnabled(bool on) {
save(); save();
} }
bool screensaverEnabled() { return screensaverOn; }
void setScreensaverEnabled(bool on) {
screensaverOn = on;
save();
}
bool hideGroundVehicles() { return hideGroundVehiclesOn; } bool hideGroundVehicles() { return hideGroundVehiclesOn; }
void setHideGroundVehicles(bool on) { void setHideGroundVehicles(bool on) {

View File

@@ -50,6 +50,12 @@ namespace SettingsStore {
bool nightDimmingEnabled(); bool nightDimmingEnabled();
void setNightDimmingEnabled(bool on); void setNightDimmingEnabled(bool on);
// Ruhebildschirm bei Inaktivitaets-Timeout (siehe main.cpp) - AUS per
// Default, damit sich am bisherigen Verhalten (Backlight komplett aus)
// nichts aendert, wer es nicht explizit einschaltet.
bool screensaverEnabled();
void setScreensaverEnabled(bool on);
bool hideGroundVehicles(); bool hideGroundVehicles();
void setHideGroundVehicles(bool on); void setHideGroundVehicles(bool on);