PR #1908 silent-restarts on exit from any wifi-using activity to defuse LWIP/mbedTLS heap fragmentation, but two of the wifi-using paths slipped through that audit: KOReaderAuthActivity (Settings -> KOReader sync -> Authenticate) OtaUpdateActivity (Settings -> Check for update, back-out paths) Both used WiFi.disconnect + WiFi.mode(WIFI_OFF) on exit and returned control to Settings, leaving ~50KB of contiguous heap stranded for the rest of the session. Mirror the FontDownloadActivity pattern: if WiFi was activated, disconnect and silentRestart. OTA's success path is unchanged: SHUTTING_DOWN already calls plain ESP.restart() so the new firmware boots normally; only the cancel/fail/no-update back-out paths now go through silentRestart(). Did you use AI tools to help write this code? partial
212 lines
7.0 KiB
C++
212 lines
7.0 KiB
C++
#include "OtaUpdateActivity.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <I18n.h>
|
|
#include <WiFi.h>
|
|
|
|
#include "MappedInputManager.h"
|
|
#include "SilentRestart.h"
|
|
#include "activities/network/WifiSelectionActivity.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
#include "network/OtaUpdater.h"
|
|
|
|
void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
|
|
if (!success) {
|
|
LOG_ERR("OTA", "WiFi connection failed, exiting");
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
LOG_DBG("OTA", "WiFi connected, checking for update");
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
state = CHECKING_FOR_UPDATE;
|
|
}
|
|
requestUpdateAndWait();
|
|
|
|
const auto res = updater.checkForUpdate();
|
|
if (res != OtaUpdater::OK) {
|
|
LOG_DBG("OTA", "Update check failed: %d", res);
|
|
{
|
|
RenderLock lock(*this);
|
|
state = FAILED;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!updater.isUpdateNewer()) {
|
|
LOG_DBG("OTA", "No new update available");
|
|
{
|
|
RenderLock lock(*this);
|
|
state = NO_UPDATE;
|
|
}
|
|
return;
|
|
}
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
state = WAITING_CONFIRMATION;
|
|
}
|
|
}
|
|
|
|
void OtaUpdateActivity::onEnter() {
|
|
Activity::onEnter();
|
|
|
|
// Turn on WiFi immediately
|
|
LOG_DBG("OTA", "Turning on WiFi...");
|
|
WiFi.mode(WIFI_STA);
|
|
|
|
// Launch WiFi selection subactivity
|
|
LOG_DBG("OTA", "Launching WifiSelectionActivity...");
|
|
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
|
[this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); });
|
|
}
|
|
|
|
void OtaUpdateActivity::onExit() {
|
|
Activity::onExit();
|
|
|
|
// Success path reboots via the SHUTTING_DOWN state's plain ESP.restart()
|
|
// (loop() above) so the new firmware boots normally. Back-out paths land
|
|
// here with wifi still active; silent-restart to free the LWIP/mbedTLS
|
|
// fragmentation, same as the other wifi activities.
|
|
if (WiFi.getMode() != WIFI_MODE_NULL) {
|
|
WiFi.disconnect(false);
|
|
delay(30);
|
|
silentRestart();
|
|
}
|
|
}
|
|
|
|
void OtaUpdateActivity::render(RenderLock&&) {
|
|
const auto& metrics = UITheme::getInstance().getMetrics();
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
const auto pageHeight = renderer.getScreenHeight();
|
|
|
|
renderer.clearScreen();
|
|
|
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_UPDATE));
|
|
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
|
const auto top = (pageHeight - height) / 2;
|
|
|
|
float updaterProgress = 0;
|
|
if (state == UPDATE_IN_PROGRESS) {
|
|
LOG_DBG("OTA", "Update progress: %d / %d", updater.getProcessedSize(), updater.getTotalSize());
|
|
updaterProgress = static_cast<float>(updater.getProcessedSize()) / static_cast<float>(updater.getTotalSize());
|
|
// Only update every 2% at the most
|
|
if (static_cast<int>(updaterProgress * 50) == lastUpdaterPercentage / 2) {
|
|
return;
|
|
}
|
|
lastUpdaterPercentage = static_cast<int>(updaterProgress * 100);
|
|
}
|
|
|
|
if (state == CHECKING_FOR_UPDATE) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_CHECKING_UPDATE));
|
|
} else if (state == WAITING_CONFIRMATION) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NEW_UPDATE), true, EpdFontFamily::BOLD);
|
|
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height + metrics.verticalSpacing,
|
|
(std::string(tr(STR_CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str());
|
|
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height * 2 + metrics.verticalSpacing * 2,
|
|
(std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str());
|
|
|
|
const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_UPDATE), "", "");
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
} else if (state == UPDATE_IN_PROGRESS) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATING));
|
|
|
|
int y = top + height + metrics.verticalSpacing;
|
|
GUI.drawProgressBar(
|
|
renderer,
|
|
Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
|
|
static_cast<int>(updaterProgress * 100), 100);
|
|
|
|
y += metrics.progressBarHeight + metrics.verticalSpacing;
|
|
// Percent label is drawn by BaseTheme::drawProgressBar; this slot is left intentionally empty
|
|
// so the bytes line below stays at the same Y it was at when the activity drew its own percent.
|
|
y += height + metrics.verticalSpacing;
|
|
renderer.drawCenteredText(
|
|
UI_10_FONT_ID, y,
|
|
(std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str());
|
|
} else if (state == NO_UPDATE) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_UPDATE), true, EpdFontFamily::BOLD);
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
} else if (state == FAILED) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD);
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
} else if (state == FINISHED) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD);
|
|
renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, tr(STR_POWER_ON_HINT));
|
|
}
|
|
|
|
renderer.displayBuffer();
|
|
}
|
|
|
|
void OtaUpdateActivity::loop() {
|
|
if (state == WAITING_CONFIRMATION) {
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
LOG_DBG("OTA", "New update available, starting download...");
|
|
{
|
|
RenderLock lock(*this);
|
|
state = UPDATE_IN_PROGRESS;
|
|
}
|
|
requestUpdateAndWait();
|
|
const auto res = updater.installUpdate(
|
|
[](void* ctx) {
|
|
// immediate=true notifies the render task directly. The default deferred path only
|
|
// sets a flag consumed at the end of ActivityManager::loop(), which never runs while
|
|
// installUpdate() blocks this task.
|
|
static_cast<OtaUpdateActivity*>(ctx)->requestUpdate(true);
|
|
},
|
|
this);
|
|
|
|
if (res != OtaUpdater::OK) {
|
|
LOG_DBG("OTA", "Update failed: %d", res);
|
|
{
|
|
RenderLock lock(*this);
|
|
state = FAILED;
|
|
}
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
state = FINISHED;
|
|
}
|
|
requestUpdateAndWait();
|
|
// Hold the completion screen briefly so the user sees it, then restart.
|
|
delay(3000);
|
|
{
|
|
RenderLock lock(*this);
|
|
state = SHUTTING_DOWN;
|
|
}
|
|
}
|
|
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
finish();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (state == FAILED) {
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
finish();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (state == NO_UPDATE) {
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
finish();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (state == SHUTTING_DOWN) {
|
|
ESP.restart();
|
|
}
|
|
}
|