Merge pull request #250 from jpirnay/chore-font
refactor: Rework font download
This commit is contained in:
@@ -34,11 +34,16 @@ void FontDownloadActivity::onEnter() {
|
||||
void FontDownloadActivity::onExit() {
|
||||
Activity::onExit();
|
||||
|
||||
// Always silentRestart on exit, regardless of WiFi state. Even if a deep
|
||||
// error path turned WiFi off, we still did expensive network/TLS work and
|
||||
// the heap is fragmented past the point a normal session can recover (see
|
||||
// [project-font-download-heap-stash]). A reboot here gives the next
|
||||
// activity a pristine heap.
|
||||
if (WiFi.getMode() != WIFI_MODE_NULL) {
|
||||
WiFi.disconnect(false);
|
||||
delay(30);
|
||||
silentRestart();
|
||||
}
|
||||
silentRestart();
|
||||
}
|
||||
|
||||
void FontDownloadActivity::onWifiSelectionComplete(const bool success) {
|
||||
@@ -72,6 +77,10 @@ void FontDownloadActivity::onWifiSelectionComplete(const bool success) {
|
||||
bool FontDownloadActivity::fetchAndParseManifest() {
|
||||
static constexpr const char* MANIFEST_TMP = "/fonts_manifest.tmp";
|
||||
|
||||
// Standalone manifest fetch: closes the TLS connection before the JSON
|
||||
// parse so the parser has full heap headroom. The Session is opened later
|
||||
// for the per-file download loop, on a heap that's been slimmed by
|
||||
// trimManifestForDownload().
|
||||
auto result = HttpDownloader::downloadToFile(FONT_MANIFEST_URL, MANIFEST_TMP, nullptr);
|
||||
if (result != HttpDownloader::OK) {
|
||||
LOG_ERR("FONT", "Failed to fetch manifest from %s", FONT_MANIFEST_URL);
|
||||
@@ -100,7 +109,10 @@ bool FontDownloadActivity::fetchAndParseManifest() {
|
||||
}
|
||||
|
||||
int version = doc["version"] | 0;
|
||||
if (version != 1) {
|
||||
// v1 (legacy, no crc32) and v2 (with crc32) are both accepted; crc check is
|
||||
// skipped per-file when the field is absent. See upstream PR #1904 for the
|
||||
// CRC32 design we mirror.
|
||||
if (version != 1 && version != 2) {
|
||||
LOG_ERR("FONT", "Unsupported manifest version: %d", version);
|
||||
errorMessage_ = "Unsupported manifest version";
|
||||
return false;
|
||||
@@ -116,19 +128,32 @@ bool FontDownloadActivity::fetchAndParseManifest() {
|
||||
ManifestFamily family;
|
||||
family.name = fObj["name"] | "";
|
||||
family.description = fObj["description"] | "";
|
||||
// styles[] in the JSON is intentionally ignored — see ManifestFamily.
|
||||
|
||||
for (JsonVariant s : fObj["styles"].as<JsonArray>()) {
|
||||
family.styles.push_back(s.as<std::string>());
|
||||
if (!FontInstaller::isValidFamilyName(family.name.c_str())) {
|
||||
LOG_ERR("FONT", "Manifest entry rejected, invalid family name: %s", family.name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
family.totalSize = 0;
|
||||
bool fileNamesOk = true;
|
||||
for (JsonObject fileObj : fObj["files"].as<JsonArray>()) {
|
||||
ManifestFile file;
|
||||
file.name = fileObj["name"] | "";
|
||||
file.size = fileObj["size"] | 0;
|
||||
if (fileObj["crc32"].is<uint32_t>()) {
|
||||
file.crc32 = fileObj["crc32"].as<uint32_t>();
|
||||
file.hasCrc32 = true;
|
||||
}
|
||||
if (!FontInstaller::isValidFontFileName(file.name.c_str())) {
|
||||
LOG_ERR("FONT", "Manifest entry rejected, invalid file name in %s: %s", family.name.c_str(), file.name.c_str());
|
||||
fileNamesOk = false;
|
||||
break;
|
||||
}
|
||||
family.totalSize += file.size;
|
||||
family.files.push_back(std::move(file));
|
||||
}
|
||||
if (!fileNamesOk) continue;
|
||||
|
||||
family.installed = fontInstaller_.isFamilyInstalled(family.name.c_str());
|
||||
|
||||
@@ -155,6 +180,12 @@ bool FontDownloadActivity::fetchAndParseManifest() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Surface leftover staging from a previously interrupted download so the
|
||||
// UI can offer "Resume" instead of restarting from scratch.
|
||||
char stagingDir[128];
|
||||
FontInstaller::buildStagingDirPath(family.name.c_str(), stagingDir, sizeof(stagingDir));
|
||||
family.hasResumableDownload = Storage.exists(stagingDir);
|
||||
}
|
||||
|
||||
families_.push_back(std::move(family));
|
||||
@@ -164,13 +195,168 @@ bool FontDownloadActivity::fetchAndParseManifest() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Stash/Restore ---
|
||||
//
|
||||
// Persist families_ to a small binary file on SD so we can free the
|
||||
// ~10 KB of scattered std::string allocations it holds. mbedtls's TLS
|
||||
// handshake needs many small allocations from a defragmented heap; with
|
||||
// families_ resident, the heap stays fragmented at ~36 KB largest contiguous
|
||||
// and the handshake fails with -0x2700 / flags=0 (internal alloc failure).
|
||||
//
|
||||
// Format (little-endian):
|
||||
// u32 magic = 'CPFM' (0x4D465043)
|
||||
// u32 count = number of families
|
||||
// for each family:
|
||||
// u8 name_len, name bytes
|
||||
// u8 desc_len, desc bytes
|
||||
// u32 totalSize
|
||||
// u8 flags bit0 installed, bit1 hasUpdate, bit2 hasResumableDownload
|
||||
// u8 file_count
|
||||
// for each file:
|
||||
// u8 name_len, name bytes
|
||||
// u32 size
|
||||
// u32 crc32
|
||||
// u8 hasCrc32
|
||||
|
||||
static constexpr const char* FAMILIES_STASH_PATH = "/fonts_families.bin";
|
||||
static constexpr uint32_t FAMILIES_STASH_MAGIC = 0x4D465043; // 'CPFM'
|
||||
|
||||
namespace {
|
||||
bool writeU8(FsFile& f, uint8_t v) { return f.write(&v, 1) == 1; }
|
||||
bool writeU32(FsFile& f, uint32_t v) {
|
||||
uint8_t buf[4] = {static_cast<uint8_t>(v), static_cast<uint8_t>(v >> 8), static_cast<uint8_t>(v >> 16),
|
||||
static_cast<uint8_t>(v >> 24)};
|
||||
return f.write(buf, 4) == 4;
|
||||
}
|
||||
bool writeStr(FsFile& f, const std::string& s) {
|
||||
if (s.size() > 255) return false;
|
||||
if (!writeU8(f, static_cast<uint8_t>(s.size()))) return false;
|
||||
return s.empty() || f.write(reinterpret_cast<const uint8_t*>(s.data()), s.size()) == s.size();
|
||||
}
|
||||
bool readU8(FsFile& f, uint8_t& v) { return f.read(&v, 1) == 1; }
|
||||
bool readU32(FsFile& f, uint32_t& v) {
|
||||
uint8_t buf[4];
|
||||
if (f.read(buf, 4) != 4) return false;
|
||||
v = static_cast<uint32_t>(buf[0]) | (static_cast<uint32_t>(buf[1]) << 8) | (static_cast<uint32_t>(buf[2]) << 16) |
|
||||
(static_cast<uint32_t>(buf[3]) << 24);
|
||||
return true;
|
||||
}
|
||||
bool readStr(FsFile& f, std::string& s) {
|
||||
uint8_t len = 0;
|
||||
if (!readU8(f, len)) return false;
|
||||
s.resize(len);
|
||||
if (len == 0) return true;
|
||||
return f.read(reinterpret_cast<uint8_t*>(&s[0]), len) == len;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool FontDownloadActivity::stashFamiliesToSd() {
|
||||
Storage.remove(FAMILIES_STASH_PATH);
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("FONT", FAMILIES_STASH_PATH, file)) {
|
||||
LOG_ERR("FONT", "Stash open failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = writeU32(file, FAMILIES_STASH_MAGIC);
|
||||
ok = ok && writeU32(file, static_cast<uint32_t>(families_.size()));
|
||||
for (const auto& fam : families_) {
|
||||
if (!ok) break;
|
||||
ok = ok && writeStr(file, fam.name);
|
||||
ok = ok && writeStr(file, fam.description);
|
||||
ok = ok && writeU32(file, static_cast<uint32_t>(fam.totalSize));
|
||||
uint8_t flags = (fam.installed ? 1 : 0) | (fam.hasUpdate ? 2 : 0) | (fam.hasResumableDownload ? 4 : 0);
|
||||
ok = ok && writeU8(file, flags);
|
||||
ok = ok && writeU8(file, static_cast<uint8_t>(fam.files.size()));
|
||||
for (const auto& fl : fam.files) {
|
||||
ok = ok && writeStr(file, fl.name);
|
||||
ok = ok && writeU32(file, static_cast<uint32_t>(fl.size));
|
||||
ok = ok && writeU32(file, fl.crc32);
|
||||
ok = ok && writeU8(file, fl.hasCrc32 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
file.flush();
|
||||
file.close();
|
||||
if (!ok) {
|
||||
LOG_ERR("FONT", "Stash write failed");
|
||||
Storage.remove(FAMILIES_STASH_PATH);
|
||||
return false;
|
||||
}
|
||||
// Free the in-memory representation now that it's safely on disk.
|
||||
families_.clear();
|
||||
families_.shrink_to_fit();
|
||||
LOG_DBG("FONT", "Stashed families_ to %s and cleared in-memory copy", FAMILIES_STASH_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FontDownloadActivity::restoreFamiliesFromSd() {
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("FONT", FAMILIES_STASH_PATH, file)) {
|
||||
LOG_ERR("FONT", "Stash file missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t magic = 0;
|
||||
uint32_t count = 0;
|
||||
bool ok = readU32(file, magic) && magic == FAMILIES_STASH_MAGIC && readU32(file, count);
|
||||
if (ok) {
|
||||
families_.clear();
|
||||
families_.reserve(count);
|
||||
for (uint32_t i = 0; i < count && ok; i++) {
|
||||
ManifestFamily fam;
|
||||
ok = ok && readStr(file, fam.name);
|
||||
ok = ok && readStr(file, fam.description);
|
||||
uint32_t totalSize = 0;
|
||||
ok = ok && readU32(file, totalSize);
|
||||
fam.totalSize = totalSize;
|
||||
uint8_t flags = 0;
|
||||
ok = ok && readU8(file, flags);
|
||||
fam.installed = (flags & 1) != 0;
|
||||
fam.hasUpdate = (flags & 2) != 0;
|
||||
fam.hasResumableDownload = (flags & 4) != 0;
|
||||
uint8_t fileCount = 0;
|
||||
ok = ok && readU8(file, fileCount);
|
||||
fam.files.reserve(fileCount);
|
||||
for (uint8_t j = 0; j < fileCount && ok; j++) {
|
||||
ManifestFile fl;
|
||||
ok = ok && readStr(file, fl.name);
|
||||
uint32_t fsize = 0, fcrc = 0;
|
||||
ok = ok && readU32(file, fsize);
|
||||
fl.size = fsize;
|
||||
ok = ok && readU32(file, fcrc);
|
||||
fl.crc32 = fcrc;
|
||||
uint8_t hasCrc = 0;
|
||||
ok = ok && readU8(file, hasCrc);
|
||||
fl.hasCrc32 = hasCrc != 0;
|
||||
fam.files.push_back(std::move(fl));
|
||||
}
|
||||
if (ok) families_.push_back(std::move(fam));
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
if (!ok) {
|
||||
LOG_ERR("FONT", "Stash read failed (magic=%08x count=%u)", magic, count);
|
||||
return false;
|
||||
}
|
||||
// Keep the stash file around so a crash mid-download can still recover.
|
||||
// It gets overwritten on next stash and is harmless if stale.
|
||||
LOG_DBG("FONT", "Restored %zu families from stash", families_.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Download ---
|
||||
|
||||
void FontDownloadActivity::downloadAll() {
|
||||
cancelRequested_ = false;
|
||||
// Snapshot indices upfront because downloadFamily() stashes/restores
|
||||
// families_ — indices remain valid as long as we don't sort or splice it.
|
||||
std::vector<int> targetIndices;
|
||||
for (size_t i = 0; i < families_.size(); i++) {
|
||||
if (families_[i].installed) continue;
|
||||
downloadFamily(families_[i]);
|
||||
if (!families_[i].installed) targetIndices.push_back(static_cast<int>(i));
|
||||
}
|
||||
for (int idx : targetIndices) {
|
||||
downloadFamily(idx);
|
||||
if (state_ == ERROR || cancelRequested_) return;
|
||||
}
|
||||
|
||||
@@ -180,9 +366,12 @@ void FontDownloadActivity::downloadAll() {
|
||||
|
||||
void FontDownloadActivity::updateAll() {
|
||||
cancelRequested_ = false;
|
||||
std::vector<int> targetIndices;
|
||||
for (size_t i = 0; i < families_.size(); i++) {
|
||||
if (!families_[i].installed || !families_[i].hasUpdate) continue;
|
||||
downloadFamily(families_[i]);
|
||||
if (families_[i].installed && families_[i].hasUpdate) targetIndices.push_back(static_cast<int>(i));
|
||||
}
|
||||
for (int idx : targetIndices) {
|
||||
downloadFamily(idx);
|
||||
if (state_ == ERROR || cancelRequested_) return;
|
||||
}
|
||||
|
||||
@@ -242,12 +431,26 @@ bool FontDownloadActivity::hasUpdateCandidates() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
void FontDownloadActivity::downloadFamily(int familyIdx) {
|
||||
if (familyIdx < 0 || familyIdx >= static_cast<int>(families_.size())) {
|
||||
LOG_ERR("FONT", "downloadFamily: invalid index %d (size %zu)", familyIdx, families_.size());
|
||||
return;
|
||||
}
|
||||
|
||||
// Snapshot the target family by value, then stash + free families_ so the
|
||||
// ~10 KB of scattered std::string allocations don't fragment the heap
|
||||
// during the TLS handshake. Render-path caches (downloadingFamilyName_,
|
||||
// downloadingFamilyHasResumable_) cover the family-name and Resume-label
|
||||
// accesses that previously read families_ during DOWNLOADING/ERROR.
|
||||
ManifestFamily family = families_[familyIdx];
|
||||
downloadingFamilyName_ = family.name;
|
||||
downloadingFamilyHasResumable_ = family.hasResumableDownload;
|
||||
|
||||
cancelRequested_ = false;
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state_ = DOWNLOADING;
|
||||
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
||||
downloadingFamilyIndex_ = familyIdx;
|
||||
currentFileIndex_ = 0;
|
||||
currentFileTotal_ = family.files.size();
|
||||
fileProgress_ = 0;
|
||||
@@ -255,29 +458,61 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
|
||||
if (!stashFamiliesToSd()) {
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
errorMessage_ = "Failed to stash manifest";
|
||||
return;
|
||||
}
|
||||
|
||||
// Run the actual download with families_ empty (defragmented heap).
|
||||
downloadFamilyImpl(family, familyIdx);
|
||||
|
||||
// Update cached render state from the impl's mutations.
|
||||
downloadingFamilyHasResumable_ = family.hasResumableDownload;
|
||||
|
||||
// Restore families_ regardless of success/error/abort outcome, then merge
|
||||
// back the mutations the impl made on the local family copy. Without the
|
||||
// restored manifest the activity can't render the family list, so a failed
|
||||
// restore is fatal — drop to ERROR rather than continuing with empty state.
|
||||
if (!restoreFamiliesFromSd()) {
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
errorMessage_ = "Failed to restore manifest";
|
||||
return;
|
||||
}
|
||||
if (familyIdx >= 0 && familyIdx < static_cast<int>(families_.size())) {
|
||||
families_[familyIdx].installed = family.installed;
|
||||
families_[familyIdx].hasUpdate = family.hasUpdate;
|
||||
families_[familyIdx].hasResumableDownload = family.hasResumableDownload;
|
||||
}
|
||||
syncSelectedIndexForNewActionCount();
|
||||
}
|
||||
|
||||
void FontDownloadActivity::downloadFamilyImpl(ManifestFamily& family, int familyIdx) {
|
||||
// httpSession_ does the TLS handshake on its first downloadToFile call;
|
||||
// subsequent files reuse the open keep-alive connection. If the server
|
||||
// dropped the connection during the idle gap (user browsing the family
|
||||
// list), the Session layer transparently reinitialises and retries once.
|
||||
char liveDir[128];
|
||||
char stagingDir[128];
|
||||
char backupDir[128];
|
||||
snprintf(liveDir, sizeof(liveDir), "%s/%s", SdCardFontRegistry::FONTS_DIR, family.name.c_str());
|
||||
snprintf(stagingDir, sizeof(stagingDir), "%s/%s__staging", SdCardFontRegistry::FONTS_DIR, family.name.c_str());
|
||||
snprintf(backupDir, sizeof(backupDir), "%s/%s__backup", SdCardFontRegistry::FONTS_DIR, family.name.c_str());
|
||||
FontInstaller::buildStagingDirPath(family.name.c_str(), stagingDir, sizeof(stagingDir));
|
||||
FontInstaller::buildBackupDirPath(family.name.c_str(), backupDir, sizeof(backupDir));
|
||||
|
||||
if (Storage.exists(stagingDir) && !Storage.removeDir(stagingDir)) {
|
||||
LOG_ERR("FONT", "Failed to clean staging dir: %s", stagingDir);
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
||||
errorMessage_ = "Failed to prepare staging area";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Storage.mkdir(stagingDir)) {
|
||||
// Resume-aware staging: if a __staging dir is left over from a previous
|
||||
// interrupted download, keep it so files already on disk can be reused.
|
||||
// Files are individually re-verified below (size + CRC) before being
|
||||
// accepted, so half-written files are caught.
|
||||
if (!Storage.exists(stagingDir) && !Storage.mkdir(stagingDir)) {
|
||||
LOG_ERR("FONT", "Failed to create staging dir: %s", stagingDir);
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
||||
downloadingFamilyIndex_ = familyIdx;
|
||||
errorMessage_ = "Failed to create staging area";
|
||||
return;
|
||||
}
|
||||
@@ -304,6 +539,34 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
char stagedPath[128];
|
||||
snprintf(stagedPath, sizeof(stagedPath), "%s/%s", stagingDir, localFilename.c_str());
|
||||
|
||||
// If this file is already present in staging from a previous run and
|
||||
// matches the manifest, skip the download. CRC32 is checked when the
|
||||
// manifest carries one (v2+); otherwise size + magic-byte check is the
|
||||
// best we can do.
|
||||
if (Storage.exists(stagedPath)) {
|
||||
FsFile f;
|
||||
bool sizeOk = false;
|
||||
if (Storage.openFileForRead("FONT", stagedPath, f)) {
|
||||
sizeOk = (f.fileSize() == file.size);
|
||||
f.close();
|
||||
}
|
||||
bool crcOk = !file.hasCrc32;
|
||||
if (sizeOk && file.hasCrc32) {
|
||||
uint32_t actualCrc = 0;
|
||||
if (FontInstaller::computeFileCrc32(stagedPath, actualCrc)) {
|
||||
crcOk = (actualCrc == file.crc32);
|
||||
}
|
||||
}
|
||||
if (sizeOk && crcOk && fontInstaller_.validateCpfontFile(stagedPath)) {
|
||||
LOG_DBG("FONT", "Resuming: reusing %s", stagedPath);
|
||||
fileProgress_ = file.size;
|
||||
fileTotal_ = file.size;
|
||||
continue;
|
||||
}
|
||||
LOG_DBG("FONT", "Resuming: re-downloading stale %s (sizeOk=%d crcOk=%d)", stagedPath, sizeOk, crcOk);
|
||||
Storage.remove(stagedPath);
|
||||
}
|
||||
|
||||
// Make sure parent directories exist for the file
|
||||
std::string stagedPathStr(stagedPath);
|
||||
size_t lastSlash = stagedPathStr.find_last_of('/');
|
||||
@@ -313,30 +576,33 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
|
||||
std::string url = baseUrl_ + file.name;
|
||||
|
||||
auto result = HttpDownloader::downloadToFile(url, stagedPath, [this](unsigned int downloaded, unsigned int total) {
|
||||
mappedInput.update();
|
||||
fileProgress_ = downloaded;
|
||||
fileTotal_ = total;
|
||||
auto result = HttpDownloader::downloadToFile(
|
||||
httpSession_, url, stagedPath, [this](unsigned int downloaded, unsigned int total) {
|
||||
mappedInput.update();
|
||||
fileProgress_ = downloaded;
|
||||
fileTotal_ = total;
|
||||
|
||||
const unsigned long now = millis();
|
||||
int percent = 0;
|
||||
if (total > 0) {
|
||||
percent = static_cast<int>((static_cast<unsigned long long>(downloaded) * 100ULL + total / 2) / total);
|
||||
}
|
||||
const bool percentChanged = percent != lastProgressPercent_;
|
||||
const bool timeElapsed = lastProgressUpdateMs_ == 0 || now - lastProgressUpdateMs_ > 2000;
|
||||
if ((percentChanged && timeElapsed) || downloaded == total) {
|
||||
requestUpdate(true);
|
||||
lastProgressPercent_ = percent;
|
||||
lastProgressUpdateMs_ = now;
|
||||
}
|
||||
const unsigned long now = millis();
|
||||
int percent = 0;
|
||||
if (total > 0) {
|
||||
percent = static_cast<int>((static_cast<unsigned long long>(downloaded) * 100ULL + total / 2) / total);
|
||||
}
|
||||
const bool percentChanged = percent != lastProgressPercent_;
|
||||
const bool timeElapsed = lastProgressUpdateMs_ == 0 || now - lastProgressUpdateMs_ > 2000;
|
||||
if ((percentChanged && timeElapsed) || downloaded == total) {
|
||||
requestUpdate(true);
|
||||
lastProgressPercent_ = percent;
|
||||
lastProgressUpdateMs_ = now;
|
||||
}
|
||||
|
||||
return !mappedInput.wasPressed(MappedInputManager::Button::Back);
|
||||
});
|
||||
return !mappedInput.wasPressed(MappedInputManager::Button::Back);
|
||||
});
|
||||
|
||||
if (result == HttpDownloader::ABORTED) {
|
||||
LOG_INF("FONT", "Download cancelled: %s", file.name.c_str());
|
||||
Storage.removeDir(stagingDir);
|
||||
// Keep staging dir so the next launch can resume.
|
||||
Storage.remove(stagedPath);
|
||||
family.hasResumableDownload = !family.installed;
|
||||
cancelRequested_ = true;
|
||||
RenderLock lock(*this);
|
||||
state_ = FAMILY_LIST;
|
||||
@@ -345,22 +611,53 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
|
||||
if (result != HttpDownloader::OK) {
|
||||
LOG_ERR("FONT", "Download failed: %s (%d)", file.name.c_str(), result);
|
||||
Storage.removeDir(stagingDir);
|
||||
// Drop just the file that failed; keep already-downloaded siblings so
|
||||
// the next retry resumes from here.
|
||||
Storage.remove(stagedPath);
|
||||
family.hasResumableDownload = !family.installed;
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
||||
downloadingFamilyIndex_ = familyIdx;
|
||||
errorMessage_ = "Download failed: " + file.name;
|
||||
return;
|
||||
}
|
||||
|
||||
// CRC32: matches upstream PR #1904 — catches truncated/torn writes.
|
||||
if (file.hasCrc32) {
|
||||
uint32_t actualCrc = 0;
|
||||
if (!FontInstaller::computeFileCrc32(stagedPath, actualCrc)) {
|
||||
LOG_ERR("FONT", "Failed to read for CRC: %s", stagedPath);
|
||||
Storage.remove(stagedPath);
|
||||
family.hasResumableDownload = !family.installed;
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
downloadingFamilyIndex_ = familyIdx;
|
||||
errorMessage_ = "Failed to verify: " + file.name;
|
||||
return;
|
||||
}
|
||||
if (actualCrc != file.crc32) {
|
||||
LOG_ERR("FONT", "CRC32 mismatch for %s: got %08x expected %08x", file.name.c_str(), actualCrc, file.crc32);
|
||||
Storage.remove(stagedPath);
|
||||
family.hasResumableDownload = !family.installed;
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
downloadingFamilyIndex_ = familyIdx;
|
||||
errorMessage_ = "Checksum mismatch: " + file.name;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fontInstaller_.validateCpfontFile(stagedPath)) {
|
||||
LOG_ERR("FONT", "Invalid .cpfont: %s", stagedPath);
|
||||
Storage.removeDir(stagingDir);
|
||||
Storage.remove(stagedPath);
|
||||
family.hasResumableDownload = !family.installed;
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
||||
downloadingFamilyIndex_ = familyIdx;
|
||||
errorMessage_ = "Invalid font file: " + file.name;
|
||||
return;
|
||||
}
|
||||
@@ -374,7 +671,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
||||
downloadingFamilyIndex_ = familyIdx;
|
||||
errorMessage_ = "Failed to prepare backup area";
|
||||
return;
|
||||
}
|
||||
@@ -385,7 +682,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
||||
downloadingFamilyIndex_ = familyIdx;
|
||||
errorMessage_ = "Failed to replace installed font";
|
||||
return;
|
||||
}
|
||||
@@ -399,7 +696,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
pendingErrorAction_ = PendingFontAction::Download;
|
||||
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
||||
downloadingFamilyIndex_ = familyIdx;
|
||||
errorMessage_ = "Failed to finalize font install";
|
||||
return;
|
||||
}
|
||||
@@ -411,7 +708,9 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
fontInstaller_.refreshRegistry();
|
||||
family.installed = true;
|
||||
family.hasUpdate = false;
|
||||
syncSelectedIndexForNewActionCount();
|
||||
family.hasResumableDownload = false;
|
||||
// syncSelectedIndexForNewActionCount() is deferred to downloadFamily() —
|
||||
// it needs families_ which is empty during this impl.
|
||||
|
||||
RenderLock lock(*this);
|
||||
state_ = COMPLETE;
|
||||
@@ -471,6 +770,7 @@ std::string FontDownloadActivity::confirmButtonLabel() const {
|
||||
const auto& family = families_[familyIndexFromList(selectedIndex_)];
|
||||
if (family.installed && !family.hasUpdate) return tr(STR_DELETE);
|
||||
if (family.hasUpdate) return tr(STR_UPDATE);
|
||||
if (family.hasResumableDownload) return tr(STR_RESUME);
|
||||
return tr(STR_DOWNLOAD);
|
||||
}
|
||||
|
||||
@@ -501,7 +801,7 @@ void FontDownloadActivity::loop() {
|
||||
if (family.installed && !family.hasUpdate) {
|
||||
promptDeleteFamily(familyIndex);
|
||||
} else {
|
||||
downloadFamily(families_[familyIndex]);
|
||||
downloadFamily(familyIndex);
|
||||
requestUpdateAndWait();
|
||||
}
|
||||
}
|
||||
@@ -528,7 +828,7 @@ void FontDownloadActivity::loop() {
|
||||
if (pendingErrorAction_ == PendingFontAction::Delete) {
|
||||
deleteFamilyAtIndex(downloadingFamilyIndex_);
|
||||
} else {
|
||||
downloadFamily(families_[downloadingFamilyIndex_]);
|
||||
downloadFamily(downloadingFamilyIndex_);
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
} else {
|
||||
@@ -615,6 +915,7 @@ void FontDownloadActivity::render(RenderLock&&) {
|
||||
const auto& f = families_[familyIndexFromList(index)];
|
||||
if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE);
|
||||
if (f.installed) return tr(STR_INSTALLED);
|
||||
if (f.hasResumableDownload) return tr(STR_RESUME);
|
||||
return "";
|
||||
},
|
||||
true);
|
||||
@@ -624,9 +925,9 @@ void FontDownloadActivity::render(RenderLock&&) {
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
} else if (state_ == DOWNLOADING) {
|
||||
const auto& family = families_[downloadingFamilyIndex_];
|
||||
|
||||
std::string statusText = std::string(tr(STR_DOWNLOADING)) + " " + family.name + " (" +
|
||||
// families_ is stashed to SD during downloadFamily(); read the cached
|
||||
// name instead of indexing families_.
|
||||
std::string statusText = std::string(tr(STR_DOWNLOADING)) + " " + downloadingFamilyName_ + " (" +
|
||||
std::to_string(currentFileIndex_ + 1) + "/" + std::to_string(currentFileTotal_) + ")";
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY - lineHeight, statusText.c_str());
|
||||
|
||||
@@ -657,7 +958,11 @@ void FontDownloadActivity::render(RenderLock&&) {
|
||||
if (!errorMessage_.empty()) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY + metrics.verticalSpacing, errorMessage_.c_str());
|
||||
}
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_RETRY), "", "");
|
||||
// Use the cached value: families_ may have just been restored (post-impl)
|
||||
// or still empty (if the failure was in the stash itself); either way the
|
||||
// cache reflects the last update from the download attempt.
|
||||
const bool canResume = pendingErrorAction_ == PendingFontAction::Download && downloadingFamilyHasResumable_;
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), canResume ? tr(STR_RESUME) : tr(STR_RETRY), "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Activity.h"
|
||||
#include "FontInstaller.h"
|
||||
#include "network/HttpDownloader.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
#ifndef FONT_MANIFEST_URL
|
||||
@@ -36,22 +38,38 @@ class FontDownloadActivity : public Activity {
|
||||
struct ManifestFile {
|
||||
std::string name;
|
||||
size_t size = 0;
|
||||
uint32_t crc32 = 0;
|
||||
bool hasCrc32 = false; // false = legacy v1 manifest, fall back to size-only check
|
||||
};
|
||||
|
||||
struct ManifestFamily {
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::vector<std::string> styles;
|
||||
// `styles` was once parsed here but never rendered — dropped to avoid
|
||||
// ArduinoJson string allocations that fragmented the heap before the
|
||||
// first TLS download. Resurrect if a UI surfaces style names.
|
||||
std::vector<ManifestFile> files;
|
||||
size_t totalSize = 0;
|
||||
bool installed = false;
|
||||
bool hasUpdate = false;
|
||||
// True iff a leftover __staging dir from a previous interrupted download
|
||||
// exists for this not-yet-installed family — the next confirm will resume
|
||||
// rather than restart.
|
||||
bool hasResumableDownload = false;
|
||||
};
|
||||
|
||||
State state_ = WIFI_SELECTION;
|
||||
FontInstaller fontInstaller_;
|
||||
ButtonNavigator buttonNavigator_;
|
||||
|
||||
// HTTP/TLS session shared across all files of a single downloadFamily()
|
||||
// call. Each family install pays the TLS handshake once (on its first
|
||||
// file); subsequent files reuse the open keep-alive connection.
|
||||
// NOT shared with the manifest fetch — holding the TLS context open
|
||||
// through the JSON parse aborts on the ~36 KB contiguous allocation
|
||||
// collision with ArduinoJson's working memory.
|
||||
HttpDownloader::Session httpSession_;
|
||||
|
||||
std::string baseUrl_;
|
||||
std::vector<ManifestFamily> families_;
|
||||
int selectedIndex_ = 0;
|
||||
@@ -67,6 +85,11 @@ class FontDownloadActivity : public Activity {
|
||||
size_t fileProgress_ = 0;
|
||||
size_t fileTotal_ = 0;
|
||||
int downloadingFamilyIndex_ = 0;
|
||||
// Cached during downloadFamily() before families_ is stashed to SD, so the
|
||||
// render path can show the family name and decide the Retry/Resume label
|
||||
// without touching families_ (which is empty during the download).
|
||||
std::string downloadingFamilyName_;
|
||||
bool downloadingFamilyHasResumable_ = false;
|
||||
PendingFontAction pendingErrorAction_ = PendingFontAction::None;
|
||||
std::string errorMessage_;
|
||||
bool cancelRequested_ = false;
|
||||
@@ -76,9 +99,24 @@ class FontDownloadActivity : public Activity {
|
||||
|
||||
void onWifiSelectionComplete(bool success);
|
||||
bool fetchAndParseManifest();
|
||||
void downloadFamily(ManifestFamily& family);
|
||||
// Download a single family by its index into families_. Internally stashes
|
||||
// families_ to SD so the TLS handshake runs on a defragmented heap; the
|
||||
// selected family's state mutations (installed/hasUpdate/hasResumableDownload)
|
||||
// are merged back into families_ on return.
|
||||
void downloadFamily(int familyIdx);
|
||||
// Internal: the body of downloadFamily after the stash. Operates only on
|
||||
// the local family copy and constants like familyIdx; never touches
|
||||
// families_ (which is empty during this call).
|
||||
void downloadFamilyImpl(ManifestFamily& family, int familyIdx);
|
||||
void downloadAll();
|
||||
void updateAll();
|
||||
|
||||
// Persist families_ to /fonts_families.bin and clear the in-memory vector.
|
||||
// Used to free the ~10 KB of scattered std::string allocations that fragment
|
||||
// the heap enough to break the TLS handshake during font downloads.
|
||||
bool stashFamiliesToSd();
|
||||
// Read /fonts_families.bin back into families_. Returns true on success.
|
||||
bool restoreFamiliesFromSd();
|
||||
bool isDownloadAllSelected() const { return hasDownloadCandidates() && selectedIndex_ == 0; }
|
||||
bool isUpdateAllSelected() const {
|
||||
if (!hasUpdateCandidates()) return false;
|
||||
|
||||
Reference in New Issue
Block a user