diff --git a/tools/cdata.js b/tools/cdata.js index 5ae7088b3e..bb884b9a80 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -26,7 +26,7 @@ const packageJson = require("../package.json"); // Export functions for testing module.exports = { isFileNewerThan, isAnyFileInFolderNewerThan }; -const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_pixelforge.h", "wled00/html_settings.h", "wled00/html_other.h", "wled00/js_iro.h", "wled00/js_omggif.h"] +const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_pixelforge.h", "wled00/html_settings.h", "wled00/html_other.h", "wled00/html_log.h", "wled00/js_iro.h", "wled00/js_omggif.h"] // \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset const wledBanner = ` @@ -478,3 +478,18 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()====="; ], "wled00/html_other.h" ); + +// Log viewer page — compiled into its own header so it can be conditionally +// included only on BOARD_HAS_PSRAM builds without bloating other targets. +writeChunks( + "wled00/data", + [ + { + file: "log.htm", + name: "PAGE_log", + method: "gzip", + filter: "html-minify", + } + ], + "wled00/html_log.h" +); diff --git a/wled00/data/log.htm b/wled00/data/log.htm new file mode 100644 index 0000000000..9a542887cb --- /dev/null +++ b/wled00/data/log.htm @@ -0,0 +1,164 @@ + + + + + + + WLED Log Viewer + + + + +

WLED Log Viewer

+
+ + + + + + -- KB + +
+ + + diff --git a/wled00/data/settings.htm b/wled00/data/settings.htm index ef20671c87..3ac1750942 100644 --- a/wled00/data/settings.htm +++ b/wled00/data/settings.htm @@ -51,5 +51,6 @@ + \ No newline at end of file diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index ffb2c1202f..1d48865c4f 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -178,6 +178,7 @@ void serializeInfo(JsonObject root); void serializeModeNames(JsonArray arr); void serializePins(JsonObject root); void serveJson(AsyncWebServerRequest* request); +void serveLog(AsyncWebServerRequest* request); #ifdef WLED_ENABLE_JSONLIVE bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); #endif diff --git a/wled00/json.cpp b/wled00/json.cpp index 74e12b5470..524841150e 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1314,6 +1314,9 @@ void serveJson(AsyncWebServerRequest* request) else if (url.indexOf(F("net")) > 0) subJson = json_target::networks; else if (url.indexOf(F("cfg")) > 0) subJson = json_target::config; else if (url.indexOf(F("pins")) > 0) subJson = json_target::pins; + #ifdef BOARD_HAS_PSRAM + else if (url.indexOf(F("log")) > 0) { serveLog(request); return; } + #endif #ifdef WLED_ENABLE_JSONLIVE else if (url.indexOf("live") > 0) { serveLiveLeds(request); @@ -1380,6 +1383,34 @@ void serveJson(AsyncWebServerRequest* request) request->send(response); } +// Serve the PSRAM log ring buffer as plain text over HTTP. +// Available only on BOARD_HAS_PSRAM builds; returns 501 otherwise. +void serveLog(AsyncWebServerRequest* request) +{ +#ifdef BOARD_HAS_PSRAM + if (!wledLogBuffer.available()) { + // PSRAM detected at compile time but not found / not initialised at runtime + request->send(503, FPSTR(CONTENT_TYPE_PLAIN), F("Log buffer unavailable (no PSRAM detected)")); + return; + } + + // Check for ?clear query parameter to wipe the buffer + if (request->hasParam(F("clear"))) { + wledLogBuffer.clear(); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Log buffer cleared.")); + return; + } + + AsyncResponseStream* response = request->beginResponseStream(FPSTR(CONTENT_TYPE_PLAIN)); + response->addHeader(F("Cache-Control"), F("no-store")); + wledLogBuffer.printTo(*response); + request->send(response); +#else + serveJsonError(request, 501, ERR_NOT_IMPL); +#endif +} + + #ifdef WLED_ENABLE_JSONLIVE #define MAX_LIVE_LEDS 256 diff --git a/wled00/log_buffer.h b/wled00/log_buffer.h new file mode 100644 index 0000000000..ed7b6993fb --- /dev/null +++ b/wled00/log_buffer.h @@ -0,0 +1,114 @@ +#pragma once +/* + * log_buffer.h — PSRAM-backed ring buffer for capturing log output. + * + * When a device has PSRAM (BOARD_HAS_PSRAM), a 32 KB ring buffer is + * allocated at runtime (after psramFound() confirms the hardware). + * Log entries written via WLED_LOG() / WLED_LOGF() are stored there and + * exposed through the /json/log HTTP endpoint and the /log web UI page, + * so end-users can retrieve diagnostic information without serial access + * or custom builds. + * + * Two thin helpers are provided: + * + * LogBuffer – bare ring buffer over a ps_malloc'd char array. + * LogPrint – Arduino Print subclass that writes into a LogBuffer. + * + * Thread / ISR safety: writes use a portMUX spinlock so the buffer + * remains coherent when both the main loop and the AsyncWebServer task + * call WLED_LOG() concurrently. + */ + +#include +#include + +class LogBuffer { +public: + // Default capacity stored in PSRAM. Raising this value is safe as long as + // the device has enough PSRAM; it has no effect when PSRAM is absent. + static constexpr size_t CAPACITY = 32 * 1024; // 32 KB + + LogBuffer() + : _buf(nullptr), _head(0), _used(0) + { + _mux = portMUX_INITIALIZER_UNLOCKED; + } + + // Allocate the PSRAM backing store. Call once after psramFound() == true. + // Returns true on success. + bool init() { + _buf = static_cast(ps_malloc(CAPACITY)); + return _buf != nullptr; + } + + bool available() const { return _buf != nullptr; } + size_t size() const { return _used; } + + // Write raw bytes into the ring buffer (overwrites oldest data when full). + void write(const char* data, size_t len) { + if (!_buf || !len) return; + portENTER_CRITICAL(&_mux); + for (size_t i = 0; i < len; i++) { + _buf[_head] = data[i]; + _head = (_head + 1) % CAPACITY; + if (_used < CAPACITY) _used++; + } + portEXIT_CRITICAL(&_mux); + } + + // Stream the ring-buffer contents (oldest byte first) into any Print sink. + // Returns the number of bytes emitted. + size_t printTo(Print& out) const { + if (!_buf || !_used) return 0; + if (_used < CAPACITY) { + // Buffer has not wrapped yet — data starts at index 0. + out.write(reinterpret_cast(_buf), _used); + return _used; + } + // Buffer has wrapped — oldest byte is at _head. + out.write(reinterpret_cast(_buf + _head), CAPACITY - _head); + out.write(reinterpret_cast(_buf), _head); + return CAPACITY; + } + + void clear() { + portENTER_CRITICAL(&_mux); + _head = 0; + _used = 0; + portEXIT_CRITICAL(&_mux); + } + +private: + char* _buf; // PSRAM allocation (nullptr until init()) + size_t _head; // Next write position (= oldest byte when full) + size_t _used; // Bytes written, capped at CAPACITY + portMUX_TYPE _mux; // Spinlock for concurrent write protection +}; + + +// Arduino Print subclass that forwards write() calls into a LogBuffer. +// Use this as the target of DEBUGOUT to capture all debug output. +class LogPrint : public Print { +public: + explicit LogPrint(LogBuffer& log) : _log(log) {} + + size_t write(uint8_t c) override { + char ch = static_cast(c); + _log.write(&ch, 1); + return 1; + } + + size_t write(const uint8_t* buf, size_t size) override { + _log.write(reinterpret_cast(buf), size); + return size; + } + + LogBuffer& buffer() { return _log; } + +private: + LogBuffer& _log; +}; + + +extern LogBuffer wledLogBuffer; // PSRAM ring buffer (allocated in wled.cpp) +extern LogPrint wledLog; // Print wrapper around wledLogBuffer diff --git a/wled00/wled.cpp b/wled00/wled.cpp index eb6019e6bf..b323369511 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -424,8 +424,13 @@ void WLED::setup() DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); #if defined(BOARD_HAS_PSRAM) - // if JSON buffer allocation fails requestJsonBufferLock() will always return false preventing crashes + // Initialise the PSRAM log ring buffer before the JSON document so the + // allocation is attempted first (it is non-critical if it fails). if (psramFound() && ESP.getPsramSize()) { + if (wledLogBuffer.init()) { + DEBUG_PRINTF_P(PSTR("WLED %s log buffer ready (%u KB)\n"), versionString, (unsigned)(LogBuffer::CAPACITY / 1024)); + } + // if JSON buffer allocation fails requestJsonBufferLock() will always return false preventing crashes pDoc = new PSRAMDynamicJsonDocument(2 * JSON_BUFFER_SIZE); DEBUG_PRINTF_P(PSTR("JSON buffer size: %ubytes\n"), (2 * JSON_BUFFER_SIZE)); DEBUG_PRINTF_P(PSTR("PSRAM: %dkB/%dkB\n"), ESP.getFreePsram()/1024, ESP.getPsramSize()/1024); @@ -873,9 +878,7 @@ void WLED::handleConnection() static bool scanDone = true; static byte stacO = 0; const unsigned long now = millis(); - #ifdef WLED_DEBUG - const unsigned long nowS = now/1000; - #endif + const unsigned long nowS = now/1000; // seconds since boot, used in debug messages const bool wifiConfigured = WLED_WIFI_CONFIGURED; // ignore connection handling if WiFi is configured and scan still running diff --git a/wled00/wled.h b/wled00/wled.h index 1a5f1b143e..9776e0ffe6 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -974,6 +974,18 @@ WLED_GLOBAL JsonDocument *pDoc _INIT(&gDoc); #endif WLED_GLOBAL volatile uint8_t jsonBufferLock _INIT(0); +// PSRAM-backed log ring buffer. +// On BOARD_HAS_PSRAM builds the DEBUG_* macros always write to the ring +// buffer so logs are retrievable via /json/log without serial access or a +// WLED_DEBUG build. WLED_DEBUG additionally routes output to Serial/NetDebug. +// Allocated at runtime only when psramFound() succeeds; writes before init() +// or on devices without physical PSRAM are silently dropped. +#ifdef BOARD_HAS_PSRAM + #include "log_buffer.h" + WLED_GLOBAL LogBuffer wledLogBuffer; + WLED_GLOBAL LogPrint wledLog _INIT(LogPrint(wledLogBuffer)); +#endif + // enable additional debug output #if defined(WLED_DEBUG_HOST) #include "net_debug.h" @@ -990,15 +1002,32 @@ WLED_GLOBAL volatile uint8_t jsonBufferLock _INIT(0); #define DEBUGOUT Serial #endif -#ifdef WLED_DEBUG +#if defined(BOARD_HAS_PSRAM) && defined(WLED_DEBUG) + // PSRAM + serial debug: ring buffer AND Serial/NetDebug + #ifndef ESP8266 + #include + #endif + #define DEBUG_PRINT(x) do { DEBUGOUT.print(x); wledLog.print(x); } while(0) + #define DEBUG_PRINTLN(x) do { DEBUGOUT.println(x); wledLog.println(x); } while(0) + #define DEBUG_PRINTF(x...) do { DEBUGOUT.printf(x); wledLog.printf(x); } while(0) + #define DEBUG_PRINTF_P(x...) do { DEBUGOUT.printf_P(x); wledLog.printf_P(x); } while(0) +#elif defined(BOARD_HAS_PSRAM) + // PSRAM only: ring buffer, no serial output + #define DEBUG_PRINT(x) wledLog.print(x) + #define DEBUG_PRINTLN(x) wledLog.println(x) + #define DEBUG_PRINTF(x...) wledLog.printf(x) + #define DEBUG_PRINTF_P(x...) wledLog.printf_P(x) +#elif defined(WLED_DEBUG) + // Serial debug only: original behaviour #ifndef ESP8266 #include #endif - #define DEBUG_PRINT(x) DEBUGOUT.print(x) - #define DEBUG_PRINTLN(x) DEBUGOUT.println(x) - #define DEBUG_PRINTF(x...) DEBUGOUT.printf(x) + #define DEBUG_PRINT(x) DEBUGOUT.print(x) + #define DEBUG_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUG_PRINTF(x...) DEBUGOUT.printf(x) #define DEBUG_PRINTF_P(x...) DEBUGOUT.printf_P(x) #else + // No PSRAM, no debug: no-ops #define DEBUG_PRINT(x) #define DEBUG_PRINTLN(x) #define DEBUG_PRINTF(x...) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 0b4d0fb546..ce68924fde 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -20,6 +20,10 @@ #include "html_cpal.h" #include "html_edit.h" +#ifdef BOARD_HAS_PSRAM + #include "html_log.h" +#endif + // forward declarations static void createEditHandler(); @@ -618,6 +622,18 @@ void initServer() } }); +#ifdef BOARD_HAS_PSRAM + // Log viewer page — only meaningful on PSRAM devices where the ring buffer + // is available. Served from the compiled-in html_log.h header. + static const char _log_htm[] PROGMEM = "/log.htm"; + server.on(_log_htm, HTTP_GET, [](AsyncWebServerRequest *request) { + handleStaticContent(request, FPSTR(_log_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_log, PAGE_log_length); + }); + server.on(F("/log"), HTTP_GET, [](AsyncWebServerRequest *request) { + handleStaticContent(request, FPSTR(_log_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_log, PAGE_log_length); + }); +#endif + #ifndef WLED_DISABLE_2D #ifdef WLED_ENABLE_PIXART static const char _pixart_htm[] PROGMEM = "/pixart.htm"; diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 812ef8c207..48c2cbc8d3 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -216,6 +216,9 @@ void getSettingsJS(byte subPage, Print& settingsScript) #ifdef WLED_ENABLE_DMX // include only if DMX is enabled settingsScript.print(F("gId('dmxbtn').style.display='';")); #endif + #ifdef BOARD_HAS_PSRAM + settingsScript.print(F("gId('logbtn').style.display='';")); + #endif } if (subPage == SUBPAGE_WIFI)