From 7b31a92606a82540a7bd96cbb63c1d5360a7d8c8 Mon Sep 17 00:00:00 2001 From: Eiswolf-BG <37225519+Eiswolf-BG@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:09:39 +0200 Subject: [PATCH] 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 --- src/i18n.h | 5 +++ src/i18n_de.h | 1 + src/i18n_en.h | 1 + src/i18n_es.h | 1 + src/i18n_fr.h | 1 + src/i18n_it.h | 1 + src/i18n_tr.h | 1 + src/main.cpp | 97 +++++++++++++++++++++++++++++++++++++----- src/menu_screen.cpp | 16 ++++--- src/ota_update.cpp | 8 +++- src/ota_update.h | 13 +++--- src/settings_store.cpp | 11 +++++ src/settings_store.h | 6 +++ 13 files changed, 138 insertions(+), 24 deletions(-) diff --git a/src/i18n.h b/src/i18n.h index 3603fb0..9078a4b 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -316,6 +316,11 @@ enum class StringId : uint8_t { // noetig. 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 // suchen", siehe menu_screen.cpp::runOtaUpdateScreen() und // ota_update.h/.cpp). diff --git a/src/i18n_de.h b/src/i18n_de.h index 2c545df..1c25381 100644 --- a/src/i18n_de.h +++ b/src/i18n_de.h @@ -240,6 +240,7 @@ static const char* const I18N_DE[] = { "Tippen zum Überspringen", "Route: ", "Mit dem Handy scannen:", + "Ruhebildschirm: ", "Nach Update suchen", "Suche nach Updates...", "Prüfung fehlgeschlagen. WLAN prüfen.", diff --git a/src/i18n_en.h b/src/i18n_en.h index 4895719..50a18b8 100644 --- a/src/i18n_en.h +++ b/src/i18n_en.h @@ -241,6 +241,7 @@ static const char* const I18N_EN[] = { "Tap to skip", "Route: ", "Scan with your phone:", + "Screensaver: ", "Check for update", "Checking for updates...", "Check failed. Check your WiFi connection.", diff --git a/src/i18n_es.h b/src/i18n_es.h index 42d15c8..3d9aae0 100644 --- a/src/i18n_es.h +++ b/src/i18n_es.h @@ -240,6 +240,7 @@ static const char* const I18N_ES[] = { "Toca para saltar", "Ruta: ", "Escanea con el móvil:", + "Salvapantallas: ", "Buscar actualización", "Buscando actualizaciones...", "Error al comprobar. Revisa tu conexión WiFi.", diff --git a/src/i18n_fr.h b/src/i18n_fr.h index 69e6561..ae6e441 100644 --- a/src/i18n_fr.h +++ b/src/i18n_fr.h @@ -240,6 +240,7 @@ static const char* const I18N_FR[] = { "Toucher pour passer", "Trajet : ", "Scanner avec le téléphone :", + "Écran de veille : ", "Rechercher une mise à jour", "Recherche de mises à jour...", "Échec de la vérification. Vérifiez votre connexion WiFi.", diff --git a/src/i18n_it.h b/src/i18n_it.h index 20b526f..bbef5b5 100644 --- a/src/i18n_it.h +++ b/src/i18n_it.h @@ -240,6 +240,7 @@ static const char* const I18N_IT[] = { "Tocca per saltare", "Rotta: ", "Scansiona con il telefono:", + "Salvaschermo: ", "Cerca aggiornamenti", "Ricerca aggiornamenti...", "Verifica fallita. Controlla la connessione WiFi.", diff --git a/src/i18n_tr.h b/src/i18n_tr.h index 069dc2a..8e37b55 100644 --- a/src/i18n_tr.h +++ b/src/i18n_tr.h @@ -240,6 +240,7 @@ static const char* const I18N_TR[] = { "Atlamak için dokun", "Rota: ", "Telefonla tarayın:", + "Ekran koruyucu: ", "Güncelleme ara", "Güncellemeler aranıyor...", "Kontrol başarısız. WiFi bağlantınızı kontrol edin.", diff --git a/src/main.cpp b/src/main.cpp index 7984942..609ced0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -60,6 +60,15 @@ uint32_t lastInteractionMs = 0; bool screenDimmed = 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 // einen PWM-Wert 0-255 um - ersetzt das bisher fest verdrahtete // 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_H = 14; @@ -604,6 +648,18 @@ void loop() { nightDimActive = shouldNightDim; screenDimmed = 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,17 +713,38 @@ void loop() { if (!screenDimmed && timeoutMin > 0) { uint32_t timeoutMs = (uint32_t)timeoutMin * 60000UL; if (nowMs - lastInteractionMs >= timeoutMs) { - // Bewusst ganz aus (0) statt nur gedimmt - fuer den - // Bildschirm-Timeout (Menue > System > Timeout). Dimmen - // uebernimmt bereits der separate Nachtmodus - // (updateNightDimming()); der Inaktivitaets-Timeout soll das - // Display wirklich abschalten, z.B. fuer Plane-Spotter, die nur - // das Flugbuch mitlaufen lassen wollen, ohne den Screen zu - // brauchen. Ein Antippen weckt ihn ueber den screenDimmed-Zweig - // oben wieder auf die eingestellte Helligkeit (bzw. Nachtmodus- - // Helligkeit, falls gerade Nachtstunden sind). - ledcWrite(BACKLIGHT_PWM_CHANNEL, 0); + 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 + // Bildschirm-Timeout (Menue > System > Timeout). Dimmen + // uebernimmt bereits der separate Nachtmodus + // (updateNightDimming()); der Inaktivitaets-Timeout soll das + // Display wirklich abschalten, z.B. fuer Plane-Spotter, die nur + // das Flugbuch mitlaufen lassen wollen, ohne den Screen zu + // brauchen. Ein Antippen weckt ihn ueber den screenDimmed-Zweig + // oben wieder auf die eingestellte Helligkeit (bzw. Nachtmodus- + // Helligkeit, falls gerade Nachtstunden sind). + ledcWrite(BACKLIGHT_PWM_CHANNEL, 0); + } screenDimmed = true; } } + + if (screenDimmed && screensaverShowing) { + MenuStars::update(tft); + if (nowMs - lastScreensaverClockMs >= 1000) { + lastScreensaverClockMs = nowMs; + drawScreensaverClock(); + } + } } \ No newline at end of file diff --git a/src/menu_screen.cpp b/src/menu_screen.cpp index 5419aff..02ca7fe 100644 --- a/src/menu_screen.cpp +++ b/src/menu_screen.cpp @@ -87,7 +87,7 @@ namespace { // Ordner-Button ersetzt wurden: -2 (Backup/Restore raus) +1 (neuer // Ordner-Button) = 9. Danach +1 fuer den neuen "Nach Update // 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_START_Y = 18; constexpr int16_t SYSTEM_END_Y = Config::SCREEN_HEIGHT - 10; @@ -466,17 +466,18 @@ void run(TFT_eSPI& tft) { Rect brightnessBtn = systemRowRect(2); Rect timeoutBtn = systemRowRect(3); Rect nightDimBtn = systemRowRect(4); - Rect webuiBtn = systemRowRect(5); + Rect screensaverBtn = systemRowRect(5); + Rect webuiBtn = systemRowRect(6); // "Sicherung & Reset" (Backup/Restore/Werksreset, siehe // Page::BackupReset unten) und "Nach Update suchen" stehen // bewusst direkt ueber "Info", das seinerseits als letzter // Punkt direkt ueber dem Zurueck-Button steht - so bleiben // beide Positionen stabil, egal wie viele weitere Punkte davor // noch dazukommen. - Rect backupResetBtn = systemRowRect(6); - Rect checkUpdateBtn = systemRowRect(7); - Rect aboutBtn = systemRowRect(8); - Rect backBtn = systemRowRect(9); + Rect backupResetBtn = systemRowRect(7); + Rect checkUpdateBtn = systemRowRect(8); + Rect aboutBtn = systemRowRect(9); + Rect backBtn = systemRowRect(10); 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, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes())); 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, backupResetBtn, I18n::t(StringId::MENU_BACKUP_RESET)); drawButton(tft, checkUpdateBtn, I18n::t(StringId::MENU_CHECK_UPDATE)); @@ -515,6 +517,8 @@ void run(TFT_eSPI& tft) { SettingsStore::setScreenTimeoutMinutes(next); } else if (nightDimBtn.contains(tap.x, tap.y)) { SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled()); + } else if (screensaverBtn.contains(tap.x, tap.y)) { + SettingsStore::setScreensaverEnabled(!SettingsStore::screensaverEnabled()); } else if (backupResetBtn.contains(tap.x, tap.y)) { page = Page::BackupReset; } else if (checkUpdateBtn.contains(tap.x, tap.y)) { diff --git a/src/ota_update.cpp b/src/ota_update.cpp index 1ceed34..a31637b 100644 --- a/src/ota_update.cpp +++ b/src/ota_update.cpp @@ -68,17 +68,21 @@ CheckInfo checkForUpdate() { strncpy(info.latestVersion, (tag[0] == 'v' || tag[0] == 'V') ? tag + 1 : tag, 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"]; for (JsonObject asset : assets) { 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"] | ""; strncpy(info.downloadUrl, url, sizeof(info.downloadUrl) - 1); 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); info.result = (cmp > 0) ? CheckResult::UpdateAvailable : CheckResult::UpToDate; diff --git a/src/ota_update.h b/src/ota_update.h index 48b0a4f..7222118 100644 --- a/src/ota_update.h +++ b/src/ota_update.h @@ -18,19 +18,20 @@ namespace OtaUpdate { // sowohl bei UpToDate als auch bei UpdateAvailable gesetzt, damit // der aufrufende Screen sie in beiden Faellen anzeigen kann. char latestVersion[16] = {0}; - // Direkter Download-Link zur firmware.bin des Releases - nur - // gesetzt, wenn result == UpdateAvailable. + // Direkter Download-Link zur CYD-flightradar.bin des Releases - + // nur gesetzt, wenn result == UpdateAvailable. char downloadUrl[192] = {0}; }; // Fragt die GitHub-Releases-API nach dem neuesten Release ab, vergleicht // dessen Versionsnummer (Tag-Name, "v"-Praefix wird ignoriert) gegen - // Config::APP_VERSION und sucht im Release den Anhang "firmware.bin". - // CheckResult::Error bei jedem Fehler unterwegs (kein WLAN, Zeitueber- - // schreitung, unerwartetes JSON, kein firmware.bin im Release). + // Config::APP_VERSION und sucht im Release den Anhang + // "CYD-flightradar.bin". CheckResult::Error bei jedem Fehler unterwegs + // (kein WLAN, Zeitueberschreitung, unerwartetes JSON, kein + // CYD-flightradar.bin im Release). 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 // NICHT selbst ESP.restart() auf - der Aufrufer zeigt zuerst eine // kurze Erfolgsmeldung an und startet danach selbst neu. onProgress diff --git a/src/settings_store.cpp b/src/settings_store.cpp index b3a392b..d79d996 100644 --- a/src/settings_store.cpp +++ b/src/settings_store.cpp @@ -22,6 +22,7 @@ namespace { bool ledHeartbeatOn = true; uint8_t screenTimeoutMin = 0; bool nightDimmingOn = true; + bool screensaverOn = false; bool hideGroundVehiclesOn = true; uint8_t languageIdx = 0; uint8_t unitsModeVal = 0; @@ -59,6 +60,8 @@ namespace { if (v >= 0 && v <= 10) screenTimeoutMin = (uint8_t)v; } else if (key == "night_dimming") { nightDimmingOn = (value.toInt() != 0); + } else if (key == "screensaver") { + screensaverOn = (value.toInt() != 0); } else if (key == "hide_ground_vehicles") { hideGroundVehiclesOn = (value.toInt() != 0); } else if (key == "language") { @@ -122,6 +125,7 @@ void save() { f.printf("led_heartbeat=%d\n", ledHeartbeatOn ? 1 : 0); f.printf("screen_timeout_min=%d\n", screenTimeoutMin); 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("language=%d\n", languageIdx); f.printf("units_mode=%d\n", unitsModeVal); @@ -219,6 +223,13 @@ void setNightDimmingEnabled(bool on) { save(); } +bool screensaverEnabled() { return screensaverOn; } + +void setScreensaverEnabled(bool on) { + screensaverOn = on; + save(); +} + bool hideGroundVehicles() { return hideGroundVehiclesOn; } void setHideGroundVehicles(bool on) { diff --git a/src/settings_store.h b/src/settings_store.h index 94c4060..3a4fbf7 100644 --- a/src/settings_store.h +++ b/src/settings_store.h @@ -50,6 +50,12 @@ namespace SettingsStore { bool nightDimmingEnabled(); 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(); void setHideGroundVehicles(bool on);