Compare commits

..

8 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c8087e0fbb Restore gap-value format description comment (lines 81-84)
Agent-Logs-Url: https://github.com/wled/WLED/sessions/5bf52c85-712d-468e-bc45-42f46ff2f14e

Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com>
2026-04-19 19:24:57 +00:00
copilot-swe-agent[bot]
028902e596 Improve 2d-gaps.json end-of-array handling and align with deserializeMap() pattern
Agent-Logs-Url: https://github.com/wled/WLED/sessions/fb4e360b-89a2-4297-83dc-7a0da2f9fc96

Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com>
2026-04-19 19:19:18 +00:00
copilot-swe-agent[bot]
ca7f8d463a Chunk-process 2d-gaps.json in setUpMatrix(), eliminating JSON buffer lock
Read gap values directly from file one number at a time (like deserializeMap()
already does for ledmap.json), removing the need for requestJSONBufferLock() /
pDoc / readObjectFromFile(). Saves ~132B flash and frees the JSON buffer for
concurrent use during matrix setup.

Agent-Logs-Url: https://github.com/wled/WLED/sessions/fb4e360b-89a2-4297-83dc-7a0da2f9fc96

Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com>
2026-04-19 19:15:06 +00:00
copilot-swe-agent[bot]
b7c863a3c0 Add clarifying comments per code review feedback
Agent-Logs-Url: https://github.com/wled/WLED/sessions/88471964-aa31-4148-9f41-fa2fbdc8da34

Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com>
2026-04-19 19:04:05 +00:00
copilot-swe-agent[bot]
5497f7e7e9 Stream /json/effects via respondModeData(namesOnly=true), eliminating JSON buffer lock
Agent-Logs-Url: https://github.com/wled/WLED/sessions/88471964-aa31-4148-9f41-fa2fbdc8da34

Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com>
2026-04-19 19:00:48 +00:00
Frank Möhle
3af2ae5f2f Merge pull request #5516 from kilrah/fix_dmx_ident
Add identifier string for DMX realtime mode
2026-04-19 18:36:25 +02:00
Kilrah
259bf3c0f8 Add identifier string for DMX realtime mode
(cherry picked from commit b4ae421fc3)
2026-04-18 16:47:06 +02:00
Damian Schneider
5e49a1cffb bugfixes in PS effects
- fix PS Sparkler for large setups: need 32bit random position, 16bit is not enough
- fix PS Fireworks 1D: need to `break` if no particles are available or it can lead to stalls on large setups
- do not use collisions by default PS Fuzzy Noise: its very slow on larger setups
2026-04-17 15:26:57 +02:00
6 changed files with 68 additions and 53 deletions

View File

@@ -375,7 +375,6 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
-D WLED_DISABLE_PARTICLESYSTEM2D
lib_deps = ${esp8266.lib_deps}
monitor_filters = esp8266_exception_decoder
custom_usermods = audioreactive
[env:nodemcuv2_compat]
extends = env:nodemcuv2
@@ -385,7 +384,6 @@ platform_packages = ${esp8266.platform_packages_compat}
build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM2D
;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9
custom_usermods = audioreactive
[env:nodemcuv2_160]
extends = env:nodemcuv2
@@ -404,7 +402,6 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
-D WLED_DISABLE_PARTICLESYSTEM2D
-D WLED_DISABLE_PARTICLESYSTEM1D
lib_deps = ${esp8266.lib_deps}
custom_usermods = audioreactive
[env:esp8266_2m_compat]
extends = env:esp8266_2m
@@ -414,7 +411,6 @@ platform_packages = ${esp8266.platform_packages_compat}
build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D
-D WLED_DISABLE_PARTICLESYSTEM1D
-D WLED_DISABLE_PARTICLESYSTEM2D
custom_usermods = audioreactive
[env:esp8266_2m_160]
extends = env:esp8266_2m

View File

@@ -140,7 +140,7 @@ static uint8_t binNum = 8; // Used to select the bin for FFT based bea
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
#define UM_AUDIOREACTIVE_USE_INTEGER_FFT // always use integer FFT on ESP32-S2 and ESP32-C3
#endif
#endif // UM_AUDIOREACTIVE_USE_ARDUINO_FFT
#endif
#if !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT)
using FFTsampleType = float;
@@ -758,8 +758,6 @@ class AudioReactive : public Usermod {
private:
#ifdef ARDUINO_ARCH_ESP32
static constexpr uint8_t SR_DMTYPE_NETWORK_ONLY = 254;
#ifndef AUDIOPIN
int8_t audioPin = -1;
#else
@@ -1441,7 +1439,7 @@ class AudioReactive : public Usermod {
break;
#endif
case SR_DMTYPE_NETWORK_ONLY: // dummy "network receive only" mode
case 254: // dummy "network receive only" mode
if (audioSource) delete audioSource; audioSource = nullptr;
disableSoundProcessing = true;
audioSyncEnabled = 2; // force udp sound receive mode
@@ -1458,25 +1456,19 @@ class AudioReactive : public Usermod {
}
delay(250); // give microphone enough time to initialise
if (!audioSource && (dmType != SR_DMTYPE_NETWORK_ONLY)) enabled = false;// audio failed to initialise
if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise
#endif
if (enabled) onUpdateBegin(false); // create FFT task, and initialize network
#ifdef ARDUINO_ARCH_ESP32
if (audioSource && FFT_Task == nullptr) enabled = false; // FFT task creation failed
if (FFT_Task == nullptr) enabled = false; // FFT task creation failed
if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync
#ifdef WLED_DEBUG
#define AR_INIT_DEBUG_PRINT DEBUG_PRINTLN
#else
#define AR_INIT_DEBUG_PRINT DEBUGSR_PRINTLN
#endif
if (dmType == SR_DMTYPE_NETWORK_ONLY) {
AR_INIT_DEBUG_PRINT(F("AR: No sound input driver configured - network receive only."));
} else {
AR_INIT_DEBUG_PRINT(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));
}
#undef AR_INIT_DEBUG_PRINT
#ifdef WLED_DEBUG
DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));
#else
DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings."));
#endif
disableSoundProcessing = true;
}
#endif
@@ -2131,9 +2123,6 @@ class AudioReactive : public Usermod {
uiScript.print(F("addOption(dd,'Generic PDM',5);"));
#endif
uiScript.print(F("addOption(dd,'ES8388',6);"));
uiScript.print(F("addOption(dd,'None - network receive only',"));
uiScript.print(SR_DMTYPE_NETWORK_ONLY);
uiScript.print(F(");"));
uiScript.print(F("dd=addDropdown(ux,'config:AGC');"));
uiScript.print(F("addOption(dd,'Off',0);"));

View File

@@ -8730,7 +8730,7 @@ void mode_particleperlin(void) {
PartSys->update(); // update and render
}
static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1";
static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5";
/*
Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with
@@ -9836,6 +9836,7 @@ void mode_particleFireworks1D(void) {
PartSys->setColorByPosition(false); // disable
for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles
int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle
if (idx < 0) break; // no more particles available
if(SEGMENT.custom3 > 23) {
if(SEGMENT.custom3 == 31) { // highest slider value
PartSys->setColorByAge(SEGMENT.check1); // color by age if colorful mode is enabled
@@ -9860,7 +9861,7 @@ void mode_particleFireworks1D(void) {
PartSys->applyFriction(1); // apply friction to all particles
PartSys->update(); // update and render
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan
else PartSys->particles[i].ttl = 0;
@@ -9910,7 +9911,7 @@ void mode_particleSparkler(void) {
PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code)
PartSys->sources[i].sat = SEGMENT.custom1; // color saturation
if (SEGMENT.speed == 255) // random position at highest speed setting
PartSys->sources[i].source.x = hw_random16(PartSys->maxX);
PartSys->sources[i].source.x = hw_random(PartSys->maxX);
else
PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler
}

View File

@@ -71,29 +71,50 @@ 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 && requestJSONBufferLock(JSON_LOCK_LEDGAP)) {
if (isFile) {
DEBUG_PRINT(F("Reading LED gap from "));
DEBUG_PRINTLN(fileName);
// read the array into global JSON buffer
if (readObjectFromFile(fileName, nullptr, pDoc)) {
gapTable = static_cast<int8_t*>(p_malloc(matrixSize));
if (gapTable) {
// 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)
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);
// 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;
}
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

View File

@@ -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
#define JSON_LOCK_LEDGAP 20
// JSON_LOCK 20 formerly used for LEDGAP (now parsed without JSON buffer)
#define JSON_LOCK_LEDMAP_ENUM 21
#define JSON_LOCK_REMOTE 22

View File

@@ -761,6 +761,7 @@ void serializeInfo(JsonObject root)
case REALTIME_MODE_ARTNET: root["lm"] = F("Art-Net"); break;
case REALTIME_MODE_TPM2NET: root["lm"] = F("tpm2.net"); break;
case REALTIME_MODE_DDP: root["lm"] = F("DDP"); break;
case REALTIME_MODE_DMX: root["lm"] = F("DMX"); break;
}
root[F("lip")] = realtimeIP[0] == 0 ? "" : realtimeIP.toString();
@@ -1212,23 +1213,32 @@ 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
// 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) {
// 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) {
size_t fx_index = 0;
request->sendChunked(FPSTR(CONTENT_TYPE_JSON),
[fx_index](uint8_t* data, size_t len, size_t) mutable {
[fx_index, namesOnly](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 : "");
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);
if (mode_bytes == 0) break; // didn't fit; break loop and try again next packet
if (fx_index == 0) *data = '[';
data += mode_bytes;
@@ -1275,7 +1285,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, palettes, networks, config, pins
};
json_target subJson = json_target::all;
@@ -1284,7 +1294,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) subJson = json_target::effects;
else if (url.indexOf(F("eff")) > 0) { respondModeData(request, true); return; }
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;
@@ -1311,7 +1321,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, false); // will clear JsonDocument
JsonVariant lDoc = response->getRoot();
@@ -1325,8 +1335,6 @@ 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: