Skip to content
Draft
17 changes: 17 additions & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,23 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented)
#define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented)

// Additional error types for better user feedback
#define ERR_NORAM_BUS 33 // Not enough RAM for bus allocation
#define ERR_NORAM_SEG 34 // Not enough RAM for segment allocation
#define ERR_NORAM_TRANS 35 // Not enough RAM for transition effects
#define ERR_PIN_CONFLICT 36 // Pin assignment conflict detected
#define ERR_PIN_INVALID 37 // Invalid pin number for this platform
#define ERR_CONFIG_LOAD 38 // Configuration loading failed
#define ERR_CONFIG_SAVE 39 // Configuration saving failed

// Warning types (starting at 100 as requested)
#define WARN_LOW_MEMORY 100 // Low memory warning
#define WARN_HIGH_TEMP 101 // Temperature approaching limits
#define WARN_LOW_VOLTAGE 102 // Voltage below optimal range
#define WARN_HIGH_CURRENT 103 // Current approaching limits
#define WARN_WIFI_WEAK 104 // Weak WiFi signal
#define WARN_FS_SPACE 105 // Filesystem space running low

// Timer mode types
#define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness
#define NL_MODE_FADE 1 //Fade to target brightness gradually
Expand Down
7 changes: 7 additions & 0 deletions wled00/data/index.htm
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,13 @@
<div id="imgw">
<img class="wi" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAFCAYAAAC5Fuf5AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABbSURBVChTlY9bDoAwDMNW7n9nwCipytQN4Z8tbrTHmDmF4oPzyldwRqp1SSdnV/NuZuzqerAByxXznBw3igkeFEfXyUuhK/yFM0CxJfyqXZEOc6/Sr9/bf7uIC5Nwd7orMvAPAAAAAElFTkSuQmCC" />
</div>
<div id="errorLogArea" style="margin: 10px auto; display: none;">
<h3 style="margin: 0 0 8px 0; font-size: 16px; text-align: center;">Error Log</h3>
<div id="errorLogContent" style="padding: 8px; background: var(--c-3); border-radius: 20px; font-size: 14px; max-height: 120px; overflow-y: auto; text-align: left;"></div>
<div style="margin-top: 8px; text-align: center;">
<button class="btn ibtn" onclick="clearErrorLog()">Clear Log</button>
</div>
</div>
<div id="kv">Loading...</div><br>
<div>
<button class="btn ibtn" onclick="requestJson()">Refresh</button>
Expand Down
206 changes: 171 additions & 35 deletions wled00/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ var pmt = 1, pmtLS = 0, pmtLast = 0;
var lastinfo = {};
var isM = false, mw = 0, mh=0;
var ws, wsRpt=0;
var errorLog = []; // Store last 5 errors/warnings
var hasUnreadErrors = false;
var cfg = {
theme:{base:"dark", bg:{url:"", rnd: false, rndGrayscale: false, rndBlur: false}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}},
comp :{colors:{picker: true, rgb: false, quick: true, hex: false},
Expand Down Expand Up @@ -384,6 +386,152 @@ function inforow(key, val, unit = "")
return `<tr><td class="keytd">${key}</td><td class="valtd">${val}${unit}</td></tr>`;
}

function getErrorMessage(errorCode) {
switch (errorCode) {
case 1: return "Operation denied by current settings";
case 2: return "Cannot process request while client is active";
case 3: return "JSON buffer is locked, try again in a moment";
case 4: return "Feature not implemented on this version";
case 7: return "Insufficient RAM to allocate pixel buffer";
case 8: return "Not enough RAM available for effect processing";
case 9: return "JSON parsing failed - data may be too large";
case 10: return "Could not initialize filesystem - check partition";
case 11: return "Not enough space to save preset to filesystem";
case 12: return "Requested preset does not exist";
case 13: return "IR configuration file 'ir.json' not found";
case 14: return "Remote configuration file 'remote.json' not found";
case 19: return "An unspecified filesystem error occurred";
case 30: return "Temperature sensor reading above safe threshold";
case 31: return "Current sensor reading above safe threshold";
case 32: return "Voltage sensor reading below safe threshold";
case 33: return "Insufficient RAM to allocate LED bus";
case 34: return "Insufficient RAM to allocate segment data";
case 35: return "Insufficient RAM for transition effects";
case 36: return "Pin assignment conflict detected";
case 37: return "Invalid pin number for this platform";
case 38: return "Failed to load configuration from filesystem";
case 39: return "Failed to save configuration to filesystem";
case 100: return "Memory usage is approaching limits";
case 101: return "Temperature is approaching safe limits";
case 102: return "Voltage is below optimal operating range";
case 103: return "Current draw is approaching safe limits";
case 104: return "WiFi signal strength is poor";
case 105: return "Filesystem space is running low";
default: return `Unknown error code ${errorCode}`;
}
}

function addToErrorLog(errorCode, timestamp = null) {
if (!timestamp) timestamp = Date.now();

const errorEntry = {
code: errorCode,
message: getErrorMessage(errorCode),
timestamp: timestamp,
isWarning: errorCode >= 100
};

// Add to beginning of array
errorLog.unshift(errorEntry);

// Keep only last 5 entries
if (errorLog.length > 5) {
errorLog = errorLog.slice(0, 5);
}

hasUnreadErrors = true;
updateInfoButtonIcon();
}

function updateInfoButtonIcon() {
const infoBtn = gId('buttonI');
const icon = infoBtn.querySelector('i');
if (hasUnreadErrors) {
// Change to red exclamation mark icon
icon.innerHTML = '&#xe18a;'; // Use add/warning icon
icon.style.color = 'var(--c-r)';
} else {
// Reset to normal info icon
icon.innerHTML = '&#xe066;'; // Info icon
icon.style.color = '';
}
}

function handleServerErrorLog(serverErrors, serverTime) {
// Clear client-side log and replace with server data
errorLog = [];
hasUnreadErrors = false;

for (let i = 0; i < serverErrors.length; i++) {
const serverEntry = serverErrors[i];
// Calculate absolute timestamp using server time and error timestamp
const absoluteTime = Date.now() - (serverTime - serverEntry.t);

const errorEntry = {
code: serverEntry.c,
// Use custom message if provided, otherwise use error code lookup
message: serverEntry.m ? serverEntry.m : getErrorMessage(serverEntry.c),
timestamp: absoluteTime,
// If custom message provided, determine warning/error based on code range
isWarning: serverEntry.c >= 100,
tag1: serverEntry.t1 || 0,
tag2: serverEntry.t2 || 0,
tag3: serverEntry.t3 || 0
};

errorLog.push(errorEntry);
}

if (errorLog.length > 0) {
hasUnreadErrors = true;
}

updateInfoButtonIcon();
}

function clearErrorLog() {
// Send clear command to server
Copy link
Collaborator

Choose a reason for hiding this comment

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

is it possible to do this using the websocket connection or is this the better way? in any case, use a short string "clrErrLog"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to shorter string "clrErrLog" in commit 4b6df49. The fetch API approach is suitable here as it allows the client to send the command and handle the response properly, ensuring the local state is synchronized with the server clearing operation.

fetch(getURL('/json/state'), {
method: 'post',
body: JSON.stringify({clrErrLog: true}),
headers: {
'Content-Type': 'application/json'
}
})
.then(res => {
// Clear local state
errorLog = [];
hasUnreadErrors = false;
updateInfoButtonIcon();
const errorArea = gId('errorLogArea');
if (errorArea) {
errorArea.style.display = 'none';
}
})
.catch((error) => {
console.log('Error clearing log:', error);
});
}

function generateErrorLogHtml() {
if (errorLog.length === 0) return '';

let html = '';
for (let i = 0; i < errorLog.length; i++) {
const entry = errorLog[i];
// Use 24h format without seconds
const timeStr = new Date(entry.timestamp).toLocaleTimeString([], {hour12: false, hour: '2-digit', minute: '2-digit'});
const prefix = entry.isWarning ? 'Warning' : 'Error';
const color = entry.isWarning ? 'var(--c-y)' : 'var(--c-r)';

html += `<div style="margin: 2px 0; padding: 3px; word-wrap: break-word; color: ${color};">
${timeStr} ${prefix} ${entry.code}: ${entry.message}
</div>`;
}

return html;
}

function getLowestUnusedP()
{
var l = 1;
Expand Down Expand Up @@ -746,7 +894,17 @@ ${inforow("Flash size",i.flash," MB")}
${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")}
${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")}
</table>`;

gId('kv').innerHTML = cn;

// Update error log area visibility and content
const errorArea = gId('errorLogArea');
if (errorLog.length > 0) {
errorArea.style.display = 'block';
gId('errorLogContent').innerHTML = generateErrorLogHtml();
} else {
errorArea.style.display = 'none';
}
// update all sliders in Info
d.querySelectorAll('#kv .sliderdisplay').forEach((sd,i) => {
let s = sd.previousElementSibling;
Expand Down Expand Up @@ -1518,40 +1676,13 @@ function readState(s,command=false)
gId('checkO3').checked = !(!i.o3);

if (s.error && s.error != 0) {
var errstr = "";
switch (s.error) {
case 1:
errstr = "Denied!";
break;
case 3:
errstr = "Buffer locked!";
break;
case 7:
errstr = "No RAM for buffer!";
break;
case 8:
errstr = "Effect RAM depleted!";
break;
case 9:
errstr = "JSON parsing error!";
break;
case 10:
errstr = "Could not mount filesystem!";
break;
case 11:
errstr = "Not enough space to save preset!";
break;
case 12:
errstr = "Preset not found.";
break;
case 13:
errstr = "Missing ir.json.";
break;
case 19:
errstr = "A filesystem error has occured.";
break;
}
showToast('Error ' + s.error + ": " + errstr, true);
// Add to error log for detailed tracking
addToErrorLog(s.error);
}

// Handle server-side error log
if (s.errorLog && s.errorLogTime) {
handleServerErrorLog(s.errorLog, s.errorLogTime);
}

selectedPal = i.pal;
Expand Down Expand Up @@ -1846,7 +1977,12 @@ function toggleInfo()
if (isNodes) toggleNodes();
if (isLv && isM) toggleLiveview();
isInfo = !isInfo;
if (isInfo) requestJson();
if (isInfo) {
requestJson();
// Mark errors as read when info panel is opened
hasUnreadErrors = false;
updateInfoButtonIcon();
}
gId('info').style.transform = (isInfo) ? "translateY(0px)":"translateY(100%)";
gId('buttonI').className = (isInfo) ? "active":"";
}
Expand Down
6 changes: 6 additions & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -555,4 +555,10 @@ void sendDataWs(AsyncWebSocketClient * client = nullptr);
void XML_response(Print& dest);
void getSettingsJS(byte subPage, Print& dest);

//util.cpp - error logging
void addToErrorLog(byte errorCode, byte tag1 = 0, byte tag2 = 0, byte tag3 = 0, const char* customMessage = nullptr);
void clearErrorLog();
byte getErrorLogCount();
const struct ErrorLogEntry& getErrorLogEntry(byte index);

#endif
31 changes: 30 additions & 1 deletion wled00/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId)
#if defined(WLED_DEBUG) && defined(WLED_DEBUG_HOST)
netDebugEnabled = root[F("debug")] | netDebugEnabled;
#endif

// Handle clear error log command
if (root[F("clrErrLog")]) {
clearErrorLog();
}

bool onBefore = bri;
getVal(root["bri"], bri);
Expand Down Expand Up @@ -639,7 +644,31 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme
}

if (!forPreset) {
if (errorFlag) {root[F("error")] = errorFlag; errorFlag = ERR_NONE;} //prevent error message to persist on screen
if (errorFlag) {
root[F("error")] = errorFlag;
addToErrorLog(errorFlag); // Add to error log
errorFlag = ERR_NONE; // Reset error flag
}

// Add error log to JSON response
if (getErrorLogCount() > 0) {
JsonArray errors = root.createNestedArray(F("errorLog"));

for (byte i = 0; i < getErrorLogCount(); i++) {
const ErrorLogEntry& entry = getErrorLogEntry(i);
JsonObject err = errors.createNestedObject();
err[F("t")] = entry.timestamp;
err[F("c")] = entry.errorCode;
err[F("t1")] = entry.tag1;
err[F("t2")] = entry.tag2;
err[F("t3")] = entry.tag3;
// Add custom message if present
if (entry.customMessage != nullptr) {
err[F("m")] = entry.customMessage;
}
}
root[F("errorLogTime")] = millis(); // Current time for client calculations
}

root["ps"] = (currentPreset > 0) ? currentPreset : -1;
root[F("pl")] = currentPlaylist;
Expand Down
Loading