Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion tools/cdata.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
Expand Down Expand Up @@ -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"
);
164 changes: 164 additions & 0 deletions wled00/data/log.htm
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.');
})
Comment on lines +51 to +58
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Check HTTP status before showing “Log cleared.”

On Line 53, clearLog() ignores r.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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/log.htm` around lines 51 - 58, The clearLog function currently
ignores the HTTP response status and always clears the textarea and shows "Log
cleared."; update clearLog to check the fetch response (r.ok) before mutating
DOM: if r.ok then clear document.getElementById('log').value, update
document.getElementById('sz').textContent and call setStatus('Log cleared.'),
otherwise do not clear and call setStatus with an error message (include
response.status/text) and add a .catch handler to surface network errors; locate
the logic inside function clearLog and the fetch(getURL('/json/log?clear'))
promise chain to implement this.

.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>
1 change: 1 addition & 0 deletions wled00/data/settings.htm
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@
<button type="submit" onclick="window.location=getURL('/settings/time')">Time & Macros</button>
<button type="submit" onclick="window.location=getURL('/settings/um')">Usermods</button>
<button type="submit" onclick="window.location=getURL('/settings/sec')">Security & Updates</button>
<button id="logbtn" style="display:none;" type="submit" onclick="window.location=getURL('/log')">Diagnostic Log</button>
</body>
</html>
1 change: 1 addition & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions wled00/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Protect /json/log with the same PIN gate as other sensitive endpoints.

serveLog() currently allows unauthenticated read/clear access. On devices with a settings PIN, this can leak diagnostic details and lets any reachable client wipe evidence via ?clear.

🔐 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/json.cpp` around lines 1388 - 1402, serveLog allows unauthenticated
access; protect it like other sensitive endpoints by checking the settings PIN
at the start of serveLog (before reading/clearing wledLogBuffer). Call the same
helper used elsewhere (e.g., isSettingsAccessible(request) /
checkSettingsPin(request) or the project's equivalent) and if it fails send an
appropriate HTTP error (401/403) and return; ensure the ?clear branch is also
gated by that check so clearing requires a valid PIN.


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

Expand Down
114 changes: 114 additions & 0 deletions wled00/log_buffer.h
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -name "log_buffer.h" -type f

Repository: wled/WLED

Length of output: 75


🏁 Script executed:

cat -n wled00/log_buffer.h

Repository: wled/WLED

Length of output: 4446


🏁 Script executed:

rg "p_malloc|ps_malloc|d_malloc" -t cpp -t h --max-count=20

Repository: 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 printTo() reads with writer updates to prevent torn snapshots

printTo() reads _used, _head, and buffer regions without locking, while write() and clear() modify them under _mux. Between the initial checks and subsequent reads, concurrent writes can change _used from below-capacity to capacity, or alter _head, causing printTo() to emit an inconsistent or partially-wrapped log snapshot.

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/log_buffer.h` around lines 61 - 71, printTo() currently reads _used,
_head and buffer contents without synchronization while write()/clear() use
_mux, leading to torn snapshots; fix by acquiring the same mutex (_mux) at the
start of printTo(), then snapshot needed state (e.g. local copies of _used and
_head) and perform the out.write() reads while the lock is held (or
alternatively copy the relevant buffer region into a temporary while holding
_mux and then release before writing to out), ensuring all accesses to _used,
_head and _buf are synchronized with write()/clear() to avoid inconsistent
wrapped/non-wrapped outputs.

}

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
11 changes: 7 additions & 4 deletions wled00/wled.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading