diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 23fafea6..b7b27c68 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -1589,6 +1589,8 @@ bool installRemoteFamily(const RemoteManifestFamily& family, const std::string& for (const auto& file : family.files) { esp_task_wdt_reset(); + yield(); + delay(500); // allow network stack to clean up sockets std::string localFilename = file.name; std::string familyPrefix = family.name + "/"; @@ -1740,6 +1742,7 @@ void CrossPointWebServer::handleFontDownload() { server->send(400, "application/json", "{\"ok\":false,\"error\":\"Invalid request\"}"); return; } + body.clear(); // Free memory early! const bool installAll = req["all"] | false; const std::string requestedFamily = req["family"] | ""; @@ -1784,14 +1787,26 @@ void CrossPointWebServer::handleFontDownload() { } } + std::vector targetCopies; + for (auto* f : targets) { + targetCopies.push_back(*f); + } + families.clear(); + families.shrink_to_fit(); + size_t installedCount = 0; - for (auto* family : targets) { + for (auto& family : targetCopies) { esp_task_wdt_reset(); - if (!installRemoteFamily(*family, baseUrl, installer, error)) { + yield(); + delay(500); // allow network stack to clean up sockets + + LOG_DBG("WEB", "Installing font family: %s", family.name.c_str()); + + if (!installRemoteFamily(family, baseUrl, installer, error)) { JsonDocument errDoc; errDoc["ok"] = false; errDoc["error"] = error; - errDoc["family"] = family->name; + errDoc["family"] = family.name; errDoc["installedCount"] = static_cast(installedCount); String out; serializeJson(errDoc, out); diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index ab6806a9..7f8aec68 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -66,9 +66,11 @@ bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent, const LOG_DBG("HTTP", "Fetching: %s", url.c_str()); http.begin(*client, url.c_str()); + http.setReuse(false); http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); http.setTimeout(30000); http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); + http.addHeader("Connection", "close"); if (!username.empty() || !password.empty()) { std::string credentials = username + ":" + password; @@ -80,12 +82,18 @@ bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent, const if (httpCode != HTTP_CODE_OK) { LOG_ERR("HTTP", "Fetch failed: %d", httpCode); http.end(); + if (client) { + client->stop(); + } return false; } http.writeToStream(&outContent); http.end(); + if (client) { + client->stop(); + } LOG_DBG("HTTP", "Fetch success"); return true; @@ -118,9 +126,11 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& LOG_DBG("HTTP", "Destination: %s", destPath.c_str()); http.begin(*client, url.c_str()); + http.setReuse(false); http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); http.setTimeout(65535); // max uint16_t (~65s) — HTTPClient::setTimeout takes uint16_t ms http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); + http.addHeader("Connection", "close"); if (!username.empty() || !password.empty()) { std::string credentials = username + ":" + password; @@ -132,6 +142,9 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& if (httpCode != HTTP_CODE_OK) { LOG_ERR("HTTP", "Download failed: %d", httpCode); http.end(); + if (client) { + client->stop(); + } return HTTP_ERROR; } @@ -156,9 +169,51 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& return FILE_ERROR; } + int writeResult = -1; + size_t downloaded = 0; + bool writeOk = true; + // Let HTTPClient handle chunked decoding and stream body bytes into the file. - FileWriteStream fileStream(file, contentLength, progress); - const int writeResult = http.writeToStream(&fileStream); + // For known sizes (Content-Length), we can stream it manually to save RAM! + // HTTPClient::writeToStream allocates a 4096-byte chunk on the heap which can + // fail (-8 / HTTPC_ERROR_TOO_LESS_RAM) if the heap is fragmented or depleted. + if (contentLength > 0) { + NetworkClient& stream = http.getStream(); + uint8_t buffer[1024]; + writeResult = 1; + while (http.connected() && downloaded < contentLength) { + size_t available = stream.available(); + if (available > 0) { + size_t toRead = available > sizeof(buffer) ? sizeof(buffer) : available; + if (downloaded + toRead > contentLength) { + toRead = contentLength - downloaded; + } + int readSize = stream.readBytes(buffer, toRead); + if (readSize > 0) { + if (file.write(buffer, readSize) != static_cast(readSize)) { + writeOk = false; + writeResult = -1; + break; + } + downloaded += readSize; + if (progress) progress(downloaded, contentLength); + } else { + break; + } + } else { + delay(1); + } + } + if (downloaded != contentLength) { + writeResult = -1; + } + } else { + // Chunked or unknown length fallback + FileWriteStream fileStream(file, contentLength, progress); + writeResult = http.writeToStream(&fileStream); + downloaded = fileStream.downloaded(); + writeOk = fileStream.ok(); + } // Flush before closing to ensure data is written to the SD card. // Without this, Storage.exists() might return false immediately after @@ -166,6 +221,9 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& file.flush(); file.close(); http.end(); + if (client) { + client->stop(); + } if (writeResult < 0) { LOG_ERR("HTTP", "writeToStream error: %d", writeResult); @@ -173,11 +231,10 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& return HTTP_ERROR; } - const size_t downloaded = fileStream.downloaded(); LOG_DBG("HTTP", "Downloaded %zu bytes", downloaded); // Guard against partial writes even if HTTPClient completes. - if (!fileStream.ok()) { + if (!writeOk) { LOG_ERR("HTTP", "Write failed during download"); Storage.remove(destPath.c_str()); return FILE_ERROR; diff --git a/src/network/html/FontsPage.html b/src/network/html/FontsPage.html index 910d8fc1..9147213e 100644 --- a/src/network/html/FontsPage.html +++ b/src/network/html/FontsPage.html @@ -148,6 +148,46 @@ align-items: center; margin-bottom: 12px; } + .download-layout { + display: grid; + grid-template-columns: minmax(240px, 1fr) 280px; + gap: 16px; + align-items: start; + } + .status-panel { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + } + .status-panel h3 { + margin: 0 0 10px 0; + font-size: 1rem; + } + .status-panel p { + margin: 0; + } + #remoteStatus { + margin-top: 8px; + min-height: 80px; + display: block; + border-radius: 6px; + padding: 12px; + background: var(--notice-bg); + border: 1px solid var(--notice-border); + color: var(--notice-color); + } + .status-ok { + background: #d4edda; + color: #155724; + border-color: #c3e6cb; + } + .status-err { + background: #f8d7da; + color: #721c24; + border-color: #f5c6cb; + } @@ -171,8 +211,13 @@ -

Catalog not loaded

-
+
+

Catalog not loaded

+
+

Download Status

+

Waiting for action...

+
+