From 53c0753ccc2bdac7a397dd249559899bf804927f Mon Sep 17 00:00:00 2001 From: Julia Nguyen Date: Thu, 21 May 2026 17:22:23 -0400 Subject: [PATCH 1/3] fix: show loading state before opds retry feat: add more debugging wifi connection diagnostics --- CHANGELOG.md | 3 + .../browser/OpdsBookBrowserActivity.cpp | 57 +++-- .../browser/OpdsBookBrowserActivity.h | 1 + .../network/WifiSelectionActivity.cpp | 214 +++++++++++++++++- .../network/WifiSelectionActivity.h | 3 + 5 files changed, 247 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f101ac7a3f..9f7a4738de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Added +- Added more detailed WiFi connection debug logs for scans, selected networks, connection status changes, disconnect reasons, and timeouts. - Added a Recent Books long-press menu in both List and Grid views with delete, cache delete, completion, and remove-from-recents actions. - Added a Minimal sleep screen option that shows the current book cover and reading progress on a dark background. - Added an in-reader confirmation message when a shortcut turns tilt-to-turn on or off. @@ -10,6 +11,8 @@ - Added Back/Cancel support while downloading books from OPDS catalogs. ### Fixed +- Fixed OPDS book download cancellation so quick Cancel taps are detected during the transfer. +- Fixed OPDS feed retry actions so the loading screen is shown before the network request starts. - Fixed the in-reader Customise Status Bar screen in landscape so the list no longer extends under the button labels. - Fixed manual WiFi connections from Settings returning immediately to the settings list after a saved-password or open-network connection succeeded, so the connected status and IP address are shown first. - Fixed missing Vietnamese labels for the sleep timeout resume settings. diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index c0f5713c8c..1a9f7bc250 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -78,9 +78,7 @@ void OpdsBookBrowserActivity::loop() { if (state == BrowserState::ERROR) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { - state = BrowserState::LOADING; - statusMessage = tr(STR_LOADING); - requestUpdate(); + showLoadingBeforeFetch(); fetchFeed(currentPath); } else { launchWifiSelection(); @@ -212,6 +210,15 @@ void OpdsBookBrowserActivity::render(RenderLock&&) { renderer.displayBuffer(); } +void OpdsBookBrowserActivity::showLoadingBeforeFetch() { + state = BrowserState::LOADING; + statusMessage = tr(STR_LOADING); + if (requestUpdateAndWait() != RequestUpdateResult::Rendered) { + LOG_ERR("OPDS", "Loading screen could not be rendered before feed fetch"); + requestUpdate(true); + } +} + void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { if (!ensureEntryBuffer()) { state = BrowserState::ERROR; @@ -296,11 +303,9 @@ void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) { const std::string feedUrl = UrlUtils::buildUrl(server.url, currentPath); currentPath = UrlUtils::buildUrl(feedUrl, entry.href); - state = BrowserState::LOADING; - statusMessage = tr(STR_LOADING); clearEntries(); selectorIndex = 0; - requestUpdate(true); + showLoadingBeforeFetch(); fetchFeed(currentPath); } @@ -310,11 +315,9 @@ void OpdsBookBrowserActivity::navigateBack() { } else { currentPath = navigationHistory.back(); navigationHistory.pop_back(); - state = BrowserState::LOADING; - statusMessage = tr(STR_LOADING); clearEntries(); selectorIndex = 0; - requestUpdate(); + showLoadingBeforeFetch(); fetchFeed(currentPath); } } @@ -333,19 +336,29 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str()); bool cancelRequested = false; + auto pollCancel = [this, &cancelRequested] { + if (cancelRequested) { + return true; + } + mappedInput.update(); + if (mappedInput.isPressed(MappedInputManager::Button::Back) || + mappedInput.wasPressed(MappedInputManager::Button::Back) || + mappedInput.wasReleased(MappedInputManager::Button::Back)) { + cancelRequested = true; + } + return cancelRequested; + }; + HttpDownloader::DownloadOptions downloadOptions; + downloadOptions.shouldCancel = pollCancel; + const auto result = HttpDownloader::downloadToFile( downloadUrl, filename, - [this, &cancelRequested](const size_t downloaded, const size_t total) { + [this](const size_t downloaded, const size_t total) { downloadProgress = downloaded; downloadTotal = total; - mappedInput.update(); - if (mappedInput.isPressed(MappedInputManager::Button::Back) || - mappedInput.wasPressed(MappedInputManager::Button::Back)) { - cancelRequested = true; - } requestUpdate(true); }, - &cancelRequested, server.username, server.password); + &cancelRequested, server.username, server.password, downloadOptions); if (result == HttpDownloader::OK) { clearBookCache(filename); @@ -407,17 +420,13 @@ void OpdsBookBrowserActivity::performSearch(const std::string& query) { navigationHistory.push_back(currentPath); // <-- add this currentPath = url; // <-- add this - state = BrowserState::LOADING; - statusMessage = tr(STR_LOADING); - requestUpdate(true); + showLoadingBeforeFetch(); fetchFeed(url); } void OpdsBookBrowserActivity::checkAndConnectWifi() { if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { - state = BrowserState::LOADING; - statusMessage = tr(STR_LOADING); - requestUpdate(); + showLoadingBeforeFetch(); fetchFeed(currentPath); return; } @@ -434,9 +443,7 @@ void OpdsBookBrowserActivity::launchWifiSelection() { void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) { if (connected) { - state = BrowserState::LOADING; - statusMessage = tr(STR_LOADING); - requestUpdate(true); + showLoadingBeforeFetch(); fetchFeed(currentPath); } else { // Leave WiFi up; onExit's silent reboot handles teardown without fragmenting. diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index 16e5848a5e..a8e661f686 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -47,6 +47,7 @@ class OpdsBookBrowserActivity final : public Activity { void checkAndConnectWifi(); void launchWifiSelection(); void onWifiSelectionComplete(bool connected); + void showLoadingBeforeFetch(); void fetchFeed(const std::string& path); bool ensureEntryBuffer(); void clearEntries(); diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 4bd14bb9c2..dd1b61ffd2 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -15,8 +15,137 @@ #include "components/UITheme.h" #include "fontIds.h" +namespace { + +#ifndef SIMULATOR +uint8_t sLastStaDisconnectReason = 0; +bool sConnectionAttemptLoggingActive = false; +bool sWifiEventLoggingRegistered = false; + +void logWifiStationEvent(WiFiEvent_t event, WiFiEventInfo_t info) { + if (!sConnectionAttemptLoggingActive) { + return; + } + + switch (event) { + case ARDUINO_EVENT_WIFI_STA_CONNECTED: + LOG_INF("WIFI", "STA event: connected to AP"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP: { + const uint8_t* ip = reinterpret_cast(&info.got_ip.ip_info.ip.addr); + LOG_INF("WIFI", "STA event: got IP %u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]); + break; + } + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: { + uint8_t reason = info.wifi_sta_disconnected.reason; + if (reason == 0) { + reason = WIFI_REASON_UNSPECIFIED; + } + sLastStaDisconnectReason = reason; + LOG_INF("WIFI", "STA event: disconnected reason=%u(%s)", reason, + WiFi.disconnectReasonName(static_cast(reason))); + break; + } + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + LOG_INF("WIFI", "STA event: lost IP"); + break; + default: + break; + } +} + +void ensureWifiEventLoggingRegistered() { + if (sWifiEventLoggingRegistered) { + return; + } + WiFi.onEvent(logWifiStationEvent, ARDUINO_EVENT_WIFI_STA_CONNECTED); + WiFi.onEvent(logWifiStationEvent, ARDUINO_EVENT_WIFI_STA_GOT_IP); + WiFi.onEvent(logWifiStationEvent, ARDUINO_EVENT_WIFI_STA_DISCONNECTED); + WiFi.onEvent(logWifiStationEvent, ARDUINO_EVENT_WIFI_STA_LOST_IP); + sWifiEventLoggingRegistered = true; +} +#else +void ensureWifiEventLoggingRegistered() {} +#endif + +const char* wifiStatusName(const wl_status_t status) { + switch (status) { + case WL_IDLE_STATUS: + return "IDLE"; + case WL_NO_SSID_AVAIL: + return "NO_SSID_AVAIL"; + case WL_CONNECTED: + return "CONNECTED"; + case WL_CONNECT_FAILED: + return "CONNECT_FAILED"; +#ifndef SIMULATOR + case WL_CONNECTION_LOST: + return "CONNECTION_LOST"; +#endif + case WL_DISCONNECTED: + return "DISCONNECTED"; +#ifndef SIMULATOR + case WL_NO_SHIELD: + return "NO_SHIELD"; + case WL_STOPPED: + return "STOPPED"; + case WL_SCAN_COMPLETED: + return "SCAN_COMPLETED"; +#endif + default: + return "UNKNOWN"; + } +} + +bool wifiStatusIsConnectionFailure(const wl_status_t status) { + if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) { + return true; + } +#ifndef SIMULATOR + return status == WL_CONNECTION_LOST; +#else + return false; +#endif +} + +const char* wifiAuthName(const int authMode) { + switch (authMode) { + case WIFI_AUTH_OPEN: + return "OPEN"; +#ifndef SIMULATOR + case WIFI_AUTH_WEP: + return "WEP"; + case WIFI_AUTH_WPA_PSK: + return "WPA_PSK"; +#endif + case WIFI_AUTH_WPA2_PSK: + return "WPA2_PSK"; +#ifndef SIMULATOR + case WIFI_AUTH_WPA_WPA2_PSK: + return "WPA_WPA2_PSK"; + case WIFI_AUTH_WPA2_ENTERPRISE: + return "WPA2_ENTERPRISE"; + case WIFI_AUTH_WPA3_PSK: + return "WPA3_PSK"; + case WIFI_AUTH_WPA2_WPA3_PSK: + return "WPA2_WPA3_PSK"; + case WIFI_AUTH_WAPI_PSK: + return "WAPI_PSK"; + case WIFI_AUTH_OWE: + return "OWE"; + case WIFI_AUTH_WPA3_ENT_192: + return "WPA3_ENT_192"; +#endif + default: + return "UNKNOWN"; + } +} + +} // namespace + void WifiSelectionActivity::onEnter() { Activity::onEnter(); + ensureWifiEventLoggingRegistered(); // Load saved WiFi credentials - SD card operations need lock as we use SPI // for both @@ -37,6 +166,8 @@ void WifiSelectionActivity::onEnter() { savePromptSelection = 0; forgetPromptSelection = 0; autoConnecting = false; + lastConnectionStatusLogTime = 0; + lastLoggedWifiStatus = -1; // Cache MAC address for display uint8_t mac[6]; @@ -55,7 +186,7 @@ void WifiSelectionActivity::onEnter() { if (!lastSsid.empty()) { const auto* cred = WIFI_STORE.findCredential(lastSsid); if (cred) { - LOG_DBG("WIFI", "Attempting to auto-connect to %s", lastSsid.c_str()); + LOG_INF("WIFI", "Auto-connect candidate: ssid=%s saved=1", lastSsid.c_str()); selectedSSID = cred->ssid; enteredPassword = cred->password; selectedRequiresPassword = !cred->password.empty(); @@ -96,12 +227,15 @@ void WifiSelectionActivity::startWifiScan() { requestUpdate(); // Set WiFi mode to station + LOG_INF("WIFI", "Starting WiFi scan (mode=%d status=%d/%s heap=%u maxAlloc=%u)", static_cast(WiFi.getMode()), + static_cast(WiFi.status()), wifiStatusName(WiFi.status()), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(100); // Start async scan - WiFi.scanNetworks(true); // true = async scan + const int scanStartResult = WiFi.scanNetworks(true); // true = async scan + LOG_INF("WIFI", "WiFi scan requested (result=%d)", scanStartResult); } void WifiSelectionActivity::processWifiScanResults() { @@ -113,35 +247,48 @@ void WifiSelectionActivity::processWifiScanResults() { } if (scanResult == WIFI_SCAN_FAILED) { + LOG_INF("WIFI", "WiFi scan failed"); state = WifiSelectionState::NETWORK_LIST; requestUpdate(); return; } + LOG_INF("WIFI", "WiFi scan complete: rawNetworks=%d", scanResult); + // Scan complete, process results // Use a map to deduplicate networks by SSID, keeping the strongest signal std::map uniqueNetworks; + int hiddenNetworks = 0; + int duplicateNetworks = 0; for (int i = 0; i < scanResult; i++) { std::string ssid = WiFi.SSID(i).c_str(); const int32_t rssi = WiFi.RSSI(i); + const int authMode = WiFi.encryptionType(i); // Skip hidden networks (empty SSID) if (ssid.empty()) { + hiddenNetworks++; continue; } // Check if we've already seen this SSID auto it = uniqueNetworks.find(ssid); + if (it != uniqueNetworks.end()) { + duplicateNetworks++; + } if (it == uniqueNetworks.end() || rssi > it->second.rssi) { // New network or stronger signal than existing entry WifiNetworkInfo network; network.ssid = ssid; network.rssi = rssi; - network.isEncrypted = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN); + network.isEncrypted = (authMode != WIFI_AUTH_OPEN); network.hasSavedPassword = WIFI_STORE.hasSavedCredential(network.ssid); uniqueNetworks[ssid] = network; } + + LOG_DBG("WIFI", "Scan result: ssid=%s rssi=%d auth=%s saved=%d", ssid.c_str(), rssi, wifiAuthName(authMode), + WIFI_STORE.hasSavedCredential(ssid)); } // Convert map to vector @@ -160,6 +307,8 @@ void WifiSelectionActivity::processWifiScanResults() { }); WiFi.scanDelete(); + LOG_INF("WIFI", "WiFi scan usable networks=%zu hidden=%d duplicates=%d", networks.size(), hiddenNetworks, + duplicateNetworks); state = WifiSelectionState::NETWORK_LIST; selectedNetworkIndex = 0; requestUpdate(); @@ -183,6 +332,8 @@ void WifiSelectionActivity::selectNetwork(const int index) { // Use saved password - connect directly enteredPassword = savedCred->password; usedSavedPassword = true; + LOG_INF("WIFI", "Selected network: ssid=%s encrypted=%d saved=1 rssi=%d", selectedSSID.c_str(), + selectedRequiresPassword, network.rssi); LOG_DBG("WiFi", "Using saved password for %s, length: %zu", selectedSSID.c_str(), enteredPassword.size()); attemptConnection(); return; @@ -206,6 +357,7 @@ void WifiSelectionActivity::selectNetwork(const int index) { }); } else { // Connect directly for open networks + LOG_INF("WIFI", "Selected open network: ssid=%s rssi=%d", selectedSSID.c_str(), network.rssi); attemptConnection(); } } @@ -215,12 +367,26 @@ void WifiSelectionActivity::attemptConnection() { connectionStartTime = millis(); connectedIP.clear(); connectionError.clear(); + lastConnectionStatusLogTime = 0; + lastLoggedWifiStatus = -1; +#ifndef SIMULATOR + sLastStaDisconnectReason = 0; + sConnectionAttemptLoggingActive = false; +#endif requestUpdate(); + LOG_INF("WIFI", "Connecting to ssid=%s auto=%d saved=%d encrypted=%d passProvided=%d heap=%u maxAlloc=%u", + selectedSSID.c_str(), autoConnecting, usedSavedPassword, selectedRequiresPassword, !enteredPassword.empty(), + ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + WiFi.persistent(false); // Credentials are managed by WifiCredentialStore; suppress SDK NVS auto-connect WiFi.mode(WIFI_STA); WiFi.disconnect(true, true); // Abort any in-progress SDK auto-connect and clear NVS-saved SSID delay(100); +#ifndef SIMULATOR + sLastStaDisconnectReason = 0; + sConnectionAttemptLoggingActive = true; +#endif // Set hostname so routers show "CrossPoint-Reader-AABBCCDDEEFF" instead of "esp32-XXXXXXXXXXXX" String mac = WiFi.macAddress(); @@ -228,11 +394,13 @@ void WifiSelectionActivity::attemptConnection() { String hostname = "CrossPoint-Reader-" + mac; WiFi.setHostname(hostname.c_str()); + wl_status_t beginStatus = WL_IDLE_STATUS; if (selectedRequiresPassword && !enteredPassword.empty()) { - WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str()); + beginStatus = WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str()); } else { - WiFi.begin(selectedSSID.c_str()); + beginStatus = WiFi.begin(selectedSSID.c_str()); } + LOG_INF("WIFI", "WiFi.begin returned status=%d/%s", static_cast(beginStatus), wifiStatusName(beginStatus)); } void WifiSelectionActivity::checkConnectionStatus() { @@ -241,6 +409,15 @@ void WifiSelectionActivity::checkConnectionStatus() { } const wl_status_t status = WiFi.status(); + const unsigned long now = millis(); + + if (lastLoggedWifiStatus != static_cast(status) || + now - lastConnectionStatusLogTime >= CONNECTION_STATUS_LOG_INTERVAL_MS) { + LOG_INF("WIFI", "Connection poll: elapsed=%lums status=%d/%s rssi=%d", now - connectionStartTime, + static_cast(status), wifiStatusName(status), status == WL_CONNECTED ? WiFi.RSSI() : 0); + lastLoggedWifiStatus = static_cast(status); + lastConnectionStatusLogTime = now; + } if (status == WL_CONNECTED) { // Successfully connected @@ -249,6 +426,10 @@ void WifiSelectionActivity::checkConnectionStatus() { snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); connectedIP = ipStr; autoConnecting = false; +#ifndef SIMULATOR + sConnectionAttemptLoggingActive = false; +#endif + LOG_INF("WIFI", "Connected to ssid=%s ip=%s rssi=%d", selectedSSID.c_str(), connectedIP.c_str(), WiFi.RSSI()); // Sync RTC from NTP on the first successful WiFi connection only. The DS3231 // drifts ~2 ppm so one sync is enough; users can force a re-sync from @@ -289,11 +470,20 @@ void WifiSelectionActivity::checkConnectionStatus() { return; } - if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) { + if (wifiStatusIsConnectionFailure(status)) { connectionError = tr(STR_ERROR_GENERAL_FAILURE); if (status == WL_NO_SSID_AVAIL) { connectionError = tr(STR_ERROR_NETWORK_NOT_FOUND); } + LOG_INF("WIFI", "Connection failed: ssid=%s status=%d/%s elapsed=%lums", selectedSSID.c_str(), + static_cast(status), wifiStatusName(status), now - connectionStartTime); +#ifndef SIMULATOR + if (sLastStaDisconnectReason != 0) { + LOG_INF("WIFI", "Last disconnect reason: %u(%s)", sLastStaDisconnectReason, + WiFi.disconnectReasonName(static_cast(sLastStaDisconnectReason))); + } + sConnectionAttemptLoggingActive = false; +#endif state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); return; @@ -303,6 +493,15 @@ void WifiSelectionActivity::checkConnectionStatus() { if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { WiFi.disconnect(); connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT); + LOG_INF("WIFI", "Connection timed out: ssid=%s elapsed=%lums lastStatus=%d/%s", selectedSSID.c_str(), + millis() - connectionStartTime, static_cast(status), wifiStatusName(status)); +#ifndef SIMULATOR + if (sLastStaDisconnectReason != 0) { + LOG_INF("WIFI", "Last disconnect reason before timeout: %u(%s)", sLastStaDisconnectReason, + WiFi.disconnectReasonName(static_cast(sLastStaDisconnectReason))); + } + sConnectionAttemptLoggingActive = false; +#endif state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); return; @@ -313,6 +512,9 @@ void WifiSelectionActivity::loop() { if ((state == WifiSelectionState::SCANNING || state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) && mappedInput.wasPressed(MappedInputManager::Button::Back)) { +#ifndef SIMULATOR + sConnectionAttemptLoggingActive = false; +#endif WiFi.disconnect(); onComplete(false); return; diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index baacf4da22..20ffa64e57 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -81,7 +81,10 @@ class WifiSelectionActivity final : public Activity { // Connection timeout static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000; + static constexpr unsigned long CONNECTION_STATUS_LOG_INTERVAL_MS = 2000; unsigned long connectionStartTime = 0; + unsigned long lastConnectionStatusLogTime = 0; + int lastLoggedWifiStatus = -1; void renderNetworkList(const Rect* screen, const ThemeMetrics* metrics) const; void renderPasswordEntry(const Rect* screen, const ThemeMetrics* metrics) const; From b0e3afa3856b82d1748eda388456deb7b8335ad7 Mon Sep 17 00:00:00 2001 From: Julia Nguyen Date: Thu, 21 May 2026 17:22:44 -0400 Subject: [PATCH 2/3] fix: make opds download cancellation more responsive --- src/network/HttpDownloader.cpp | 36 ++++++++++++++++++++++++++-------- src/network/HttpDownloader.h | 8 ++++++-- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index 8d37eaba28..26c7fca258 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -32,6 +32,19 @@ void logNetworkState(const char* phase) { static_cast(WiFi.status()), WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : 0); } +bool isCancelRequested(bool* cancelFlag, const HttpDownloader::CancelCallback& shouldCancel) { + if (cancelFlag && *cancelFlag) { + return true; + } + if (shouldCancel && shouldCancel()) { + if (cancelFlag) { + *cancelFlag = true; + } + return true; + } + return false; +} + class ProgressNotifier { public: ProgressNotifier(size_t total, HttpDownloader::ProgressCallback progress) @@ -58,8 +71,12 @@ class ProgressNotifier { class FileWriteStream final : public Stream { public: - FileWriteStream(FsFile& file, size_t total, HttpDownloader::ProgressCallback progress, const bool* cancelFlag) - : file_(file), progress_(total, std::move(progress)), cancelFlag_(cancelFlag) {} + FileWriteStream(FsFile& file, size_t total, HttpDownloader::ProgressCallback progress, bool* cancelFlag, + HttpDownloader::CancelCallback shouldCancel) + : file_(file), + progress_(total, std::move(progress)), + cancelFlag_(cancelFlag), + shouldCancel_(std::move(shouldCancel)) {} size_t write(uint8_t byte) override { return write(&byte, 1); } @@ -68,7 +85,7 @@ class FileWriteStream final : public Stream { return 0; } - if (cancelFlag_ && *cancelFlag_) { + if (isCancelRequested(cancelFlag_, shouldCancel_)) { writeOk_ = false; return 0; } @@ -95,12 +112,14 @@ class FileWriteStream final : public Stream { size_t downloaded_ = 0; bool writeOk_ = true; ProgressNotifier progress_; - const bool* cancelFlag_; + bool* cancelFlag_; + HttpDownloader::CancelCallback shouldCancel_; }; HttpDownloader::DownloadError downloadKnownLengthBody(HTTPClient& http, FsFile& file, const size_t contentLength, HttpDownloader::ProgressCallback progress, size_t& downloaded, - const bool* cancelFlag) { + bool* cancelFlag, + const HttpDownloader::CancelCallback& shouldCancel) { auto* stream = http.getStreamPtr(); if (!stream) { LOG_ERR("HTTP", "Failed to get response stream"); @@ -116,7 +135,7 @@ HttpDownloader::DownloadError downloadKnownLengthBody(HTTPClient& http, FsFile& ProgressNotifier progressNotifier(contentLength, std::move(progress)); uint32_t lastProgressMs = millis(); while (downloaded < contentLength) { - if (cancelFlag && *cancelFlag) { + if (isCancelRequested(cancelFlag, shouldCancel)) { return HttpDownloader::ABORTED; } const size_t remaining = contentLength - downloaded; @@ -344,10 +363,11 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& int writeResult = 0; if (contentLength > 0) { - transferError = downloadKnownLengthBody(http, file, contentLength, std::move(progress), downloaded, cancelFlag); + transferError = downloadKnownLengthBody(http, file, contentLength, std::move(progress), downloaded, cancelFlag, + options.shouldCancel); } else { // Let HTTPClient handle chunked decoding and stream body bytes into the file. - FileWriteStream fileStream(file, contentLength, std::move(progress), cancelFlag); + FileWriteStream fileStream(file, contentLength, std::move(progress), cancelFlag, std::move(options.shouldCancel)); writeResult = http.writeToStream(&fileStream); fileStream.finishProgress(); downloaded = fileStream.downloaded(); diff --git a/src/network/HttpDownloader.h b/src/network/HttpDownloader.h index 34dad4574d..4d4f4644d6 100644 --- a/src/network/HttpDownloader.h +++ b/src/network/HttpDownloader.h @@ -4,6 +4,7 @@ #include #include +#include /** * HTTP client utility for fetching content and downloading files. @@ -12,6 +13,7 @@ class HttpDownloader { public: using ProgressCallback = std::function; + using CancelCallback = std::function; enum DownloadError { OK = 0, @@ -21,11 +23,13 @@ class HttpDownloader { }; struct DownloadOptions { - explicit constexpr DownloadOptions(bool preservePartial = false, bool resumePartial = false) - : preservePartial(preservePartial), resumePartial(resumePartial) {} + explicit DownloadOptions(bool preservePartial = false, bool resumePartial = false, + CancelCallback shouldCancel = nullptr) + : preservePartial(preservePartial), resumePartial(resumePartial), shouldCancel(std::move(shouldCancel)) {} bool preservePartial; bool resumePartial; + CancelCallback shouldCancel; }; /** From b7a6a39d3a0e959ce77eec4530ac13ed3c93d15b Mon Sep 17 00:00:00 2001 From: Julia Nguyen Date: Thu, 21 May 2026 17:29:41 -0400 Subject: [PATCH 3/3] docs: update changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f7a4738de..6cee7e525e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,6 @@ - Added Back/Cancel support while downloading books from OPDS catalogs. ### Fixed -- Fixed OPDS book download cancellation so quick Cancel taps are detected during the transfer. - Fixed OPDS feed retry actions so the loading screen is shown before the network request starts. - Fixed the in-reader Customise Status Bar screen in landscape so the list no longer extends under the button labels. - Fixed manual WiFi connections from Settings returning immediately to the settings list after a saved-password or open-network connection succeeded, so the connected status and IP address are shown first.