diff --git a/src/main.cpp b/src/main.cpp index 74e1d56..ba25faa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -693,12 +693,12 @@ String getState() jsonState["playing"] = audio.isRunning(); - if (currentNode != nullptr) + if (currentNode != nullptr && currentNode->getCurrentPlaying() != nullptr) jsonState["title"] = *currentNode->getCurrentPlaying(); else jsonState["title"] = "Stopped"; - if (currentNode != nullptr) + if (currentNode != nullptr && currentNode->getCurrentPlaying() != nullptr) jsonState["filepath"] = currentNode->getCurrentPlayingFilePath(); else jsonState["filepath"] = ""; @@ -1105,191 +1105,98 @@ void readRFID() lastInteraction = millis(); } +static void serveStaticFile(AsyncWebServerRequest *request, + const String &plainPath, + const String &gzPath, + const char *contentType, + const char *cacheControl, + const __FlashStringHelper *notFoundMsg, + bool allowGzip = true) +{ + webreq_enter(); + // Ensure SD is active and RFID is deactivated while serving files. + deactivateRFID(); + activateSD(); + + // Prefer gz if present + bool useGz = allowGzip && SD.exists(gzPath); + const String &sendPath = useGz ? gzPath : plainPath; + + if (SD.exists(sendPath)) + { +#ifdef DEBUG + Serial.printf("Serving %s heap=%u webreq_cnt=%u\n", sendPath.c_str(), (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt); +#endif + // Chunked streaming with short SD lock per read + struct FileCtx { File f; }; + FileCtx *ctx = new FileCtx(); + ctx->f = SD.open(sendPath); + if (!ctx->f) { delete ctx; request->send(500, txt_plain, F("Open failed")); webreq_exit(); return; } + auto resp = request->beginChunkedResponse(contentType, + [ctx](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + size_t toRead = maxLen; + if (toRead > 512) toRead = 512; + sd_lock_acquire(); + size_t n = ctx->f.read(buffer, toRead); + sd_lock_release(); + if (n == 0) { ctx->f.close(); } + return n; + }); + resp->addHeader(hdr_cache_control_key, cacheControl); + resp->addHeader(hdr_connection_key, hdr_connection_val); + if (useGz) { + resp->addHeader(F("Content-Encoding"), F("gzip")); + } + // Ensure FileCtx cleanup even on aborted connections + request->onDisconnect([ctx](){ + sd_lock_acquire(); + if (ctx->f) ctx->f.close(); + sd_lock_release(); + delete ctx; + webreq_exit(); + }); + request->send(resp); + } + else + { + // Fallback: 404 for missing asset + request->send(404, txt_plain, notFoundMsg); + webreq_exit(); + } +} + void init_webserver() { -server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { - webreq_enter(); - /* onDisconnect set later with cleanup */ - deactivateRFID(); - activateSD(); static String htmlPath = ""; static String htmlPathGz = ""; if (htmlPath.isEmpty()) { htmlPath = getSysDir(index_file); htmlPathGz = htmlPath + F(".gz"); } - - // Prefer gz if present - bool useGz = SD.exists(htmlPathGz); - const String &sendPath = useGz ? htmlPathGz : htmlPath; - - if (SD.exists(sendPath)) - { -#ifdef DEBUG - Serial.printf("Serving %s heap=%u webreq_cnt=%u\n", sendPath.c_str(), (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt); -#endif - // Chunked streaming with short SD lock per read - struct FileCtx { File f; }; - FileCtx* ctx = new FileCtx(); - ctx->f = SD.open(sendPath); - if (!ctx->f) { delete ctx; request->send(500, txt_plain, F("Open failed")); return; } - auto response = request->beginChunkedResponse(String(txt_html_charset), - [ctx](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { - size_t toRead = maxLen; - if (toRead > 512) toRead = 512; - sd_lock_acquire(); - size_t n = ctx->f.read(buffer, toRead); - sd_lock_release(); - if (n == 0) { ctx->f.close(); } - return n; - }); - response->addHeader(hdr_cache_control_key, hdr_cache_control_val); - response->addHeader(hdr_connection_key, hdr_connection_val); - if (useGz) { - response->addHeader(F("Content-Encoding"), F("gzip")); - } - // Ensure FileCtx cleanup even on aborted connections - request->onDisconnect([ctx](){ - sd_lock_acquire(); - if (ctx->f) ctx->f.close(); - sd_lock_release(); - delete ctx; - webreq_exit(); - }); - request->send(response); - } - else - { - // Fallback: serve minimal error if file not found - request->send(404, txt_plain, F("ERROR: /system/index.html(.gz) not found!")); - } - + serveStaticFile(request, htmlPath, htmlPathGz, txt_html_charset, hdr_cache_control_val, F("ERROR: /system/index.html(.gz) not found!"), true); }); server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request) { - webreq_enter(); - /* onDisconnect set later with cleanup */ - deactivateRFID(); - activateSD(); - // Ensure SD is active and RFID is deactivated while serving files. static String cssPath = ""; static String cssPathGz = ""; if (cssPath.isEmpty()) { cssPath = getSysDir(style_file); cssPathGz = cssPath + F(".gz"); } - - // Prefer gz if present - bool useGz = SD.exists(cssPathGz); - const String &sendPath = useGz ? cssPathGz : cssPath; - - if (SD.exists(sendPath)) - { -#ifdef DEBUG - Serial.printf("Serving %s heap=%u webreq_cnt=%u\n", sendPath.c_str(), (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt); -#endif - { - // Chunked streaming with short SD lock per read - struct FileCtx { File f; }; - FileCtx* ctx = new FileCtx(); - ctx->f = SD.open(sendPath); - if (!ctx->f) { delete ctx; request->send(500, txt_plain, F("Open failed")); return; } - auto resp = request->beginChunkedResponse(F("text/css"), - [ctx](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { - size_t toRead = maxLen; - if (toRead > 512) toRead = 512; - sd_lock_acquire(); - size_t n = ctx->f.read(buffer, toRead); - sd_lock_release(); - if (n == 0) { ctx->f.close(); } - return n; - }); - resp->addHeader(hdr_cache_control_key, F("public, max-age=300")); - resp->addHeader(hdr_connection_key, hdr_connection_val); - if (useGz) { - resp->addHeader(F("Content-Encoding"), F("gzip")); - } - // Ensure FileCtx cleanup even on aborted connections - request->onDisconnect([ctx](){ - sd_lock_acquire(); - if (ctx->f) ctx->f.close(); - sd_lock_release(); - delete ctx; - webreq_exit(); - }); - request->send(resp); - } - } - else - { - // Fallback: serve minimal CSS if file not found - request->send(404, txt_plain, F("ERROR: /system/style.css(.gz) not found!")); - } - + serveStaticFile(request, cssPath, cssPathGz, "text/css", "public, max-age=300", F("ERROR: /system/style.css(.gz) not found!"), true); }); server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request) { - webreq_enter(); - /* onDisconnect set later with cleanup */ - deactivateRFID(); - activateSD(); - static String jsPath = ""; static String jsPathGz = ""; if (jsPath.isEmpty()) { jsPath = getSysDir(script_file); jsPathGz = jsPath + F(".gz"); } - - // Prefer gz if present - bool useGz = SD.exists(jsPathGz); - const String &sendPath = useGz ? jsPathGz : jsPath; - - if (SD.exists(sendPath)) - { -#ifdef DEBUG - Serial.printf("Serving %s heap=%u webreq_cnt=%u\n", sendPath.c_str(), (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt); -#endif - { - // Chunked streaming with short SD lock per read - struct FileCtx { File f; }; - FileCtx* ctx = new FileCtx(); - ctx->f = SD.open(sendPath); - if (!ctx->f) { delete ctx; request->send(500, txt_plain, F("Open failed")); return; } - auto resp = request->beginChunkedResponse(F("application/javascript"), - [ctx](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { - size_t toRead = maxLen; - if (toRead > 512) toRead = 512; - sd_lock_acquire(); - size_t n = ctx->f.read(buffer, toRead); - sd_lock_release(); - if (n == 0) { ctx->f.close(); } - return n; - }); - resp->addHeader(hdr_cache_control_key, F("public, max-age=300")); - resp->addHeader(hdr_connection_key, hdr_connection_val); - if (useGz) { - resp->addHeader(F("Content-Encoding"), F("gzip")); - } - // Ensure FileCtx cleanup even on aborted connections - request->onDisconnect([ctx](){ - sd_lock_acquire(); - if (ctx->f) ctx->f.close(); - sd_lock_release(); - delete ctx; - webreq_exit(); - }); - request->send(resp); - } - } - else - { - // Fallback: serve minimal JS if file not found - request->send(404, txt_plain, F("ERROR: /system/script.js(.gz) not found!")); - } - + serveStaticFile(request, jsPath, jsPathGz, "application/javascript", "public, max-age=300", F("ERROR: /system/script.js(.gz) not found!"), true); }); // Dynamic endpoints to avoid template processing heap spikes