Compare commits

...

47 Commits

Author SHA1 Message Date
Will Tatam
15ee7fdf2e Remove K suffix 2025-11-22 13:02:06 +00:00
Will Tatam
3ae86048f3 Remove MB suffix 2025-11-22 12:58:07 +00:00
Will Tatam
43722e3c13 Fix styling issues 2025-11-22 12:51:23 +00:00
Will Tatam
5dfc524868 Use deviceId not mac 2025-11-22 12:33:16 +00:00
Will Tatam
28b19a88bd Update to use deviceId 2025-11-22 12:33:16 +00:00
copilot-swe-agent[bot]
574f168e4e Initial plan 2025-11-22 12:33:16 +00:00
Will Tatam
44811ea9c0 swap to using ESP.getFlashChipId for the 8266 2025-11-22 12:33:16 +00:00
Will Tatam
a086adeb7c Add efuse based data to salt 2025-11-22 12:33:16 +00:00
Will Tatam
4f7c49ddf3 use correct value for deviceString for 8266 and add comments 2025-11-22 12:33:16 +00:00
Will Tatam
8cda70022e fix whitespace 2025-11-22 12:33:16 +00:00
Will Tatam
7079e4e194 deviceString for esp32 2025-11-22 12:33:16 +00:00
Will Tatam
0744aa51ba deviceString for 8266 2025-11-22 12:33:16 +00:00
Will Tatam
e95405f928 salt using additional hardware details 2025-11-22 12:33:16 +00:00
Will Tatam
726d5227a3 refactor to use a common sha1 function 2025-11-22 12:33:16 +00:00
Will Tatam
2f5056e5ab Add salf and checksum 2025-11-22 12:33:16 +00:00
Will Tatam
321a2c3492 Update wled00/json.cpp
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-22 12:33:16 +00:00
Will Tatam
08fee10ad9 Add deviceId to JSON info respose, to be used for the post-upgrade notfication system 2025-11-22 12:33:16 +00:00
Frank
bd593d32af Fix debug message for servicing wait
forgot to adjust the debug condition in my previous commit.

NB: the condition only shows a debug message when the max wait time was exceeded, which can only happen when line 1692 has waited for the maximum allowed time. ->Is this intended?
2025-11-22 12:33:16 +00:00
Frank
ee1880e29a make waitForIt() timing logic robust against millis() rollover
the timing logic did not work in case that millis()+100 + frametime rolls over; in this case millis() > maxWait, and waiting would be skipped which might lead to crashes.
-> logic slightly adjusted to be robust against rollover.
2025-11-22 12:33:16 +00:00
Damian Schneider
c3aab84869 fix for 0byte size files, also made reading ledmaps more efficient
when a ledmap is read from a file, it first parses the keys, putting the in front is more efficient as it will find them in the first 256 byte chunk.
2025-11-22 12:33:16 +00:00
Damian Schneider
a1324b31ba show minimum of 0.1KB for small files in file editor 2025-11-22 12:33:16 +00:00
Damian Schneider
d1efa46390 add ctrl+s support to file editor, also add toast instead of alert 2025-11-22 12:33:16 +00:00
copilot-swe-agent[bot]
5b9cac549a Suppress version reporting prompt when WLED is in AP mode
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-21 08:08:15 +00:00
copilot-swe-agent[bot]
0f03105693 Add privacy policy link to version reporting modal
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-20 19:54:00 +00:00
copilot-swe-agent[bot]
e79696fd75 Move flashSize to always-present fields (not optional)
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-20 03:08:38 +00:00
copilot-swe-agent[bot]
c1d2f2181d Remove conditional checks for brand and product fields (always present)
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-20 03:05:41 +00:00
copilot-swe-agent[bot]
75851f2f00 Fix releaseName to use release field instead of cn (codename)
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-20 02:59:35 +00:00
copilot-swe-agent[bot]
82dc46b5d3 Add optional fields from updated OpenAPI spec (brand, product, flashSize, psramSize)
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-20 02:49:56 +00:00
Will Tatam
79ced7d57e update usage prompt text 2025-11-19 23:34:37 +00:00
Will Tatam
a3d9fed00f Fix buttons 2025-11-19 23:20:41 +00:00
Will Tatam
551240b9a9 Pass the correct field for release 2025-11-19 22:49:30 +00:00
Will Tatam
58e63ad9d9 Fix usage url 2025-11-19 22:42:59 +00:00
copilot-swe-agent[bot]
2de76a5b6b Refactor showVersionUpgradePrompt to reduce duplication
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-19 21:42:51 +00:00
copilot-swe-agent[bot]
348ab4c485 Use existing /edit endpoint instead of dedicated version-info.json GET endpoint
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-19 21:36:55 +00:00
copilot-swe-agent[bot]
fce849eae9 Add different messaging for install vs upgrade prompts
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-19 21:13:33 +00:00
copilot-swe-agent[bot]
5bd4c23319 Return 404 when version-info.json doesn't exist instead of default JSON
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-19 21:01:03 +00:00
copilot-swe-agent[bot]
91ad8a89bc Reuse /upload endpoint instead of dedicated POST endpoint for version-info.json
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-19 20:52:58 +00:00
copilot-swe-agent[bot]
010eefe52a Add bootloaderSHA256 field to UpgradeEventRequest
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-17 19:20:51 +00:00
copilot-swe-agent[bot]
97c35d09cf Map fields to UpgradeEventRequest schema from OpenAPI spec
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-17 19:06:33 +00:00
copilot-swe-agent[bot]
1d006d0243 Wrap info data in 'info' field per UpgradeEventRequest structure
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-17 19:01:31 +00:00
copilot-swe-agent[bot]
97c3e10796 Only save version-info when API call succeeds, not on failure
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-17 18:53:08 +00:00
copilot-swe-agent[bot]
b50c050a89 Map info response fields to upgradeData structure field by field
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-17 18:50:55 +00:00
copilot-swe-agent[bot]
366f3d3854 Fetch fresh data from /json/info endpoint before reporting upgrade
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-17 18:39:14 +00:00
copilot-swe-agent[bot]
783634de86 Add opt-in version upgrade reporting with persistent user preferences
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-17 18:20:48 +00:00
copilot-swe-agent[bot]
bdc7e8cc91 Fix version reporting to pass oldVersion correctly and send full info object
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-17 18:14:52 +00:00
copilot-swe-agent[bot]
12387d424f Add version reporting feature - backend and frontend implementation
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-11-17 18:08:35 +00:00
copilot-swe-agent[bot]
be981a4f91 Initial plan 2025-11-17 17:58:29 +00:00
7 changed files with 310 additions and 11 deletions

View File

@@ -1687,10 +1687,11 @@ void WS2812FX::setTransitionMode(bool t) {
// rare circumstances are: setting FPS to high number (i.e. 120) and have very slow effect that will need more
// time than 2 * _frametime (1000/FPS) to draw content
void WS2812FX::waitForIt() {
unsigned long maxWait = millis() + 2*getFrameTime() + 100; // TODO: this needs a proper fix for timeout! see #4779
while (isServicing() && maxWait > millis()) delay(1);
unsigned long waitStart = millis();
unsigned long maxWait = 2*getFrameTime() + 100; // TODO: this needs a proper fix for timeout! see #4779
while (isServicing() && (millis() - waitStart < maxWait)) delay(1); // safe even when millis() rolls over
#ifdef WLED_DEBUG
if (millis() >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
if (millis()-waitStart >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing."));
#endif
};

View File

@@ -213,7 +213,7 @@ function createTop(element, editor){
function httpPostCb(st,resp){
if (st!=200) alert("ERROR "+st+": "+resp);
else {
alert("Upload successful!");
showToast("Upload successful!");
refreshTree();
}
}
@@ -264,7 +264,7 @@ function createTree(element, editor){
leaf.textContent=name;
var span = cE("span");
span.style.cssText = "font-size: 14px; color: #aaa; margin-left: 8px;";
span.textContent = (size / 1024).toFixed(1) + "KB";
span.textContent = (size > 0 ? Math.max(0.1, (size / 1024)).toFixed(1) : 0) + "KB"; // show size in KB, minimum 0.1 to not show 0KB for small files
leaf.appendChild(span);
leaf.onmouseover=function(){ leaf.style.background="#333"; };
leaf.onmouseout=function(){ leaf.style.background=""; };
@@ -377,13 +377,13 @@ function prettyLedmap(json){
rows.push(" " + obj.map.slice(i, i + width).map(pad).join(", "));
}
let pretty = "{\n \"map\": [\n" + rows.join(",\n") + "\n ]";
let pretty = "{\n";
for (let k of Object.keys(obj)) {
if (k !== "map") {
pretty += ",\n \"" + k + "\": " + JSON.stringify(obj[k]);
pretty += " \"" + k + "\": " + JSON.stringify(obj[k]) + ",\n"; // print all keys first (speeds up loading)
}
}
pretty += "\n}";
pretty += " \"map\": [\n" + rows.join(",\n") + "\n ]\n}";
return pretty;
} catch (e) {
return json;
@@ -493,7 +493,7 @@ function createEditor(element,file){
req.add("POST","/upload",fd,function(st,resp){
if (st!=200) alert("ERROR "+st+": "+resp);
else {
alert("File saved successfully!");
showToast("File saved");
refreshTree();
}
});
@@ -567,10 +567,18 @@ function onBodyLoad(){
var editor=createEditor("editor",vars.file);
globalTree=createTree("tree",editor);
createTop("top",editor);
// Add Ctrl+S / Cmd+S override to save the file
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
editor.save();
}
});
}
</script>
</head>
<body onload="onBodyLoad()">
<div id="toast"></div>
<div id="loader"><div class="loader"></div></div>
<div id="top"></div>
<div style="flex:1;position:relative">

View File

@@ -794,7 +794,7 @@ input[type=range]::-moz-range-thumb {
/* buttons */
.btn {
padding: 8px;
/*margin: 10px 4px;*/
margin: 10px 4px;
width: 230px;
font-size: 19px;
color: var(--c-d);

View File

@@ -1756,6 +1756,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);
if (simplifiedUI) simplifyUI();
@@ -3304,6 +3305,195 @@ function simplifyUI() {
gId("btns").style.display = "none";
}
// Version reporting feature
var versionCheckDone = false;
function checkVersionUpgrade(info) {
// Only check once per page load
if (versionCheckDone) return;
versionCheckDone = true;
// Suppress feature if in AP mode (no internet connection available)
if (info.wifi && info.wifi.ap) return;
// Fetch version-info.json using existing /edit endpoint
fetch(getURL('/edit?func=edit&path=/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);
// On error, save current version for next time
if (info && info.ver) {
updateVersionInfo(info.ver, false);
}
});
}
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!'
: '🎉 WLED Upgrade Detected!';
const description = isInstall
? `You are now running WLED <strong style="text-wrap: nowrap">${newVersion}</strong>.`
: `Your WLED has been upgraded from <strong style="text-wrap: nowrap">${oldVersion}</strong> to <strong style="text-wrap: nowrap">${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>
<p style="color:var(--c-f);font-size:0.9em;">
<a href="https://kno.wled.ge/about/privacy-policy/" target="_blank" style="color:var(--c-6);">Learn more about what data is collected and why</a>
</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(info, 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(info, oldVersion, newVersion) {
showToast('Reporting upgrade...');
// Fetch fresh data from /json/info endpoint as requested
fetch(getURL('/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
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.psram !== undefined) upgradeData.psramSize = infoData.psram;
// Note: partitionSizes not currently available in /json/info endpoint
// 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)
});
})
.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(getURL('/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);

View File

@@ -401,6 +401,8 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL
int16_t extractModeDefaults(uint8_t mode, const char *segVar);
void checkSettingsPIN(const char *pin);
uint16_t crc16(const unsigned char* data_p, size_t length);
String computeSHA1(const String& input);
String getDeviceId();
uint16_t beatsin88_t(accum88 beats_per_minute_88, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0);
uint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0);
uint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0);

View File

@@ -1,5 +1,6 @@
#include "wled.h"
#define JSON_PATH_STATE 1
#define JSON_PATH_INFO 2
#define JSON_PATH_STATE_INFO 3
@@ -690,6 +691,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme
}
}
void serializeInfo(JsonObject root)
{
root[F("ver")] = versionString;
@@ -697,6 +699,7 @@ void serializeInfo(JsonObject root)
root[F("cn")] = F(WLED_CODENAME);
root[F("release")] = releaseString;
root[F("repo")] = repoString;
root[F("deviceId")] = getDeviceId();
JsonObject leds = root.createNestedObject(F("leds"));
leds[F("count")] = strip.getLengthTotal();

View File

@@ -3,6 +3,7 @@
#include "const.h"
#ifdef ESP8266
#include "user_interface.h" // for bootloop detection
#include <Hash.h> // for SHA1 on ESP8266
#else
#include <Update.h>
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
@@ -10,6 +11,8 @@
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)
#include "soc/rtc.h"
#endif
#include "mbedtls/sha1.h" // for SHA1 on ESP32
#include "esp_efuse.h"
#endif
@@ -1125,4 +1128,96 @@ 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
}
}
// Platform-agnostic SHA1 computation from String input
String computeSHA1(const String& input) {
#ifdef ESP8266
return sha1(input); // ESP8266 has built-in sha1() function
#else
// ESP32: Compute SHA1 hash using mbedtls
unsigned char shaResult[20]; // SHA1 produces 20 bytes
mbedtls_sha1_context ctx;
mbedtls_sha1_init(&ctx);
mbedtls_sha1_starts_ret(&ctx);
mbedtls_sha1_update_ret(&ctx, (const unsigned char*)input.c_str(), input.length());
mbedtls_sha1_finish_ret(&ctx, shaResult);
mbedtls_sha1_free(&ctx);
// Convert to hexadecimal string
char hexString[41];
for (int i = 0; i < 20; i++) {
sprintf(&hexString[i*2], "%02x", shaResult[i]);
}
hexString[40] = '\0';
return String(hexString);
#endif
}
#ifdef ESP32
static String dump_raw_block(esp_efuse_block_t block)
{
const int WORDS = 8; // ESP32: 8×32-bit words per block i.e. 256bits
uint32_t buf[WORDS] = {0};
const esp_efuse_desc_t d = {
.efuse_block = block,
.bit_start = 0,
.bit_count = WORDS * 32
};
const esp_efuse_desc_t* field[2] = { &d, NULL };
esp_err_t err = esp_efuse_read_field_blob(field, buf, WORDS * 32);
if (err != ESP_OK) {
return "";
}
String result = "";
for (const unsigned int i : buf) {
char line[32];
sprintf(line, "0x%08X", i);
result += line;
}
return result;
}
#endif
// Generate a device ID based on SHA1 hash of MAC address salted with "WLED"
// Returns: original SHA1 + last 2 chars of double-hashed SHA1 (42 chars total)
String getDeviceId() {
static String cachedDeviceId = "";
if (cachedDeviceId.length() > 0) return cachedDeviceId;
uint8_t mac[6];
WiFi.macAddress(mac);
char macStr[18];
sprintf(macStr, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// The device string is deterministic as it needs to be consistent for the same device, even after a full flash erase
// MAC is salted with other consistent device info to avoid rainbow table attacks.
// If the MAC address is known by malicious actors, they could precompute SHA1 hashes to impersonate devices,
// but as WLED developers are just looking at statistics and not authenticating devices, this is acceptable.
// If the usage data was exfiltrated, you could not easily determine the MAC from the device ID without brute forcing SHA1
#ifdef ESP8266
String deviceString = String(macStr) + "WLED" + ESP.getFlashChipId();
#else
String deviceString = String(macStr) + "WLED" + ESP.getChipModel() + ESP.getChipRevision();
deviceString += dump_raw_block(EFUSE_BLK0);
deviceString += dump_raw_block(EFUSE_BLK1);
deviceString += dump_raw_block(EFUSE_BLK2);
deviceString += dump_raw_block(EFUSE_BLK3);
#endif
String firstHash = computeSHA1(deviceString);
// Second hash: SHA1 of the first hash
String secondHash = computeSHA1(firstHash);
// Concatenate first hash + last 2 chars of second hash
cachedDeviceId = firstHash + secondHash.substring(38);
return cachedDeviceId;
}