fix: prevent infinite render loop in Calibre Wireless after file transfer (#1070)

## 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 >**_
This commit is contained in:
pablohc
2026-02-27 06:34:11 +01:00
committed by GitHub
parent d05cb220bb
commit 5b11e45a36
2 changed files with 7 additions and 1 deletions

View File

@@ -28,6 +28,7 @@ void CalibreConnectActivity::onEnter() {
currentUploadName.clear(); currentUploadName.clear();
lastCompleteName.clear(); lastCompleteName.clear();
lastCompleteAt = 0; lastCompleteAt = 0;
lastProcessedCompleteAt = 0;
exitRequested = false; exitRequested = false;
if (WiFi.status() != WL_CONNECTED) { if (WiFi.status() != WL_CONNECTED) {
@@ -147,14 +148,18 @@ void CalibreConnectActivity::loop() {
currentUploadName.clear(); currentUploadName.clear();
changed = true; 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; lastCompleteAt = status.lastCompleteAt;
lastCompleteName = status.lastCompleteName; lastCompleteName = status.lastCompleteName;
lastProcessedCompleteAt = status.lastCompleteAt; // Mark this value as processed
changed = true; changed = true;
} }
if (lastCompleteAt > 0 && (millis() - lastCompleteAt) >= 6000) { if (lastCompleteAt > 0 && (millis() - lastCompleteAt) >= 6000) {
lastCompleteAt = 0; lastCompleteAt = 0;
lastCompleteName.clear(); lastCompleteName.clear();
// Note: we DON'T reset lastProcessedCompleteAt here, so we won't re-process the old server value
changed = true; changed = true;
} }
if (changed) { if (changed) {

View File

@@ -26,6 +26,7 @@ class CalibreConnectActivity final : public ActivityWithSubactivity {
std::string currentUploadName; std::string currentUploadName;
std::string lastCompleteName; std::string lastCompleteName;
unsigned long lastCompleteAt = 0; unsigned long lastCompleteAt = 0;
unsigned long lastProcessedCompleteAt = 0; // Track which server value we've already processed
bool exitRequested = false; bool exitRequested = false;
void renderServerRunning() const; void renderServerRunning() const;