feat(update): SD-card firmware update + X3 bootloader compatibility (#1786)
This commit is contained in:
@@ -50,8 +50,12 @@ struct FootnoteResult {
|
||||
std::string href;
|
||||
};
|
||||
|
||||
struct FilePathResult {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
using ResultVariant = std::variant<std::monostate, WifiResult, KeyboardResult, MenuResult, ChapterResult, PercentResult,
|
||||
PageResult, SyncResult, NetworkModeResult, FootnoteResult>;
|
||||
PageResult, SyncResult, NetworkModeResult, FootnoteResult, FilePathResult>;
|
||||
|
||||
struct ActivityResult {
|
||||
bool isCancelled = false;
|
||||
|
||||
@@ -91,9 +91,14 @@ void FileBrowserActivity::loadFiles() {
|
||||
files.emplace_back(std::string(name) + "/");
|
||||
} else {
|
||||
std::string_view filename{name};
|
||||
if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename) ||
|
||||
FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename) ||
|
||||
FsHelpers::hasBmpExtension(filename)) {
|
||||
if (mode == Mode::PickFirmware) {
|
||||
// Firmware picker: only show .bin files.
|
||||
if (FsHelpers::checkFileExtension(filename, ".bin")) {
|
||||
files.emplace_back(filename);
|
||||
}
|
||||
} else if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename) ||
|
||||
FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename) ||
|
||||
FsHelpers::hasBmpExtension(filename)) {
|
||||
files.emplace_back(filename);
|
||||
}
|
||||
}
|
||||
@@ -106,6 +111,10 @@ void FileBrowserActivity::onEnter() {
|
||||
|
||||
selectorIndex = 0;
|
||||
|
||||
// If Confirm was held while this activity opened (typical when launched from a menu), ignore
|
||||
// its release — otherwise we'd immediately auto-open whatever is at index 0.
|
||||
lockNextConfirmRelease = mappedInput.isPressed(MappedInputManager::Button::Confirm);
|
||||
|
||||
auto root = Storage.open(basepath.c_str());
|
||||
if (!root) {
|
||||
basepath = "/";
|
||||
@@ -141,11 +150,10 @@ void FileBrowserActivity::clearFileMetadata(const std::string& fullPath) {
|
||||
}
|
||||
|
||||
void FileBrowserActivity::loop() {
|
||||
// Long press BACK (1s+) goes to root folder
|
||||
// but Long press BACK (1s+) from ReaderActivity sends us here with the MappedInput already set.
|
||||
// So ignore it the first time.
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS &&
|
||||
basepath != "/" && !lockLongPressBack) {
|
||||
// Long press BACK (1s+) goes to root folder (Books mode only).
|
||||
// In firmware-pick mode we keep navigation simple: short Back = up dir / cancel.
|
||||
if (mode == Mode::Books && mappedInput.isPressed(MappedInputManager::Button::Back) &&
|
||||
mappedInput.getHeldTime() >= GO_HOME_MS && basepath != "/" && !lockLongPressBack) {
|
||||
basepath = "/";
|
||||
loadFiles();
|
||||
selectorIndex = 0;
|
||||
@@ -162,12 +170,27 @@ void FileBrowserActivity::loop() {
|
||||
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, pathReserved);
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (lockNextConfirmRelease) {
|
||||
lockNextConfirmRelease = false;
|
||||
return;
|
||||
}
|
||||
if (files.empty()) return;
|
||||
|
||||
const std::string& entry = files[selectorIndex];
|
||||
bool isDirectory = (entry.back() == '/');
|
||||
|
||||
if (mappedInput.getHeldTime() >= GO_HOME_MS && !isDirectory) {
|
||||
// Firmware picker: select file -> return path; navigate into directories normally.
|
||||
if (mode == Mode::PickFirmware && !isDirectory) {
|
||||
std::string cleanBasePath = basepath;
|
||||
if (cleanBasePath.back() != '/') cleanBasePath += "/";
|
||||
ActivityResult res{FilePathResult{cleanBasePath + entry}};
|
||||
res.isCancelled = false;
|
||||
setResult(std::move(res));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == Mode::Books && mappedInput.getHeldTime() >= GO_HOME_MS && !isDirectory) {
|
||||
// --- LONG PRESS ACTION: DELETE FILE ---
|
||||
std::string cleanBasePath = basepath;
|
||||
if (cleanBasePath.back() != '/') cleanBasePath += "/";
|
||||
@@ -231,6 +254,12 @@ void FileBrowserActivity::loop() {
|
||||
selectorIndex = findEntry(dirName);
|
||||
|
||||
requestUpdate();
|
||||
} else if (mode == Mode::PickFirmware) {
|
||||
// Firmware picker at root: cancel back to caller instead of going home.
|
||||
ActivityResult res;
|
||||
res.isCancelled = true;
|
||||
setResult(std::move(res));
|
||||
finish();
|
||||
} else {
|
||||
onGoHome();
|
||||
}
|
||||
@@ -286,7 +315,10 @@ void FileBrowserActivity::render(RenderLock&&) {
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
|
||||
std::string folderName = (basepath == "/") ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1);
|
||||
std::string folderName =
|
||||
(mode == Mode::PickFirmware)
|
||||
? std::string(tr(STR_SELECT_FIRMWARE_FILE))
|
||||
: ((basepath == "/") ? std::string(tr(STR_SD_CARD)) : basepath.substr(basepath.rfind('/') + 1));
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName.c_str());
|
||||
|
||||
const int pathLineHeight = renderer.getLineHeight(SMALL_FONT_ID);
|
||||
@@ -295,7 +327,8 @@ void FileBrowserActivity::render(RenderLock&&) {
|
||||
const int contentHeight =
|
||||
pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing - pathReserved;
|
||||
if (files.empty()) {
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_FILES_FOUND));
|
||||
const char* emptyMsg = (mode == Mode::PickFirmware) ? tr(STR_NO_BIN_FILES) : tr(STR_NO_FILES_FOUND);
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, emptyMsg);
|
||||
} else {
|
||||
GUI.drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex,
|
||||
@@ -332,9 +365,13 @@ void FileBrowserActivity::render(RenderLock&&) {
|
||||
}
|
||||
|
||||
// Help text
|
||||
const auto labels =
|
||||
mappedInput.mapLabels(basepath == "/" ? tr(STR_HOME) : tr(STR_BACK), files.empty() ? "" : tr(STR_OPEN),
|
||||
files.empty() ? "" : tr(STR_DIR_UP), files.empty() ? "" : tr(STR_DIR_DOWN));
|
||||
const char* backLabel = (basepath == "/") ? (mode == Mode::PickFirmware ? tr(STR_BACK) : tr(STR_HOME)) : tr(STR_BACK);
|
||||
// In PickFirmware mode, Confirm on a .bin returns the path to the caller (not "open"); show
|
||||
// STR_SELECT instead. Directories in the same picker still descend, so keep STR_OPEN there.
|
||||
const bool selectingFirmwareFile = mode == Mode::PickFirmware && !files.empty() && files[selectorIndex].back() != '/';
|
||||
const char* confirmLabel = files.empty() ? "" : (selectingFirmwareFile ? tr(STR_SELECT) : tr(STR_OPEN));
|
||||
const auto labels = mappedInput.mapLabels(backLabel, confirmLabel, files.empty() ? "" : tr(STR_DIR_UP),
|
||||
files.empty() ? "" : tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
class FileBrowserActivity final : public Activity {
|
||||
public:
|
||||
// Books = standard reader browser; PickFirmware = filter to .bin only and return path via ActivityResult.
|
||||
enum class Mode { Books, PickFirmware };
|
||||
|
||||
private:
|
||||
// Deletion
|
||||
void clearFileMetadata(const std::string& fullPath);
|
||||
@@ -18,6 +22,11 @@ class FileBrowserActivity final : public Activity {
|
||||
size_t selectorIndex = 0;
|
||||
|
||||
bool lockLongPressBack = false;
|
||||
// True when this activity was entered while Confirm was already held; we must swallow the next
|
||||
// release so we don't immediately auto-open the first entry.
|
||||
bool lockNextConfirmRelease = false;
|
||||
|
||||
Mode mode = Mode::Books;
|
||||
|
||||
// Files state
|
||||
std::string basepath = "/";
|
||||
@@ -28,8 +37,11 @@ class FileBrowserActivity final : public Activity {
|
||||
size_t findEntry(const std::string& name) const;
|
||||
|
||||
public:
|
||||
explicit FileBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialPath = "/")
|
||||
: Activity("FileBrowser", renderer, mappedInput), basepath(initialPath.empty() ? "/" : std::move(initialPath)) {}
|
||||
explicit FileBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialPath = "/",
|
||||
Mode mode = Mode::Books)
|
||||
: Activity("FileBrowser", renderer, mappedInput),
|
||||
mode(mode),
|
||||
basepath(initialPath.empty() ? "/" : std::move(initialPath)) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
#include "SdFirmwareUpdateActivity.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "activities/home/FileBrowserActivity.h"
|
||||
#include "activities/util/ConfirmationActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "network/FirmwareFlasher.h"
|
||||
|
||||
void SdFirmwareUpdateActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
// Build-identity marker — confirms which firmware build owns the SD update flow.
|
||||
LOG_INF("FW", "SdFirmwareUpdateActivity build=%s %s recovery=%d", __DATE__, __TIME__, recoveryMode ? 1 : 0);
|
||||
state = State::PICKING;
|
||||
launchPicker();
|
||||
}
|
||||
|
||||
void SdFirmwareUpdateActivity::launchPicker() {
|
||||
// Reuse the standard file browser, restricted to .bin files only.
|
||||
startActivityForResult(
|
||||
std::make_unique<FileBrowserActivity>(renderer, mappedInput, "/", FileBrowserActivity::Mode::PickFirmware),
|
||||
[this](const ActivityResult& result) { onPickerResult(result); });
|
||||
}
|
||||
|
||||
void SdFirmwareUpdateActivity::onPickerResult(const ActivityResult& result) {
|
||||
if (result.isCancelled) {
|
||||
if (recoveryMode) {
|
||||
// Recovery mode: re-launch the picker so the user cannot escape into a half-initialised UI.
|
||||
launchPicker();
|
||||
return;
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* path = std::get_if<FilePathResult>(&result.data);
|
||||
if (!path) {
|
||||
LOG_ERR("FW", "Picker returned no path");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
firmwarePath = path->path;
|
||||
LOG_DBG("FW", "Selected: %s", firmwarePath.c_str());
|
||||
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = State::VALIDATING;
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
|
||||
if (!validateFirmware()) {
|
||||
RenderLock lock(*this);
|
||||
state = State::FAILED;
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
promptConfirmation();
|
||||
}
|
||||
|
||||
bool SdFirmwareUpdateActivity::validateFirmware() {
|
||||
HalFile file;
|
||||
if (!Storage.openFileForRead("FW", firmwarePath.c_str(), file) || !file) {
|
||||
errorMessage = tr(STR_FIRMWARE_FILE_OPEN_FAILED);
|
||||
return false;
|
||||
}
|
||||
firmwareSize = file.fileSize();
|
||||
file.close();
|
||||
|
||||
// Resolve the next-update partition directly via the OTA API. Previously this
|
||||
// probed via Update.begin(firmwareSize)/Update.abort() to learn the partition
|
||||
// size, which had the side effect of erasing partition state and was wasted
|
||||
// work since we only need the size bound for validation here.
|
||||
const esp_partition_t* dest = esp_ota_get_next_update_partition(nullptr);
|
||||
if (!dest) {
|
||||
LOG_ERR("FW", "no next-update partition available");
|
||||
errorMessage = tr(STR_INVALID_FIRMWARE);
|
||||
return false;
|
||||
}
|
||||
const size_t partitionLimit = dest->size;
|
||||
if (firmwareSize > partitionLimit) {
|
||||
LOG_ERR("FW", "firmware (%u bytes) exceeds partition (%u bytes)", static_cast<unsigned>(firmwareSize),
|
||||
static_cast<unsigned>(partitionLimit));
|
||||
errorMessage = tr(STR_FIRMWARE_TOO_LARGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Run the same end-to-end integrity check (header / segment table / XOR checksum / SHA256
|
||||
// trailer) that the shared firmware-flasher applies right before raw-writing otadata. This
|
||||
// catches truncated or corrupted .bin files at confirmation time, before the user ever sees
|
||||
// the "Updating…" progress bar.
|
||||
const auto vr = firmware_flash::validateImageFile(firmwarePath.c_str(), partitionLimit);
|
||||
if (vr != firmware_flash::Result::OK) {
|
||||
LOG_ERR("FW", "image validation failed: %s", firmware_flash::resultName(vr));
|
||||
if (vr == firmware_flash::Result::TOO_LARGE) {
|
||||
errorMessage = tr(STR_FIRMWARE_TOO_LARGE);
|
||||
} else if (vr == firmware_flash::Result::TOO_SMALL) {
|
||||
errorMessage = tr(STR_FIRMWARE_TOO_SMALL);
|
||||
} else {
|
||||
errorMessage = tr(STR_INVALID_FIRMWARE);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdFirmwareUpdateActivity::promptConfirmation() {
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = State::CONFIRMING;
|
||||
}
|
||||
// Show "Update firmware?" with the file path as the body line.
|
||||
std::string heading = tr(STR_FIRMWARE_UPDATE_PROMPT);
|
||||
// Use the basename only to keep the body short.
|
||||
std::string body = firmwarePath;
|
||||
const auto pos = body.find_last_of('/');
|
||||
if (pos != std::string::npos) body = body.substr(pos + 1);
|
||||
|
||||
startActivityForResult(std::make_unique<ConfirmationActivity>(renderer, mappedInput, heading, body),
|
||||
[this](const ActivityResult& result) { onConfirmationResult(result); });
|
||||
}
|
||||
|
||||
void SdFirmwareUpdateActivity::onConfirmationResult(const ActivityResult& result) {
|
||||
if (result.isCancelled) {
|
||||
if (recoveryMode) {
|
||||
// Go back to the picker rather than exiting recovery.
|
||||
launchPicker();
|
||||
return;
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = State::UPDATING;
|
||||
writtenBytes = 0;
|
||||
lastRenderedPercent = 101;
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
performUpdate();
|
||||
}
|
||||
|
||||
void SdFirmwareUpdateActivity::performUpdate() {
|
||||
LOG_INF("FW", "SD update: %s (%u bytes)", firmwarePath.c_str(), static_cast<unsigned>(firmwareSize));
|
||||
|
||||
auto progressCb = +[](size_t written, size_t total, void* ctx) {
|
||||
auto* self = static_cast<SdFirmwareUpdateActivity*>(ctx);
|
||||
self->writtenBytes = written;
|
||||
self->firmwareSize = total;
|
||||
// immediate=true: wake the render task directly. We're in a tight sync
|
||||
// loop so the main loop won't drain the requestedUpdate flag for us.
|
||||
self->requestUpdate(true);
|
||||
};
|
||||
|
||||
// Re-validate at flash time (TOCTOU): SD is removable, so don't trust the
|
||||
// pre-confirmation pass. The alreadyValidated parameter on the API stays
|
||||
// for callers (e.g. an OTA staging path) where the same byte stream was
|
||||
// just hashed and there's no removable-media gap.
|
||||
const auto result = firmware_flash::flashFromSdPath(firmwarePath.c_str(), progressCb, this);
|
||||
if (result != firmware_flash::Result::OK) {
|
||||
LOG_ERR("FW", "flash failed: %s", firmware_flash::resultName(result));
|
||||
errorMessage = tr(STR_FIRMWARE_WRITE_FAILED);
|
||||
RenderLock lock(*this);
|
||||
state = State::FAILED;
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INF("FW", "SD firmware update complete, restarting");
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = State::SUCCESS;
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
delay(1500);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
void SdFirmwareUpdateActivity::loop() {
|
||||
if (state == State::FAILED) {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
if (recoveryMode) {
|
||||
// Go back to picker so user can try a different .bin
|
||||
state = State::PICKING;
|
||||
launchPicker();
|
||||
return;
|
||||
}
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SdFirmwareUpdateActivity::render(RenderLock&&) {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
renderer.clearScreen();
|
||||
|
||||
const char* headerText = recoveryMode ? tr(STR_RECOVERY_MODE) : tr(STR_SD_FIRMWARE_UPDATE);
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, headerText);
|
||||
|
||||
const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
||||
const auto top = (pageHeight - lineHeight) / 2;
|
||||
|
||||
if (state == State::VALIDATING) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_VALIDATING_FIRMWARE));
|
||||
} else if (state == State::UPDATING) {
|
||||
// Throttle redraws to once per percent.
|
||||
const unsigned int pct = firmwareSize > 0 ? static_cast<unsigned int>((writtenBytes * 100) / firmwareSize) : 0;
|
||||
if (pct == lastRenderedPercent) {
|
||||
return;
|
||||
}
|
||||
lastRenderedPercent = pct;
|
||||
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATING), true, EpdFontFamily::BOLD);
|
||||
|
||||
int y = top + lineHeight + metrics.verticalSpacing;
|
||||
GUI.drawProgressBar(
|
||||
renderer,
|
||||
Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
|
||||
static_cast<int>(pct), 100);
|
||||
y += metrics.progressBarHeight + metrics.verticalSpacing;
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y, (std::to_string(pct) + "%").c_str());
|
||||
y += lineHeight + metrics.verticalSpacing;
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF));
|
||||
} else if (state == State::SUCCESS) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top + lineHeight + metrics.verticalSpacing, tr(STR_RESTARTING_HINT));
|
||||
} else if (state == State::FAILED) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD);
|
||||
if (!errorMessage.empty()) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top + lineHeight + metrics.verticalSpacing, errorMessage.c_str());
|
||||
}
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
} else {
|
||||
// PICKING / CONFIRMING: a sub-activity is on top, nothing to draw.
|
||||
if (recoveryMode) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_RECOVERY_MODE_HINT));
|
||||
}
|
||||
}
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "activities/Activity.h"
|
||||
|
||||
/**
|
||||
* SD-card based firmware update activity.
|
||||
*
|
||||
* Flow:
|
||||
* 1) onEnter -> push FileBrowserActivity in PickFirmware mode (only .bin files visible).
|
||||
* 2) On result: validate the .bin (header magic, size fits OTA partition).
|
||||
* 3) Push ConfirmationActivity ("Update firmware?").
|
||||
* 4) On confirm: stream the file into the OTA partition via the Arduino Update API,
|
||||
* drawing a progress bar; on success ESP.restart().
|
||||
*
|
||||
* Used both from Settings -> System -> "SD Card Firmware Update", and as the only
|
||||
* activity launched in boot recovery mode (left side button + power on X3).
|
||||
*/
|
||||
class SdFirmwareUpdateActivity : public Activity {
|
||||
public:
|
||||
enum class State {
|
||||
PICKING,
|
||||
VALIDATING,
|
||||
CONFIRMING,
|
||||
UPDATING,
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
};
|
||||
|
||||
explicit SdFirmwareUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool recoveryMode = false)
|
||||
: Activity("SdFirmwareUpdate", renderer, mappedInput), recoveryMode(recoveryMode) {}
|
||||
|
||||
void onEnter() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
bool preventAutoSleep() override { return state == State::UPDATING || state == State::VALIDATING; }
|
||||
bool skipLoopDelay() override { return state == State::UPDATING; }
|
||||
|
||||
private:
|
||||
State state = State::PICKING;
|
||||
bool recoveryMode = false;
|
||||
|
||||
std::string firmwarePath;
|
||||
size_t firmwareSize = 0;
|
||||
size_t writtenBytes = 0;
|
||||
unsigned int lastRenderedPercent = 101;
|
||||
std::string errorMessage;
|
||||
|
||||
void launchPicker();
|
||||
void onPickerResult(const ActivityResult& result);
|
||||
bool validateFirmware();
|
||||
void promptConfirmation();
|
||||
void onConfirmationResult(const ActivityResult& result);
|
||||
void performUpdate();
|
||||
};
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "MappedInputManager.h"
|
||||
#include "OpdsServerListActivity.h"
|
||||
#include "OtaUpdateActivity.h"
|
||||
#include "SdFirmwareUpdateActivity.h"
|
||||
#include "SettingsList.h"
|
||||
#include "StatusBarSettingsActivity.h"
|
||||
#include "activities/network/WifiSelectionActivity.h"
|
||||
@@ -51,6 +52,7 @@ void SettingsActivity::onEnter() {
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_SD_FIRMWARE_UPDATE, SettingAction::SdFirmwareUpdate));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language));
|
||||
readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
|
||||
|
||||
@@ -189,6 +191,9 @@ void SettingsActivity::toggleCurrentSetting() {
|
||||
case SettingAction::CheckForUpdates:
|
||||
startActivityForResult(std::make_unique<OtaUpdateActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::SdFirmwareUpdate:
|
||||
startActivityForResult(std::make_unique<SdFirmwareUpdateActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::Language:
|
||||
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
|
||||
@@ -20,6 +20,7 @@ enum class SettingAction {
|
||||
Network,
|
||||
ClearCache,
|
||||
CheckForUpdates,
|
||||
SdFirmwareUpdate,
|
||||
Language,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user