From 4fb1cf46e64f1b7b1965cacaedffd2f6ecb97714 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Mon, 4 May 2026 12:49:38 +0100 Subject: [PATCH 1/3] Remove usermods from release --- .github/platformio_release.ini.template | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/platformio_release.ini.template b/.github/platformio_release.ini.template index 499243822b..174dc4cd7f 100644 --- a/.github/platformio_release.ini.template +++ b/.github/platformio_release.ini.template @@ -4,7 +4,7 @@ ; Copied to platformio_release.ini by the release CI workflow ; (.github/workflows/release.yml -> build.yml with `release: true`) ; in order to extend the matrix of `default_envs` built and published -; for tagged releases. +; for tagged releases and remove the debugging usersmods env ; ; This file overrides `[platformio].default_envs` from platformio.ini via ; `extra_configs`. It MUST list every env that should be released - including @@ -35,7 +35,6 @@ default_envs = nodemcuv2 esp32s3dev_8MB_opi esp32s3dev_8MB_qspi esp32s3_4M_qspi - usermods ; HUB75 release-only envs esp32dev_hub75 esp32dev_hub75_forum_pinout From afd8c5dd6aa6142e41c1c3139b88141d5748c661 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 6 May 2026 13:27:10 +0100 Subject: [PATCH 2/3] docs: clarify when usermod IDs are required Add explicit guidance explaining that a unique USERMOD_ID_* is only needed when a usermod uses inter-mod communication, pin ownership via pinManager, or needs to be identifiable in JSON info output. Updates both AGENTS.md and the const.h comment block to reflect this. --- AGENTS.md | 12 +++++++++++- wled00/const.h | 11 ++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cd18ff5c57..733a17d7ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -159,7 +159,17 @@ static MyUsermod myUsermod; REGISTER_USERMOD(myUsermod); ``` -- Add usermod IDs to `wled00/const.h` +### Usermod IDs + +A unique ID (registered in `wled00/const.h` and overriding `getId()`) is **only required** when a usermod needs one or more of the following: + +1. **Inter-usermod communication** — another usermod or an FX effect calls `UsermodManager::lookup(mod_id)` or `UsermodManager::getUMData(..., mod_id)` to find or request data from this specific usermod. +2. **Pin ownership via `pinManager`** — the usermod allocates GPIO pins through `pinManager`. Pin ownership is tracked by `PinOwner` enum values that map directly to `USERMOD_ID_*` constants (see `wled00/pin_manager.h`). This prevents pin-conflict bugs. +3. **Identification in JSON info** — `UsermodManager::addToJsonInfo` emits each mod's ID into the `"um"` array; a unique ID makes the mod identifiable in that output. + +If none of the above apply, the usermod may omit `getId()` (or return the default `USERMOD_ID_UNSPECIFIED`) and does **not** need an entry in `const.h`. + +- Add usermod IDs to `wled00/const.h` **only when a unique ID is required** (see above) - Activate via `custom_usermods` in platformio build config - Base new usermods on `usermods/EXAMPLE/` (never edit the example directly) - Store repeated strings as `static const char[] PROGMEM` diff --git a/wled00/const.h b/wled00/const.h index 62d9c45f4d..70373316fd 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -165,9 +165,14 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define WLED_MAX_PANELS 18 // must not be more than 32 -//Usermod IDs -#define USERMOD_ID_RESERVED 0 //Unused. Might indicate no usermod present -#define USERMOD_ID_UNSPECIFIED 1 //Default value for a general user mod that does not specify a custom ID +// Usermod IDs +// A unique ID is only required when a usermod needs one or more of: +// 1. Inter-usermod communication: UsermodManager::lookup(mod_id) or getUMData(..., mod_id) +// 2. Pin ownership via pinManager: PinOwner enum entries map to these IDs (see pin_manager.h) +// 3. Identification in JSON info: addToJsonInfo emits each mod's ID into the "um" array +// If none of the above apply, omit getId() (or return USERMOD_ID_UNSPECIFIED) and do NOT add an entry here. +#define USERMOD_ID_RESERVED 0 //Unused. Reserved; may indicate no usermod present +#define USERMOD_ID_UNSPECIFIED 1 //Default for usermods that do not require a unique ID #define USERMOD_ID_EXAMPLE 2 //Usermod "usermod_v2_example.h" #define USERMOD_ID_TEMPERATURE 3 //Usermod "usermod_temperature.h" #define USERMOD_ID_FIXNETSERVICES 4 //Usermod "usermod_Fix_unreachable_netservices.h" From e6eacb9aac64e38003688f0327474ccce6d12fa1 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 9 May 2026 10:21:26 +0100 Subject: [PATCH 3/3] feat: add PSRAM-backed debug log buffer with /log viewer On BOARD_HAS_PSRAM builds, DEBUG_* macros always route to a 32 KB PSRAM ring buffer (LogBuffer) regardless of WLED_DEBUG. The buffer is exposed via /json/log (plain-text streaming, ?clear support) and a /log web UI page with refresh, auto-refresh, copy and clear controls. A 'Diagnostic Log' button is shown on the Settings menu on PSRAM builds. --- tools/cdata.js | 17 +++- wled00/data/log.htm | 164 +++++++++++++++++++++++++++++++++++++++ wled00/data/settings.htm | 1 + wled00/fcn_declare.h | 1 + wled00/json.cpp | 31 ++++++++ wled00/log_buffer.h | 114 +++++++++++++++++++++++++++ wled00/wled.cpp | 11 ++- wled00/wled.h | 37 ++++++++- wled00/wled_server.cpp | 16 ++++ wled00/xml.cpp | 3 + 10 files changed, 386 insertions(+), 9 deletions(-) create mode 100644 wled00/data/log.htm create mode 100644 wled00/log_buffer.h 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)