diff --git a/wled00/const.h b/wled00/const.h index 8891dfcaee..01ddee4f16 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -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 diff --git a/wled00/data/index.htm b/wled00/data/index.htm index 22f1987e93..9e8910eb28 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -325,6 +325,13 @@
+
Loading...

diff --git a/wled00/data/index.js b/wled00/data/index.js index 2514f03fb1..b5c65bf6f8 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -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}, @@ -384,6 +386,152 @@ function inforow(key, val, unit = "") return `${key}${val}${unit}`; } +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 = ''; // Use add/warning icon + icon.style.color = 'var(--c-r)'; + } else { + // Reset to normal info icon + icon.innerHTML = ''; // 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 + 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 += `
+ ${timeStr} ${prefix} ${entry.code}: ${entry.message} +
`; + } + + return html; +} + function getLowestUnusedP() { var l = 1; @@ -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 + ")")} `; + 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; @@ -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; @@ -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":""; } diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 1d81655d6d..e9a3b14150 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -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 diff --git a/wled00/json.cpp b/wled00/json.cpp index d2b771c590..06106dd95f 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -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); @@ -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; diff --git a/wled00/util.cpp b/wled00/util.cpp index 8aaaf34cab..6b7c163cb8 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -1124,4 +1124,73 @@ uint8_t perlin8(uint16_t x, uint16_t y) { uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) { return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 2015) >> 10) + 33168) >> 8; //scale to 16 bit, offset, then scale to 8bit +} + +// Error logging system +struct ErrorLogEntry { + unsigned long timestamp; // millis() when error occurred + byte errorCode; // error number (8bit) + byte tag1; // future use tag 1 + byte tag2; // future use tag 2 + byte tag3; // future use tag 3 + char* customMessage; // optional custom message string (nullptr if not used) +}; + +#define ERROR_LOG_SIZE 5 +static ErrorLogEntry errorLog[ERROR_LOG_SIZE]; +static byte errorLogIndex = 0; +static byte errorLogCount = 0; + +// Error logging functions +void addToErrorLog(byte errorCode, byte tag1, byte tag2, byte tag3, const char* customMessage) { + // Free existing custom message if present + if (errorLog[errorLogIndex].customMessage != nullptr) { + delete[] errorLog[errorLogIndex].customMessage; + errorLog[errorLogIndex].customMessage = nullptr; + } + + errorLog[errorLogIndex].timestamp = millis(); + errorLog[errorLogIndex].errorCode = errorCode; + errorLog[errorLogIndex].tag1 = tag1; + errorLog[errorLogIndex].tag2 = tag2; + errorLog[errorLogIndex].tag3 = tag3; + + // Copy custom message if provided + if (customMessage != nullptr) { + size_t len = strlen(customMessage); + errorLog[errorLogIndex].customMessage = new char[len + 1]; + if (errorLog[errorLogIndex].customMessage != nullptr) { + strcpy(errorLog[errorLogIndex].customMessage, customMessage); + } + } else { + errorLog[errorLogIndex].customMessage = nullptr; + } + + errorLogIndex = (errorLogIndex + 1) % ERROR_LOG_SIZE; + if (errorLogCount < ERROR_LOG_SIZE) { + errorLogCount++; + } +} + +void clearErrorLog() { + // Free all custom message strings + for (byte i = 0; i < ERROR_LOG_SIZE; i++) { + if (errorLog[i].customMessage != nullptr) { + delete[] errorLog[i].customMessage; + errorLog[i].customMessage = nullptr; + } + } + + errorLogIndex = 0; + errorLogCount = 0; +} + +// Helper functions to read error log +byte getErrorLogCount() { + return errorLogCount; +} + +const ErrorLogEntry& getErrorLogEntry(byte index) { + byte actualIndex = (errorLogIndex + ERROR_LOG_SIZE - errorLogCount + index) % ERROR_LOG_SIZE; + return errorLog[actualIndex]; } \ No newline at end of file