Compare commits

..

11 Commits

Author SHA1 Message Date
Frank
38c266d33c ZigBee partitions file: 2.5MB program, 2.9MB LittleFS
extra 400KB margin for ZigBee stack

(flash program: 2555904 bytes)
2026-03-31 00:18:22 +02:00
Frank
c8ce317244 8MB partition: 2.2MB program, 3.8MB LittleFS
* increase program partition by 132 KB
* add zigbee compatible 8MB partitions file
2026-03-30 22:39:03 +02:00
Frank
a5f28d0fcb fix for HUB75 builds
replaces the last remaining FastLED.h with fstled_slim.h
2026-03-30 11:40:58 +02:00
Frank Möhle
fb8f8f0b08 Clarify 16_x branch description
Updated description of the 16_x branch for clarity.
We are not at "feature-freeze" yet
2026-03-30 02:34:43 +02:00
Frank Möhle
53fdf9a89c Update version numbers in copilot instructions 2026-03-30 01:39:44 +02:00
Frank Möhle
a1316034c1 Fix typo in version tag comment 2026-03-30 01:38:20 +02:00
Frank Möhle
75df4affa8 moving stuff around 2026-03-30 01:35:59 +02:00
Frank Möhle
820c841376 add tag scheme information for old/historical versions 2026-03-30 01:28:18 +02:00
Frank Möhle
34d50710b3 Update copilot-instructions with branch structure overview
Added basic project branch and release structure section to AI information
2026-03-30 01:18:19 +02:00
Frank Möhle
d0d62d9493 Clarify instructions to always use the correct source code branch
Updated instructions for providing references in analysis results.
2026-03-30 00:16:05 +02:00
Will Tatam
89b2bc2992 17.0.0-dev 2026-03-29 22:38:13 +01:00
13 changed files with 68 additions and 182 deletions

View File

@@ -78,6 +78,18 @@ After making changes to web UI, always test:
## Common Tasks
### Project Branch / Release Structure
```
main # Main development trunk (daily/nightly) 17.0.0-dev
├── V5 # special branch: code rework for esp-idf 5.5.x (unstable)
├── V5-C6 # special branch: integration of new MCU types: esp32-c5, esp32-c6, esp32-p4 (unstable)
16_x # current beta, preparations for next release 16.0.0
0_15_x # maintainance (bugfixes only) for current release 0.15.4
(tag) v0.14.4 # previous version 0.14.4 (no maintainance)
(tag) v0.13.3 # old version 0.13.3 (no maintainance)
(tag) v0. ... . ... # historical versions 0.12.x and before
```
### Repository Structure
```
wled00/ # Main firmware source (C++)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "wled",
"version": "16.0.0-beta",
"version": "17.0.0-dev",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wled",
"version": "16.0.0-beta",
"version": "17.0.0-dev",
"license": "ISC",
"dependencies": {
"clean-css": "^5.3.3",

View File

@@ -1,6 +1,6 @@
{
"name": "wled",
"version": "16.0.0-beta",
"version": "17.0.0-dev",
"description": "Tools for WLED project",
"main": "tools/cdata.js",
"directories": {

View File

@@ -1,7 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x200000,
app1, app, ota_1, 0x210000,0x200000,
spiffs, data, spiffs, 0x410000,0x3E0000,
app0, app, ota_0, 0x10000, 0x220000,
app1, app, ota_1, 0x230000,0x220000,
spiffs, data, spiffs, 0x450000,0x3A0000,
coredump, data, coredump,,64K
1 # Name, Type, SubType, Offset, Size, Flags
2 nvs, data, nvs, 0x9000, 0x5000,
3 otadata, data, ota, 0xe000, 0x2000,
4 app0, app, ota_0, 0x10000, 0x200000, app0, app, ota_0, 0x10000, 0x220000,
5 app1, app, ota_1, 0x210000,0x200000, app1, app, ota_1, 0x230000,0x220000,
6 spiffs, data, spiffs, 0x410000,0x3E0000, spiffs, data, spiffs, 0x450000,0x3A0000,
7 coredump, data, coredump,,64K

View File

@@ -0,0 +1,9 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x270000,
app1, app, ota_1, 0x280000,0x270000,
spiffs, data, spiffs, 0x4F0000,0x2FB000,
zb_storage, data, fat, 0x7EB000,0x4000,
zb_fct, data, fat, 0x7EF000,0x1000,
coredump, data, coredump,0x7F0000,0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x270000
5 app1 app ota_1 0x280000 0x270000
6 spiffs data spiffs 0x4F0000 0x2FB000
7 zb_storage data fat 0x7EB000 0x4000
8 zb_fct data fat 0x7EF000 0x1000
9 coredump data coredump 0x7F0000 0x10000

View File

@@ -3498,14 +3498,11 @@ void candle(bool multi)
{
if (multi && SEGLEN > 1) {
//allocate segment data
unsigned dataSize = sizeof(uint32_t) + max(1, (int)SEGLEN -1) *3;
unsigned dataSize = sizeof(uint32_t) + max(1, (int)SEGLEN -1) *3; //max. 1365 pixels (ESP8266)
if (!SEGENV.allocateData(dataSize)) candle(false); //allocation failed
} else {
unsigned dataSize = sizeof(uint32_t); // for last call timestamp
if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed
}
uint32_t* lastcall = reinterpret_cast<uint32_t*>(SEGENV.data);
uint8_t* candleData = reinterpret_cast<uint8_t*>(SEGENV.data + sizeof(uint32_t)); // only used for multi-candle
uint8_t* candleData = reinterpret_cast<uint8_t*>(SEGENV.data + sizeof(uint32_t));
//limit update rate
if (strip.now - *lastcall < FRAMETIME_FIXED) return;
@@ -4428,8 +4425,7 @@ void mode_flow(void)
if (zones & 0x01) zones++; //zones must be even
if (zones < 2) zones = 2;
int zoneLen = SEGLEN / zones;
int requiredZones = (SEGLEN + zoneLen - 1) / zoneLen;
zones = requiredZones + 2; //add extra zones to cover beginning and end of segment (compensate integer truncation)
zones += 2; //add two extra zones to cover beginning and end of segment (compensate integer truncation)
int offset = ((int)SEGLEN - (zones * zoneLen)) / 2; // center the zones on the segment (can not use bit shift on negative number)
for (int z = 0; z < zones; z++)

View File

@@ -310,10 +310,10 @@ function onLoad()
sl.addEventListener('touchstart', toggleBubble);
sl.addEventListener('touchend', toggleBubble);
});
// limiter for all number inputs except segment inputs: limit inputs instantly note: segment inputs are special if matrix is enabled, they allow for trailing strips, need a lot of special cases to handle that
// limiter for all number inputs: limit inputs instantly
d.addEventListener("input", function(e) {
const t = e.target;
if (t.tagName === "INPUT" && t.type === "number" && !(t.id && t.id.startsWith("seg"))) {
if (t.tagName === "INPUT" && t.type === "number") {
let val = parseFloat(t.value);
const max = parseFloat(t.max);
const min = parseFloat(t.min);
@@ -1182,7 +1182,7 @@ function updateLen(s)
let mySD = gId("mkSYD");
if (isM) {
// do we have 1D segment *after* the matrix?
if (start >= mw*mh && s > 0) {
if (start >= mw*mh) {
if (sY) { sY.value = 0; sY.max = 0; sY.min = 0; }
if (eY) { eY.value = 1; eY.max = 1; eY.min = 0; }
sX.min = mw*mh; sX.max = ledCount-1;
@@ -3413,23 +3413,13 @@ function showVersionUpgradePrompt(info, oldVersion, newVersion) {
function reportUpgradeEvent(info, oldVersion, alwaysReport) {
showToast('Reporting upgrade...');
const IR_TYPES = {
0: null, // not configured — omit field entirely
1: "24-key", // white 24-key remote
2: "24-key-ct", // white 24-key with CW, WW, CT+, CT- keys
3: "40-key", // blue 40-key remote
4: "44-key", // white 44-key remote
5: "21-key", // white 21-key remote
6: "6-key", // black 6-key learning remote
7: "9-key", // 9-key remote
8: "json-remote", // ir.json configurable remote
};
// Reuse the info argument and fetch only /json/cfg (serialize requests to avoid 503s on low-heap devices)
const infoData = info;
fetch(getURL('/json/cfg'), {method: 'get'})
.then(res => res.ok ? res.json() : Promise.reject(new Error('Failed to fetch /json/cfg')))
.then(cfgData => {
// 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 = {
@@ -3444,58 +3434,13 @@ function reportUpgradeEvent(info, oldVersion, alwaysReport) {
brand: infoData.brand, // Device brand (always present)
product: infoData.product, // Product name (always present)
flashSize: infoData.flash, // Flash size (always present)
repo: infoData.repo, // GitHub repository (always present)
fsUsed: infoData.fs?.u, // Filesystem used space in kB
fsTotal: infoData.fs?.t, // Filesystem total space in kB
// LED hardware
busCount: cfgData.hw?.led?.ins?.length ?? 1,
busTypes: (cfgData.hw?.led?.ins ?? []).map(b => busTypeToString(b.type)),
matrixWidth: infoData.leds?.matrix?.w,
matrixHeight: infoData.leds?.matrix?.h,
ledFeatures: [
...(infoData.leds?.lc & 0x02 ? ["rgbw"] : []),
...(infoData.leds?.lc & 0x04 ? ["cct"] : []),
...((infoData.leds?.maxpwr ?? 0) > 0 ? ["abl"] : []),
...(cfgData.hw?.led?.cr ? ["cct-from-rgb"] : []),
...(cfgData.hw?.led?.cct ? ["white-balance"] : []),
...((cfgData.light?.gc?.col ?? 1.0) > 1.0 || (cfgData.light?.gc?.bri ?? 1.0) > 1.0 ? ["gamma"] : []),
...(cfgData.light?.aseg ? ["auto-segments"] : []),
...((cfgData.light?.nl?.mode ?? 0) > 0 ? ["nightlight"] : []),
],
// peripherals (note: i2c/spi may reflect board defaults, not user-configured hardware)
peripherals: [
...((cfgData.hw?.relay?.pin ?? -1) >= 0 ? ["relay"] : []),
...((cfgData.hw?.btn?.ins ?? []).filter(b => b.type !== 0).length > 0 ? ["buttons"] : []),
...((cfgData.eth?.type ?? 0) > 0 ? ["ethernet"] : []),
...((cfgData.if?.live?.dmx?.inputRxPin ?? 0) > 0 ? ["dmx-input"] : []),
...((cfgData.hw?.ir?.type ?? 0) > 0 ? ["ir-remote"] : []),
],
buttonCount: (cfgData.hw?.btn?.ins ?? []).filter(b => b.type !== 0).length,
// integrations
integrations: [
...(cfgData.if?.hue?.en ? ["hue"] : []),
...(cfgData.if?.mqtt?.en ? ["mqtt"] : []),
...(cfgData.if?.va?.alexa ? ["alexa"] : []),
...(cfgData.if?.sync?.send?.en ? ["wled-sync"] : []),
...(cfgData.nw?.espnow ? ["esp-now"] : []),
...(cfgData.if?.sync?.espnow ? ["esp-now-sync"] : []),
],
// usermods
usermods: Object.keys(cfgData.um ?? {}),
usermodIds: infoData.um ?? [],
};
// IR remote — only include if configured
const irType = IR_TYPES[cfgData.hw?.ir?.type ?? 0];
if (irType) upgradeData.irRemoteType = irType;
repo: infoData.repo // GitHub repository (always present)
};
// Add optional fields if available
if (infoData.psrSz !== undefined) upgradeData.psramSize = infoData.psrSz; // Total PSRAM size in MB; can be 0
// Note: partitionSizes not currently available in /json/info endpoint
// Make AJAX call to postUpgradeEvent API
return fetch('https://usage.wled.me/api/usage/upgrade', {
@@ -3526,17 +3471,6 @@ function reportUpgradeEvent(info, oldVersion, alwaysReport) {
});
}
function busTypeToString(t) {
if (t === 0) return "none";
if (t === 40) return "on-off";
if (t >= 16 && t <= 39) return "digital"; // WS2812, SK6812, etc.
if (t >= 41 && t <= 47) return "pwm"; // analog RGB/CCT/single
if (t >= 48 && t <= 63) return "digital-spi"; // APA102, WS2801, etc.
if (t >= 64 && t <= 71) return "hub75"; // HUB75 matrix panels
if (t >= 80 && t <= 95) return "network"; // DDP, E1.31, ArtNet
return "unknown";
}
function updateVersionInfo(version, neverAsk, alwaysReport) {
const versionInfo = {
version: version,

View File

@@ -1195,7 +1195,7 @@ async function imgPlay(url,name){
on:true,
seg: cur!==null && cur!==tgt
? [{id:cur,fx:0,n:""},{id:tgt,fx:53,frz:false,sx:128,n:name}]
: {id:tgt,fx:53,frz:false,sx:128,ix:0,n:name}
: {id:tgt,fx:53,frz:false,sx:128,n:name}
};
const r=await fetch(getURL('/json/state'),{method:'POST',body:JSON.stringify(j)});
const out=await r.json();

View File

@@ -176,6 +176,7 @@ void serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool
void serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true, bool selectedSegmentsOnly = false);
void serializeInfo(JsonObject root);
void serializeModeNames(JsonArray arr);
void serializeModeData(JsonArray fxdata);
void serializePins(JsonObject root);
void serveJson(AsyncWebServerRequest* request);
#ifdef WLED_ENABLE_JSONLIVE

View File

@@ -28,10 +28,6 @@ int fileReadCallback(void) {
}
int fileReadBlockCallback(void * buffer, int numberOfBytes) {
#ifdef CONFIG_IDF_TARGET_ESP32C3
unsigned t0 = millis();
while (strip.isUpdating() && (millis() - t0 < 15)) yield(); // be nice, but not too nice. Waits up to 15ms to avoid glitches
#endif
return file.read((uint8_t*)buffer, numberOfBytes);
}

View File

@@ -1160,6 +1160,21 @@ void serializePins(JsonObject root)
}
}
// deserializes mode data string into JsonArray
void serializeModeData(JsonArray fxdata)
{
char lineBuffer[256];
for (size_t i = 0; i < strip.getModeCount(); i++) {
strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1);
lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string
if (lineBuffer[0] != 0) {
char* dataPtr = strchr(lineBuffer,'@');
if (dataPtr) fxdata.add(dataPtr+1);
else fxdata.add("");
}
}
}
// deserializes mode names string into JsonArray
// also removes effect data extensions (@...) from deserialised names
void serializeModeNames(JsonArray arr)
@@ -1176,78 +1191,6 @@ void serializeModeNames(JsonArray arr)
}
}
// Writes a JSON-escaped string (with surrounding quotes) into dest[0..maxLen-1].
// Returns bytes written, or 0 if the buffer was too small.
static size_t writeJSONString(uint8_t* dest, size_t maxLen, const char* src) {
size_t pos = 0;
auto emit = [&](char c) -> bool {
if (pos >= maxLen) return false;
dest[pos++] = (uint8_t)c;
return true;
};
if (!emit('"')) return 0;
for (const char* p = src; *p; ++p) {
char esc = ARDUINOJSON_NAMESPACE::EscapeSequence::escapeChar(*p);
if (esc) {
if (!emit('\\') || !emit(esc)) return 0;
} else {
if (!emit(*p)) return 0;
}
}
if (!emit('"')) return 0;
return pos;
}
// Writes ,"<escaped_src>" into dest[0..maxLen-1] (no null terminator).
// Returns bytes written, or 0 if the buffer was too small.
static size_t writeJSONStringElement(uint8_t* dest, size_t maxLen, const char* src) {
if (maxLen == 0) return 0;
dest[0] = ',';
size_t n = writeJSONString(dest + 1, maxLen - 1, src);
if (n == 0) return 0;
return 1 + n;
}
// Generate a streamed JSON response for the mode data
// This uses sendChunked to send the reply in blocks based on how much fit in the outbound
// packet buffer, minimizing the required state (ie. just the next index to send). This
// allows us to send an arbitrarily large response without using any significant amount of
// memory (so no worries about buffer limits).
void respondModeData(AsyncWebServerRequest* request) {
size_t fx_index = 0;
request->sendChunked(FPSTR(CONTENT_TYPE_JSON),
[fx_index](uint8_t* data, size_t len, size_t) mutable {
size_t bytes_written = 0;
char lineBuffer[256];
while (fx_index < strip.getModeCount()) {
strncpy_P(lineBuffer, strip.getModeData(fx_index), sizeof(lineBuffer)-1); // Copy to stack buffer for strchr
if (lineBuffer[0] != 0) {
lineBuffer[sizeof(lineBuffer)-1] = '\0'; // terminate string (only needed if strncpy filled the buffer)
const char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one
size_t mode_bytes = writeJSONStringElement(data, len, dataPtr ? dataPtr + 1 : "");
if (mode_bytes == 0) break; // didn't fit; break loop and try again next packet
if (fx_index == 0) *data = '[';
data += mode_bytes;
len -= mode_bytes;
bytes_written += mode_bytes;
}
++fx_index;
}
if ((fx_index == strip.getModeCount()) && (len >= 1)) {
*data = ']';
++bytes_written;
++fx_index; // we're really done
}
return bytes_written;
});
}
// Global buffer locking response helper class (to make sure lock is released when AsyncJsonResponse is destroyed)
class LockedJsonResponse: public AsyncJsonResponse {
bool _holding_lock;
@@ -1275,7 +1218,7 @@ class LockedJsonResponse: public AsyncJsonResponse {
void serveJson(AsyncWebServerRequest* request)
{
enum class json_target {
all, state, info, state_info, nodes, effects, palettes, networks, config, pins
all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config, pins
};
json_target subJson = json_target::all;
@@ -1286,7 +1229,7 @@ void serveJson(AsyncWebServerRequest* request)
else if (url.indexOf(F("nodes")) > 0) subJson = json_target::nodes;
else if (url.indexOf(F("eff")) > 0) subJson = json_target::effects;
else if (url.indexOf(F("palx")) > 0) subJson = json_target::palettes;
else if (url.indexOf(F("fxda")) > 0) { respondModeData(request); return; }
else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata;
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;
@@ -1311,7 +1254,7 @@ void serveJson(AsyncWebServerRequest* request)
}
// releaseJSONBufferLock() will be called when "response" is destroyed (from AsyncWebServer)
// make sure you delete "response" if no "request->send(response);" is made
LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary
LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::fxdata || subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary
JsonVariant lDoc = response->getRoot();
@@ -1327,6 +1270,8 @@ void serveJson(AsyncWebServerRequest* request)
serializePalettes(lDoc, request->hasParam(F("page")) ? request->getParam(F("page"))->value().toInt() : 0); break;
case json_target::effects:
serializeModeNames(lDoc); break;
case json_target::fxdata:
serializeModeData(lDoc); break;
case json_target::networks:
serializeNetworks(lDoc); break;
case json_target::config:

View File

@@ -377,7 +377,6 @@ bool isWiFiConfigured() {
static u8_t blockRouterAdvertisements(void* arg, struct raw_pcb* pcb, struct pbuf* p, const ip_addr_t* addr) {
// ICMPv6 type is the first byte of the payload, so we skip the header
if (p->len > 0 && (pbuf_get_at(p, sizeof(struct ip6_hdr)) == ICMP6_TYPE_RA)) {
pbuf_free(p);
return 1; // claim the packet — lwIP will not pass it further
}
return 0; // not consumed, pass it on

View File

@@ -85,7 +85,6 @@ private:
IPAddress ipMulti;
uint32_t mac24; //bottom 24 bits of mac
String escapedMac=""; //lowercase mac address
String bridgeId=""; //uppercase EUI-64 bridge ID (16 hex chars)
//private member functions
const char* modeString(EspalexaColorMode m)
@@ -298,13 +297,13 @@ private:
snprintf_P(buf, sizeof(buf), PSTR("HTTP/1.1 200 OK\r\n"
"EXT:\r\n"
"CACHE-CONTROL: max-age=86400\r\n" // SSDP_INTERVAL
"CACHE-CONTROL: max-age=100\r\n" // SSDP_INTERVAL
"LOCATION: http://%s:80/description.xml\r\n"
"SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.17.0\r\n" // _modelName, _modelNumber
"hue-bridgeid: %s\r\n"
"ST: urn:schemas-upnp-org:device:Basic:1\r\n" // _deviceType
"USN: uuid:2f402f80-da50-11e1-9b23-%s::urn:schemas-upnp-org:device:Basic:1\r\n" // _uuid::_deviceType
"\r\n"),s,bridgeId.c_str(),escapedMac.c_str());
"ST: urn:schemas-upnp-org:device:basic:1\r\n" // _deviceType
"USN: uuid:2f402f80-da50-11e1-9b23-%s::upnp:rootdevice\r\n" // _uuid::_deviceType
"\r\n"),s,escapedMac.c_str(),escapedMac.c_str());
espalexaUdp.beginPacket(espalexaUdp.remoteIP(), espalexaUdp.remotePort());
#ifdef ARDUINO_ARCH_ESP32
@@ -334,11 +333,6 @@ public:
escapedMac.replace(":", "");
escapedMac.toLowerCase();
// Compute EUI-64 bridge ID from MAC-48: insert standard "FFFE" padding between
// the first 6 hex chars (OUI/manufacturer) and last 6 hex chars (device), then uppercase
bridgeId = escapedMac.substring(0, 6) + "fffe" + escapedMac.substring(6);
bridgeId.toUpperCase();
String macSubStr = escapedMac.substring(6, 12);
mac24 = strtol(macSubStr.c_str(), 0, 16);