From 5b11e45a360be7af0cc4279af9cba1125966949b Mon Sep 17 00:00:00 2001 From: pablohc Date: Fri, 27 Feb 2026 06:34:11 +0100 Subject: [PATCH] fix: prevent infinite render loop in Calibre Wireless after file transfer (#1070) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * **What is the goal of this PR?** Fix an infinite render loop bug in CalibreConnectActivity that caused the e-ink display to refresh continuously every ~421ms after a file transfer completed. * **What changes are included?** - Added lastProcessedCompleteAt member variable to track which server completion timestamp has already been processed - Modified the completion status update logic to only accept new values from the server, preventing re-processing of old timestamps - Added clarifying comments explaining the fix ## Problem Description After receiving a file via Calibre Wireless, the activity displays "Received: [filename]" for 6 seconds, then clears the message. However, the web server's wsLastCompleteAt timestamp persists indefinitely and is never cleared. This created a race condition: After 6 seconds, lastCompleteAt is set to 0 (timeout) In the next loop iteration, status.lastCompleteAt (still has the old timestamp) ≠ lastCompleteAt (0) The code restores lastCompleteAt from the server value Immediately, the 6-second timeout condition is met again This creates an infinite cycle causing unnecessary e-ink refreshes ## Solution The fix introduces lastProcessedCompleteAt to track which server timestamp value has already been processed: Only accept a new status.lastCompleteAt if it differs from lastProcessedCompleteAt Update lastProcessedCompleteAt when processing a new value Do NOT reset lastProcessedCompleteAt when the 6-second timeout clears lastCompleteAt This prevents re-processing the same old server value after the timeout. ## Testing Tested on device with multiple file transfer scenarios: ✅ File received message appears correctly after transfer ✅ Message clears after 6 seconds as expected ✅ No infinite render loop after timeout ✅ Multiple consecutive transfers work correctly ✅ Exiting and re-entering Calibre Wireless works as expected ## Performance Impact Before: Infinite refreshes every ~421ms after timeout (high battery drain, display wear) After: 2-3 refreshes after timeout, then stops (normal behavior) ## Additional Context This is a targeted fix that only affects the Calibre Wireless file transfer screen. The root cause is the architectural difference between the persistent web server state (wsLastCompleteAt) and the per-activity display state (lastCompleteAt). An alternative fix would be to clear wsLastCompleteAt in the web server after some timeout, but that would affect all consumers of the web server status. The chosen solution keeps the fix localized to CalibreConnectActivity. --- ### AI Usage Did you use AI tools to help write this code? _**< YES >**_ --- src/activities/network/CalibreConnectActivity.cpp | 7 ++++++- src/activities/network/CalibreConnectActivity.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/activities/network/CalibreConnectActivity.cpp b/src/activities/network/CalibreConnectActivity.cpp index 69014cf4..fbf9dd04 100644 --- a/src/activities/network/CalibreConnectActivity.cpp +++ b/src/activities/network/CalibreConnectActivity.cpp @@ -28,6 +28,7 @@ void CalibreConnectActivity::onEnter() { currentUploadName.clear(); lastCompleteName.clear(); lastCompleteAt = 0; + lastProcessedCompleteAt = 0; exitRequested = false; if (WiFi.status() != WL_CONNECTED) { @@ -147,14 +148,18 @@ void CalibreConnectActivity::loop() { currentUploadName.clear(); changed = true; } - if (status.lastCompleteAt != 0 && status.lastCompleteAt != lastCompleteAt) { + // Only update lastCompleteAt if the server has a NEW value (not one we already processed) + // This prevents restoring an old value after the 6s timeout clears it + if (status.lastCompleteAt != 0 && status.lastCompleteAt != lastProcessedCompleteAt) { lastCompleteAt = status.lastCompleteAt; lastCompleteName = status.lastCompleteName; + lastProcessedCompleteAt = status.lastCompleteAt; // Mark this value as processed changed = true; } if (lastCompleteAt > 0 && (millis() - lastCompleteAt) >= 6000) { lastCompleteAt = 0; lastCompleteName.clear(); + // Note: we DON'T reset lastProcessedCompleteAt here, so we won't re-process the old server value changed = true; } if (changed) { diff --git a/src/activities/network/CalibreConnectActivity.h b/src/activities/network/CalibreConnectActivity.h index d1d2bfcf..451d3bc9 100644 --- a/src/activities/network/CalibreConnectActivity.h +++ b/src/activities/network/CalibreConnectActivity.h @@ -26,6 +26,7 @@ class CalibreConnectActivity final : public ActivityWithSubactivity { std::string currentUploadName; std::string lastCompleteName; unsigned long lastCompleteAt = 0; + unsigned long lastProcessedCompleteAt = 0; // Track which server value we've already processed bool exitRequested = false; void renderServerRunning() const;