mirror of
https://github.com/wled/WLED.git
synced 2026-04-22 15:12:45 +00:00
Compare commits
4 Commits
copilot/re
...
nightly
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01328a65c1 | ||
|
|
a2d970e155 | ||
|
|
cb666cedc5 | ||
|
|
5e0a0a7561 |
@@ -177,6 +177,7 @@ No automated linting is configured. Match existing code style in files you edit.
|
||||
|
||||
- Repository language is English
|
||||
- Never edit or commit auto-generated `wled00/html_*.h` / `wled00/js_*.h`
|
||||
- When updating an existing PR, retain the original description. Only modify it to ensure technical accuracy. Add change logs after the existing description.
|
||||
- No force-push on open PRs
|
||||
- Changes to `platformio.ini` require maintainer approval
|
||||
- Remove dead/unused code — justify or delete it
|
||||
|
||||
@@ -30,7 +30,8 @@ This lets you update your PR if needed, while you can work on other tasks in 'ma
|
||||
|
||||
### Target branch for pull requests
|
||||
|
||||
Please make all PRs against the `main` branch.
|
||||
> [!IMPORTANT]
|
||||
> Please make all PRs against the `main` branch.
|
||||
|
||||
### Describing your PR
|
||||
|
||||
|
||||
@@ -71,50 +71,29 @@ void WS2812FX::setUpMatrix() {
|
||||
// allowed values are: -1 (missing pixel/no LED attached), 0 (inactive/unused pixel), 1 (active/used pixel)
|
||||
char fileName[32]; strcpy_P(fileName, PSTR("/2d-gaps.json"));
|
||||
bool isFile = WLED_FS.exists(fileName);
|
||||
size_t gapSize = 0;
|
||||
int8_t *gapTable = nullptr;
|
||||
|
||||
if (isFile) {
|
||||
if (isFile && requestJSONBufferLock(JSON_LOCK_LEDGAP)) {
|
||||
DEBUG_PRINT(F("Reading LED gap from "));
|
||||
DEBUG_PRINTLN(fileName);
|
||||
gapTable = static_cast<int8_t*>(p_malloc(matrixSize));
|
||||
if (gapTable) {
|
||||
// read the array into global JSON buffer
|
||||
if (readObjectFromFile(fileName, nullptr, pDoc)) {
|
||||
// the array is similar to ledmap, except it has only 3 values:
|
||||
// -1 ... missing pixel (do not increase pixel count)
|
||||
// 0 ... inactive pixel (it does count, but should be mapped out (-1))
|
||||
// 1 ... active pixel (it will count and will be mapped)
|
||||
// read entries directly from the file, one number at a time
|
||||
// (no JSON buffer / pDoc needed — the file is a plain JSON array)
|
||||
// follows the same parsing pattern used by deserializeMap() for ledmap.json
|
||||
size_t gapIdx = 0;
|
||||
File f = WLED_FS.open(fileName, "r");
|
||||
if (f) {
|
||||
f.find('['); // skip to start of array
|
||||
while (f.available() && gapIdx < matrixSize) {
|
||||
char number[8];
|
||||
size_t numRead = f.readBytesUntil(',', number, sizeof(number) - 1); // last entry reads up to ']' (no trailing comma)
|
||||
number[numRead] = 0;
|
||||
if (numRead > 0) {
|
||||
char *end = strchr(number, ']'); // check for end-of-array marker
|
||||
bool foundDigit = (end == nullptr); // no ']' means the whole token is a number
|
||||
if (end != nullptr) {
|
||||
// ']' present — only accept if a digit (or '-') appears before it
|
||||
for (int k = 0; &number[k] != end; k++) {
|
||||
if (number[k] >= '0' && number[k] <= '9') { foundDigit = true; break; }
|
||||
if (number[k] == '-') { foundDigit = true; break; }
|
||||
}
|
||||
}
|
||||
if (!foundDigit) break; // ']' with no number — array ended
|
||||
gapTable[gapIdx++] = constrain(atoi(number), -1, 1);
|
||||
} else break;
|
||||
JsonArray map = pDoc->as<JsonArray>();
|
||||
gapSize = map.size();
|
||||
if (!map.isNull() && gapSize >= matrixSize) { // not an empty map
|
||||
gapTable = static_cast<int8_t*>(p_malloc(gapSize));
|
||||
if (gapTable) for (size_t i = 0; i < gapSize; i++) {
|
||||
gapTable[i] = constrain(map[i], -1, 1);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
if (gapIdx < matrixSize) { // file was too short or could not be read — discard
|
||||
p_free(gapTable);
|
||||
gapTable = nullptr;
|
||||
}
|
||||
}
|
||||
DEBUG_PRINTLN(F("Gaps loaded."));
|
||||
releaseJSONBufferLock();
|
||||
}
|
||||
|
||||
unsigned x, y, pix=0; //pixel
|
||||
|
||||
@@ -478,7 +478,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
|
||||
#define JSON_LOCK_SERVEJSON 17
|
||||
#define JSON_LOCK_NOTIFY 18
|
||||
#define JSON_LOCK_PRESET_NAME 19
|
||||
// JSON_LOCK 20 formerly used for LEDGAP (now parsed without JSON buffer)
|
||||
#define JSON_LOCK_LEDGAP 20
|
||||
#define JSON_LOCK_LEDMAP_ENUM 21
|
||||
#define JSON_LOCK_REMOTE 22
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@
|
||||
let copyColor = '#000';
|
||||
let ws = null;
|
||||
let maxCol; // max colors to send out in one chunk, ESP8266 is limited to ~50 (500 bytes), ESP32 can do ~128 (1340 bytes)
|
||||
let _applySeq = 0; // incremented each time applyLED fires; used to cancel stale in-flight previews
|
||||
let _httpQueue = [], _httpRun = 0;
|
||||
|
||||
// load external resources in sequence to avoid 503 errors if heap is low, repeats indefinitely until loaded
|
||||
(function loadFiles() {
|
||||
@@ -621,21 +623,16 @@
|
||||
|
||||
async function requestJson(cmd)
|
||||
{
|
||||
if (ws && ws.readyState == 1) {
|
||||
if (ws && ws.readyState == 1 && ws.bufferedAmount < 32768) {
|
||||
try {
|
||||
ws.send(JSON.stringify(cmd));
|
||||
await new Promise(r => setTimeout(r, 15)); // short delay to give ESP time to process (fewer packets dropped)
|
||||
return 1;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!window._httpQueue) {
|
||||
window._httpQueue = [];
|
||||
window._httpRun = 0;
|
||||
}
|
||||
if (_httpQueue.length >= 5) {
|
||||
return Promise.resolve(-1); // reject if too many queued requests
|
||||
}
|
||||
|
||||
// HTTP fallback
|
||||
if (_httpQueue.length >= 5) return -1; // queue full; applyLED cancels stale queues before sending
|
||||
return new Promise(resolve => {
|
||||
_httpQueue.push({ cmd, resolve });
|
||||
(async function run() {
|
||||
@@ -650,7 +647,7 @@
|
||||
cache: 'no-store'
|
||||
});
|
||||
} catch (e) {}
|
||||
await new Promise(r => setTimeout(r, 120));
|
||||
await new Promise(r => setTimeout(r, 120)); // delay between requests (go slow, this is the http fallback if WS fails)
|
||||
q.resolve(0);
|
||||
}
|
||||
_httpRun = 0;
|
||||
@@ -662,8 +659,12 @@
|
||||
async function applyLED()
|
||||
{
|
||||
if (!palCache.length) return;
|
||||
const seq = ++_applySeq;
|
||||
// discard pending HTTP chunks from any previous preview so stale data doesn't drain slowly
|
||||
while (_httpQueue.length) _httpQueue.shift().resolve(-1); // resolve dropped entries so their awaiters can observe the seq change and exit
|
||||
try {
|
||||
let st = await (await fetch(getURL('/json/state'), { cache: 'no-store' })).json();
|
||||
if (seq !== _applySeq) return; // superseded by a newer preview request
|
||||
if (!st.seg || !st.seg.length) return;
|
||||
|
||||
// get selected segments, use main segment if none selected
|
||||
@@ -680,6 +681,7 @@
|
||||
arr.push(palCache[len > 1 ? Math.round(i * 255 / (len - 1)) : 0]);
|
||||
// send colors in chunks
|
||||
for (let j = 0; j < arr.length; j += maxCol) {
|
||||
if (seq !== _applySeq) return; // superseded mid-send
|
||||
let chunk = [s.start + j, ...arr.slice(j, j + maxCol)];
|
||||
await requestJson({ seg: { id: s.id, i: chunk } });
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ var pN = "", pI = 0, pNum = 0;
|
||||
var pmt = 1, pmtLS = 0;
|
||||
var lastinfo = {};
|
||||
var isM = false, mw = 0, mh=0;
|
||||
var bsOpts = null; // blending style options snapshot, used for dynamic filtering based on matrix mode (iOS compatibility)
|
||||
var ws, wsRpt=0;
|
||||
var cfg = {
|
||||
theme:{base:"dark", bg:{url:"", rnd: false, rndGrayscale: false, rndBlur: false}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}},
|
||||
@@ -660,12 +661,14 @@ function parseInfo(i) {
|
||||
mw = i.leds.matrix ? i.leds.matrix.w : 0;
|
||||
mh = i.leds.matrix ? i.leds.matrix.h : 0;
|
||||
isM = mw>0 && mh>0;
|
||||
if (!bsOpts) bsOpts = Array.from(gId('bs').options).map(o => o.cloneNode(true)); // snapshot all options on first call
|
||||
const bsSel = gId('bs');
|
||||
// note: style.display='none' for option elements is not supported on all browsers (notably iOS)
|
||||
bsSel.replaceChildren(...bsOpts.filter(o => isM || o.dataset.type !== "2D").map(o => o.cloneNode(true))); // allow all in matrix mode, filter 2D blends otherwise
|
||||
if (!isM) {
|
||||
gId("filter2D").classList.add('hide');
|
||||
gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='none';});
|
||||
gId("filter2D").classList.add('hide'); // hide 2D effects in non-matrix mode
|
||||
} else {
|
||||
gId("filter2D").classList.remove('hide');
|
||||
gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='';});
|
||||
gId("filter2D").classList.remove('hide');
|
||||
}
|
||||
gId("updBt").style.display = (i.opt & 1) ? '':'none';
|
||||
// if (i.noaudio) {
|
||||
@@ -1457,7 +1460,9 @@ function readState(s,command=false)
|
||||
|
||||
tr = s.transition;
|
||||
gId('tt').value = tr/10;
|
||||
gId('bs').value = s.bs || 0;
|
||||
const bsSel = gId('bs');
|
||||
bsSel.value = s.bs || 0; // assign blending style
|
||||
if (!bsSel.value) bsSel.value = 0; // fall back to Fade if option does not exist
|
||||
if (tr===0) gId('bsp').classList.add('hide')
|
||||
else gId('bsp').classList.remove('hide')
|
||||
|
||||
|
||||
@@ -1213,32 +1213,23 @@ static size_t writeJSONStringElement(uint8_t* dest, size_t maxLen, const char* s
|
||||
return 1 + n;
|
||||
}
|
||||
|
||||
// Generate a streamed JSON response for the mode data (namesOnly=false) or mode names
|
||||
// (namesOnly=true). 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, bool namesOnly = false) {
|
||||
// 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, namesOnly](uint8_t* data, size_t len, size_t) mutable {
|
||||
[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)
|
||||
char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one; non-const so namesOnly mode can truncate lineBuffer here
|
||||
const char* value;
|
||||
// namesOnly=true → emit the display name (everything before '@')
|
||||
// namesOnly=false → emit the fx-data string (everything after '@')
|
||||
if (namesOnly) {
|
||||
if (dataPtr) *dataPtr = '\0'; // truncate at '@' to get name only
|
||||
value = lineBuffer;
|
||||
} else {
|
||||
value = dataPtr ? dataPtr + 1 : ""; // everything after '@' is the fx data
|
||||
}
|
||||
size_t mode_bytes = writeJSONStringElement(data, len, value);
|
||||
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;
|
||||
@@ -1285,7 +1276,7 @@ class LockedJsonResponse: public AsyncJsonResponse {
|
||||
void serveJson(AsyncWebServerRequest* request)
|
||||
{
|
||||
enum class json_target {
|
||||
all, state, info, state_info, nodes, palettes, networks, config, pins
|
||||
all, state, info, state_info, nodes, effects, palettes, networks, config, pins
|
||||
};
|
||||
json_target subJson = json_target::all;
|
||||
|
||||
@@ -1294,7 +1285,7 @@ void serveJson(AsyncWebServerRequest* request)
|
||||
else if (url.indexOf("info") > 0) subJson = json_target::info;
|
||||
else if (url.indexOf("si") > 0) subJson = json_target::state_info;
|
||||
else if (url.indexOf(F("nodes")) > 0) subJson = json_target::nodes;
|
||||
else if (url.indexOf(F("eff")) > 0) { respondModeData(request, true); return; }
|
||||
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("net")) > 0) subJson = json_target::networks;
|
||||
@@ -1321,7 +1312,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, false); // will clear JsonDocument
|
||||
LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary
|
||||
|
||||
JsonVariant lDoc = response->getRoot();
|
||||
|
||||
@@ -1335,6 +1326,8 @@ void serveJson(AsyncWebServerRequest* request)
|
||||
serializeNodes(lDoc); break;
|
||||
case json_target::palettes:
|
||||
serializePalettes(lDoc, request->hasParam(F("page")) ? request->getParam(F("page"))->value().toInt() : 0); break;
|
||||
case json_target::effects:
|
||||
serializeModeNames(lDoc); break;
|
||||
case json_target::networks:
|
||||
serializeNetworks(lDoc); break;
|
||||
case json_target::config:
|
||||
|
||||
Reference in New Issue
Block a user