Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ compile_commands.json
/wled00/Release
/wled00/wled00.ino.cpp
/wled00/html_*.h
_codeql_detected_source_root
6 changes: 5 additions & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1264,8 +1264,11 @@ monitor_filters = esp32_exception_decoder
extends = esp32_4MB_S_base
build_flags = ${common.build_flags} ${esp32_legacy.build_flags} ${common_mm.build_flags_S} ${common_mm.build_flags_M} ;; we don't want common_mm.build_disable_sync_interfaces, so we cannot inherit from esp32_4MB_S_base
build_unflags = ${esp32_4MB_S_base.build_unflags}
${common_mm.animartrix_build_flags} ;; exceeds flash limits in V3 builds
-DUSERMOD_ARTIFX ;; exceeds flash limits in V3 builds
lib_deps = ${esp32_4MB_S_base.lib_deps} ${common_mm.lib_deps_M}

lib_ignore = ${esp32_4MB_S_base.lib_ignore}
${common_mm.animartrix_lib_ignore}
[esp32_4MB_XL_base]
extends = esp32_4MB_M_base
build_flags = ${esp32_4MB_M_base.build_flags} ${common_mm.build_flags_XL}
Expand Down Expand Up @@ -2879,6 +2882,7 @@ board_build.partitions = ${esp32.extreme_partitions} ;; WLED extended for 16MB f
[env:athom_music_esp32_4MB_M]
extends = esp32_4MB_M_base
build_unflags = ${esp32_legacy.build_unflags}
${common_mm.animartrix_build_flags} ;; exceeds flash limits in V3 builds
-D USERMOD_ARTIFX ;; disabled to save some program space in flash
-D USERMOD_DALLASTEMPERATURE ;; disabled - flash space is too tight for this
-D USERMOD_ROTARY_ENCODER_UI ;; see above
Expand Down
13 changes: 12 additions & 1 deletion tools/cdata.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ function filter(str, type) {
}
}

// Generate build timestamp as UNIX timestamp (seconds since epoch)
function generateBuildTime() {
return Math.floor(Date.now() / 1000);
}

function writeHtmlGzipped(sourceFile, resultFile, page) {
console.info("Reading " + sourceFile);
new inliner(sourceFile, function (error, html) {
Expand Down Expand Up @@ -141,7 +146,13 @@ function writeHtmlGzipped(sourceFile, resultFile, page) {
* Please see https://mm.kno.wled.ge/advanced/custom-features/#changing-web-ui
* to find out how to easily modify the web UI source!
*/


// Automatically generated build time for cache busting (UNIX timestamp)
#ifdef WEB_BUILD_TIME // avoid duplicate defintions
#undef WEB_BUILD_TIME
#endif
#define WEB_BUILD_TIME ${generateBuildTime()}

// Autogenerated from ${sourceFile}, do not edit!!
const uint16_t PAGE_${page}_L = ${result.length};
const uint8_t PAGE_${page}[] PROGMEM = {
Expand Down
185 changes: 182 additions & 3 deletions wled00/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -717,11 +717,11 @@ ${inforow("Uptime",getRuntimeStr(i.uptime))}
${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB, " +Math.round(i.fs.u*100/i.fs.t) + "%")}
${theap>0?inforow("Heap ☾",((i.totalheap-i.freeheap)/1000).toFixed(0)+"/"+theap.toFixed(0)+" kB",", "+Math.round((i.totalheap-i.freeheap)/(10*theap))+"%"):inforow("Free heap",heap," kB")} <!--WLEDMM different for 8266-->
${i.minfreeheap?inforow("Max used heap ☾",((i.totalheap-i.minfreeheap)/1000).toFixed(0)+" kB",", "+Math.round((i.totalheap-i.minfreeheap)/(10*theap))+"%"):""}
${i.psram?inforow("PSRAM ☾",((i.tpram-i.psram)/1024).toFixed(0)+"/"+(i.tpram/1024).toFixed(0)+" kB",", "+((i.tpram-i.psram)*100.0/i.tpram).toFixed(1)+"%"):""}
${i.psusedram?inforow("Max used PSRAM ☾",((i.tpram-i.psusedram)/1024).toFixed(0)+" kB",", "+((i.tpram-i.psusedram)*100.0/i.tpram).toFixed(1)+"%"):""}
${i.psram?inforow("PSRAM ☾",((i.tpsram-i.psram)/1024).toFixed(0)+"/"+(i.tpsram/1024).toFixed(0)+" kB",", "+((i.tpsram-i.psram)*100.0/i.tpsram).toFixed(1)+"%"):""}
${i.psusedram?inforow("Max used PSRAM ☾",((i.tpsram-i.psusedram)/1024).toFixed(0)+" kB",", "+((i.tpsram-i.psusedram)*100.0/i.tpsram).toFixed(1)+"%"):""}
${i.freestack?inforow("Free stack ☾",(i.freestack/1000).toFixed(3)," kB"):""} <!--WLEDMM-->
<tr><td colspan=2><hr style="height:1px;border-width:0;color:SeaGreen;background-color:SeaGreen"></td></tr>
${i.tpram?inforow("PSRAM " + (i.psrmode?"("+i.psrmode+" mode) ":"") + " ☾",(i.tpram/1024/1024).toFixed(0)," MB"):inforow("NO PSRAM found.", "")}
${i.tpsram?inforow("PSRAM " + (i.psrmode?"("+i.psrmode+" mode) ":"") + " ☾",(i.tpsram/1024/1024).toFixed(0)," MB"):inforow("NO PSRAM found.", "")}
${i.e32flash?inforow("Flash mode "+i.e32flashmode+i.e32flashtext + " ☾",i.e32flash+" MB, "+i.e32flashspeed," Mhz"):""}
${i.e32model?inforow(i.e32model + " ☾",i.e32cores +" core(s),"," "+i.e32speed+" Mhz"):""}
${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")}
Expand Down Expand Up @@ -2229,6 +2229,7 @@ function requestJson(command=null)
if (json.info) {
let i = json.info;
parseInfo(i);
checkVersionUpgrade(i); // Check for version upgrade
populatePalettes(i);
if (isInfo) populateInfo(i);
}
Comment on lines 2229 to 2235
Copy link

@coderabbitai coderabbitai bot Nov 29, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

version-info.json is likely never saved (/upload field name mismatch, no loc support)

The version-reporting flow is well thought out overall, but there are two concrete issues that will likely break persistence and file-mode behavior:

  1. Wrong form field name for /upload

Existing code uses uploadFileWithText() to POST files to /upload:

function uploadFileWithText(name, text) {
  ...
  var formData = new FormData();
  var blob = new Blob([text], {type : 'application/text'});
  var fileOfBlob = new File([blob], name);
  formData.append("upload", fileOfBlob);
  req.send(formData);
}

The new updateVersionInfo() instead does:

const blob = new Blob([JSON.stringify(versionInfo)], { type: 'application/json' });
const formData = new FormData();
formData.append('data', blob, 'version-info.json');

fetch('/upload', {
  method: 'POST',
  body: formData
})

The server-side upload handler in WLED expects the field name "upload" (as evidenced by uploadFileWithText); sending "data" means the firmware will not recognize or store the file. As a result:

  • version-info.json will never be created/updated.
  • checkVersionUpgrade() will keep treating this as a first install or as a version mismatch, so users will be re-prompted indefinitely.

You should either:

  • Reuse the existing helper:
-function updateVersionInfo(version, neverAsk) {
-  const versionInfo = { version, neverAsk };
-  // Create a Blob with JSON content and use /upload endpoint
-  const blob = new Blob([JSON.stringify(versionInfo)], { type: 'application/json' });
-  const formData = new FormData();
-  formData.append('data', blob, 'version-info.json');
-
-  fetch('/upload', { method: 'POST', body: formData });
-}
+function updateVersionInfo(version, neverAsk) {
+  const versionInfo = { version, neverAsk };
+  uploadFileWithText('/version-info.json', JSON.stringify(versionInfo));
+}
  • Or at least change the field name to "upload" and ensure the filename path matches what /edit?edit=version-info.json expects.
  1. Hard‑coded device-local URLs ignore loc / locip

Other fetches in this file consistently use (loc ? \http://${locip}` : '')as a prefix so the UI works both when served by the device and when opened fromfile:`. The new logic uses bare paths:

  • fetch('/edit?edit=version-info.json', ...)
  • fetch('/json/info', ...)
  • fetch('/upload', ...)

In file‑mode these will target the local filesystem origin instead of the WLED device, so:

  • checkVersionUpgrade() will fail its fetch, fall into the catch, and attempt to POST /upload to the wrong origin.
  • reportUpgradeEvent() will fail to fetch /json/info at all.

If you care about file‑mode parity, you should follow the same pattern used elsewhere:

-function checkVersionUpgrade(info) {
+function checkVersionUpgrade(info) {
+  const base = loc ? `http://${locip}` : '';
   ...
-  fetch('/edit?edit=version-info.json', { method: 'get' })
+  fetch(base + '/edit?edit=version-info.json', { method: 'get' })
   ...

and similarly for /json/info and /upload.

Once those two points are fixed, the flow (single per-load check, honor neverAsk, prompt on real version changes, and build an upgradeData payload from /json/info) looks structurally sound.

Also applies to: 3690-3871

Copy link
Collaborator

Choose a reason for hiding this comment

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

@coderabbitai i have tested the code (latest commit of this PR) on several boards, and version-info.json is properly created on the device, and it is also loaded and recognized by the JS function.

please re-check your analysis, I think you made some wrong assumptions.

Copy link

Choose a reason for hiding this comment

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

@softhack007, thank you for testing and providing that feedback! I apologize for the incorrect analysis. If version-info.json is being properly created and recognized on the device, then my assumptions about the /upload endpoint were clearly wrong.

I'm curious about one thing for my own learning: I noticed that the new updateVersionInfo() function uses 'data' as the FormData field name:

formData.append('data', blob, 'version-info.json');

While the existing uploadFileWithText() function uses 'upload':

formData.append("upload", fileOfBlob);

Does the WLED firmware /upload endpoint accept both field names, or does it perhaps just look at the filename regardless of the field name? Understanding this would help me avoid similar misassumptions in the future.

In any case, I appreciate you taking the time to test this thoroughly—empirical evidence always beats theoretical analysis!


🧠 Learnings used
Learnt from: softhack007
Repo: MoonModules/WLED-MM PR: 270
File: wled00/fcn_declare.h:403-406
Timestamp: 2025-10-22T21:59:24.331Z
Learning: In MoonModules/WLED-MM PR #270, the out-of-bounds null-terminator write in wled00/util.cpp (extractModeSlider) is deferred and tracked in Issue #272; do not address it within PR #270.

Expand Down Expand Up @@ -3686,6 +3687,184 @@ function mergeDeep(target, ...sources)
}
return mergeDeep(target, ...sources);
}
// Version reporting feature
var versionCheckDone = false;

function checkVersionUpgrade(info) {
// Only check once per page load
if (versionCheckDone) return;
versionCheckDone = true;

// Fetch version-info.json using existing /edit endpoint
fetch('/edit?edit=/version-info.json', {
method: 'get'
})
.then(res => {
if (res.status === 404) {
// File doesn't exist - first install, show install prompt
showVersionUpgradePrompt(info, null, info.ver);
return null;
}
if (!res.ok) {
throw new Error('Failed to fetch version-info.json');
}
return res.json();
})
.then(versionInfo => {
if (!versionInfo) return; // 404 case already handled

// Check if user opted out
if (versionInfo.neverAsk) return;

// Check if version has changed
const currentVersion = info.ver;
const storedVersion = versionInfo.version || '';

if (storedVersion && storedVersion !== currentVersion) {
// Version has changed, show upgrade prompt
showVersionUpgradePrompt(info, storedVersion, currentVersion);
} else if (!storedVersion) {
// Empty version in file, show install prompt
showVersionUpgradePrompt(info, null, currentVersion);
}
})
.catch(e => {
console.log('Failed to load version-info.json', e);
});
}

function showVersionUpgradePrompt(info, oldVersion, newVersion) {
// Determine if this is an install or upgrade
const isInstall = !oldVersion;

// Create overlay and dialog
const overlay = d.createElement('div');
overlay.id = 'versionUpgradeOverlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;';

const dialog = d.createElement('div');
dialog.style.cssText = 'background:var(--c-1);border-radius:10px;padding:25px;max-width:500px;margin:20px;box-shadow:0 4px 6px rgba(0,0,0,0.3);';

// Build contextual message based on install vs upgrade
const title = isInstall
? '🎉 Thank you for installing WLED-MM!'
: '🎉 WLED-MM Upgrade Detected!';

const description = isInstall
? `You are now running WLED-MM <strong>${newVersion}</strong>.`
: `Your WLED-MM has been upgraded from <strong>${oldVersion}</strong> to <strong>${newVersion}</strong>.`;

const question = 'Would you like to help the WLED development team by reporting your installation? This helps us understand what hardware and versions are being used.'

dialog.innerHTML = `
<h2 style="margin-top:0;color:var(--c-f);">${title}</h2>
<p style="color:var(--c-f);">${description}</p>
<p style="color:var(--c-f);">${question}</p>
<div style="margin-top:20px;">
<button id="versionReportYes" class="btn">Yes</button>
<button id="versionReportNo" class="btn">Not Now</button>
<button id="versionReportNever" class="btn">Never Ask</button>
</div>
`;

overlay.appendChild(dialog);
d.body.appendChild(overlay);

// Add event listeners
gId('versionReportYes').addEventListener('click', () => {
reportUpgradeEvent(oldVersion, newVersion);
d.body.removeChild(overlay);
});

gId('versionReportNo').addEventListener('click', () => {
// Don't update version, will ask again on next load
d.body.removeChild(overlay);
});

gId('versionReportNever').addEventListener('click', () => {
updateVersionInfo(newVersion, true);
d.body.removeChild(overlay);
showToast('You will not be asked again.');
});
}

function reportUpgradeEvent(oldVersion, newVersion) {
showToast('Reporting upgrade...');

// Fetch fresh data from /json/info endpoint as requested
fetch('/json/info', {
method: 'get'
})
.then(res => res.json())
.then(infoData => {
// Map to UpgradeEventRequest structure per OpenAPI spec
// Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256
const upgradeData = {
deviceId: infoData.deviceId, // Use anonymous unique device ID
version: infoData.ver || '', // Current version string
previousVersion: oldVersion || '', // Previous version from version-info.json
releaseName: infoData.release || '', // Release name (e.g., "WLED 0.15.0")
chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc)
ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs
isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup
bootloaderSHA256: infoData.bootloaderSHA256 || '', // Bootloader SHA256 hash - not yet availeable in WLEDMM
brand: infoData.brand, // Device brand (always present)
product: infoData.product, // Product name (always present)
flashSize: infoData.flash // Flash size (always present)
};
// Add optional fields if available
if (infoData.tpsram !== undefined) upgradeData.psramSize = Math.round(infoData.tpsram / (1024 * 1024)); // convert bytes to MB - tpsram is MM specific
// Note: partitionSizes not currently available in /json/info endpoint
// it is availeable in WLEDMM => infoData.t = total FS size in bytes

// Make AJAX call to postUpgradeEvent API
return fetch('https://usage.wled.me/api/usage/upgrade', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(upgradeData)
});
Comment on lines +3821 to +3827
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add timeout and improve error handling for external API call.

The fetch request to https://usage.wled.me/api/usage/upgrade has no timeout and could hang indefinitely. Additionally, the hardcoded external URL may face CORS restrictions depending on the server configuration.

Add a timeout and better error context:

 		// Make AJAX call to postUpgradeEvent API
-		return fetch('https://usage.wled.me/api/usage/upgrade', {
+		const controller = new AbortController();
+		const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
+		
+		return fetch('https://usage.wled.me/api/usage/upgrade', {
 			method: 'POST',
 			headers: {
 				'Content-Type': 'application/json'
 			},
-			body: JSON.stringify(upgradeData)
+			body: JSON.stringify(upgradeData),
+			signal: controller.signal
+		}).finally(() => {
+			clearTimeout(timeoutId);
 		});
 	})
 	.then(res => {
 		if (res.ok) {
 			showToast('Thank you for reporting!');
 			updateVersionInfo(newVersion, false);
 		} else {
-			showToast('Report failed. Please try again later.', true);
+			showToast(`Report failed (${res.status}). Please try again later.`, true);
 			// Do NOT update version info on failure - user will be prompted again
 		}
 	})
 	.catch(e => {
-		console.log('Failed to report upgrade', e);
-		showToast('Report failed. Please try again later.', true);
+		const isTimeout = e.name === 'AbortError';
+		console.log('Failed to report upgrade', e);
+		showToast(isTimeout ? 'Report timed out. Please try again later.' : 'Report failed. Please try again later.', true);
 		// Do NOT update version info on error - user will be prompted again
 	});

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In wled00/data/index.js around lines 3825-3831, the fetch to
https://usage.wled.me/api/usage/upgrade has no timeout and lacks robust error
handling and CORS consideration; wrap the call in an async try/catch, use an
AbortController with a configurable timeout (clear the timeout on completion),
check response.ok and parse/attach response body or status to any thrown error,
and surface/log the enriched error context; additionally make the URL
configurable (env/config) or move the request to a server-side endpoint to avoid
client-side CORS issues.

})
.then(res => {
if (res.ok) {
showToast('Thank you for reporting!');
updateVersionInfo(newVersion, false);
} else {
showToast('Report failed. Please try again later.', true);
// Do NOT update version info on failure - user will be prompted again
}
})
.catch(e => {
console.log('Failed to report upgrade', e);
showToast('Report failed. Please try again later.', true);
// Do NOT update version info on error - user will be prompted again
});
}

function updateVersionInfo(version, neverAsk) {
const versionInfo = {
version: version,
neverAsk: neverAsk
};

// Create a Blob with JSON content and use /upload endpoint
const blob = new Blob([JSON.stringify(versionInfo)], { type: 'application/json' });
const formData = new FormData();
formData.append('data', blob, 'version-info.json');

fetch('/upload', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(data => {
console.log('Version info updated', data);
})
.catch(e => {
console.log('Failed to update version-info.json', e);
});
}

size();
_C.style.setProperty('--n', N);
Expand Down
4 changes: 2 additions & 2 deletions wled00/data/simple.js
Original file line number Diff line number Diff line change
Expand Up @@ -528,8 +528,8 @@ ${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.
${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")}
${theap>0?inforow("Total heap",theap," kB"):""}
${i.minfreeheap?inforow("Max used heap",((i.totalheap-i.minfreeheap)/1000).toFixed(1)," kB"):""}
${i.tpram?inforow("Total PSRAM",(i.tpram/1024).toFixed(1)," kB"):""}
${i.psusedram?((i.tpram-i.psusedram)>16383?inforow("Max Used PSRAM",((i.tpram-i.psusedram)/1024).toFixed(1)," kB"):inforow("Max Used PSRAM",(i.tpram-i.psusedram)," B")):""}
${i.tpsram?inforow("Total PSRAM",(i.tpsram/1024).toFixed(1)," kB"):""}
${i.psusedram?((i.tpsram-i.psusedram)>16383?inforow("Max Used PSRAM",((i.tpsram-i.psusedram)/1024).toFixed(1)," kB"):inforow("Max Used PSRAM",(i.tpsram-i.psusedram)," B")):""}
${i.e32model?inforow(i.e32model,i.e32cores +" core(s)"," "+i.e32speed+" Mhz"):""}
${i.e32flash?inforow("Flash "+i.e32flash+" MB"+", mode "+i.e32flashmode+i.e32flashtext,i.e32flashspeed," Mhz"):""}
<!-- WLEDMM end-->
Expand Down
10 changes: 7 additions & 3 deletions wled00/json.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "wled.h"
#include "ota_update.h"

#include "palettes.h"

Expand Down Expand Up @@ -938,7 +939,7 @@ void serializeInfo(JsonObject root)
//root[F("cn")] = F(WLED_CODENAME); //WLEDMM removed
root[F("release")] = FPSTR(releaseString);
root[F("rel")] = FPSTR(releaseString); //WLEDMM to add bin name

//root[F("repo")] = repoString; // WLEDMM not availeable
root[F("deviceId")] = getDeviceId();

JsonObject leds = root.createNestedObject("leds");
Expand Down Expand Up @@ -1083,6 +1084,9 @@ void serializeInfo(JsonObject root)

root[F("lwip")] = 0; //deprecated
root[F("totalheap")] = ESP.getHeapSize(); //WLEDMM
#ifndef WLED_DISABLE_OTA
root[F("bootloaderSHA256")] = getBootloaderSHA256Hex();
#endif
#else
root[F("arch")] = "esp8266";
root[F("core")] = ESP.getCoreVersion();
Expand All @@ -1104,7 +1108,7 @@ void serializeInfo(JsonObject root)
#endif
#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
if (psramFound()) {
root[F("tpram")] = ESP.getPsramSize(); //WLEDMM
root[F("tpsram")] = ESP.getPsramSize(); //WLEDMM
root[F("psram")] = ESP.getFreePsram();
root[F("psusedram")] = ESP.getMinFreePsram();
#if CONFIG_ESP32S3_SPIRAM_SUPPORT // WLEDMM -S3 has "qspi" or "opi" PSRAM mode
Expand All @@ -1117,7 +1121,7 @@ void serializeInfo(JsonObject root)
}
#else
// for testing
// root[F("tpram")] = 4194304; //WLEDMM
// root[F("tpsram")] = 4194304; //WLEDMM
// root[F("psram")] = 4193000;
// root[F("psusedram")] = 3083000;
#endif
Expand Down
Loading