From a490e9cfb57526d25bd8c8b910eac902a8083eaf Mon Sep 17 00:00:00 2001 From: M-Factory Date: Fri, 14 Nov 2025 10:43:12 +0900 Subject: [PATCH] Web UI: Provision embedded HTML to LittleFS via streaming to prevent memory crashes and stabilize serving. This update changes the root web handler to serve the configuration page strictly from LittleFS. On the first boot, the large HTML content is safely copied from the embedded PROGMEM string to a file using a streaming write loop, which prevents heap fragmentation and subsequent memory allocation failures. This guarantees stable and reliable loading of the configuration page across all devices. --- ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino | 66 +++++++++++++++++---- ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino | 66 +++++++++++++++++---- 2 files changed, 112 insertions(+), 20 deletions(-) diff --git a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino index b9ea51f..441a2d8 100644 --- a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino +++ b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino @@ -697,11 +697,7 @@ void setupWebServer() { server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { Serial.println(F("[WEBSERVER] Request: /")); -#ifdef ESP8266 - request->send_P(200, "text/html", index_html); -#else - request->send(200, "text/html", FPSTR(index_html)); -#endif + request->send(LittleFS, "/index.html", "text/html"); }); server.on("/generate_204", HTTP_GET, handleCaptivePortal); // Android @@ -712,16 +708,16 @@ void setupWebServer() { server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(204); // 204 No Content response }); - server.on("/apple-touch-icon.png", HTTP_GET, [](AsyncWebServerRequest *request) { // iOS icon check + server.on("/apple-touch-icon.png", HTTP_GET, [](AsyncWebServerRequest *request) { // iOS icon check request->send(204); }); - server.on("/gen_204", HTTP_GET, [](AsyncWebServerRequest *request) { // Android short probe (already in handleCaptivePortal, but safe to also silence if somehow missed) + server.on("/gen_204", HTTP_GET, [](AsyncWebServerRequest *request) { // Android short probe (already in handleCaptivePortal, but safe to also silence if somehow missed) request->send(204); }); - server.on("/library/test/success.html", HTTP_GET, [](AsyncWebServerRequest *request) { // iOS/macOS generic check + server.on("/library/test/success.html", HTTP_GET, [](AsyncWebServerRequest *request) { // iOS/macOS generic check request->send(204); }); - server.on("/connecttest.txt", HTTP_GET, [](AsyncWebServerRequest *request) { // Windows NCSI check + server.on("/connecttest.txt", HTTP_GET, [](AsyncWebServerRequest *request) { // Windows NCSI check request->send(204); }); server.on("/msdownload/update/v3/static/trustedr/en/disallowedcertstl.cab", HTTP_GET, [](AsyncWebServerRequest *request) { @@ -1579,7 +1575,7 @@ void setupWebServer() { delay(500); // --- Remove configuration and uptime files --- - const char *filesToRemove[] = { "/config.json", "/uptime.dat" }; + const char *filesToRemove[] = { "/config.json", "/uptime.dat", "/index.html" }; for (auto &file : filesToRemove) { if (LittleFS.exists(file)) { if (LittleFS.remove(file)) { @@ -2157,6 +2153,7 @@ void setup() { } Serial.println(F("[SETUP] LittleFS file system mounted successfully.")); loadUptime(); + ensureHtmlFileExists(); P.begin(); // Initialize Parola library P.setCharSpacing(0); @@ -2192,6 +2189,55 @@ void setup() { saveUptime(); } +void ensureHtmlFileExists() { + Serial.println(F("[FS] Checking for /index.html on LittleFS...")); + + // If the file exists, we're done (the file was provisioned on a previous boot). + if (LittleFS.exists("/index.html")) { + Serial.println(F("[FS] /index.html found. Using file system version.")); + return; + } + + Serial.println(F("[FS] /index.html NOT found. Writing embedded content to LittleFS...")); + + // Open the file for writing + File f = LittleFS.open("/index.html", "w"); + if (!f) { + Serial.println(F("[FS] ERROR: Failed to create /index.html for writing!")); + // Since we are now serving from LittleFS, failing here means the web page will be unavailable + // until a new file system write is successful. + return; + } + + // Write the entire PROGMEM string to the file character-by-character to prevent + // the memory exception (Exception 3) that occurs when trying to buffer a large string at once + // on the ESP8266 heap. + size_t htmlLength = strlen_P(index_html); + size_t bytesWritten = 0; + + for (size_t i = 0; i < htmlLength; i++) { + // Safely read one byte from PROGMEM + char c = pgm_read_byte_near(index_html + i); + + // Write the byte to the file + if (f.write((uint8_t *)&c, 1) == 1) { + bytesWritten++; + } else { + Serial.printf("[FS] Write failure at character %u. Aborting write.\n", i); + f.close(); + return; // Stop on first error + } + } + + f.close(); + + if (bytesWritten == htmlLength) { + Serial.printf("[FS] Successfully wrote %u bytes to /index.html.\n", bytesWritten); + } else { + // This case should ideally not happen with the loop above unless a catastrophic failure occurred + Serial.printf("[FS] WARNING: Only wrote %u of %u bytes to /index.html (might be incomplete).\n", bytesWritten, htmlLength); + } +} void advanceDisplayMode() { prevDisplayMode = displayMode; diff --git a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino index d7c68b8..5640771 100644 --- a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino +++ b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino @@ -694,11 +694,7 @@ void setupWebServer() { server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { Serial.println(F("[WEBSERVER] Request: /")); -#ifdef ESP8266 - request->send_P(200, "text/html", index_html); -#else - request->send(200, "text/html", FPSTR(index_html)); -#endif + request->send(LittleFS, "/index.html", "text/html"); }); server.on("/generate_204", HTTP_GET, handleCaptivePortal); // Android @@ -709,16 +705,16 @@ void setupWebServer() { server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(204); // 204 No Content response }); - server.on("/apple-touch-icon.png", HTTP_GET, [](AsyncWebServerRequest *request) { // iOS icon check + server.on("/apple-touch-icon.png", HTTP_GET, [](AsyncWebServerRequest *request) { // iOS icon check request->send(204); }); - server.on("/gen_204", HTTP_GET, [](AsyncWebServerRequest *request) { // Android short probe (already in handleCaptivePortal, but safe to also silence if somehow missed) + server.on("/gen_204", HTTP_GET, [](AsyncWebServerRequest *request) { // Android short probe (already in handleCaptivePortal, but safe to also silence if somehow missed) request->send(204); }); - server.on("/library/test/success.html", HTTP_GET, [](AsyncWebServerRequest *request) { // iOS/macOS generic check + server.on("/library/test/success.html", HTTP_GET, [](AsyncWebServerRequest *request) { // iOS/macOS generic check request->send(204); }); - server.on("/connecttest.txt", HTTP_GET, [](AsyncWebServerRequest *request) { // Windows NCSI check + server.on("/connecttest.txt", HTTP_GET, [](AsyncWebServerRequest *request) { // Windows NCSI check request->send(204); }); server.on("/msdownload/update/v3/static/trustedr/en/disallowedcertstl.cab", HTTP_GET, [](AsyncWebServerRequest *request) { @@ -1575,7 +1571,7 @@ void setupWebServer() { delay(500); // --- Remove configuration and uptime files --- - const char *filesToRemove[] = { "/config.json", "/uptime.dat" }; + const char *filesToRemove[] = { "/config.json", "/uptime.dat", "/index.html" }; for (auto &file : filesToRemove) { if (LittleFS.exists(file)) { if (LittleFS.remove(file)) { @@ -2149,6 +2145,7 @@ void setup() { } Serial.println(F("[SETUP] LittleFS file system mounted successfully.")); loadUptime(); + ensureHtmlFileExists(); P.begin(); // Initialize Parola library P.setCharSpacing(0); @@ -2184,6 +2181,55 @@ void setup() { saveUptime(); } +void ensureHtmlFileExists() { + Serial.println(F("[FS] Checking for /index.html on LittleFS...")); + + // If the file exists, we're done (the file was provisioned on a previous boot). + if (LittleFS.exists("/index.html")) { + Serial.println(F("[FS] /index.html found. Using file system version.")); + return; + } + + Serial.println(F("[FS] /index.html NOT found. Writing embedded content to LittleFS...")); + + // Open the file for writing + File f = LittleFS.open("/index.html", "w"); + if (!f) { + Serial.println(F("[FS] ERROR: Failed to create /index.html for writing!")); + // Since we are now serving from LittleFS, failing here means the web page will be unavailable + // until a new file system write is successful. + return; + } + + // Write the entire PROGMEM string to the file character-by-character to prevent + // the memory exception (Exception 3) that occurs when trying to buffer a large string at once + // on the ESP8266 heap. + size_t htmlLength = strlen_P(index_html); + size_t bytesWritten = 0; + + for (size_t i = 0; i < htmlLength; i++) { + // Safely read one byte from PROGMEM + char c = pgm_read_byte_near(index_html + i); + + // Write the byte to the file + if (f.write((uint8_t *)&c, 1) == 1) { + bytesWritten++; + } else { + Serial.printf("[FS] Write failure at character %u. Aborting write.\n", i); + f.close(); + return; // Stop on first error + } + } + + f.close(); + + if (bytesWritten == htmlLength) { + Serial.printf("[FS] Successfully wrote %u bytes to /index.html.\n", bytesWritten); + } else { + // This case should ideally not happen with the loop above unless a catastrophic failure occurred + Serial.printf("[FS] WARNING: Only wrote %u of %u bytes to /index.html (might be incomplete).\n", bytesWritten, htmlLength); + } +} void advanceDisplayMode() { prevDisplayMode = displayMode;