From bab374a675d93f7be1f90f698b1b8904d26bb75b Mon Sep 17 00:00:00 2001 From: cottongin Date: Thu, 29 Jan 2026 17:57:56 -0500 Subject: [PATCH] fixes webserver uploads and general stability --- src/RecentBooksStore.cpp | 7 +++ src/RecentBooksStore.h | 7 ++- src/main.cpp | 27 ++++++++++ src/network/CrossPointWebServer.cpp | 81 ++++++++++++++++++++++++++--- src/network/CrossPointWebServer.h | 5 ++ 5 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 9fee37f..3564293 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -52,6 +52,13 @@ void RecentBooksStore::clearAll() { Serial.printf("[%lu] [RBS] Cleared all recent books\n", millis()); } +void RecentBooksStore::clearFromMemory() { + const size_t count = recentBooks.size(); + recentBooks.clear(); + recentBooks.shrink_to_fit(); // Actually free the vector capacity + Serial.printf("[%lu] [RBS] Cleared %d recent books from memory (not saved)\n", millis(), count); +} + bool RecentBooksStore::saveToFile() const { // Make sure the directory exists SdMan.mkdir("/.crosspoint"); diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index a7a3e79..2617835 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -29,9 +29,14 @@ class RecentBooksStore { // Returns true if the book was found and removed bool removeBook(const std::string& path); - // Clear all recent books from the list + // Clear all recent books from the list (and save to file) void clearAll(); + // Clear recent books from memory without saving to file + // Used to free memory when entering modes that don't need this data (e.g., File Transfer) + // Call loadFromFile() to restore the data when needed again + void clearFromMemory(); + // Get the list of recent books (most recent first) const std::vector& getBooks() const { return recentBooks; } diff --git a/src/main.cpp b/src/main.cpp index 6759e4d..6551e03 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -381,6 +381,15 @@ void onGoToListsOrPinned() { void onGoToFileTransfer() { exitActivity(); + + // Free memory not needed during file transfer to maximize heap for webserver + RECENT_BOOKS.clearFromMemory(); + APP_STATE.openBookTitle.clear(); + APP_STATE.openBookTitle.shrink_to_fit(); + APP_STATE.openBookAuthor.clear(); + APP_STATE.openBookAuthor.shrink_to_fit(); + Serial.printf("[%lu] [FT] Cleared non-essential memory before File Transfer\n", millis()); + enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); } @@ -396,12 +405,24 @@ void onGoToClearCache() { void onGoToMyLibrary() { exitActivity(); + + // Reload recent books if they were cleared (e.g., when exiting File Transfer mode) + if (RECENT_BOOKS.getCount() == 0) { + RECENT_BOOKS.loadFromFile(); + } + enterNewActivity( new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList)); } void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) { exitActivity(); + + // Reload recent books if they were cleared (e.g., when exiting File Transfer mode) + if (RECENT_BOOKS.getCount() == 0) { + RECENT_BOOKS.loadFromFile(); + } + enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList, tab, path)); } @@ -413,6 +434,12 @@ void onGoToBrowser() { void onGoHome() { exitActivity(); + + // Reload recent books if they were cleared (e.g., when exiting File Transfer mode) + if (RECENT_BOOKS.getCount() == 0) { + RECENT_BOOKS.loadFromFile(); + } + enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToListsOrPinned, onGoToMyLibrary, onGoToSettings, onGoToFileTransfer, onGoToBrowser)); } diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index beb74a2..dd07e77 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -441,17 +441,33 @@ void CrossPointWebServer::handleFileListData() const { showHidden = server->arg("showHidden") == "true"; } + // Check client connection before starting + if (!server->client().connected()) { + Serial.printf("[%lu] [WEB] Client disconnected before file list could start\n", millis()); + return; + } + server->setContentLength(CONTENT_LENGTH_UNKNOWN); server->send(200, "application/json", ""); - server->sendContent("["); + if (!sendContentSafe("[")) { + Serial.printf("[%lu] [WEB] Client disconnected at start of file list\n", millis()); + return; + } + char output[512]; constexpr size_t outputSize = sizeof(output); bool seenFirst = false; + bool clientDisconnected = false; JsonDocument doc; scanFiles( currentPath.c_str(), - [this, &output, &doc, seenFirst](const FileInfo& info) mutable { + [this, &output, &doc, &seenFirst, &clientDisconnected](const FileInfo& info) mutable { + // Skip remaining files if client already disconnected + if (clientDisconnected) { + return; + } + doc.clear(); doc["name"] = info.name; doc["size"] = info.size; @@ -475,18 +491,33 @@ void CrossPointWebServer::handleFileListData() const { return; } + // Send comma separator before all entries except the first if (seenFirst) { - server->sendContent(","); + if (!sendContentSafe(",")) { + clientDisconnected = true; + Serial.printf("[%lu] [WEB] Client disconnected during file list\n", millis()); + return; + } } else { seenFirst = true; } - server->sendContent(output); + + // Send the JSON entry with flow control + if (!sendContentSafe(output)) { + clientDisconnected = true; + Serial.printf("[%lu] [WEB] Client disconnected during file list\n", millis()); + return; + } }, showHidden); - server->sendContent("]"); - // End of streamed response, empty chunk to signal client - server->sendContent(""); - Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str()); + + // Only send closing bracket if client is still connected + if (!clientDisconnected) { + sendContentSafe("]"); + // End of streamed response, empty chunk to signal client + server->sendContent(""); + Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str()); + } } // Static variables for upload handling @@ -1264,6 +1295,40 @@ void CrossPointWebServer::handleRename() const { } } +// Counter for flow control pacing +static uint8_t sendContentCounter = 0; + +bool CrossPointWebServer::sendContentSafe(const char* content) const { + if (!server || !server->client().connected()) { + return false; + } + + // Send the content + server->sendContent(content); + + // Flow control: give TCP stack time to transmit data and drain the send buffer + // The ESP32 TCP buffer is limited and fills quickly when streaming many small chunks. + // We use progressive delays: + // - yield() after every send to allow WiFi processing + // - delay(5ms) every send to allow buffer draining + // - delay(50ms) every 10 sends to allow larger buffer flush + yield(); + sendContentCounter++; + + if (sendContentCounter >= 10) { + sendContentCounter = 0; + delay(50); // Longer pause every 10 sends for buffer catchup + } else { + delay(5); // Short pause each send + } + + return server->client().connected(); +} + +bool CrossPointWebServer::sendContentSafe(const String& content) const { + return sendContentSafe(content.c_str()); +} + bool CrossPointWebServer::copyFile(const String& srcPath, const String& destPath) const { FsFile srcFile; FsFile destFile; diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index 050f7eb..6f7e59f 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -108,6 +108,11 @@ class CrossPointWebServer { bool copyFile(const String& srcPath, const String& destPath) const; bool copyFolder(const String& srcPath, const String& destPath) const; + // Helper for safe content sending with flow control + // Returns false if client disconnected, true otherwise + bool sendContentSafe(const char* content) const; + bool sendContentSafe(const String& content) const; + // List management handlers void handleListGet() const; void handleListPost() const;