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:
54
CLAUDE.md
54
CLAUDE.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
10
src/i18n.h
10
src/i18n.h
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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().
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user