-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Log buffer #5583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Log buffer #5583
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"> | ||
| <meta name="theme-color" content="#222222"> | ||
| <title>WLED Log Viewer</title> | ||
| <script> | ||
| // load common.js with retry on error | ||
| (function loadCommon() { | ||
| const l = document.createElement('script'); | ||
| l.src = 'common.js'; | ||
| l.onload = init; | ||
| l.onerror = () => setTimeout(loadCommon, 100); | ||
| document.head.appendChild(l); | ||
| })(); | ||
|
|
||
| var autoRefresh = null; | ||
|
|
||
| function init() { | ||
| getLoc(); | ||
| fetchLog(); | ||
| } | ||
|
|
||
| function fetchLog() { | ||
| fetch(getURL('/json/log')) | ||
| .then(r => { | ||
| if (!r.ok) { | ||
| if (r.status === 501) { | ||
| setStatus('Log buffer not available on this device (no PSRAM build).'); | ||
| } else if (r.status === 503) { | ||
| setStatus('Log buffer not ready \u2014 PSRAM not detected at runtime.'); | ||
| } else { | ||
| setStatus('Error fetching log: HTTP ' + r.status); | ||
| } | ||
| return null; | ||
| } | ||
| return r.text(); | ||
| }) | ||
| .then(txt => { | ||
| if (txt === null) return; | ||
| const ta = document.getElementById('log'); | ||
| ta.value = txt; | ||
| // Scroll to bottom so latest entries are visible | ||
| ta.scrollTop = ta.scrollHeight; | ||
| document.getElementById('sz').textContent = (txt.length / 1024).toFixed(1) + ' KB'; | ||
| }) | ||
| .catch(e => setStatus('Fetch error: ' + e)); | ||
| } | ||
|
|
||
| function clearLog() { | ||
| fetch(getURL('/json/log?clear')) | ||
| .then(r => r.text()) | ||
| .then(() => { | ||
| document.getElementById('log').value = ''; | ||
| document.getElementById('sz').textContent = '0 KB'; | ||
| setStatus('Log cleared.'); | ||
| }) | ||
| .catch(e => setStatus('Clear error: ' + e)); | ||
| } | ||
|
|
||
| function copyLog() { | ||
| const ta = document.getElementById('log'); | ||
| if (navigator.clipboard) { | ||
| navigator.clipboard.writeText(ta.value).then(() => setStatus('Copied to clipboard.')); | ||
| } else { | ||
| ta.select(); | ||
| document.execCommand('copy'); | ||
| setStatus('Copied to clipboard.'); | ||
| } | ||
| } | ||
|
|
||
| function toggleAuto() { | ||
| const btn = document.getElementById('autobtn'); | ||
| if (autoRefresh) { | ||
| clearInterval(autoRefresh); | ||
| autoRefresh = null; | ||
| btn.textContent = 'Auto-refresh: Off'; | ||
| btn.classList.remove('active'); | ||
| } else { | ||
| fetchLog(); | ||
| autoRefresh = setInterval(fetchLog, 5000); | ||
| btn.textContent = 'Auto-refresh: On'; | ||
| btn.classList.add('active'); | ||
| } | ||
| } | ||
|
|
||
| function setStatus(msg) { | ||
| document.getElementById('status').textContent = msg; | ||
| setTimeout(() => { document.getElementById('status').textContent = ''; }, 4000); | ||
| } | ||
| </script> | ||
| <style> | ||
| @import url("style.css"); | ||
| body { | ||
| margin: 0 auto; | ||
| max-width: 900px; | ||
| padding: 0 8px 8px; | ||
| box-sizing: border-box; | ||
| } | ||
| h2 { | ||
| margin: 8px 0 4px; | ||
| font-size: 1.2rem; | ||
| } | ||
| .toolbar { | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: 6px; | ||
| margin-bottom: 6px; | ||
| align-items: center; | ||
| } | ||
| .toolbar button { | ||
| padding: 4px 12px; | ||
| border-radius: 4px; | ||
| font-size: 0.9rem; | ||
| cursor: pointer; | ||
| border: none; | ||
| background: var(--c-3, #444); | ||
| color: var(--c-f, #fff); | ||
| } | ||
| .toolbar button.active { | ||
| background: var(--c-6, #888); | ||
| } | ||
| #status { | ||
| font-size: 0.85rem; | ||
| color: var(--c-6, #aaa); | ||
| flex: 1; | ||
| text-align: right; | ||
| } | ||
| #sz { | ||
| font-size: 0.85rem; | ||
| color: var(--c-6, #aaa); | ||
| } | ||
| #log { | ||
| width: 100%; | ||
| height: calc(100vh - 120px); | ||
| min-height: 200px; | ||
| box-sizing: border-box; | ||
| font-family: monospace; | ||
| font-size: 0.8rem; | ||
| background: #111; | ||
| color: #cfc; | ||
| border: 1px solid #444; | ||
| border-radius: 4px; | ||
| resize: vertical; | ||
| padding: 6px; | ||
| white-space: pre; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h2>WLED Log Viewer</h2> | ||
| <div class="toolbar"> | ||
| <button onclick="fetchLog()">Refresh</button> | ||
| <button id="autobtn" onclick="toggleAuto()">Auto-refresh: Off</button> | ||
| <button onclick="copyLog()">Copy</button> | ||
| <button onclick="clearLog()">Clear</button> | ||
| <button onclick="window.location=getURL('/')">Back</button> | ||
| <span id="sz">-- KB</span> | ||
| <span id="status"></span> | ||
| </div> | ||
| <textarea id="log" readonly spellcheck="false" placeholder="Loading log\u2026"></textarea> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
|
Comment on lines
+1388
to
+1402
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Protect
🔐 Suggested minimal guard void serveLog(AsyncWebServerRequest* request)
{
`#ifdef` BOARD_HAS_PSRAM
+ if (strlen(settingsPIN) > 0 && !correctPIN) {
+ serveJsonError(request, 401, ERR_DENIED);
+ return;
+ }
+
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;
}🤖 Prompt for AI Agents |
||
|
|
||
| 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 | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <Arduino.h> | ||
| #include <Print.h> | ||
|
|
||
| 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<char*>(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<const uint8_t*>(_buf), _used); | ||
| return _used; | ||
| } | ||
| // Buffer has wrapped — oldest byte is at _head. | ||
| out.write(reinterpret_cast<const uint8_t*>(_buf + _head), CAPACITY - _head); | ||
| out.write(reinterpret_cast<const uint8_t*>(_buf), _head); | ||
| return CAPACITY; | ||
|
Comment on lines
+61
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "log_buffer.h" -type fRepository: wled/WLED Length of output: 75 🏁 Script executed: cat -n wled00/log_buffer.hRepository: wled/WLED Length of output: 4446 🏁 Script executed: rg "p_malloc|ps_malloc|d_malloc" -t cpp -t h --max-count=20Repository: wled/WLED Length of output: 3367 🏁 Script executed: rg -A 3 -B 1 "p_malloc|ps_malloc" wled00/Repository: wled/WLED Length of output: 7029 Synchronize
Proposed fix (snapshot indices under lock) size_t printTo(Print& out) const {
if (!_buf || !_used) return 0;
+ size_t used;
+ size_t head;
+ portENTER_CRITICAL(const_cast<portMUX_TYPE*>(&_mux));
+ used = _used;
+ head = _head;
+ portEXIT_CRITICAL(const_cast<portMUX_TYPE*>(&_mux));
- if (_used < CAPACITY) {
+ if (used < CAPACITY) {
// Buffer has not wrapped yet — data starts at index 0.
- out.write(reinterpret_cast<const uint8_t*>(_buf), _used);
- return _used;
+ out.write(reinterpret_cast<const uint8_t*>(_buf), used);
+ return used;
}
// Buffer has wrapped — oldest byte is at _head.
- out.write(reinterpret_cast<const uint8_t*>(_buf + _head), CAPACITY - _head);
- out.write(reinterpret_cast<const uint8_t*>(_buf), _head);
+ out.write(reinterpret_cast<const uint8_t*>(_buf + head), CAPACITY - head);
+ out.write(reinterpret_cast<const uint8_t*>(_buf), head);
return CAPACITY;
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| 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<char>(c); | ||
| _log.write(&ch, 1); | ||
| return 1; | ||
| } | ||
|
|
||
| size_t write(const uint8_t* buf, size_t size) override { | ||
| _log.write(reinterpret_cast<const char*>(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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check HTTP status before showing “Log cleared.”
On Line 53,
clearLog()ignoresr.ok, so failed clear requests still wipe the textarea and show success.Proposed fix
function clearLog() { fetch(getURL('/json/log?clear')) - .then(r => r.text()) - .then(() => { + .then(r => { + if (!r.ok) { + setStatus('Clear failed: HTTP ' + r.status); + return null; + } + return r.text(); + }) + .then(txt => { + if (txt === null) return; document.getElementById('log').value = ''; document.getElementById('sz').textContent = '0 KB'; setStatus('Log cleared.'); }) .catch(e => setStatus('Clear error: ' + e)); }🤖 Prompt for AI Agents