Files
Crosspoint/src/activities/settings/OtaUpdateActivity.cpp
T
Jeremy Klein 151bf1dae4 fix: silent-restart on exit from KOReader auth and OTA update (#2036)
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
2026-05-18 21:04:11 -04:00

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();
}
}