mirror of
https://github.com/wled/WLED.git
synced 2025-10-19 16:58:50 +00:00
Compare commits
16 Commits
copilot/fi
...
coderabbit
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2703f04a0d | ||
![]() |
ca5debef32 | ||
![]() |
3bc728e068 | ||
![]() |
7f1f986f13 | ||
![]() |
91fdb5822b | ||
![]() |
e4cabf8de6 | ||
![]() |
f034601512 | ||
![]() |
151a974607 | ||
![]() |
9f583f16f8 | ||
![]() |
4c4436f48c | ||
![]() |
3562fa264e | ||
![]() |
359d46c3e1 | ||
![]() |
2b73a349dd | ||
![]() |
d86ae7db40 | ||
![]() |
f096320e63 | ||
![]() |
649d43b581 |
53
.github/copilot-instructions.md
vendored
53
.github/copilot-instructions.md
vendored
@@ -30,6 +30,27 @@ The build has two main phases:
|
||||
- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`
|
||||
- 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
|
||||
|
||||
### Web UI Testing
|
||||
@@ -44,7 +65,7 @@ The build has two main phases:
|
||||
- **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
|
||||
- **Always run tests before finishing**: `npm test`
|
||||
- **Always run a build for the common environment before finishing**
|
||||
- **MANDATORY: Always run a hardware build before finishing** (see "Before Finishing Work" section below)
|
||||
|
||||
### Manual Testing Scenarios
|
||||
After making changes to web UI, always test:
|
||||
@@ -100,10 +121,16 @@ package.json # Node.js dependencies and scripts
|
||||
|
||||
## Build Timing and Timeouts
|
||||
|
||||
- **Web UI build**: 3 seconds - Set timeout to 30 seconds minimum
|
||||
- **Test suite**: 40 seconds - Set timeout to 2 minutes minimum
|
||||
- **Hardware builds**: 15+ minutes - Set timeout to 30+ minutes minimum
|
||||
- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation can take significant time
|
||||
**IMPORTANT: Use these timeout values when running builds:**
|
||||
|
||||
- **Web UI build** (`npm run build`): 3 seconds typical - Set timeout to 30 seconds minimum
|
||||
- **Test suite** (`npm test`): 40 seconds typical - Set timeout to 120 seconds (2 minutes) minimum
|
||||
- **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
|
||||
|
||||
@@ -129,11 +156,17 @@ package.json # Node.js dependencies and scripts
|
||||
- **Hardware builds require appropriate ESP32/ESP8266 development board**
|
||||
|
||||
## CI/CD Pipeline
|
||||
The GitHub Actions workflow:
|
||||
|
||||
**The GitHub Actions CI workflow will:**
|
||||
1. Installs Node.js and Python dependencies
|
||||
2. Runs `npm test` to validate build system
|
||||
3. Builds web UI with `npm run build`
|
||||
4. Compiles firmware for multiple hardware targets
|
||||
2. Runs `npm test` to validate build system (MUST pass)
|
||||
3. Builds web UI with `npm run build` (automatically run by PlatformIO)
|
||||
4. Compiles firmware for ALL hardware targets listed in `default_envs` (MUST succeed for all)
|
||||
5. Uploads build artifacts
|
||||
|
||||
Match this workflow in your local development to ensure CI success.
|
||||
**To ensure CI success, you MUST locally:**
|
||||
- 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.**
|
||||
|
@@ -450,7 +450,7 @@ board_build.partitions = ${esp32.large_partitions}
|
||||
board_upload.flash_size = 8MB
|
||||
board_upload.maximum_size = 8388608
|
||||
; board_build.f_flash = 80000000L
|
||||
; board_build.flash_mode = qio
|
||||
board_build.flash_mode = dio
|
||||
|
||||
[env:esp32dev_16M]
|
||||
board = esp32dev
|
||||
|
@@ -135,7 +135,8 @@ uint16_t mode_copy_segment(void) {
|
||||
SEGMENT.fadeToBlackBy(5); // fade out
|
||||
return FRAMETIME;
|
||||
}
|
||||
Segment sourcesegment = strip.getSegment(sourceid);
|
||||
Segment& sourcesegment = strip.getSegment(sourceid);
|
||||
|
||||
if (sourcesegment.isActive()) {
|
||||
uint32_t sourcecolor;
|
||||
uint32_t destcolor;
|
||||
@@ -677,7 +678,7 @@ uint16_t mode_twinkle(void) {
|
||||
SEGENV.step = it;
|
||||
}
|
||||
|
||||
unsigned PRNG16 = SEGENV.aux1;
|
||||
uint16_t PRNG16 = SEGENV.aux1;
|
||||
|
||||
for (unsigned i = 0; i < SEGENV.aux0; i++)
|
||||
{
|
||||
@@ -1714,8 +1715,8 @@ static const char _data_FX_MODE_TRICOLOR_WIPE[] PROGMEM = "Tri Wipe@!;1,2,3;!";
|
||||
* Modified by Aircoookie
|
||||
*/
|
||||
uint16_t mode_tricolor_fade(void) {
|
||||
unsigned counter = strip.now * ((SEGMENT.speed >> 3) +1);
|
||||
uint16_t prog = (counter * 768) >> 16;
|
||||
uint16_t counter = strip.now * ((SEGMENT.speed >> 3) +1);
|
||||
uint32_t prog = (counter * 768) >> 16;
|
||||
|
||||
uint32_t color1 = 0, color2 = 0;
|
||||
unsigned stage = 0;
|
||||
|
@@ -625,6 +625,9 @@ class Segment {
|
||||
DEBUGFX_PRINTLN();
|
||||
#endif
|
||||
clearName();
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
p_free(pixels);
|
||||
}
|
||||
|
@@ -448,6 +448,9 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
|
||||
// apply change immediately
|
||||
if (i2 <= i1) { //disable segment
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
p_free(pixels);
|
||||
pixels = nullptr;
|
||||
@@ -466,6 +469,9 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
|
||||
#endif
|
||||
// safety check
|
||||
if (start >= stop || startY >= stopY) {
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
p_free(pixels);
|
||||
pixels = nullptr;
|
||||
@@ -479,6 +485,9 @@ 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));
|
||||
if (!pixels) {
|
||||
DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!"));
|
||||
#ifdef WLED_ENABLE_GIF
|
||||
endImagePlayback(this);
|
||||
#endif
|
||||
deallocateData();
|
||||
errorFlag = ERR_NORAM_PX;
|
||||
stop = 0;
|
||||
|
@@ -72,11 +72,10 @@ 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
|
||||
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 quarterMax = maxc >> 2; // note: using half of max results in color artefacts
|
||||
addRemains = r && r > quarterMax ? 0x00010000 : 0;
|
||||
addRemains |= g && g > quarterMax ? 0x00000100 : 0;
|
||||
addRemains |= b && b > quarterMax ? 0x00000001 : 0;
|
||||
addRemains |= w ? 0x01000000 : 0;
|
||||
addRemains = r && (r<<5) > maxc ? 0x00010000 : 0; // note: setting color preservation threshold too high results in flickering and
|
||||
addRemains |= g && (g<<5) > maxc ? 0x00000100 : 0; // jumping colors in low brightness gradients. Multiplying the color preserves
|
||||
addRemains |= b && (b<<5) > maxc ? 0x00000001 : 0; // better accuracy than dividing the maxc. Shifting by 5 is a good compromise
|
||||
addRemains |= w ? 0x01000000 : 0; // i.e. remove color channel if <13% of max
|
||||
}
|
||||
const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;
|
||||
uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue
|
||||
|
@@ -8,7 +8,6 @@
|
||||
<script>
|
||||
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
|
||||
var customStarts=false,startsDirty=[];
|
||||
var busChanged=false,originalBusTypes=[];
|
||||
function off(n) { gN(n).value = -1;}
|
||||
// these functions correspond to C macros found in const.h
|
||||
function gT(t) { for (let type of d.ledTypes) if (t == type.i) return type; } // getType from available ledTypes
|
||||
@@ -38,7 +37,6 @@
|
||||
}, ()=>{
|
||||
checkSi();
|
||||
setABL();
|
||||
captureInitialBusState(); // capture initial bus configuration
|
||||
d.Sf.addEventListener("submit", trySubmit);
|
||||
if (d.um_p[0]==-1) d.um_p.shift();
|
||||
pinDropdowns();
|
||||
@@ -114,23 +112,6 @@
|
||||
d.Sf.data.value = '';
|
||||
e.preventDefault();
|
||||
if (!pinsOK()) {e.stopPropagation();return false;} // Prevent form submission and contact with server
|
||||
|
||||
// check for bus changes that require reboot
|
||||
checkBusChanges();
|
||||
if (busChanged) {
|
||||
var msg = "LED hardware changed. Reboot required to apply changes.\n\nContinue and reboot after saving?";
|
||||
if (!confirm(msg)) {
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
// set reboot flag
|
||||
let rebootField = d.createElement('input');
|
||||
rebootField.type = 'hidden';
|
||||
rebootField.name = 'RBT';
|
||||
rebootField.value = '1';
|
||||
d.Sf.appendChild(rebootField);
|
||||
}
|
||||
|
||||
if (bquot > 200) {var msg = "Too many LEDs! Can't handle that!"; alert(msg); e.stopPropagation(); return false;}
|
||||
else {
|
||||
if (bquot > 80) {var msg = "Memory usage is high, reboot recommended!\n\rSet transitions to 0 to save memory.";
|
||||
@@ -289,7 +270,7 @@
|
||||
let dC = 0; // count of digital buses (for parallel I2S)
|
||||
let LTs = d.Sf.querySelectorAll("#mLC select[name^=LT]");
|
||||
LTs.forEach((s,i)=>{
|
||||
// no longer disable type changes - allow all bus type changes
|
||||
if (i < LTs.length-1) s.disabled = true; // prevent changing type (as we can't update options)
|
||||
// is the field a LED type?
|
||||
var n = s.name.substring(2,3); // bus number (0-Z)
|
||||
var t = parseInt(s.value);
|
||||
@@ -486,7 +467,7 @@
|
||||
var cn = `<div class="iST">
|
||||
<hr class="sml">
|
||||
${i+1}:
|
||||
<select name="LT${s}" onchange="checkBusChanges();UI(true)"></select><br>
|
||||
<select name="LT${s}" onchange="UI(true)"></select><br>
|
||||
<div id="abl${s}">
|
||||
mA/LED: <select name="LAsel${s}" onchange="enLA(this,'${s}');UI();">
|
||||
<option value="55" selected>55mA (typ. 5V WS281x)</option>
|
||||
@@ -559,7 +540,6 @@ mA/LED: <select name="LAsel${s}" onchange="enLA(this,'${s}');UI();">
|
||||
gId("-").style.display = (i>0) ? "inline":"none";
|
||||
|
||||
if (!init) {
|
||||
checkBusChanges(); // check if bus count changed
|
||||
UI();
|
||||
}
|
||||
}
|
||||
@@ -654,32 +634,6 @@ Swap: <select id="xw${s}" name="XW${s}">
|
||||
gId("si").checked = cs;
|
||||
tglSi(cs);
|
||||
}
|
||||
function captureInitialBusState() {
|
||||
// capture initial bus types
|
||||
let buses = d.Sf.querySelectorAll("#mLC select[name^=LT]");
|
||||
originalBusTypes = [];
|
||||
buses.forEach((s) => {
|
||||
originalBusTypes.push(parseInt(s.value));
|
||||
});
|
||||
busChanged = false;
|
||||
}
|
||||
function checkBusChanges() {
|
||||
// check if bus configuration has changed
|
||||
let buses = d.Sf.querySelectorAll("#mLC select[name^=LT]");
|
||||
|
||||
// check if any bus type changed (excluding virtual and network buses)
|
||||
for (let i = 0; i < buses.length; i++) {
|
||||
let oldType = originalBusTypes[i] || 0;
|
||||
let newType = parseInt(buses[i].value) || 0;
|
||||
if (oldType != newType) {
|
||||
// only flag for reboot if neither old nor new type is virtual/network
|
||||
if (!isVir(oldType) && !isNet(oldType) && !isVir(newType) && !isNet(newType)) {
|
||||
busChanged = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// https://stackoverflow.com/questions/7346563/loading-local-json-file
|
||||
function loadCfg(o) {
|
||||
var f, fr;
|
||||
|
@@ -48,7 +48,7 @@
|
||||
To enable OTA, for security reasons you need to also enter the correct password!<br>
|
||||
The password should be changed when OTA is enabled.<br>
|
||||
<b>Disable OTA when not in use, otherwise an attacker can reflash device software!</b><br>
|
||||
<i>Settings on this page are only changable if OTA lock is disabled!</i><br>
|
||||
<i>Settings on this page are only changeable if OTA lock is disabled!</i><br>
|
||||
Deny access to WiFi settings if locked: <input type="checkbox" name="OW"><br><br>
|
||||
Factory reset: <input type="checkbox" name="RS"><br>
|
||||
All settings and presets will be erased.<br><br>
|
||||
|
@@ -64,6 +64,22 @@ 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)
|
||||
{
|
||||
byte id = elem["id"] | it;
|
||||
@@ -205,7 +221,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):
|
||||
// "col":[int|string|object|array, int|string|object|array, int|string|object|array]
|
||||
// int = Kelvin temperature or 0 for black
|
||||
// string = hex representation of [WW]RRGGBB
|
||||
// string = hex representation of [WW]RRGGBB or "r" for random color
|
||||
// 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)
|
||||
int rgbw[] = {0,0,0,0};
|
||||
@@ -229,6 +245,9 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
|
||||
if (kelvin == 0) seg.setColor(i, 0);
|
||||
if (kelvin > 0) colorKtoRGB(kelvin, brgbw);
|
||||
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"
|
||||
colValid = colorFromHexString(brgbw, hexCol);
|
||||
}
|
||||
@@ -1240,4 +1259,4 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient)
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#endif
|
@@ -364,9 +364,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
if (t > 0) briMultiplier = t;
|
||||
|
||||
doInitBusses = busesChanged;
|
||||
|
||||
// handle reboot request for bus changes
|
||||
if (request->hasArg(F("RBT"))) doReboot = true;
|
||||
}
|
||||
|
||||
//UI
|
||||
|
@@ -896,7 +896,8 @@ static bool detectBootLoop() {
|
||||
bl_crashcounter++;
|
||||
if (bl_crashcounter >= BOOTLOOP_THRESHOLD) {
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
|
Reference in New Issue
Block a user