diff --git a/images/screenshot.jpg b/images/screenshot.jpg index de6533e..c581467 100644 Binary files a/images/screenshot.jpg and b/images/screenshot.jpg differ diff --git a/platformio.ini b/platformio.ini index 981cb62..1c2e5b0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -5,13 +5,17 @@ framework = arduino monitor_speed = 115200 upload_speed = 921600 -; No OTA (over-the-air firmware update) in use - always flashed via -; cable/web-flasher. The default partition scheme reserves two app -; partitions (~1.25 MB each) plus an unused SPIFFS data area (~1.375 MB) - -; over 2.5 MB of 4 MB flash wasted. "no_ota.csv" (shipped with the -; Espressif framework) uses a single, 2 MB app partition instead - doubles -; the space available for the actual firmware, no code changes needed. -board_build.partitions = no_ota.csv +; Zwei App-Partitionen noetig fuer OTA-Updates (aktuell laufende + neu +; heruntergeladene, siehe ota_update.cpp). "min_spiffs.csv" (von Espressif +; mitgeliefert) gibt jeder der beiden App-Partitionen 0x1E0000 (1,875 MB) +; und schrumpft dafuer die SPIFFS-Partition auf nur noch 0x20000 (128 KB) - +; unproblematisch, da dieses Projekt den internen Flash-Speicher fuer Daten +; gar nicht nutzt (alles liegt auf der SD-Karte, siehe Config::SD_*). Bei +; aktuell ca. 1,25 MB Firmware-Groesse bleiben so noch reichlich ueber +; 600 KB Luft pro Partition. Vorher (bis v3.4.0) kam "no_ota.csv" zum +; Einsatz - eine einzige 2 MB grosse App-Partition ohne OTA-Faehigkeit, da +; OTA damals noch nicht existierte. +board_build.partitions = min_spiffs.csv lib_deps = bodmer/TFT_eSPI @ ^2.5.43 diff --git a/src/i18n.h b/src/i18n.h index e138236..3603fb0 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -316,6 +316,19 @@ enum class StringId : uint8_t { // noetig. WEBUI_QR_HINT, + // OTA-Firmware-Update ueber WLAN (Menue > System > "Nach Update + // suchen", siehe menu_screen.cpp::runOtaUpdateScreen() und + // ota_update.h/.cpp). + MENU_CHECK_UPDATE, + OTA_CHECKING, + OTA_CHECK_FAILED, + OTA_UP_TO_DATE_PREFIX, + OTA_UPDATE_AVAILABLE_PREFIX, + OTA_CONFIRM_BODY, + OTA_INSTALLING_PREFIX, + OTA_UPDATE_FAILED, + OTA_UPDATE_SUCCESS, + COUNT }; diff --git a/src/i18n_de.h b/src/i18n_de.h index 8360516..2c545df 100644 --- a/src/i18n_de.h +++ b/src/i18n_de.h @@ -240,6 +240,15 @@ static const char* const I18N_DE[] = { "Tippen zum Überspringen", "Route: ", "Mit dem Handy scannen:", + "Nach Update suchen", + "Suche nach Updates...", + "Prüfung fehlgeschlagen. WLAN prüfen.", + "Bereits aktuell: v", + "Update verfügbar: v", + "Ein neues Firmware-Update wird heruntergeladen und installiert. Das Gerät startet danach automatisch neu. Bitte WLAN-Verbindung und Stromversorgung während des Vorgangs nicht unterbrechen.", + "Installiere Update ", + "Update fehlgeschlagen.", + "Installiert! Neustart...", }; static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_en.h b/src/i18n_en.h index 8f8355c..4895719 100644 --- a/src/i18n_en.h +++ b/src/i18n_en.h @@ -241,6 +241,15 @@ static const char* const I18N_EN[] = { "Tap to skip", "Route: ", "Scan with your phone:", + "Check for update", + "Checking for updates...", + "Check failed. Check your WiFi connection.", + "Already up to date: v", + "Update available: v", + "A new firmware update will be downloaded and installed. The device will restart automatically afterwards. Please don't interrupt the WiFi connection or power supply during this process.", + "Installing update ", + "Update failed.", + "Installed! Restarting...", }; static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_es.h b/src/i18n_es.h index 2a206e8..42d15c8 100644 --- a/src/i18n_es.h +++ b/src/i18n_es.h @@ -240,6 +240,15 @@ static const char* const I18N_ES[] = { "Toca para saltar", "Ruta: ", "Escanea con el móvil:", + "Buscar actualización", + "Buscando actualizaciones...", + "Error al comprobar. Revisa tu conexión WiFi.", + "Ya actualizado: v", + "Actualización disponible: v", + "Se descargará e instalará una nueva actualización de firmware. El dispositivo se reiniciará automáticamente después. No interrumpas la conexión WiFi ni la alimentación durante este proceso.", + "Instalando actualización ", + "Error al actualizar.", + "¡Instalado! Reiniciando...", }; static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_fr.h b/src/i18n_fr.h index d9241e9..69e6561 100644 --- a/src/i18n_fr.h +++ b/src/i18n_fr.h @@ -240,6 +240,15 @@ static const char* const I18N_FR[] = { "Toucher pour passer", "Trajet : ", "Scanner avec le téléphone :", + "Rechercher une mise à jour", + "Recherche de mises à jour...", + "Échec de la vérification. Vérifiez votre connexion WiFi.", + "Déjà à jour : v", + "Mise à jour disponible : v", + "Une nouvelle mise à jour du micrologiciel va être téléchargée et installée. L'appareil redémarrera automatiquement ensuite. Merci de ne pas interrompre la connexion WiFi ni l'alimentation pendant ce processus.", + "Installation de la mise à jour ", + "Échec de la mise à jour.", + "Installé ! Redémarrage...", }; static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_it.h b/src/i18n_it.h index f898835..20b526f 100644 --- a/src/i18n_it.h +++ b/src/i18n_it.h @@ -240,6 +240,15 @@ static const char* const I18N_IT[] = { "Tocca per saltare", "Rotta: ", "Scansiona con il telefono:", + "Cerca aggiornamenti", + "Ricerca aggiornamenti...", + "Verifica fallita. Controlla la connessione WiFi.", + "Già aggiornato: v", + "Aggiornamento disponibile: v", + "Un nuovo aggiornamento del firmware verrà scaricato e installato. Il dispositivo si riavvierà automaticamente al termine. Non interrompere la connessione WiFi o l'alimentazione durante questo processo.", + "Installazione aggiornamento ", + "Aggiornamento fallito.", + "Installato! Riavvio...", }; static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_tr.h b/src/i18n_tr.h index e0d7722..069dc2a 100644 --- a/src/i18n_tr.h +++ b/src/i18n_tr.h @@ -240,6 +240,15 @@ static const char* const I18N_TR[] = { "Atlamak için dokun", "Rota: ", "Telefonla tarayın:", + "Güncelleme ara", + "Güncellemeler aranıyor...", + "Kontrol başarısız. WiFi bağlantınızı kontrol edin.", + "Zaten güncel: v", + "Güncelleme mevcut: v", + "Yeni bir donanım yazılımı güncellemesi indirilip yüklenecek. Cihaz bundan sonra otomatik olarak yeniden başlayacak. Lütfen bu işlem sırasında WiFi bağlantısını veya güç kaynağını kesmeyin.", + "Güncelleme yükleniyor ", + "Güncelleme başarısız.", + "Yüklendi! Yeniden başlatılıyor...", }; static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT, diff --git a/src/menu_screen.cpp b/src/menu_screen.cpp index b3afe32..5419aff 100644 --- a/src/menu_screen.cpp +++ b/src/menu_screen.cpp @@ -6,6 +6,7 @@ #include "stats_history_screen.h" #include "logbook_files_screen.h" #include "webui_screen.h" +#include "ota_update.h" #include "location_presets_screen.h" #include "airline_filter_screen.h" #include "aircraft_watchlist_screen.h" @@ -84,8 +85,9 @@ namespace { // "Sicherung & Reset"-Unterseite ausgelagert wurden (siehe // BACKUP_RESET_ROW_COUNT unten) und dort durch einen einzelnen // Ordner-Button ersetzt wurden: -2 (Backup/Restore raus) +1 (neuer - // Ordner-Button) = 9. - constexpr uint8_t SYSTEM_ROW_COUNT = 9; + // 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 int16_t SYSTEM_ROW_GAP = 4; constexpr int16_t SYSTEM_START_Y = 18; constexpr int16_t SYSTEM_END_Y = Config::SCREEN_HEIGHT - 10; @@ -216,7 +218,15 @@ namespace { // allen anderen Menue-Screens (nur der Radar-Screen selbst spart sich // das wegen der CPU-Last durch Abfragen/Zeichnen). Gibt true zurueck, // wenn "OK" angetippt wurde, false bei "Zurueck". - bool confirmWarningScreen(TFT_eSPI& tft, StringId titleId, StringId bodyId) { + // + // title/body stehen VOR dem Aufruf per I18n::t() fest (statt StringIds + // entgegenzunehmen), damit auch dynamisch zusammengesetzte Texte (z.B. + // mit eingefuegter Versionsnummer bei OTA) moeglich sind. accentColor + // (Default TFT_RED fuer die beiden bestehenden, wirklich destruktiven + // Aufrufer) faerbt Rahmen und Titel - der OTA-Aufrufer uebergibt + // TFT_GREEN, da ein Update zwar bestaetigt werden sollte, aber keine + // "gefaehrliche" Loesch-Aktion wie Werksreset/Flugbuch ist. + bool confirmWarningScreen(TFT_eSPI& tft, const String& title, const String& body, uint16_t accentColor = TFT_RED) { constexpr int16_t BOX_X = 4; constexpr int16_t BOX_Y = 4; constexpr int16_t BOX_W = Config::SCREEN_WIDTH - 2 * BOX_X; @@ -241,7 +251,6 @@ namespace { constexpr int16_t VIEW_BOTTOM_NO_SCROLL = OK_Y - 8; constexpr int16_t VIEW_BOTTOM_SCROLL = VIEW_BOTTOM_NO_SCROLL - SCROLL_ROW_H - SCROLL_ROW_GAP; - String body = I18n::t(bodyId); int16_t totalH = layoutWrapped(tft, BOX_X + 10, VIEW_TOP, TEXT_MAX_WIDTH, LINE_H, body, 0, 0, 0, false); bool scrollable = (totalH - VIEW_BOTTOM_NO_SCROLL) > 0; @@ -261,12 +270,12 @@ namespace { auto redraw = [&]() { tft.fillScreen(TFT_BLACK); - tft.drawRoundRect(BOX_X, BOX_Y, BOX_W, BOX_H, 6, TFT_RED); + tft.drawRoundRect(BOX_X, BOX_Y, BOX_W, BOX_H, 6, accentColor); tft.setTextDatum(MC_DATUM); - tft.setTextColor(TFT_RED, TFT_BLACK); + tft.setTextColor(accentColor, TFT_BLACK); tft.setTextSize(2); - tft.drawString(I18n::t(titleId), BOX_X + BOX_W / 2, TITLE_Y); + tft.drawString(title, BOX_X + BOX_W / 2, TITLE_Y); tft.setTextSize(1); tft.setTextDatum(TL_DATUM); @@ -303,6 +312,75 @@ namespace { } } + // Fortschrittsanzeige waehrend OtaUpdate::performUpdate() laeuft - + // gleiches Namespace-globale-Zeiger-Prinzip wie progressTft oben (siehe + // Settings-Backup-Fortschrittspunkte), da OtaUpdate::performUpdate() + // ebenfalls einen einfachen C-Funktionszeiger erwartet, keine + // Lambda-Capture erlaubt. + TFT_eSPI* otaProgressTft = nullptr; + + void drawOtaProgress(uint8_t percent) { + if (!otaProgressTft) return; + TFT_eSPI& t = *otaProgressTft; + String label = String(I18n::t(StringId::OTA_INSTALLING_PREFIX)) + String(percent) + "%"; + t.fillRect(0, (int16_t)(Config::SCREEN_HEIGHT / 2 - 14), Config::SCREEN_WIDTH, 28, TFT_BLACK); + t.setTextDatum(MC_DATUM); + t.setTextColor(TFT_GREEN, TFT_BLACK); + t.setTextSize(2); + t.drawString(label, Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2); + t.setTextSize(1); + t.setTextDatum(TL_DATUM); + } + + // Kompletter Ablauf fuer "Nach Update suchen" (System-Menue) - Pruefung + // gegen GitHub-Releases, bei verfuegbarem Update explizite Bestaetigung + // (confirmWarningScreen() mit gruenem statt rotem Akzent - ein Update + // ist keine destruktive Aktion wie Werksreset, verdient aber trotzdem + // eine bewusste Bestaetigung, da WLAN/Strom waehrend des Vorgangs nicht + // unterbrochen werden sollten), danach Fortschrittsanzeige waehrend + // Download+Flash. Startet das Geraet bei Erfolg selbst neu. + void runOtaUpdateScreen(TFT_eSPI& tft) { + MenuStars::reset(); + tft.fillScreen(TFT_BLACK); + tft.setTextDatum(MC_DATUM); + tft.setTextColor(TFT_GREEN, TFT_BLACK); + tft.drawString(I18n::t(StringId::OTA_CHECKING), Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2); + tft.setTextDatum(TL_DATUM); + + OtaUpdate::CheckInfo info = OtaUpdate::checkForUpdate(); + + if (info.result == OtaUpdate::CheckResult::Error) { + showBriefMessage(tft, I18n::t(StringId::OTA_CHECK_FAILED), TFT_RED); + return; + } + if (info.result == OtaUpdate::CheckResult::UpToDate) { + showBriefMessage(tft, String(I18n::t(StringId::OTA_UP_TO_DATE_PREFIX)) + info.latestVersion, TFT_GREEN); + return; + } + + String title = String(I18n::t(StringId::OTA_UPDATE_AVAILABLE_PREFIX)) + info.latestVersion; + bool confirmed = confirmWarningScreen(tft, title, I18n::t(StringId::OTA_CONFIRM_BODY), TFT_GREEN); + if (!confirmed) return; + + otaProgressTft = &tft; + tft.fillScreen(TFT_BLACK); + drawOtaProgress(0); + bool ok = OtaUpdate::performUpdate(info.downloadUrl, drawOtaProgress); + otaProgressTft = nullptr; + + if (ok) { + tft.fillScreen(TFT_BLACK); + tft.setTextDatum(MC_DATUM); + tft.setTextColor(TFT_GREEN, TFT_BLACK); + tft.drawString(I18n::t(StringId::OTA_UPDATE_SUCCESS), Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2); + tft.setTextDatum(TL_DATUM); + delay(1500); + ESP.restart(); + } else { + showBriefMessage(tft, I18n::t(StringId::OTA_UPDATE_FAILED), TFT_RED); + } + } + enum class Page { Main, Region, System, Flight, BackupReset }; } @@ -390,13 +468,15 @@ void run(TFT_eSPI& tft) { Rect nightDimBtn = systemRowRect(4); Rect webuiBtn = systemRowRect(5); // "Sicherung & Reset" (Backup/Restore/Werksreset, siehe - // Page::BackupReset unten) steht 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. + // 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 aboutBtn = systemRowRect(7); - Rect backBtn = systemRowRect(8); + Rect checkUpdateBtn = systemRowRect(7); + Rect aboutBtn = systemRowRect(8); + Rect backBtn = systemRowRect(9); drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE)); @@ -410,6 +490,7 @@ void run(TFT_eSPI& tft) { drawButton(tft, nightDimBtn, I18n::t(StringId::MENU_NIGHT_DIMMING) + onOff(SettingsStore::nightDimmingEnabled())); 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)); drawButton(tft, aboutBtn, I18n::t(StringId::MENU_ABOUT)); drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW)); @@ -436,6 +517,8 @@ void run(TFT_eSPI& tft) { SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled()); } else if (backupResetBtn.contains(tap.x, tap.y)) { page = Page::BackupReset; + } else if (checkUpdateBtn.contains(tap.x, tap.y)) { + runOtaUpdateScreen(tft); } else if (aboutBtn.contains(tap.x, tap.y)) { AboutScreen::run(tft); } else if (webuiBtn.contains(tap.x, tap.y)) { @@ -490,8 +573,8 @@ void run(TFT_eSPI& tft) { ok ? TFT_GREEN : TFT_RED); } } else if (resetBtn.contains(tap.x, tap.y)) { - if (confirmWarningScreen(tft, StringId::MENU_LOGBOOK_WARNING_TITLE, - StringId::MENU_FACTORY_RESET_WARNING_BODY)) { + if (confirmWarningScreen(tft, I18n::t(StringId::MENU_LOGBOOK_WARNING_TITLE), + I18n::t(StringId::MENU_FACTORY_RESET_WARNING_BODY))) { tft.fillScreen(TFT_BLACK); tft.setTextDatum(MC_DATUM); tft.setTextColor(TFT_RED, TFT_BLACK); @@ -591,8 +674,8 @@ void run(TFT_eSPI& tft) { SettingsStore::setFlightLogbookEnabled(false); SettingsStore::setFlightLogbookEnabledAtEpoch(0); SettingsStore::setFlightLogbookSessionFile(""); - } else if (confirmWarningScreen(tft, StringId::MENU_LOGBOOK_WARNING_TITLE, - StringId::MENU_LOGBOOK_WARNING_BODY)) { + } else if (confirmWarningScreen(tft, I18n::t(StringId::MENU_LOGBOOK_WARNING_TITLE), + I18n::t(StringId::MENU_LOGBOOK_WARNING_BODY))) { SettingsStore::setFlightLogbookEnabled(true); SettingsStore::setFlightLogbookEnabledAtEpoch((uint32_t)time(nullptr)); // Leerer Eintrag erzwingt eine frische Sitzungsdatei beim diff --git a/src/ota_update.cpp b/src/ota_update.cpp new file mode 100644 index 0000000..1ceed34 --- /dev/null +++ b/src/ota_update.cpp @@ -0,0 +1,107 @@ +#include "ota_update.h" +#include "config.h" +#include +#include +#include +#include +#include +#include +#include + +namespace OtaUpdate { + +namespace { + constexpr const char* RELEASES_API_URL = + "https://api.github.com/repos/Eiswolf-BG/eiswolfs-flightradar-CYD/releases/latest"; + // GitHub verlangt bei API-Anfragen einen aussagekraeftigen User-Agent + // (sonst HTTP 403) - gleiches Prinzip wie Config::NOMINATIM_USER_AGENT + // fuer die Adresssuche. + constexpr const char* USER_AGENT = + "EiswolfsFlightradarCYD-OTA/1.0 (+https://github.com/Eiswolf-BG/eiswolfs-flightradar-CYD)"; + + bool parseVersion(const char* s, int& major, int& minor, int& patch) { + major = minor = patch = 0; + if (!s || !s[0]) return false; + if (s[0] == 'v' || s[0] == 'V') s++; + return sscanf(s, "%d.%d.%d", &major, &minor, &patch) == 3; + } + + // > 0 wenn a neuer als b, 0 wenn gleich, < 0 wenn a aelter als b. + // Bewusst eine echte numerische Versionsvergleich statt eines simplen + // String-Vergleichs (der wuerde z.B. "3.10.0" faelschlich als "kleiner" + // als "3.9.0" einordnen). + int compareVersions(const char* a, const char* b) { + int aMaj, aMin, aPat, bMaj, bMin, bPat; + if (!parseVersion(a, aMaj, aMin, aPat) || !parseVersion(b, bMaj, bMin, bPat)) return 0; + if (aMaj != bMaj) return aMaj - bMaj; + if (aMin != bMin) return aMin - bMin; + return aPat - bPat; + } +} + +CheckInfo checkForUpdate() { + CheckInfo info; + + WiFiClientSecure client; + client.setInsecure(); + client.setTimeout(8000); + + HTTPClient http; + http.setTimeout(8000); + if (!http.begin(client, RELEASES_API_URL)) return info; + http.addHeader("User-Agent", USER_AGENT); + + int code = http.GET(); + if (code != HTTP_CODE_OK) { + http.end(); + return info; + } + String body = http.getString(); + http.end(); + + JsonDocument doc; + if (deserializeJson(doc, body)) return info; + + const char* tag = doc["tag_name"] | ""; + if (!tag[0]) return info; + + strncpy(info.latestVersion, (tag[0] == 'v' || tag[0] == 'V') ? tag + 1 : tag, + sizeof(info.latestVersion) - 1); + + JsonArray assets = doc["assets"]; + for (JsonObject asset : assets) { + const char* name = asset["name"] | ""; + if (strcmp(name, "firmware.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 + + int cmp = compareVersions(info.latestVersion, Config::APP_VERSION); + info.result = (cmp > 0) ? CheckResult::UpdateAvailable : CheckResult::UpToDate; + return info; +} + +bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) { + WiFiClientSecure client; + client.setInsecure(); + client.setTimeout(15000); + + // Wir zeigen nach erfolgreicher Installation selbst noch eine kurze + // Erfolgsmeldung an, bevor das Geraet neu startet - siehe + // menu_screen.cpp::runOtaUpdateScreen(). + httpUpdate.rebootOnUpdate(false); + httpUpdate.onProgress([onProgress](int cur, int total) { + if (onProgress && total > 0) { + onProgress((uint8_t)((cur * 100) / total)); + } + }); + + t_httpUpdate_return result = httpUpdate.update(client, url); + return result == HTTP_UPDATE_OK; +} + +} diff --git a/src/ota_update.h b/src/ota_update.h new file mode 100644 index 0000000..48b0a4f --- /dev/null +++ b/src/ota_update.h @@ -0,0 +1,42 @@ +#pragma once +#include + +// Firmware-Update ueber WLAN (Menue > System > "Nach Update suchen") - +// prueft gegen die neueste Version im GitHub-Repository (Releases-API) und +// installiert sie bei Bestaetigung direkt auf dem Geraet, ohne Kabel/ +// Web-Flasher. Blockierende, synchrone HTTPS-Aufrufe - werden nur bei +// explizitem Tastendruck ausgefuehrt (kein Hintergrund-Polling), analog zu +// AircraftDetails' Modell-/Routen-Abfragen, nur eben auf Core 1 statt +// Core 0, da hier direkt auf Nutzer-Interaktion reagiert wird. +namespace OtaUpdate { + + enum class CheckResult { Error, UpToDate, UpdateAvailable }; + + struct CheckInfo { + CheckResult result = CheckResult::Error; + // Neueste verfuegbare Version OHNE "v"-Praefix (z.B. "3.5.0") - + // 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. + 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). + CheckInfo checkForUpdate(); + + // Laedt die firmware.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 + // wird waehrend des Downloads wiederholt mit 0-100 (Prozent) aufgerufen + // (darf nullptr sein). Gibt false zurueck, wenn Download oder Flash- + // Vorgang fehlschlagen - das Geraet laeuft in dem Fall unveraendert mit + // der bisherigen Firmware weiter. + bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)); +}