Compare commits

..

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
25a875e4d9 Fix race condition in 2D config save by deferring resume()
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-10-02 17:45:54 +00:00
copilot-swe-agent[bot]
ed3b092a47 Initial plan 2025-10-02 17:37:06 +00:00
11 changed files with 26 additions and 87 deletions

View File

@@ -30,27 +30,6 @@ The build has two main phases:
- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m` - Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`
- List all targets: `pio run --list-targets` - List all targets: `pio run --list-targets`
## Before Finishing Work
**CRITICAL: You MUST complete ALL of these steps before marking your work as complete:**
1. **Run the test suite**: `npm test` -- Set timeout to 2+ minutes. NEVER CANCEL.
- All tests MUST pass
- If tests fail, fix the issue before proceeding
2. **Build at least one hardware environment**: `pio run -e esp32dev` -- Set timeout to 30+ minutes. NEVER CANCEL.
- Choose `esp32dev` as it's a common, representative environment
- See "Hardware Compilation" section above for the full list of common environments
- The build MUST complete successfully without errors
- If the build fails, fix the issue before proceeding
- **DO NOT skip this step** - it validates that firmware compiles with your changes
3. **For web UI changes only**: Manually test the interface
- See "Manual Testing Scenarios" section below
- Verify the UI loads and functions correctly
**If any of these validation steps fail, you MUST fix the issues before finishing. Do NOT mark work as complete with failing builds or tests.**
## Validation and Testing ## Validation and Testing
### Web UI Testing ### Web UI Testing
@@ -65,7 +44,7 @@ The build has two main phases:
- **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files - **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files
- **C++ formatting available**: `clang-format` is installed but not in CI - **C++ formatting available**: `clang-format` is installed but not in CI
- **Always run tests before finishing**: `npm test` - **Always run tests before finishing**: `npm test`
- **MANDATORY: Always run a hardware build before finishing** (see "Before Finishing Work" section below) - **Always run a build for the common environment before finishing**
### Manual Testing Scenarios ### Manual Testing Scenarios
After making changes to web UI, always test: After making changes to web UI, always test:
@@ -121,16 +100,10 @@ package.json # Node.js dependencies and scripts
## Build Timing and Timeouts ## Build Timing and Timeouts
**IMPORTANT: Use these timeout values when running builds:** - **Web UI build**: 3 seconds - Set timeout to 30 seconds minimum
- **Test suite**: 40 seconds - Set timeout to 2 minutes minimum
- **Web UI build** (`npm run build`): 3 seconds typical - Set timeout to 30 seconds minimum - **Hardware builds**: 15+ minutes - Set timeout to 30+ minutes minimum
- **Test suite** (`npm test`): 40 seconds typical - Set timeout to 120 seconds (2 minutes) minimum - **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation can take significant time
- **Hardware builds** (`pio run -e [target]`): 15-20 minutes typical for first build - Set timeout to 1800 seconds (30 minutes) minimum
- Subsequent builds are faster due to caching
- First builds download toolchains and dependencies which takes significant time
- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation require patience
**When validating your changes before finishing, you MUST wait for the hardware build to complete successfully. Set the timeout appropriately and be patient.**
## Troubleshooting ## Troubleshooting
@@ -156,17 +129,11 @@ package.json # Node.js dependencies and scripts
- **Hardware builds require appropriate ESP32/ESP8266 development board** - **Hardware builds require appropriate ESP32/ESP8266 development board**
## CI/CD Pipeline ## CI/CD Pipeline
The GitHub Actions workflow:
**The GitHub Actions CI workflow will:**
1. Installs Node.js and Python dependencies 1. Installs Node.js and Python dependencies
2. Runs `npm test` to validate build system (MUST pass) 2. Runs `npm test` to validate build system
3. Builds web UI with `npm run build` (automatically run by PlatformIO) 3. Builds web UI with `npm run build`
4. Compiles firmware for ALL hardware targets listed in `default_envs` (MUST succeed for all) 4. Compiles firmware for multiple hardware targets
5. Uploads build artifacts 5. Uploads build artifacts
**To ensure CI success, you MUST locally:** Match this workflow in your local development to ensure CI success.
- Run `npm test` and ensure it passes
- Run `pio run -e esp32dev` (or another common environment from "Hardware Compilation" section) and ensure it completes successfully
- If either fails locally, it WILL fail in CI
**Match this workflow in your local development to ensure CI success. Do not mark work complete until you have validated builds locally.**

View File

@@ -450,7 +450,7 @@ board_build.partitions = ${esp32.large_partitions}
board_upload.flash_size = 8MB board_upload.flash_size = 8MB
board_upload.maximum_size = 8388608 board_upload.maximum_size = 8388608
; board_build.f_flash = 80000000L ; board_build.f_flash = 80000000L
board_build.flash_mode = dio ; board_build.flash_mode = qio
[env:esp32dev_16M] [env:esp32dev_16M]
board = esp32dev board = esp32dev

View File

@@ -135,8 +135,7 @@ uint16_t mode_copy_segment(void) {
SEGMENT.fadeToBlackBy(5); // fade out SEGMENT.fadeToBlackBy(5); // fade out
return FRAMETIME; return FRAMETIME;
} }
Segment& sourcesegment = strip.getSegment(sourceid); Segment sourcesegment = strip.getSegment(sourceid);
if (sourcesegment.isActive()) { if (sourcesegment.isActive()) {
uint32_t sourcecolor; uint32_t sourcecolor;
uint32_t destcolor; uint32_t destcolor;
@@ -678,7 +677,7 @@ uint16_t mode_twinkle(void) {
SEGENV.step = it; SEGENV.step = it;
} }
uint16_t PRNG16 = SEGENV.aux1; unsigned PRNG16 = SEGENV.aux1;
for (unsigned i = 0; i < SEGENV.aux0; i++) for (unsigned i = 0; i < SEGENV.aux0; i++)
{ {

View File

@@ -625,9 +625,6 @@ class Segment {
DEBUGFX_PRINTLN(); DEBUGFX_PRINTLN();
#endif #endif
clearName(); clearName();
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
deallocateData(); deallocateData();
p_free(pixels); p_free(pixels);
} }

View File

@@ -113,7 +113,7 @@ void WS2812FX::setUpMatrix() {
// delete gap array as we no longer need it // delete gap array as we no longer need it
p_free(gapTable); p_free(gapTable);
resume(); // NOTE: do not resume() here; caller must call resume() after segments are updated
#ifdef WLED_DEBUG #ifdef WLED_DEBUG
DEBUG_PRINT(F("Matrix ledmap:")); DEBUG_PRINT(F("Matrix ledmap:"));
@@ -130,6 +130,7 @@ void WS2812FX::setUpMatrix() {
Segment::maxWidth = _length; Segment::maxWidth = _length;
Segment::maxHeight = 1; Segment::maxHeight = 1;
resetSegments(); resetSegments();
resume(); // resume here since resetSegments() was called
} }
} }
#else #else

View File

@@ -448,9 +448,6 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
// apply change immediately // apply change immediately
if (i2 <= i1) { //disable segment if (i2 <= i1) { //disable segment
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
deallocateData(); deallocateData();
p_free(pixels); p_free(pixels);
pixels = nullptr; pixels = nullptr;
@@ -469,9 +466,6 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
#endif #endif
// safety check // safety check
if (start >= stop || startY >= stopY) { if (start >= stop || startY >= stopY) {
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
deallocateData(); deallocateData();
p_free(pixels); p_free(pixels);
pixels = nullptr; pixels = nullptr;
@@ -485,9 +479,6 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
pixels = static_cast<uint32_t*>(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS)); pixels = static_cast<uint32_t*>(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS));
if (!pixels) { if (!pixels) {
DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!"));
#ifdef WLED_ENABLE_GIF
endImagePlayback(this);
#endif
deallocateData(); deallocateData();
errorFlag = ERR_NORAM_PX; errorFlag = ERR_NORAM_PX;
stop = 0; stop = 0;

View File

@@ -72,10 +72,11 @@ uint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) {
// video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue // video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue
uint8_t r = byte(c1>>16), g = byte(c1>>8), b = byte(c1), w = byte(c1>>24); // extract r, g, b, w channels uint8_t r = byte(c1>>16), g = byte(c1>>8), b = byte(c1), w = byte(c1>>24); // extract r, g, b, w channels
uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation
addRemains = r && (r<<5) > maxc ? 0x00010000 : 0; // note: setting color preservation threshold too high results in flickering and uint8_t quarterMax = maxc >> 2; // note: using half of max results in color artefacts
addRemains |= g && (g<<5) > maxc ? 0x00000100 : 0; // jumping colors in low brightness gradients. Multiplying the color preserves addRemains = r && r > quarterMax ? 0x00010000 : 0;
addRemains |= b && (b<<5) > maxc ? 0x00000001 : 0; // better accuracy than dividing the maxc. Shifting by 5 is a good compromise addRemains |= g && g > quarterMax ? 0x00000100 : 0;
addRemains |= w ? 0x01000000 : 0; // i.e. remove color channel if <13% of max addRemains |= b && b > quarterMax ? 0x00000001 : 0;
addRemains |= w ? 0x01000000 : 0;
} }
const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;
uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue

View File

@@ -64,22 +64,6 @@ namespace {
} }
} }
/**
* Deserialize a segment description from a JSON object and apply it to the specified segment slot.
*
* Parses and applies geometry, naming, grouping/spacing/offset, 2D bounds, mode, palette, colors
* (supports kelvin, hex, "r" random, object or array formats), per-LED assignments, options (on/frz/sel/rev/mi),
* speed/intensity, custom channels, checks, blend mode, and LOXONE mappings. The function may append a new
* segment, delete a segment, perform a repeat expansion to create multiple segments, and mark global state
* as changed when segment parameters differ.
*
* @param elem JSON object describing the segment (API format).
* @param it Default segment index to use when `elem["id"]` is not provided.
* @param presetId Optional preset identifier; when nonzero, preset-related side effects (e.g., playlist unloading)
* are suppressed or handled differently.
* @return true if the JSON was valid for the target id and the segment was applied (or created); false if the
* target id is out of range or the descriptor is invalid (e.g., attempting to create an empty segment).
*/
static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
{ {
byte id = elem["id"] | it; byte id = elem["id"] | it;
@@ -221,7 +205,7 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
// JSON "col" array can contain the following values for each of segment's colors (primary, background, custom): // JSON "col" array can contain the following values for each of segment's colors (primary, background, custom):
// "col":[int|string|object|array, int|string|object|array, int|string|object|array] // "col":[int|string|object|array, int|string|object|array, int|string|object|array]
// int = Kelvin temperature or 0 for black // int = Kelvin temperature or 0 for black
// string = hex representation of [WW]RRGGBB or "r" for random color // string = hex representation of [WW]RRGGBB
// object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {}) // object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {})
// array = direct channel values [r,g,b,w] (w element being optional) // array = direct channel values [r,g,b,w] (w element being optional)
int rgbw[] = {0,0,0,0}; int rgbw[] = {0,0,0,0};
@@ -245,9 +229,6 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
if (kelvin == 0) seg.setColor(i, 0); if (kelvin == 0) seg.setColor(i, 0);
if (kelvin > 0) colorKtoRGB(kelvin, brgbw); if (kelvin > 0) colorKtoRGB(kelvin, brgbw);
colValid = true; colValid = true;
} else if (hexCol[0] == 'r' && hexCol[1] == '\0') { // Random colors via JSON API in Segment object like col=["r","r","r"] · Issue #4996
setRandomColor(brgbw);
colValid = true;
} else { //HEX string, e.g. "FFAA00" } else { //HEX string, e.g. "FFAA00"
colValid = colorFromHexString(brgbw, hexCol); colValid = colorFromHexString(brgbw, hexCol);
} }
@@ -1259,4 +1240,4 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient)
#endif #endif
return true; return true;
} }
#endif #endif

View File

@@ -817,6 +817,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
strip.panel.shrink_to_fit(); // release unused memory strip.panel.shrink_to_fit(); // release unused memory
strip.deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist) strip.deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist)
strip.makeAutoSegments(true); // force re-creation of segments strip.makeAutoSegments(true); // force re-creation of segments
strip.resume(); // resume strip service after 2D config changes are complete
} }
#endif #endif

View File

@@ -896,8 +896,7 @@ static bool detectBootLoop() {
bl_crashcounter++; bl_crashcounter++;
if (bl_crashcounter >= BOOTLOOP_THRESHOLD) { if (bl_crashcounter >= BOOTLOOP_THRESHOLD) {
DEBUG_PRINTLN(F("!BOOTLOOP DETECTED!")); DEBUG_PRINTLN(F("!BOOTLOOP DETECTED!"));
bl_crashcounter = 0; bl_crashcounter = 0;
if(bl_actiontracker > BOOTLOOP_ACTION_DUMP) bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // reset action tracker if out of bounds
result = true; result = true;
} }
} else { } else {

View File

@@ -193,6 +193,7 @@ void WLED::loop()
strip.finalizeInit(); // will create buses and also load default ledmap if present strip.finalizeInit(); // will create buses and also load default ledmap if present
if (aligned) strip.makeAutoSegments(); if (aligned) strip.makeAutoSegments();
else strip.fixInvalidSegments(); else strip.fixInvalidSegments();
strip.resume(); // resume strip service after bus re-initialization
BusManager::setBrightness(scaledBri(bri)); // fix re-initialised bus' brightness #4005 and #4824 BusManager::setBrightness(scaledBri(bri)); // fix re-initialised bus' brightness #4005 and #4824
configNeedsWrite = true; configNeedsWrite = true;
} }
@@ -563,6 +564,7 @@ void WLED::beginStrip()
strip.setTransition(0); // temporarily prevent transitions to reduce segment copies strip.setTransition(0); // temporarily prevent transitions to reduce segment copies
strip.finalizeInit(); // busses created during deserializeConfig() if config existed strip.finalizeInit(); // busses created during deserializeConfig() if config existed
strip.makeAutoSegments(); strip.makeAutoSegments();
strip.resume(); // resume strip service after initialization
strip.setBrightness(0); strip.setBrightness(0);
strip.setShowCallback(handleOverlayDraw); strip.setShowCallback(handleOverlayDraw);
doInitBusses = false; doInitBusses = false;