Fix OTA download (follow GitHub redirect), rework result screens, drop asset rename

OTA downloads always failed because GitHub's browser_download_url
returns a 301/302 redirect to a signed objects.githubusercontent.com
URL, which HTTPUpdate doesn't follow by default. Now sets
HTTPC_STRICT_FOLLOW_REDIRECTS before downloading.

Update results (success and failure) are now shown as a persistent
info screen requiring an explicit tap to dismiss, instead of a
1.2-1.5s auto-vanishing message followed by an automatic restart -
a firmware update result is important enough that it shouldn't be
missable. Also fixes long dynamic titles (e.g. "Update available:
v3.5.0") and the installing-progress label overflowing the screen
edge in some languages.

Release workflow no longer renames the build artifact to
CYD-flightradar.bin before upload; the release asset is just
firmware.bin now, matching what OtaUpdate::checkForUpdate() looks
for. CLAUDE.md's release workflow updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Eiswolf-BG
2026-08-13 02:13:24 +02:00
parent 3dd308b39a
commit 0bf8518ac5
11 changed files with 215 additions and 86 deletions

View File

@@ -213,39 +213,15 @@ in dieser Reihenfolge:
6. Alle diese Änderungen (Code + README + Web-Flasher-Dateien) zusammen 6. Alle diese Änderungen (Code + README + Web-Flasher-Dateien) zusammen
committen und pushen (`git push`, plus `git push origin vX.Y.Z` falls ein committen und pushen (`git push`, plus `git push origin vX.Y.Z` falls ein
Tag erstellt wurde). Tag erstellt wurde).
7. ERST DANACH (nach Schritt 5, wenn bootloader/firmware/partitions bereits 7. GitHub Release erstellen UND die `.bin`-Datei in einem Schritt hochladen
ins Hauptverzeichnis kopiert sind): die `firmware.bin` im
`.pio/build/esp32dev/`-Ordner zusätzlich zu `CYD-flightradar.bin`
umbenennen (bzw. kopieren) - das ist die Datei für den GitHub-Release-
Upload. WICHTIG: diese Umbenennung darf NICHT vor dem Auto-Flash (siehe
unten) passieren, sonst schlägt `pio run --target upload` fehl, weil es
die Datei unter dem Namen `firmware.bin` erwartet.
SICHERHEITSPROBLEM bei dieser Umbenennung: Falls im
`.pio/build/esp32dev/`-Ordner bereits eine `CYD-flightradar.bin` von
einem vorherigen Release liegt (weil sie nach dem letzten Release aus
irgendeinem Grund nicht geloescht wurde), darf das Umbenennen NICHT
einfach übersprungen werden (z.B. weil ein `mv`/Kopiervorgang auf eine
bereits existierende Zieldatei fehlschlägt oder stillschweigend nichts
tut) - sonst würde beim GitHub-Release versehentlich die ALTE,
veraltete `CYD-flightradar.bin` hochgeladen, obwohl der frisch gebaute
Code neuer ist. Das ist ein ernsthaftes Risiko (veraltete Firmware wird
veröffentlicht, ohne dass es auffällt). Deshalb bei JEDEM Release als
expliziter, nicht überspringbarer Schritt:
a) Prüfen, ob im `.pio/build/esp32dev/`-Ordner bereits eine Datei
namens `CYD-flightradar.bin` existiert.
b) Falls ja: diese alte Datei IMMER ZUERST LÖSCHEN, bevor die aktuelle
`firmware.bin` umbenannt wird (nicht überspringen, nicht
stillschweigend stehen lassen).
c) Danach die aktuelle `firmware.bin` zu `CYD-flightradar.bin`
umbenennen (wie oben beschrieben).
d) Am Ende der Release-Zusammenfassung IMMER explizit erwähnen, ob eine
alte `CYD-flightradar.bin` gefunden und gelöscht wurde.
8. GitHub Release erstellen UND die `.bin`-Datei in einem Schritt hochladen
(per `gh` CLI, seit v2.7.5 eingerichtet und authentifiziert - siehe (per `gh` CLI, seit v2.7.5 eingerichtet und authentifiziert - siehe
`gh auth status`): `gh auth status`). Das Release-Asset heißt einfach `firmware.bin`, keine
Umbenennung nötig - die meisten Nutzer laden ohnehin über den
Web-Flasher, das Asset ist nur noch für die wenigen Leute relevant, die
über eine CYD-Launcher-App direkt eine `.bin`-Datei brauchen (dafür ist
der Dateiname egal):
gh release create vX.Y.Z .pio/build/esp32dev/CYD-flightradar.bin \ gh release create vX.Y.Z .pio/build/esp32dev/firmware.bin \
--repo Eiswolf-BG/eiswolfs-flightradar-CYD \ --repo Eiswolf-BG/eiswolfs-flightradar-CYD \
--title "vX.Y.Z" \ --title "vX.Y.Z" \
--notes "<Release-Notes-Text>" --notes "<Release-Notes-Text>"
@@ -254,16 +230,12 @@ in dieser Reihenfolge:
Text, der auch für die Tag-Message verwendet wird) - falls im Text, der auch für die Tag-Message verwendet wird) - falls im
Push-Wunsch kein Text mitgegeben wurde, aus dem `git log` seit dem Push-Wunsch kein Text mitgegeben wurde, aus dem `git log` seit dem
letzten Tag ableiten, wie bisher auch für die Tag-Message. letzten Tag ableiten, wie bisher auch für die Tag-Message.
9. Nach erfolgreichem Upload: die lokale Kopie 8. Kurze Zusammenfassung am Ende: was committet/getaggt/gepusht wurde, ob
`.pio/build/esp32dev/CYD-flightradar.bin` wieder löschen - reines die README aktualisiert wurde (und falls ja, welche Abschnitte), sowie
Build-Artefakt fürs Hochladen, gehört nicht ins Repo und wird nicht die URL des erstellten GitHub Release. Der GitHub-Release-Schritt ist
committet. damit vollautomatisch - kein manuelles Nacharbeiten von Alex mehr
10. Kurze Zusammenfassung am Ende: was committet/getaggt/gepusht wurde, ob nötig, außer `gh` sollte einmal die Authentifizierung verlieren (dann
die README aktualisiert wurde (und falls ja, welche Abschnitte), sowie erneut `gh auth login`, siehe oben).
die URL des erstellten GitHub Release. Der GitHub-Release-Schritt ist
damit vollautomatisch - kein manuelles Nacharbeiten von Alex mehr
nötig, außer `gh` sollte einmal die Authentifizierung verlieren (dann
erneut `gh auth login`, siehe oben).
## Nach jedem erfolgreichen Build automatisch flashen ## Nach jedem erfolgreichen Build automatisch flashen

View File

@@ -334,6 +334,16 @@ enum class StringId : uint8_t {
OTA_UPDATE_FAILED, OTA_UPDATE_FAILED,
OTA_UPDATE_SUCCESS, OTA_UPDATE_SUCCESS,
// Erfolgs-/Fehler-Ergebnis nach Download+Flash wird jetzt als
// dauerhafter Info-Screen mit explizitem Button angezeigt (statt
// automatischem Neustart bzw. kurzer, automatisch verschwindender
// Meldung) - der Nutzer muss bei einer sicherheitsrelevanten Aktion wie
// einem Firmware-Update IMMER aktiv informiert werden und selbst
// bestaetigen, siehe menu_screen.cpp::infoScreen().
OTA_SUCCESS_BODY,
OTA_RESTART_BUTTON,
OTA_FAILED_BODY,
COUNT COUNT
}; };

View File

@@ -246,10 +246,13 @@ static const char* const I18N_DE[] = {
"Prüfung fehlgeschlagen. WLAN prüfen.", "Prüfung fehlgeschlagen. WLAN prüfen.",
"Bereits aktuell: v", "Bereits aktuell: v",
"Update verfügbar: 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.", "Ein neues Firmware-Update wird heruntergeladen und installiert. Bitte WLAN-Verbindung und Stromversorgung während des Vorgangs nicht unterbrechen. Nach der Installation wirst du zum Neustart aufgefordert.",
"Installiere Update ", "Installiere Update ",
"Update fehlgeschlagen.", "Update fehlgeschlagen",
"Installiert! Neustart...", "Update installiert",
"Die neue Firmware wurde erfolgreich installiert. Bitte jetzt neu starten, um sie zu verwenden.",
"Jetzt neu starten",
"Das Update konnte nicht heruntergeladen oder installiert werden. Das Gerät läuft unverändert mit der bisherigen Firmware weiter. Bitte WLAN-Verbindung prüfen und es später erneut versuchen.",
}; };
static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT,

View File

@@ -247,10 +247,13 @@ static const char* const I18N_EN[] = {
"Check failed. Check your WiFi connection.", "Check failed. Check your WiFi connection.",
"Already up to date: v", "Already up to date: v",
"Update available: 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.", "A new firmware update will be downloaded and installed. Please don't interrupt the WiFi connection or power supply during this process. You'll be asked to restart once installation is complete.",
"Installing update ", "Installing update ",
"Update failed.", "Update failed",
"Installed! Restarting...", "Update installed",
"The new firmware was installed successfully. Please restart now to start using it.",
"Restart now",
"The update could not be downloaded or installed. The device is still running the previous firmware, unchanged. Please check your WiFi connection and try again later.",
}; };
static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT,

View File

@@ -246,10 +246,13 @@ static const char* const I18N_ES[] = {
"Error al comprobar. Revisa tu conexión WiFi.", "Error al comprobar. Revisa tu conexión WiFi.",
"Ya actualizado: v", "Ya actualizado: v",
"Actualización disponible: 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.", "Se descargará e instalará una nueva actualización de firmware. No interrumpas la conexión WiFi ni la alimentación durante este proceso. Se te pedirá reiniciar una vez completada la instalación.",
"Instalando actualización ", "Instalando actualización ",
"Error al actualizar.", "Error al actualizar",
"¡Instalado! Reiniciando...", "Actualización instalada",
"El nuevo firmware se instaló correctamente. Reinicia ahora para empezar a usarlo.",
"Reiniciar ahora",
"No se pudo descargar o instalar la actualización. El dispositivo sigue funcionando sin cambios con el firmware anterior. Comprueba tu conexión WiFi e inténtalo de nuevo más tarde.",
}; };
static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT,

View File

@@ -246,10 +246,13 @@ static const char* const I18N_FR[] = {
"Échec de la vérification. Vérifiez votre connexion WiFi.", "Échec de la vérification. Vérifiez votre connexion WiFi.",
"Déjà à jour : v", "Déjà à jour : v",
"Mise à jour disponible : 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.", "Une nouvelle mise à jour du micrologiciel va être téléchargée et installée. Merci de ne pas interrompre la connexion WiFi ni l'alimentation pendant ce processus. Un redémarrage vous sera demandé une fois l'installation terminée.",
"Installation de la mise à jour ", "Installation de la mise à jour ",
"Échec de la mise à jour.", "Échec de la mise à jour",
"Installé ! Redémarrage...", "Mise à jour installée",
"Le nouveau micrologiciel a été installé avec succès. Veuillez redémarrer maintenant pour l'utiliser.",
"Redémarrer maintenant",
"La mise à jour n'a pas pu être téléchargée ou installée. L'appareil continue de fonctionner normalement avec le micrologiciel précédent. Merci de vérifier votre connexion WiFi et de réessayer plus tard.",
}; };
static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT,

View File

@@ -246,10 +246,13 @@ static const char* const I18N_IT[] = {
"Verifica fallita. Controlla la connessione WiFi.", "Verifica fallita. Controlla la connessione WiFi.",
"Già aggiornato: v", "Già aggiornato: v",
"Aggiornamento disponibile: 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.", "Un nuovo aggiornamento del firmware verrà scaricato e installato. Non interrompere la connessione WiFi o l'alimentazione durante questo processo. Al termine dell'installazione ti verrà chiesto di riavviare.",
"Installazione aggiornamento ", "Installazione aggiornamento ",
"Aggiornamento fallito.", "Aggiornamento fallito",
"Installato! Riavvio...", "Aggiornamento installato",
"Il nuovo firmware è stato installato correttamente. Riavvia ora per iniziare a usarlo.",
"Riavvia ora",
"Non è stato possibile scaricare o installare l'aggiornamento. Il dispositivo continua a funzionare senza modifiche con il firmware precedente. Controlla la connessione WiFi e riprova più tardi.",
}; };
static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT,

View File

@@ -246,10 +246,13 @@ static const char* const I18N_TR[] = {
"Kontrol başarısız. WiFi bağlantınızı kontrol edin.", "Kontrol başarısız. WiFi bağlantınızı kontrol edin.",
"Zaten güncel: v", "Zaten güncel: v",
"Güncelleme mevcut: 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.", "Yeni bir donanım yazılımı güncellemesi indirilip yüklenecek. Lütfen bu işlem sırasında WiFi bağlantısını veya güç kaynağını kesmeyin. Kurulum tamamlandığında yeniden başlatmanız istenecek.",
"Güncelleme yükleniyor ", "Güncelleme yükleniyor ",
"Güncelleme başarısız.", "Güncelleme başarısız",
"Yüklendi! Yeniden başlatılıyor...", "Güncelleme yüklendi",
"Yeni donanım yazılımı başarıyla yüklendi. Kullanmaya başlamak için lütfen şimdi yeniden başlatın.",
"Şimdi yeniden başlat",
"Güncelleme indirilemedi veya yüklenemedi. Cihaz değişmeden önceki donanım yazılımıyla çalışmaya devam ediyor. Lütfen WiFi bağlantınızı kontrol edip daha sonra tekrar deneyin.",
}; };
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,

View File

@@ -274,7 +274,17 @@ namespace {
tft.setTextDatum(MC_DATUM); tft.setTextDatum(MC_DATUM);
tft.setTextColor(accentColor, TFT_BLACK); tft.setTextColor(accentColor, TFT_BLACK);
// Statische Titel ("Achtung!!!") sind kurz genug fuer Groesse 2,
// aber der OTA-Aufrufer baut den Titel dynamisch mit
// Versionsnummer zusammen (z.B. "Update verfuegbar: v3.5.0") -
// das passt bei Groesse 2 nicht mehr in die Box und lief vorher
// links/rechts ueber den Bildschirmrand hinaus. Deshalb hier
// die Breite bei Groesse 2 pruefen und bei Bedarf auf Groesse 1
// zurueckfallen, statt eine feste Groesse anzunehmen.
tft.setTextSize(2); tft.setTextSize(2);
if (tft.textWidth(title) > (BOX_W - 20)) {
tft.setTextSize(1);
}
tft.drawString(title, BOX_X + BOX_W / 2, TITLE_Y); tft.drawString(title, BOX_X + BOX_W / 2, TITLE_Y);
tft.setTextSize(1); tft.setTextSize(1);
tft.setTextDatum(TL_DATUM); tft.setTextDatum(TL_DATUM);
@@ -312,6 +322,96 @@ namespace {
} }
} }
// Einfacher Info-Screen mit nur EINEM Button (kein Abbrechen) - fuer
// Endzustaende, bei denen es nichts mehr zu entscheiden gibt, nur zu
// bestaetigen (z.B. Ergebnis eines OTA-Updates). Anders als
// showBriefMessage() (kurze Meldung unten am Bildschirmrand,
// verschwindet nach 1,2s automatisch von selbst) bleibt dieser Screen
// stehen, bis aktiv bestaetigt wird - wichtig bei sicherheitsrelevanten
// Meldungen wie einem fehlgeschlagenen oder erfolgreichen Firmware-
// Update, die der Nutzer auf keinen Fall verpassen darf. Gleicher
// Kasten-/Scroll-Aufbau wie confirmWarningScreen(), nur mit einem
// einzigen, ueber die volle Breite gehenden Button statt OK/Zurueck.
void infoScreen(TFT_eSPI& tft, const String& title, const String& body, uint16_t accentColor, const String& buttonLabel) {
constexpr int16_t BOX_X = 4;
constexpr int16_t BOX_Y = 4;
constexpr int16_t BOX_W = Config::SCREEN_WIDTH - 2 * BOX_X;
constexpr int16_t BOX_H = Config::SCREEN_HEIGHT - 2 * BOX_Y;
constexpr int16_t TEXT_MAX_WIDTH = BOX_W - 20;
constexpr int16_t LINE_H = 16;
constexpr int16_t TITLE_Y = BOX_Y + 16;
constexpr int16_t VIEW_TOP = TITLE_Y + 12 + LINE_H;
constexpr int16_t BTN_H = 40;
constexpr int16_t BOTTOM_MARGIN = 10;
constexpr int16_t BTN_Y = BOX_Y + BOX_H - BOTTOM_MARGIN - BTN_H;
constexpr int16_t SCROLL_ROW_H = 28;
constexpr int16_t SCROLL_ROW_GAP = 8;
constexpr int16_t VIEW_BOTTOM_NO_SCROLL = BTN_Y - 8;
constexpr int16_t VIEW_BOTTOM_SCROLL = VIEW_BOTTOM_NO_SCROLL - SCROLL_ROW_H - SCROLL_ROW_GAP;
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;
int16_t viewBottom = scrollable ? VIEW_BOTTOM_SCROLL : VIEW_BOTTOM_NO_SCROLL;
int16_t maxScroll = totalH - viewBottom;
if (maxScroll < 0) maxScroll = 0;
int16_t scrollY = 0;
constexpr int16_t SCROLL_STEP = 48;
Rect okBtn = {(int16_t)(BOX_X + 10), BTN_Y, (int16_t)(BOX_W - 20), BTN_H};
int16_t scrollRowY = VIEW_BOTTOM_SCROLL + SCROLL_ROW_GAP;
Rect upBtn = {(int16_t)(BOX_X + BOX_W / 2 - 64), scrollRowY, 60, SCROLL_ROW_H};
Rect downBtn = {(int16_t)(BOX_X + BOX_W / 2 + 4), scrollRowY, 60, SCROLL_ROW_H};
MenuStars::reset();
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.drawRoundRect(BOX_X, BOX_Y, BOX_W, BOX_H, 6, accentColor);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(accentColor, TFT_BLACK);
tft.setTextSize(2);
if (tft.textWidth(title) > (BOX_W - 20)) {
tft.setTextSize(1);
}
tft.drawString(title, BOX_X + BOX_W / 2, TITLE_Y);
tft.setTextSize(1);
tft.setTextDatum(TL_DATUM);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
layoutWrapped(tft, BOX_X + 10, VIEW_TOP, TEXT_MAX_WIDTH, LINE_H, body, scrollY, VIEW_TOP, viewBottom, true);
drawButton(tft, okBtn, buttonLabel);
if (scrollable) {
drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v");
}
};
redraw();
while (true) {
TouchInput::Point tap;
if (TouchInput::wasTapped(tap)) {
if (okBtn.contains(tap.x, tap.y)) return;
if (scrollable && upBtn.contains(tap.x, tap.y) && scrollY > 0) {
scrollY -= SCROLL_STEP;
if (scrollY < 0) scrollY = 0;
redraw();
} else if (scrollable && downBtn.contains(tap.x, tap.y) && scrollY < maxScroll) {
scrollY += SCROLL_STEP;
if (scrollY > maxScroll) scrollY = maxScroll;
redraw();
}
}
MenuStars::update(tft);
delay(20);
}
}
// Fortschrittsanzeige waehrend OtaUpdate::performUpdate() laeuft - // Fortschrittsanzeige waehrend OtaUpdate::performUpdate() laeuft -
// gleiches Namespace-globale-Zeiger-Prinzip wie progressTft oben (siehe // gleiches Namespace-globale-Zeiger-Prinzip wie progressTft oben (siehe
// Settings-Backup-Fortschrittspunkte), da OtaUpdate::performUpdate() // Settings-Backup-Fortschrittspunkte), da OtaUpdate::performUpdate()
@@ -322,12 +422,25 @@ namespace {
void drawOtaProgress(uint8_t percent) { void drawOtaProgress(uint8_t percent) {
if (!otaProgressTft) return; if (!otaProgressTft) return;
TFT_eSPI& t = *otaProgressTft; TFT_eSPI& t = *otaProgressTft;
String label = String(I18n::t(StringId::OTA_INSTALLING_PREFIX)) + String(percent) + "%"; // Zwei Zeilen statt einer langen: der Praefix-Text
t.fillRect(0, (int16_t)(Config::SCREEN_HEIGHT / 2 - 14), Config::SCREEN_WIDTH, 28, TFT_BLACK); // (OTA_INSTALLING_PREFIX) ist in manchen Sprachen zu lang, um
// zusammen mit der Prozentzahl auf einer Zeile bei lesbarer
// Schriftgroesse zu passen (lief vorher links/rechts ueber den
// Bildschirmrand hinaus). Jetzt: Beschriftung klein oben, Prozent
// gross darunter.
constexpr int16_t BAND_H = 60;
int16_t cy = Config::SCREEN_HEIGHT / 2;
t.fillRect(0, (int16_t)(cy - BAND_H / 2), Config::SCREEN_WIDTH, BAND_H, TFT_BLACK);
t.setTextDatum(MC_DATUM); t.setTextDatum(MC_DATUM);
t.setTextColor(TFT_GREEN, TFT_BLACK); t.setTextColor(TFT_GREEN, TFT_BLACK);
t.setTextSize(2); t.setTextSize(1);
t.drawString(label, Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2); t.drawString(I18n::t(StringId::OTA_INSTALLING_PREFIX), Config::SCREEN_WIDTH / 2, (int16_t)(cy - 14));
String percentLabel = String(percent) + "%";
t.setTextSize(3);
t.drawString(percentLabel, Config::SCREEN_WIDTH / 2, (int16_t)(cy + 12));
t.setTextSize(1); t.setTextSize(1);
t.setTextDatum(TL_DATUM); t.setTextDatum(TL_DATUM);
} }
@@ -338,7 +451,12 @@ namespace {
// ist keine destruktive Aktion wie Werksreset, verdient aber trotzdem // ist keine destruktive Aktion wie Werksreset, verdient aber trotzdem
// eine bewusste Bestaetigung, da WLAN/Strom waehrend des Vorgangs nicht // eine bewusste Bestaetigung, da WLAN/Strom waehrend des Vorgangs nicht
// unterbrochen werden sollten), danach Fortschrittsanzeige waehrend // unterbrochen werden sollten), danach Fortschrittsanzeige waehrend
// Download+Flash. Startet das Geraet bei Erfolg selbst neu. // Download+Flash. WICHTIG: startet NICHT mehr automatisch neu und
// springt bei einem Fehler auch nicht einfach stillschweigend zurueck
// ins Menue - jedes Ergebnis (Erfolg wie Fehler) wird ueber infoScreen()
// als eigener, stehenbleibender Screen angezeigt, den der Nutzer aktiv
// bestaetigen muss. Bei Erfolg startet erst ein expliziter Tap auf
// "Jetzt neu starten" tatsaechlich neu.
void runOtaUpdateScreen(TFT_eSPI& tft) { void runOtaUpdateScreen(TFT_eSPI& tft) {
MenuStars::reset(); MenuStars::reset();
tft.fillScreen(TFT_BLACK); tft.fillScreen(TFT_BLACK);
@@ -350,11 +468,12 @@ namespace {
OtaUpdate::CheckInfo info = OtaUpdate::checkForUpdate(); OtaUpdate::CheckInfo info = OtaUpdate::checkForUpdate();
if (info.result == OtaUpdate::CheckResult::Error) { if (info.result == OtaUpdate::CheckResult::Error) {
showBriefMessage(tft, I18n::t(StringId::OTA_CHECK_FAILED), TFT_RED); infoScreen(tft, I18n::t(StringId::OTA_CHECK_FAILED), "", TFT_RED, I18n::t(StringId::OK));
return; return;
} }
if (info.result == OtaUpdate::CheckResult::UpToDate) { if (info.result == OtaUpdate::CheckResult::UpToDate) {
showBriefMessage(tft, String(I18n::t(StringId::OTA_UP_TO_DATE_PREFIX)) + info.latestVersion, TFT_GREEN); String upToDateTitle = String(I18n::t(StringId::OTA_UP_TO_DATE_PREFIX)) + info.latestVersion;
infoScreen(tft, upToDateTitle, "", TFT_GREEN, I18n::t(StringId::OK));
return; return;
} }
@@ -369,15 +488,20 @@ namespace {
otaProgressTft = nullptr; otaProgressTft = nullptr;
if (ok) { if (ok) {
tft.fillScreen(TFT_BLACK); // Bewusst KEIN automatischer Neustart mehr - der Nutzer
tft.setTextDatum(MC_DATUM); // bestaetigt aktiv per Button, damit er den Erfolg auch wirklich
tft.setTextColor(TFT_GREEN, TFT_BLACK); // mitbekommt (vorher lief die Meldung nur 1,5s an, dann
tft.drawString(I18n::t(StringId::OTA_UPDATE_SUCCESS), Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2); // Neustart - leicht zu verpassen).
tft.setTextDatum(TL_DATUM); infoScreen(tft, I18n::t(StringId::OTA_UPDATE_SUCCESS), I18n::t(StringId::OTA_SUCCESS_BODY),
delay(1500); TFT_GREEN, I18n::t(StringId::OTA_RESTART_BUTTON));
ESP.restart(); ESP.restart();
} else { } else {
showBriefMessage(tft, I18n::t(StringId::OTA_UPDATE_FAILED), TFT_RED); // Bewusst ein stehenbleibender Info-Screen statt der alten
// showBriefMessage() (1,2s, dann automatisch zurueck ins Menue)
// - ein fehlgeschlagenes Firmware-Update ist keine
// Nebensaechlichkeit, die man verpassen darf.
infoScreen(tft, I18n::t(StringId::OTA_UPDATE_FAILED), I18n::t(StringId::OTA_FAILED_BODY),
TFT_RED, I18n::t(StringId::OK));
} }
} }

View File

@@ -68,21 +68,17 @@ 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, "CYD-flightradar.bin") == 0) { if (strcmp(name, "firmware.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 CYD-flightradar.bin-Anhang if (!info.downloadUrl[0]) return info; // Release ohne firmware.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;
@@ -94,6 +90,16 @@ bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) {
client.setInsecure(); client.setInsecure();
client.setTimeout(15000); client.setTimeout(15000);
// WICHTIG: GitHubs "browser_download_url" fuer Release-Assets ist KEIN
// direkter Download-Link, sondern liefert erst ein HTTP 301/302-Redirect
// auf eine signierte objects.githubusercontent.com-URL. HTTPUpdate folgt
// Redirects standardmaessig NICHT (HTTPC_DISABLE_FOLLOW_REDIRECTS ist der
// Default) - ohne diese Zeile bricht der Download mit HTTP_UPDATE_FAILED
// ab, weil statt der .bin-Datei nur die Redirect-Antwort ankommt. Siehe
// z.B. espressif/arduino-esp32#3020. HTTPC_STRICT_FOLLOW_REDIRECTS
// reicht, da wir nur GET verwenden.
httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
// Wir zeigen nach erfolgreicher Installation selbst noch eine kurze // Wir zeigen nach erfolgreicher Installation selbst noch eine kurze
// Erfolgsmeldung an, bevor das Geraet neu startet - siehe // Erfolgsmeldung an, bevor das Geraet neu startet - siehe
// menu_screen.cpp::runOtaUpdateScreen(). // menu_screen.cpp::runOtaUpdateScreen().

View File

@@ -18,20 +18,19 @@ 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 CYD-flightradar.bin des Releases - // Direkter Download-Link zur firmware.bin des Releases - nur
// nur gesetzt, wenn result == UpdateAvailable. // 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 // Config::APP_VERSION und sucht im Release den Anhang "firmware.bin".
// "CYD-flightradar.bin". CheckResult::Error bei jedem Fehler unterwegs // CheckResult::Error bei jedem Fehler unterwegs (kein WLAN, Zeitueber-
// (kein WLAN, Zeitueberschreitung, unerwartetes JSON, kein // schreitung, unerwartetes JSON, kein firmware.bin im Release).
// CYD-flightradar.bin im Release).
CheckInfo checkForUpdate(); CheckInfo checkForUpdate();
// Laedt die CYD-flightradar.bin von 'url' herunter und flasht sie (ueber die // Laedt die firmware.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