From 87a568c928c6d855f3a1e4c8361b03fd814edba0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 20:05:10 +0000 Subject: [PATCH] Fix WiFi file transfer stability and improve upload performance - Add WiFi connection health monitoring for STA mode - Detects disconnection and exits gracefully instead of locking up - Logs weak signal warnings (< -75 dBm) for debugging - Implement time-based handleClient processing (50ms budget) - Replaces fixed 10-iteration loop with adaptive time-based approach - Adds yield() between calls to let WiFi stack receive packets - Improves throughput by processing more data when available - Add 8KB upload write buffer - Batches small writes into larger SD card operations - Reduces SD write overhead by ~4-8x - Includes diagnostic counters for performance analysis - Add watchdog reset during file scanning - Prevents timeout on large directories --- .../network/CrossPointWebServerActivity.cpp | 44 ++++++-- src/network/CrossPointWebServer.cpp | 101 +++++++++++++----- 2 files changed, 112 insertions(+), 33 deletions(-) diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index dde05614..228f84d4 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -283,8 +283,30 @@ void CrossPointWebServerActivity::loop() { dnsServer->processNextRequest(); } - // Handle web server requests - call handleClient multiple times per loop - // to improve responsiveness and upload throughput + // STA mode: Monitor WiFi connection health + if (!isApMode && webServer && webServer->isRunning()) { + static unsigned long lastWifiCheck = 0; + if (millis() - lastWifiCheck > 2000) { // Check every 2 seconds + lastWifiCheck = millis(); + const wl_status_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + Serial.printf("[%lu] [WEBACT] WiFi disconnected! Status: %d\n", millis(), wifiStatus); + // Show error and exit gracefully + state = WebServerActivityState::SHUTTING_DOWN; + updateRequired = true; + return; + } + // Log weak signal warnings + const int rssi = WiFi.RSSI(); + if (rssi < -75) { + Serial.printf("[%lu] [WEBACT] Warning: Weak WiFi signal: %d dBm\n", millis(), rssi); + } + } + } + + // Handle web server requests using time-based processing + // Process requests for up to TIME_BUDGET_MS to maximize throughput + // while still allowing other loop activities to run if (webServer && webServer->isRunning()) { const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime; @@ -294,12 +316,20 @@ void CrossPointWebServerActivity::loop() { timeSinceLastHandleClient); } - // Call handleClient multiple times to process pending requests faster - // This is critical for upload performance - HTTP file uploads send data - // in chunks and each handleClient() call processes incoming data - constexpr int HANDLE_CLIENT_ITERATIONS = 10; - for (int i = 0; i < HANDLE_CLIENT_ITERATIONS && webServer->isRunning(); i++) { + // Time-based processing: handle requests for up to 50ms per loop iteration + // This is more efficient than a fixed iteration count because: + // 1. Processes more data when available (during uploads) + // 2. Returns quickly when idle (no wasted spinning) + // 3. yield() between calls lets WiFi stack receive more data + constexpr unsigned long TIME_BUDGET_MS = 50; + const unsigned long handleStart = millis(); + + while (webServer->isRunning() && (millis() - handleStart) < TIME_BUDGET_MS) { webServer->handleClient(); + // Yield between calls to let WiFi stack process incoming packets + // This is critical for throughput - without it, TCP flow control + // throttles the sender because our receive buffer fills up + yield(); } lastHandleClientTime = millis(); } diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 8703c2ae..7f8f0545 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -230,6 +231,7 @@ void CrossPointWebServer::scanFiles(const char* path, const std::function 0 && uploadFile) { + const unsigned long writeStart = millis(); + const size_t written = uploadFile.write(uploadBuffer, uploadBufferPos); + totalWriteTime += millis() - writeStart; + writeCount++; + + if (written != uploadBufferPos) { + Serial.printf("[%lu] [WEB] [UPLOAD] Buffer flush failed: expected %d, wrote %d\n", millis(), uploadBufferPos, + written); + uploadBufferPos = 0; + return false; + } + uploadBufferPos = 0; + } + return true; +} + void CrossPointWebServer::handleUpload() const { - static unsigned long lastWriteTime = 0; - static unsigned long uploadStartTime = 0; static size_t lastLoggedSize = 0; // Safety check: ensure server is still valid @@ -320,8 +349,10 @@ void CrossPointWebServer::handleUpload() const { uploadSuccess = false; uploadError = ""; uploadStartTime = millis(); - lastWriteTime = millis(); lastLoggedSize = 0; + uploadBufferPos = 0; + totalWriteTime = 0; + writeCount = 0; // Get upload path from query parameter (defaults to root if not specified) // Note: We use query parameter instead of form data because multipart form @@ -364,44 +395,62 @@ void CrossPointWebServer::handleUpload() const { Serial.printf("[%lu] [WEB] [UPLOAD] File created successfully: %s\n", millis(), filePath.c_str()); } else if (upload.status == UPLOAD_FILE_WRITE) { if (uploadFile && uploadError.isEmpty()) { - const unsigned long writeStartTime = millis(); - const size_t written = uploadFile.write(upload.buf, upload.currentSize); - const unsigned long writeEndTime = millis(); - const unsigned long writeDuration = writeEndTime - writeStartTime; + // Buffer incoming data and flush when buffer is full + // This reduces SD card write operations and improves throughput + const uint8_t* data = upload.buf; + size_t remaining = upload.currentSize; - if (written != upload.currentSize) { - uploadError = "Failed to write to SD card - disk may be full"; - uploadFile.close(); - Serial.printf("[%lu] [WEB] [UPLOAD] WRITE ERROR - expected %d, wrote %d\n", millis(), upload.currentSize, - written); - } else { - uploadSize += written; + while (remaining > 0) { + const size_t space = UPLOAD_BUFFER_SIZE - uploadBufferPos; + const size_t toCopy = (remaining < space) ? remaining : space; - // Log progress every 50KB or if write took >100ms - if (uploadSize - lastLoggedSize >= 51200 || writeDuration > 100) { - const unsigned long timeSinceStart = millis() - uploadStartTime; - const unsigned long timeSinceLastWrite = millis() - lastWriteTime; - const float kbps = (uploadSize / 1024.0) / (timeSinceStart / 1000.0); + memcpy(uploadBuffer + uploadBufferPos, data, toCopy); + uploadBufferPos += toCopy; + data += toCopy; + remaining -= toCopy; - Serial.printf( - "[%lu] [WEB] [UPLOAD] Progress: %d bytes (%.1f KB), %.1f KB/s, write took %lu ms, gap since last: %lu " - "ms\n", - millis(), uploadSize, uploadSize / 1024.0, kbps, writeDuration, timeSinceLastWrite); - lastLoggedSize = uploadSize; + // Flush buffer when full + if (uploadBufferPos >= UPLOAD_BUFFER_SIZE) { + if (!flushUploadBuffer()) { + uploadError = "Failed to write to SD card - disk may be full"; + uploadFile.close(); + return; + } } - lastWriteTime = millis(); + } + + uploadSize += upload.currentSize; + + // Log progress every 100KB + if (uploadSize - lastLoggedSize >= 102400) { + const unsigned long elapsed = millis() - uploadStartTime; + const float kbps = (elapsed > 0) ? (uploadSize / 1024.0) / (elapsed / 1000.0) : 0; + Serial.printf("[%lu] [WEB] [UPLOAD] %d bytes (%.1f KB), %.1f KB/s, %d writes\n", millis(), uploadSize, + uploadSize / 1024.0, kbps, writeCount); + lastLoggedSize = uploadSize; } } } else if (upload.status == UPLOAD_FILE_END) { if (uploadFile) { + // Flush any remaining buffered data + if (!flushUploadBuffer()) { + uploadError = "Failed to write final data to SD card"; + } uploadFile.close(); if (uploadError.isEmpty()) { uploadSuccess = true; - Serial.printf("[%lu] [WEB] Upload complete: %s (%d bytes)\n", millis(), uploadFileName.c_str(), uploadSize); + const unsigned long elapsed = millis() - uploadStartTime; + const float avgKbps = (elapsed > 0) ? (uploadSize / 1024.0) / (elapsed / 1000.0) : 0; + const float writePercent = (elapsed > 0) ? (totalWriteTime * 100.0 / elapsed) : 0; + Serial.printf("[%lu] [WEB] [UPLOAD] Complete: %s (%d bytes in %lu ms, avg %.1f KB/s)\n", millis(), + uploadFileName.c_str(), uploadSize, elapsed, avgKbps); + Serial.printf("[%lu] [WEB] [UPLOAD] Diagnostics: %d writes, total write time: %lu ms (%.1f%%)\n", millis(), + writeCount, totalWriteTime, writePercent); } } } else if (upload.status == UPLOAD_FILE_ABORTED) { + uploadBufferPos = 0; // Discard buffered data if (uploadFile) { uploadFile.close(); // Try to delete the incomplete file